button_like.rs

  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}