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