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/// Sets the visual appearance of a button.
 99#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
100pub enum ButtonStyle {
101    /// A filled button with a solid background color. Provides emphasis versus
102    /// the more common subtle button.
103    Filled,
104
105    /// Used to emphasize a button in some way, like a selected state, or a semantic
106    /// coloring like an error or success button.
107    Tinted(TintColor),
108
109    /// The default button style, used for most buttons. Has a transparent background,
110    /// but has a background color to indicate states like hover and active.
111    #[default]
112    Subtle,
113
114    /// Used for buttons that only change forground color on hover and active states.
115    ///
116    /// TODO: Better docs for this.
117    Transparent,
118}
119
120#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
121pub(crate) enum ButtonLikeRounding {
122    All,
123    Left,
124    Right,
125}
126
127#[derive(Debug, Clone)]
128pub(crate) struct ButtonLikeStyles {
129    pub background: Hsla,
130    #[allow(unused)]
131    pub border_color: Hsla,
132    #[allow(unused)]
133    pub label_color: Hsla,
134    #[allow(unused)]
135    pub icon_color: Hsla,
136}
137
138impl ButtonStyle {
139    pub(crate) fn enabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
140        match self {
141            ButtonStyle::Filled => ButtonLikeStyles {
142                background: cx.theme().colors().element_background,
143                border_color: transparent_black(),
144                label_color: Color::Default.color(cx),
145                icon_color: Color::Default.color(cx),
146            },
147            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
148            ButtonStyle::Subtle => ButtonLikeStyles {
149                background: cx.theme().colors().ghost_element_background,
150                border_color: transparent_black(),
151                label_color: Color::Default.color(cx),
152                icon_color: Color::Default.color(cx),
153            },
154            ButtonStyle::Transparent => ButtonLikeStyles {
155                background: transparent_black(),
156                border_color: transparent_black(),
157                label_color: Color::Default.color(cx),
158                icon_color: Color::Default.color(cx),
159            },
160        }
161    }
162
163    pub(crate) fn hovered(self, cx: &mut WindowContext) -> ButtonLikeStyles {
164        match self {
165            ButtonStyle::Filled => ButtonLikeStyles {
166                background: cx.theme().colors().element_hover,
167                border_color: transparent_black(),
168                label_color: Color::Default.color(cx),
169                icon_color: Color::Default.color(cx),
170            },
171            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
172            ButtonStyle::Subtle => ButtonLikeStyles {
173                background: cx.theme().colors().ghost_element_hover,
174                border_color: transparent_black(),
175                label_color: Color::Default.color(cx),
176                icon_color: Color::Default.color(cx),
177            },
178            ButtonStyle::Transparent => ButtonLikeStyles {
179                background: transparent_black(),
180                border_color: transparent_black(),
181                // TODO: These are not great
182                label_color: Color::Muted.color(cx),
183                // TODO: These are not great
184                icon_color: Color::Muted.color(cx),
185            },
186        }
187    }
188
189    pub(crate) fn active(self, cx: &mut WindowContext) -> ButtonLikeStyles {
190        match self {
191            ButtonStyle::Filled => ButtonLikeStyles {
192                background: cx.theme().colors().element_active,
193                border_color: transparent_black(),
194                label_color: Color::Default.color(cx),
195                icon_color: Color::Default.color(cx),
196            },
197            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
198            ButtonStyle::Subtle => ButtonLikeStyles {
199                background: cx.theme().colors().ghost_element_active,
200                border_color: transparent_black(),
201                label_color: Color::Default.color(cx),
202                icon_color: Color::Default.color(cx),
203            },
204            ButtonStyle::Transparent => ButtonLikeStyles {
205                background: transparent_black(),
206                border_color: transparent_black(),
207                // TODO: These are not great
208                label_color: Color::Muted.color(cx),
209                // TODO: These are not great
210                icon_color: Color::Muted.color(cx),
211            },
212        }
213    }
214
215    #[allow(unused)]
216    pub(crate) fn focused(self, cx: &mut WindowContext) -> ButtonLikeStyles {
217        match self {
218            ButtonStyle::Filled => ButtonLikeStyles {
219                background: cx.theme().colors().element_background,
220                border_color: cx.theme().colors().border_focused,
221                label_color: Color::Default.color(cx),
222                icon_color: Color::Default.color(cx),
223            },
224            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
225            ButtonStyle::Subtle => ButtonLikeStyles {
226                background: cx.theme().colors().ghost_element_background,
227                border_color: cx.theme().colors().border_focused,
228                label_color: Color::Default.color(cx),
229                icon_color: Color::Default.color(cx),
230            },
231            ButtonStyle::Transparent => ButtonLikeStyles {
232                background: transparent_black(),
233                border_color: cx.theme().colors().border_focused,
234                label_color: Color::Accent.color(cx),
235                icon_color: Color::Accent.color(cx),
236            },
237        }
238    }
239
240    #[allow(unused)]
241    pub(crate) fn disabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
242        match self {
243            ButtonStyle::Filled => ButtonLikeStyles {
244                background: cx.theme().colors().element_disabled,
245                border_color: cx.theme().colors().border_disabled,
246                label_color: Color::Disabled.color(cx),
247                icon_color: Color::Disabled.color(cx),
248            },
249            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
250            ButtonStyle::Subtle => ButtonLikeStyles {
251                background: cx.theme().colors().ghost_element_disabled,
252                border_color: cx.theme().colors().border_disabled,
253                label_color: Color::Disabled.color(cx),
254                icon_color: Color::Disabled.color(cx),
255            },
256            ButtonStyle::Transparent => ButtonLikeStyles {
257                background: transparent_black(),
258                border_color: transparent_black(),
259                label_color: Color::Disabled.color(cx),
260                icon_color: Color::Disabled.color(cx),
261            },
262        }
263    }
264}
265
266/// Sets the height of a button. Can also be used to size  non-button elements to align with [Button]s.
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}