1use gpui::{relative, DefiniteLength, MouseButton};
2use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
3use smallvec::SmallVec;
4
5use crate::prelude::*;
6
7pub trait ButtonCommon: Clickable + Disableable {
8 /// A unique element ID to identify the button.
9 fn id(&self) -> &ElementId;
10
11 /// The visual style of the button.
12 ///
13 /// Most commonly will be [`ButtonStyle::Subtle`], or [`ButtonStyle::Filled`]
14 /// for an emphasized button.
15 fn style(self, style: ButtonStyle) -> Self;
16
17 /// The size of the button.
18 ///
19 /// Most buttons will use the default size.
20 ///
21 /// [`ButtonSize`] can also be used to help build non-button elements
22 /// that are consistently sized with buttons.
23 fn size(self, size: ButtonSize) -> Self;
24
25 /// The tooltip that shows when a user hovers over the button.
26 ///
27 /// Nearly all interactable elements should have a tooltip. Some example
28 /// exceptions might a scroll bar, or a slider.
29 fn tooltip(self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self;
30}
31
32#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
33pub enum IconPosition {
34 #[default]
35 Start,
36 End,
37}
38
39#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
40pub enum TintColor {
41 #[default]
42 Accent,
43 Negative,
44 Positive,
45 Warning,
46}
47
48impl TintColor {
49 fn button_style(self, cx: &mut WindowContext) -> ButtonLikeStyles {
50 match self {
51 TintColor::Accent => ButtonLikeStyles {
52 background: cx.theme().status().info_background,
53 border_color: cx.theme().status().info_border,
54 label_color: cx.theme().colors().text,
55 icon_color: cx.theme().colors().text,
56 },
57 _ => ButtonLikeStyles {
58 background: gpui::red(),
59 border_color: gpui::red(),
60 label_color: gpui::red(),
61 icon_color: gpui::red(),
62 },
63 }
64 }
65}
66
67#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
68pub enum ButtonStyle {
69 /// A filled button with a solid background color. Provides emphasis versus
70 /// the more common subtle button.
71 Filled,
72
73 /// Used to emphasize a button in some way, like a selected state, or a semantic
74 /// coloring like an error or success button.
75 Tinted(TintColor),
76
77 /// The default button style, used for most buttons. Has a transparent background,
78 /// but has a background color to indicate states like hover and active.
79 #[default]
80 Subtle,
81
82 /// Used for buttons that only change forground color on hover and active states.
83 ///
84 /// TODO: Better docs for this.
85 Transparent,
86}
87
88#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
89pub(crate) enum ButtonLikeRounding {
90 All,
91 Left,
92 Right,
93}
94
95#[derive(Debug, Clone)]
96pub(crate) struct ButtonLikeStyles {
97 pub background: Hsla,
98 #[allow(unused)]
99 pub border_color: Hsla,
100 #[allow(unused)]
101 pub label_color: Hsla,
102 #[allow(unused)]
103 pub icon_color: Hsla,
104}
105
106impl ButtonStyle {
107 pub(crate) fn enabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
108 match self {
109 ButtonStyle::Filled => ButtonLikeStyles {
110 background: cx.theme().colors().element_background,
111 border_color: transparent_black(),
112 label_color: Color::Default.color(cx),
113 icon_color: Color::Default.color(cx),
114 },
115 ButtonStyle::Tinted(TintColor::Accent) => TintColor::Accent.button_style(cx),
116 // TODO: Finish tint colors
117 ButtonStyle::Tinted(_) => ButtonLikeStyles {
118 background: gpui::red(),
119 border_color: gpui::red(),
120 label_color: gpui::red(),
121 icon_color: gpui::red(),
122 },
123 ButtonStyle::Subtle => ButtonLikeStyles {
124 background: cx.theme().colors().ghost_element_background,
125 border_color: transparent_black(),
126 label_color: Color::Default.color(cx),
127 icon_color: Color::Default.color(cx),
128 },
129 ButtonStyle::Transparent => ButtonLikeStyles {
130 background: transparent_black(),
131 border_color: transparent_black(),
132 label_color: Color::Default.color(cx),
133 icon_color: Color::Default.color(cx),
134 },
135 }
136 }
137
138 pub(crate) fn hovered(self, cx: &mut WindowContext) -> ButtonLikeStyles {
139 match self {
140 ButtonStyle::Filled => ButtonLikeStyles {
141 background: cx.theme().colors().element_hover,
142 border_color: transparent_black(),
143 label_color: Color::Default.color(cx),
144 icon_color: Color::Default.color(cx),
145 },
146 ButtonStyle::Tinted(TintColor::Accent) => TintColor::Accent.button_style(cx),
147 // TODO: Finish tint colors
148 ButtonStyle::Tinted(_) => ButtonLikeStyles {
149 background: gpui::red(),
150 border_color: gpui::red(),
151 label_color: gpui::red(),
152 icon_color: gpui::red(),
153 },
154 ButtonStyle::Subtle => ButtonLikeStyles {
155 background: cx.theme().colors().ghost_element_hover,
156 border_color: transparent_black(),
157 label_color: Color::Default.color(cx),
158 icon_color: Color::Default.color(cx),
159 },
160 ButtonStyle::Transparent => ButtonLikeStyles {
161 background: transparent_black(),
162 border_color: transparent_black(),
163 // TODO: These are not great
164 label_color: Color::Muted.color(cx),
165 // TODO: These are not great
166 icon_color: Color::Muted.color(cx),
167 },
168 }
169 }
170
171 pub(crate) fn active(self, cx: &mut WindowContext) -> ButtonLikeStyles {
172 match self {
173 ButtonStyle::Filled => ButtonLikeStyles {
174 background: cx.theme().colors().element_active,
175 border_color: transparent_black(),
176 label_color: Color::Default.color(cx),
177 icon_color: Color::Default.color(cx),
178 },
179 ButtonStyle::Tinted(TintColor::Accent) => TintColor::Accent.button_style(cx),
180 // TODO: Finish tint colors
181 ButtonStyle::Tinted(_) => ButtonLikeStyles {
182 background: gpui::red(),
183 border_color: gpui::red(),
184 label_color: gpui::red(),
185 icon_color: gpui::red(),
186 },
187 ButtonStyle::Subtle => ButtonLikeStyles {
188 background: cx.theme().colors().ghost_element_active,
189 border_color: transparent_black(),
190 label_color: Color::Default.color(cx),
191 icon_color: Color::Default.color(cx),
192 },
193 ButtonStyle::Transparent => ButtonLikeStyles {
194 background: transparent_black(),
195 border_color: transparent_black(),
196 // TODO: These are not great
197 label_color: Color::Muted.color(cx),
198 // TODO: These are not great
199 icon_color: Color::Muted.color(cx),
200 },
201 }
202 }
203
204 #[allow(unused)]
205 pub(crate) fn focused(self, cx: &mut WindowContext) -> ButtonLikeStyles {
206 match self {
207 ButtonStyle::Filled => ButtonLikeStyles {
208 background: cx.theme().colors().element_background,
209 border_color: cx.theme().colors().border_focused,
210 label_color: Color::Default.color(cx),
211 icon_color: Color::Default.color(cx),
212 },
213 ButtonStyle::Tinted(TintColor::Accent) => TintColor::Accent.button_style(cx),
214 // TODO: Finish tint colors
215 ButtonStyle::Tinted(_) => ButtonLikeStyles {
216 background: gpui::red(),
217 border_color: gpui::red(),
218 label_color: gpui::red(),
219 icon_color: gpui::red(),
220 },
221 ButtonStyle::Subtle => ButtonLikeStyles {
222 background: cx.theme().colors().ghost_element_background,
223 border_color: cx.theme().colors().border_focused,
224 label_color: Color::Default.color(cx),
225 icon_color: Color::Default.color(cx),
226 },
227 ButtonStyle::Transparent => ButtonLikeStyles {
228 background: transparent_black(),
229 border_color: cx.theme().colors().border_focused,
230 label_color: Color::Accent.color(cx),
231 icon_color: Color::Accent.color(cx),
232 },
233 }
234 }
235
236 #[allow(unused)]
237 pub(crate) fn disabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
238 match self {
239 ButtonStyle::Filled => ButtonLikeStyles {
240 background: cx.theme().colors().element_disabled,
241 border_color: cx.theme().colors().border_disabled,
242 label_color: Color::Disabled.color(cx),
243 icon_color: Color::Disabled.color(cx),
244 },
245 ButtonStyle::Tinted(TintColor::Accent) => TintColor::Accent.button_style(cx),
246 // TODO: Finish tint colors
247 ButtonStyle::Tinted(_) => ButtonLikeStyles {
248 background: gpui::red(),
249 border_color: gpui::red(),
250 label_color: gpui::red(),
251 icon_color: gpui::red(),
252 },
253 ButtonStyle::Subtle => ButtonLikeStyles {
254 background: cx.theme().colors().ghost_element_disabled,
255 border_color: cx.theme().colors().border_disabled,
256 label_color: Color::Disabled.color(cx),
257 icon_color: Color::Disabled.color(cx),
258 },
259 ButtonStyle::Transparent => ButtonLikeStyles {
260 background: transparent_black(),
261 border_color: transparent_black(),
262 label_color: Color::Disabled.color(cx),
263 icon_color: Color::Disabled.color(cx),
264 },
265 }
266 }
267}
268
269/// ButtonSize can also be used to help build non-button elements
270/// that are consistently sized with buttons.
271#[derive(Default, PartialEq, Clone, Copy)]
272pub enum ButtonSize {
273 Large,
274 #[default]
275 Default,
276 Compact,
277 None,
278}
279
280impl ButtonSize {
281 fn height(self) -> Rems {
282 match self {
283 ButtonSize::Large => rems(32. / 16.),
284 ButtonSize::Default => rems(22. / 16.),
285 ButtonSize::Compact => rems(18. / 16.),
286 ButtonSize::None => rems(16. / 16.),
287 }
288 }
289}
290
291/// A button-like element that can be used to create a custom button when
292/// prebuilt buttons are not sufficient. Use this sparingly, as it is
293/// unconstrained and may make the UI feel less consistent.
294///
295/// This is also used to build the prebuilt buttons.
296#[derive(IntoElement)]
297pub struct ButtonLike {
298 base: Div,
299 id: ElementId,
300 pub(super) style: ButtonStyle,
301 pub(super) disabled: bool,
302 pub(super) selected: bool,
303 pub(super) width: Option<DefiniteLength>,
304 size: ButtonSize,
305 rounding: Option<ButtonLikeRounding>,
306 tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
307 on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
308 children: SmallVec<[AnyElement; 2]>,
309}
310
311impl ButtonLike {
312 pub fn new(id: impl Into<ElementId>) -> Self {
313 Self {
314 base: div(),
315 id: id.into(),
316 style: ButtonStyle::default(),
317 disabled: false,
318 selected: false,
319 width: None,
320 size: ButtonSize::Default,
321 rounding: Some(ButtonLikeRounding::All),
322 tooltip: None,
323 children: SmallVec::new(),
324 on_click: None,
325 }
326 }
327
328 pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
329 self.rounding = rounding.into();
330 self
331 }
332}
333
334impl Disableable for ButtonLike {
335 fn disabled(mut self, disabled: bool) -> Self {
336 self.disabled = disabled;
337 self
338 }
339}
340
341impl Selectable for ButtonLike {
342 fn selected(mut self, selected: bool) -> Self {
343 self.selected = selected;
344 self
345 }
346}
347
348impl Clickable for ButtonLike {
349 fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
350 self.on_click = Some(Box::new(handler));
351 self
352 }
353}
354
355impl FixedWidth for ButtonLike {
356 fn width(mut self, width: DefiniteLength) -> Self {
357 self.width = Some(width);
358 self
359 }
360
361 fn full_width(mut self) -> Self {
362 self.width = Some(relative(1.));
363 self
364 }
365}
366
367impl ButtonCommon for ButtonLike {
368 fn id(&self) -> &ElementId {
369 &self.id
370 }
371
372 fn style(mut self, style: ButtonStyle) -> Self {
373 self.style = style;
374 self
375 }
376
377 fn size(mut self, size: ButtonSize) -> Self {
378 self.size = size;
379 self
380 }
381
382 fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
383 self.tooltip = Some(Box::new(tooltip));
384 self
385 }
386}
387
388impl VisibleOnHover for ButtonLike {
389 fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
390 self.base = self.base.visible_on_hover(group_name);
391 self
392 }
393}
394
395impl ParentElement for ButtonLike {
396 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
397 &mut self.children
398 }
399}
400
401impl RenderOnce for ButtonLike {
402 fn render(self, cx: &mut WindowContext) -> impl IntoElement {
403 self.base
404 .h_flex()
405 .id(self.id.clone())
406 .group("")
407 .flex_none()
408 .h(self.size.height())
409 .when_some(self.width, |this, width| this.w(width).justify_center())
410 .when_some(self.rounding, |this, rounding| match rounding {
411 ButtonLikeRounding::All => this.rounded_md(),
412 ButtonLikeRounding::Left => this.rounded_l_md(),
413 ButtonLikeRounding::Right => this.rounded_r_md(),
414 })
415 .gap_1()
416 .map(|this| match self.size {
417 ButtonSize::Large => this.px_2(),
418 ButtonSize::Default | ButtonSize::Compact => this.px_1(),
419 ButtonSize::None => this,
420 })
421 .bg(self.style.enabled(cx).background)
422 .when(self.disabled, |this| this.cursor_not_allowed())
423 .when(!self.disabled, |this| {
424 this.cursor_pointer()
425 .hover(|hover| hover.bg(self.style.hovered(cx).background))
426 .active(|active| active.bg(self.style.active(cx).background))
427 })
428 .when_some(
429 self.on_click.filter(|_| !self.disabled),
430 |this, on_click| {
431 this.on_mouse_down(MouseButton::Left, |_, cx| cx.prevent_default())
432 .on_click(move |event, cx| {
433 cx.stop_propagation();
434 (on_click)(event, cx)
435 })
436 },
437 )
438 .when_some(self.tooltip, |this, tooltip| {
439 this.tooltip(move |cx| tooltip(cx))
440 })
441 .children(self.children)
442 }
443}