button_like.rs

  1use gpui::{relative, CursorStyle, DefiniteLength, MouseButton};
  2use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
  3use smallvec::SmallVec;
  4
  5use crate::{prelude::*, Elevation, ElevationIndex, Spacing};
  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    fn layer(self, elevation: ElevationIndex) -> Self;
 38}
 39
 40#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
 41pub enum IconPosition {
 42    #[default]
 43    Start,
 44    End,
 45}
 46
 47#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
 48pub enum TintColor {
 49    #[default]
 50    Accent,
 51    Negative,
 52    Warning,
 53    Positive,
 54}
 55
 56impl TintColor {
 57    fn button_like_style(self, cx: &mut WindowContext) -> ButtonLikeStyles {
 58        match self {
 59            TintColor::Accent => ButtonLikeStyles {
 60                background: cx.theme().status().info_background,
 61                border_color: cx.theme().status().info_border,
 62                label_color: cx.theme().colors().text,
 63                icon_color: cx.theme().colors().text,
 64            },
 65            TintColor::Negative => ButtonLikeStyles {
 66                background: cx.theme().status().error_background,
 67                border_color: cx.theme().status().error_border,
 68                label_color: cx.theme().colors().text,
 69                icon_color: cx.theme().colors().text,
 70            },
 71            TintColor::Warning => ButtonLikeStyles {
 72                background: cx.theme().status().warning_background,
 73                border_color: cx.theme().status().warning_border,
 74                label_color: cx.theme().colors().text,
 75                icon_color: cx.theme().colors().text,
 76            },
 77            TintColor::Positive => ButtonLikeStyles {
 78                background: cx.theme().status().success_background,
 79                border_color: cx.theme().status().success_border,
 80                label_color: cx.theme().colors().text,
 81                icon_color: cx.theme().colors().text,
 82            },
 83        }
 84    }
 85}
 86
 87impl From<TintColor> for Color {
 88    fn from(tint: TintColor) -> Self {
 89        match tint {
 90            TintColor::Accent => Color::Accent,
 91            TintColor::Negative => Color::Error,
 92            TintColor::Warning => Color::Warning,
 93            TintColor::Positive => Color::Success,
 94        }
 95    }
 96}
 97
 98// Used to go from ButtonStyle -> Color through tint colors.
 99impl From<ButtonStyle> for Color {
100    fn from(style: ButtonStyle) -> Self {
101        match style {
102            ButtonStyle::Tinted(tint) => tint.into(),
103            _ => Color::Default,
104        }
105    }
106}
107
108/// The visual appearance of a button.
109#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
110pub enum ButtonStyle {
111    /// A filled button with a solid background color. Provides emphasis versus
112    /// the more common subtle button.
113    Filled,
114
115    /// Used to emphasize a button in some way, like a selected state, or a semantic
116    /// coloring like an error or success button.
117    Tinted(TintColor),
118
119    /// The default button style, used for most buttons. Has a transparent background,
120    /// but has a background color to indicate states like hover and active.
121    #[default]
122    Subtle,
123
124    /// Used for buttons that only change foreground color on hover and active states.
125    ///
126    /// TODO: Better docs for this.
127    Transparent,
128}
129
130#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
131pub(crate) enum ButtonLikeRounding {
132    All,
133    Left,
134    Right,
135}
136
137#[derive(Debug, Clone)]
138pub(crate) struct ButtonLikeStyles {
139    pub background: Hsla,
140    #[allow(unused)]
141    pub border_color: Hsla,
142    #[allow(unused)]
143    pub label_color: Hsla,
144    #[allow(unused)]
145    pub icon_color: Hsla,
146}
147
148fn element_bg_from_elevation(elevation: Option<Elevation>, cx: &mut WindowContext) -> Hsla {
149    match elevation {
150        Some(Elevation::ElevationIndex(ElevationIndex::Background)) => {
151            cx.theme().colors().element_background
152        }
153        Some(Elevation::ElevationIndex(ElevationIndex::ElevatedSurface)) => {
154            cx.theme().colors().surface_background
155        }
156        Some(Elevation::ElevationIndex(ElevationIndex::Surface)) => {
157            cx.theme().colors().elevated_surface_background
158        }
159        Some(Elevation::ElevationIndex(ElevationIndex::ModalSurface)) => {
160            cx.theme().colors().background
161        }
162        _ => cx.theme().colors().element_background,
163    }
164}
165
166impl ButtonStyle {
167    pub(crate) fn enabled(
168        self,
169        elevation: Option<Elevation>,
170        cx: &mut WindowContext,
171    ) -> ButtonLikeStyles {
172        let filled_background = element_bg_from_elevation(elevation, cx);
173
174        match self {
175            ButtonStyle::Filled => ButtonLikeStyles {
176                background: filled_background,
177                border_color: transparent_black(),
178                label_color: Color::Default.color(cx),
179                icon_color: Color::Default.color(cx),
180            },
181            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
182            ButtonStyle::Subtle => ButtonLikeStyles {
183                background: cx.theme().colors().ghost_element_background,
184                border_color: transparent_black(),
185                label_color: Color::Default.color(cx),
186                icon_color: Color::Default.color(cx),
187            },
188            ButtonStyle::Transparent => ButtonLikeStyles {
189                background: transparent_black(),
190                border_color: transparent_black(),
191                label_color: Color::Default.color(cx),
192                icon_color: Color::Default.color(cx),
193            },
194        }
195    }
196
197    pub(crate) fn hovered(
198        self,
199        elevation: Option<Elevation>,
200        cx: &mut WindowContext,
201    ) -> ButtonLikeStyles {
202        let mut filled_background = element_bg_from_elevation(elevation, cx);
203        filled_background.fade_out(0.92);
204
205        match self {
206            ButtonStyle::Filled => ButtonLikeStyles {
207                background: filled_background,
208                border_color: transparent_black(),
209                label_color: Color::Default.color(cx),
210                icon_color: Color::Default.color(cx),
211            },
212            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
213            ButtonStyle::Subtle => ButtonLikeStyles {
214                background: cx.theme().colors().ghost_element_hover,
215                border_color: transparent_black(),
216                label_color: Color::Default.color(cx),
217                icon_color: Color::Default.color(cx),
218            },
219            ButtonStyle::Transparent => ButtonLikeStyles {
220                background: transparent_black(),
221                border_color: transparent_black(),
222                // TODO: These are not great
223                label_color: Color::Muted.color(cx),
224                // TODO: These are not great
225                icon_color: Color::Muted.color(cx),
226            },
227        }
228    }
229
230    pub(crate) fn active(self, cx: &mut WindowContext) -> ButtonLikeStyles {
231        match self {
232            ButtonStyle::Filled => ButtonLikeStyles {
233                background: cx.theme().colors().element_active,
234                border_color: transparent_black(),
235                label_color: Color::Default.color(cx),
236                icon_color: Color::Default.color(cx),
237            },
238            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
239            ButtonStyle::Subtle => ButtonLikeStyles {
240                background: cx.theme().colors().ghost_element_active,
241                border_color: transparent_black(),
242                label_color: Color::Default.color(cx),
243                icon_color: Color::Default.color(cx),
244            },
245            ButtonStyle::Transparent => ButtonLikeStyles {
246                background: transparent_black(),
247                border_color: transparent_black(),
248                // TODO: These are not great
249                label_color: Color::Muted.color(cx),
250                // TODO: These are not great
251                icon_color: Color::Muted.color(cx),
252            },
253        }
254    }
255
256    #[allow(unused)]
257    pub(crate) fn focused(self, cx: &mut WindowContext) -> ButtonLikeStyles {
258        match self {
259            ButtonStyle::Filled => ButtonLikeStyles {
260                background: cx.theme().colors().element_background,
261                border_color: cx.theme().colors().border_focused,
262                label_color: Color::Default.color(cx),
263                icon_color: Color::Default.color(cx),
264            },
265            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
266            ButtonStyle::Subtle => ButtonLikeStyles {
267                background: cx.theme().colors().ghost_element_background,
268                border_color: cx.theme().colors().border_focused,
269                label_color: Color::Default.color(cx),
270                icon_color: Color::Default.color(cx),
271            },
272            ButtonStyle::Transparent => ButtonLikeStyles {
273                background: transparent_black(),
274                border_color: cx.theme().colors().border_focused,
275                label_color: Color::Accent.color(cx),
276                icon_color: Color::Accent.color(cx),
277            },
278        }
279    }
280
281    #[allow(unused)]
282    pub(crate) fn disabled(
283        self,
284        elevation: Option<Elevation>,
285        cx: &mut WindowContext,
286    ) -> ButtonLikeStyles {
287        element_bg_from_elevation(elevation, cx).fade_out(0.82);
288
289        match self {
290            ButtonStyle::Filled => ButtonLikeStyles {
291                background: cx.theme().colors().element_disabled,
292                border_color: cx.theme().colors().border_disabled,
293                label_color: Color::Disabled.color(cx),
294                icon_color: Color::Disabled.color(cx),
295            },
296            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
297            ButtonStyle::Subtle => ButtonLikeStyles {
298                background: cx.theme().colors().ghost_element_disabled,
299                border_color: cx.theme().colors().border_disabled,
300                label_color: Color::Disabled.color(cx),
301                icon_color: Color::Disabled.color(cx),
302            },
303            ButtonStyle::Transparent => ButtonLikeStyles {
304                background: transparent_black(),
305                border_color: transparent_black(),
306                label_color: Color::Disabled.color(cx),
307                icon_color: Color::Disabled.color(cx),
308            },
309        }
310    }
311}
312
313/// The height of a button.
314///
315/// Can also be used to size non-button elements to align with [`Button`]s.
316#[derive(Default, PartialEq, Clone, Copy)]
317pub enum ButtonSize {
318    Large,
319    #[default]
320    Default,
321    Compact,
322    None,
323}
324
325impl ButtonSize {
326    pub fn rems(self) -> Rems {
327        match self {
328            ButtonSize::Large => rems_from_px(32.),
329            ButtonSize::Default => rems_from_px(22.),
330            ButtonSize::Compact => rems_from_px(18.),
331            ButtonSize::None => rems_from_px(16.),
332        }
333    }
334}
335
336/// A button-like element that can be used to create a custom button when
337/// prebuilt buttons are not sufficient. Use this sparingly, as it is
338/// unconstrained and may make the UI feel less consistent.
339///
340/// This is also used to build the prebuilt buttons.
341#[derive(IntoElement)]
342pub struct ButtonLike {
343    pub(super) base: Div,
344    id: ElementId,
345    pub(super) style: ButtonStyle,
346    pub(super) disabled: bool,
347    pub(super) selected: bool,
348    pub(super) selected_style: Option<ButtonStyle>,
349    pub(super) width: Option<DefiniteLength>,
350    pub(super) height: Option<DefiniteLength>,
351    pub(super) layer: Option<Elevation>,
352    size: ButtonSize,
353    rounding: Option<ButtonLikeRounding>,
354    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
355    cursor_style: CursorStyle,
356    on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
357    children: SmallVec<[AnyElement; 2]>,
358}
359
360impl ButtonLike {
361    pub fn new(id: impl Into<ElementId>) -> Self {
362        Self {
363            base: div(),
364            id: id.into(),
365            style: ButtonStyle::default(),
366            disabled: false,
367            selected: false,
368            selected_style: None,
369            width: None,
370            height: None,
371            size: ButtonSize::Default,
372            rounding: Some(ButtonLikeRounding::All),
373            tooltip: None,
374            children: SmallVec::new(),
375            cursor_style: CursorStyle::PointingHand,
376            on_click: None,
377            layer: None,
378        }
379    }
380
381    pub fn new_rounded_left(id: impl Into<ElementId>) -> Self {
382        Self::new(id).rounding(ButtonLikeRounding::Left)
383    }
384
385    pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
386        Self::new(id).rounding(ButtonLikeRounding::Right)
387    }
388
389    pub(crate) fn height(mut self, height: DefiniteLength) -> Self {
390        self.height = Some(height);
391        self
392    }
393
394    pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
395        self.rounding = rounding.into();
396        self
397    }
398}
399
400impl Disableable for ButtonLike {
401    fn disabled(mut self, disabled: bool) -> Self {
402        self.disabled = disabled;
403        self
404    }
405}
406
407impl Selectable for ButtonLike {
408    fn selected(mut self, selected: bool) -> Self {
409        self.selected = selected;
410        self
411    }
412}
413
414impl SelectableButton for ButtonLike {
415    fn selected_style(mut self, style: ButtonStyle) -> Self {
416        self.selected_style = Some(style);
417        self
418    }
419}
420
421impl Clickable for ButtonLike {
422    fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
423        self.on_click = Some(Box::new(handler));
424        self
425    }
426
427    fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
428        self.cursor_style = cursor_style;
429        self
430    }
431}
432
433impl FixedWidth for ButtonLike {
434    fn width(mut self, width: DefiniteLength) -> Self {
435        self.width = Some(width);
436        self
437    }
438
439    fn full_width(mut self) -> Self {
440        self.width = Some(relative(1.));
441        self
442    }
443}
444
445impl ButtonCommon for ButtonLike {
446    fn id(&self) -> &ElementId {
447        &self.id
448    }
449
450    fn style(mut self, style: ButtonStyle) -> Self {
451        self.style = style;
452        self
453    }
454
455    fn size(mut self, size: ButtonSize) -> Self {
456        self.size = size;
457        self
458    }
459
460    fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
461        self.tooltip = Some(Box::new(tooltip));
462        self
463    }
464
465    fn layer(mut self, elevation: ElevationIndex) -> Self {
466        self.layer = Some(elevation.into());
467        self
468    }
469}
470
471impl VisibleOnHover for ButtonLike {
472    fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
473        self.base = self.base.visible_on_hover(group_name);
474        self
475    }
476}
477
478impl ParentElement for ButtonLike {
479    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
480        self.children.extend(elements)
481    }
482}
483
484impl RenderOnce for ButtonLike {
485    fn render(self, cx: &mut WindowContext) -> impl IntoElement {
486        let style = self
487            .selected_style
488            .filter(|_| self.selected)
489            .unwrap_or(self.style);
490
491        self.base
492            .h_flex()
493            .id(self.id.clone())
494            .group("")
495            .flex_none()
496            .h(self.height.unwrap_or(self.size.rems().into()))
497            .when_some(self.width, |this, width| this.w(width).justify_center())
498            .when_some(self.rounding, |this, rounding| match rounding {
499                ButtonLikeRounding::All => this.rounded_md(),
500                ButtonLikeRounding::Left => this.rounded_l_md(),
501                ButtonLikeRounding::Right => this.rounded_r_md(),
502            })
503            .gap(Spacing::Small.rems(cx))
504            .map(|this| match self.size {
505                ButtonSize::Large => this.px(Spacing::Medium.rems(cx)),
506                ButtonSize::Default | ButtonSize::Compact => this.px(Spacing::Small.rems(cx)),
507                ButtonSize::None => this,
508            })
509            .bg(style.enabled(self.layer, cx).background)
510            .when(self.disabled, |this| this.cursor_not_allowed())
511            .when(!self.disabled, |this| {
512                this.cursor_pointer()
513                    .hover(|hover| hover.bg(style.hovered(self.layer, cx).background))
514                    .active(|active| active.bg(style.active(cx).background))
515            })
516            .when_some(
517                self.on_click.filter(|_| !self.disabled),
518                |this, on_click| {
519                    this.on_mouse_down(MouseButton::Left, |_, cx| cx.prevent_default())
520                        .on_click(move |event, cx| {
521                            cx.stop_propagation();
522                            (on_click)(event, cx)
523                        })
524                },
525            )
526            .when(!self.selected, |this| {
527                this.when_some(self.tooltip, |this, tooltip| {
528                    this.tooltip(move |cx| tooltip(cx))
529                })
530            })
531            .children(self.children)
532    }
533}