button_like.rs

  1use documented::Documented;
  2use gpui::{
  3    AnyElement, AnyView, ClickEvent, CursorStyle, DefiniteLength, Hsla, MouseButton,
  4    MouseDownEvent, MouseUpEvent, Rems, relative, transparent_black,
  5};
  6use smallvec::SmallVec;
  7
  8use crate::{DynamicSpacing, ElevationIndex, prelude::*};
  9
 10/// A trait for buttons that can be Selected. Enables setting the [`ButtonStyle`] of a button when it is selected.
 11pub trait SelectableButton: Toggleable {
 12    fn selected_style(self, style: ButtonStyle) -> Self;
 13}
 14
 15/// A common set of traits all buttons must implement.
 16pub trait ButtonCommon: Clickable + Disableable {
 17    /// A unique element ID to identify the button.
 18    fn id(&self) -> &ElementId;
 19
 20    /// The visual style of the button.
 21    ///
 22    /// Most commonly will be [`ButtonStyle::Subtle`], or [`ButtonStyle::Filled`]
 23    /// for an emphasized button.
 24    fn style(self, style: ButtonStyle) -> Self;
 25
 26    /// The size of the button.
 27    ///
 28    /// Most buttons will use the default size.
 29    ///
 30    /// [`ButtonSize`] can also be used to help build non-button elements
 31    /// that are consistently sized with buttons.
 32    fn size(self, size: ButtonSize) -> Self;
 33
 34    /// The tooltip that shows when a user hovers over the button.
 35    ///
 36    /// Nearly all interactable elements should have a tooltip. Some example
 37    /// exceptions might a scroll bar, or a slider.
 38    fn tooltip(self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self;
 39
 40    fn layer(self, elevation: ElevationIndex) -> Self;
 41}
 42
 43#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
 44pub enum IconPosition {
 45    #[default]
 46    Start,
 47    End,
 48}
 49
 50#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
 51pub enum KeybindingPosition {
 52    Start,
 53    #[default]
 54    End,
 55}
 56
 57#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
 58pub enum TintColor {
 59    #[default]
 60    Accent,
 61    Error,
 62    Warning,
 63    Success,
 64}
 65
 66impl TintColor {
 67    fn button_like_style(self, cx: &mut App) -> ButtonLikeStyles {
 68        match self {
 69            TintColor::Accent => ButtonLikeStyles {
 70                background: cx.theme().status().info_background,
 71                border_color: cx.theme().status().info_border,
 72                label_color: cx.theme().colors().text,
 73                icon_color: cx.theme().colors().text,
 74            },
 75            TintColor::Error => ButtonLikeStyles {
 76                background: cx.theme().status().error_background,
 77                border_color: cx.theme().status().error_border,
 78                label_color: cx.theme().colors().text,
 79                icon_color: cx.theme().colors().text,
 80            },
 81            TintColor::Warning => ButtonLikeStyles {
 82                background: cx.theme().status().warning_background,
 83                border_color: cx.theme().status().warning_border,
 84                label_color: cx.theme().colors().text,
 85                icon_color: cx.theme().colors().text,
 86            },
 87            TintColor::Success => ButtonLikeStyles {
 88                background: cx.theme().status().success_background,
 89                border_color: cx.theme().status().success_border,
 90                label_color: cx.theme().colors().text,
 91                icon_color: cx.theme().colors().text,
 92            },
 93        }
 94    }
 95}
 96
 97impl From<TintColor> for Color {
 98    fn from(tint: TintColor) -> Self {
 99        match tint {
100            TintColor::Accent => Color::Accent,
101            TintColor::Error => Color::Error,
102            TintColor::Warning => Color::Warning,
103            TintColor::Success => Color::Success,
104        }
105    }
106}
107
108// Used to go from ButtonStyle -> Color through tint colors.
109impl From<ButtonStyle> for Color {
110    fn from(style: ButtonStyle) -> Self {
111        match style {
112            ButtonStyle::Tinted(tint) => tint.into(),
113            _ => Color::Default,
114        }
115    }
116}
117
118/// The visual appearance of a button.
119#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Default)]
120pub enum ButtonStyle {
121    /// A filled button with a solid background color. Provides emphasis versus
122    /// the more common subtle button.
123    Filled,
124
125    /// Used to emphasize a button in some way, like a selected state, or a semantic
126    /// coloring like an error or success button.
127    Tinted(TintColor),
128
129    /// The default button style, used for most buttons. Has a transparent background,
130    /// but has a background color to indicate states like hover and active.
131    #[default]
132    Subtle,
133
134    /// Used for buttons that only change foreground color on hover and active states.
135    ///
136    /// TODO: Better docs for this.
137    Transparent,
138}
139
140#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
141pub(crate) enum ButtonLikeRounding {
142    All,
143    Left,
144    Right,
145}
146
147#[derive(Debug, Clone)]
148pub(crate) struct ButtonLikeStyles {
149    pub background: Hsla,
150    #[allow(unused)]
151    pub border_color: Hsla,
152    #[allow(unused)]
153    pub label_color: Hsla,
154    #[allow(unused)]
155    pub icon_color: Hsla,
156}
157
158fn element_bg_from_elevation(elevation: Option<ElevationIndex>, cx: &mut App) -> Hsla {
159    match elevation {
160        Some(ElevationIndex::Background) => cx.theme().colors().element_background,
161        Some(ElevationIndex::ElevatedSurface) => cx.theme().colors().elevated_surface_background,
162        Some(ElevationIndex::Surface) => cx.theme().colors().surface_background,
163        Some(ElevationIndex::ModalSurface) => cx.theme().colors().background,
164        _ => cx.theme().colors().element_background,
165    }
166}
167
168impl ButtonStyle {
169    pub(crate) fn enabled(
170        self,
171        elevation: Option<ElevationIndex>,
172
173        cx: &mut App,
174    ) -> ButtonLikeStyles {
175        match self {
176            ButtonStyle::Filled => ButtonLikeStyles {
177                background: element_bg_from_elevation(elevation, cx),
178                border_color: transparent_black(),
179                label_color: Color::Default.color(cx),
180                icon_color: Color::Default.color(cx),
181            },
182            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
183            ButtonStyle::Subtle => ButtonLikeStyles {
184                background: cx.theme().colors().ghost_element_background,
185                border_color: transparent_black(),
186                label_color: Color::Default.color(cx),
187                icon_color: Color::Default.color(cx),
188            },
189            ButtonStyle::Transparent => ButtonLikeStyles {
190                background: transparent_black(),
191                border_color: transparent_black(),
192                label_color: Color::Default.color(cx),
193                icon_color: Color::Default.color(cx),
194            },
195        }
196    }
197
198    pub(crate) fn hovered(
199        self,
200        elevation: Option<ElevationIndex>,
201
202        cx: &mut App,
203    ) -> ButtonLikeStyles {
204        match self {
205            ButtonStyle::Filled => {
206                let mut filled_background = element_bg_from_elevation(elevation, cx);
207                filled_background.fade_out(0.92);
208
209                ButtonLikeStyles {
210                    background: filled_background,
211                    border_color: transparent_black(),
212                    label_color: Color::Default.color(cx),
213                    icon_color: Color::Default.color(cx),
214                }
215            }
216            ButtonStyle::Tinted(tint) => {
217                let mut styles = tint.button_like_style(cx);
218                let theme = cx.theme();
219                styles.background = theme.darken(styles.background, 0.05, 0.2);
220                styles
221            }
222            ButtonStyle::Subtle => ButtonLikeStyles {
223                background: cx.theme().colors().ghost_element_hover,
224                border_color: transparent_black(),
225                label_color: Color::Default.color(cx),
226                icon_color: Color::Default.color(cx),
227            },
228            ButtonStyle::Transparent => ButtonLikeStyles {
229                background: transparent_black(),
230                border_color: transparent_black(),
231                // TODO: These are not great
232                label_color: Color::Muted.color(cx),
233                // TODO: These are not great
234                icon_color: Color::Muted.color(cx),
235            },
236        }
237    }
238
239    pub(crate) fn active(self, cx: &mut App) -> ButtonLikeStyles {
240        match self {
241            ButtonStyle::Filled => ButtonLikeStyles {
242                background: cx.theme().colors().element_active,
243                border_color: transparent_black(),
244                label_color: Color::Default.color(cx),
245                icon_color: Color::Default.color(cx),
246            },
247            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
248            ButtonStyle::Subtle => ButtonLikeStyles {
249                background: cx.theme().colors().ghost_element_active,
250                border_color: transparent_black(),
251                label_color: Color::Default.color(cx),
252                icon_color: Color::Default.color(cx),
253            },
254            ButtonStyle::Transparent => ButtonLikeStyles {
255                background: transparent_black(),
256                border_color: transparent_black(),
257                // TODO: These are not great
258                label_color: Color::Muted.color(cx),
259                // TODO: These are not great
260                icon_color: Color::Muted.color(cx),
261            },
262        }
263    }
264
265    #[allow(unused)]
266    pub(crate) fn focused(self, window: &mut Window, cx: &mut App) -> ButtonLikeStyles {
267        match self {
268            ButtonStyle::Filled => ButtonLikeStyles {
269                background: cx.theme().colors().element_background,
270                border_color: cx.theme().colors().border_focused,
271                label_color: Color::Default.color(cx),
272                icon_color: Color::Default.color(cx),
273            },
274            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
275            ButtonStyle::Subtle => ButtonLikeStyles {
276                background: cx.theme().colors().ghost_element_background,
277                border_color: cx.theme().colors().border_focused,
278                label_color: Color::Default.color(cx),
279                icon_color: Color::Default.color(cx),
280            },
281            ButtonStyle::Transparent => ButtonLikeStyles {
282                background: transparent_black(),
283                border_color: cx.theme().colors().border_focused,
284                label_color: Color::Accent.color(cx),
285                icon_color: Color::Accent.color(cx),
286            },
287        }
288    }
289
290    #[allow(unused)]
291    pub(crate) fn disabled(
292        self,
293        elevation: Option<ElevationIndex>,
294        window: &mut Window,
295        cx: &mut App,
296    ) -> ButtonLikeStyles {
297        match self {
298            ButtonStyle::Filled => ButtonLikeStyles {
299                background: cx.theme().colors().element_disabled,
300                border_color: cx.theme().colors().border_disabled,
301                label_color: Color::Disabled.color(cx),
302                icon_color: Color::Disabled.color(cx),
303            },
304            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
305            ButtonStyle::Subtle => ButtonLikeStyles {
306                background: cx.theme().colors().ghost_element_disabled,
307                border_color: cx.theme().colors().border_disabled,
308                label_color: Color::Disabled.color(cx),
309                icon_color: Color::Disabled.color(cx),
310            },
311            ButtonStyle::Transparent => ButtonLikeStyles {
312                background: transparent_black(),
313                border_color: transparent_black(),
314                label_color: Color::Disabled.color(cx),
315                icon_color: Color::Disabled.color(cx),
316            },
317        }
318    }
319}
320
321/// The height of a button.
322///
323/// Can also be used to size non-button elements to align with [`Button`]s.
324#[derive(Default, PartialEq, Clone, Copy)]
325pub enum ButtonSize {
326    Large,
327    #[default]
328    Default,
329    Compact,
330    None,
331}
332
333impl ButtonSize {
334    pub fn rems(self) -> Rems {
335        match self {
336            ButtonSize::Large => rems_from_px(32.),
337            ButtonSize::Default => rems_from_px(22.),
338            ButtonSize::Compact => rems_from_px(18.),
339            ButtonSize::None => rems_from_px(16.),
340        }
341    }
342}
343
344/// A button-like element that can be used to create a custom button when
345/// prebuilt buttons are not sufficient. Use this sparingly, as it is
346/// unconstrained and may make the UI feel less consistent.
347///
348/// This is also used to build the prebuilt buttons.
349#[derive(IntoElement, Documented, RegisterComponent)]
350pub struct ButtonLike {
351    pub(super) base: Div,
352    id: ElementId,
353    pub(super) style: ButtonStyle,
354    pub(super) disabled: bool,
355    pub(super) selected: bool,
356    pub(super) selected_style: Option<ButtonStyle>,
357    pub(super) width: Option<DefiniteLength>,
358    pub(super) height: Option<DefiniteLength>,
359    pub(super) layer: Option<ElevationIndex>,
360    size: ButtonSize,
361    rounding: Option<ButtonLikeRounding>,
362    tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
363    cursor_style: CursorStyle,
364    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
365    on_right_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
366    children: SmallVec<[AnyElement; 2]>,
367}
368
369impl ButtonLike {
370    pub fn new(id: impl Into<ElementId>) -> Self {
371        Self {
372            base: div(),
373            id: id.into(),
374            style: ButtonStyle::default(),
375            disabled: false,
376            selected: false,
377            selected_style: None,
378            width: None,
379            height: None,
380            size: ButtonSize::Default,
381            rounding: Some(ButtonLikeRounding::All),
382            tooltip: None,
383            children: SmallVec::new(),
384            cursor_style: CursorStyle::PointingHand,
385            on_click: None,
386            on_right_click: None,
387            layer: None,
388        }
389    }
390
391    pub fn new_rounded_left(id: impl Into<ElementId>) -> Self {
392        Self::new(id).rounding(ButtonLikeRounding::Left)
393    }
394
395    pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
396        Self::new(id).rounding(ButtonLikeRounding::Right)
397    }
398
399    pub fn opacity(mut self, opacity: f32) -> Self {
400        self.base = self.base.opacity(opacity);
401        self
402    }
403
404    pub fn height(mut self, height: DefiniteLength) -> Self {
405        self.height = Some(height);
406        self
407    }
408
409    pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
410        self.rounding = rounding.into();
411        self
412    }
413
414    pub fn on_right_click(
415        mut self,
416        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
417    ) -> Self {
418        self.on_right_click = Some(Box::new(handler));
419        self
420    }
421}
422
423impl Disableable for ButtonLike {
424    fn disabled(mut self, disabled: bool) -> Self {
425        self.disabled = disabled;
426        self
427    }
428}
429
430impl Toggleable for ButtonLike {
431    fn toggle_state(mut self, selected: bool) -> Self {
432        self.selected = selected;
433        self
434    }
435}
436
437impl SelectableButton for ButtonLike {
438    fn selected_style(mut self, style: ButtonStyle) -> Self {
439        self.selected_style = Some(style);
440        self
441    }
442}
443
444impl Clickable for ButtonLike {
445    fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
446        self.on_click = Some(Box::new(handler));
447        self
448    }
449
450    fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
451        self.cursor_style = cursor_style;
452        self
453    }
454}
455
456impl FixedWidth for ButtonLike {
457    fn width(mut self, width: DefiniteLength) -> Self {
458        self.width = Some(width);
459        self
460    }
461
462    fn full_width(mut self) -> Self {
463        self.width = Some(relative(1.));
464        self
465    }
466}
467
468impl ButtonCommon for ButtonLike {
469    fn id(&self) -> &ElementId {
470        &self.id
471    }
472
473    fn style(mut self, style: ButtonStyle) -> Self {
474        self.style = style;
475        self
476    }
477
478    fn size(mut self, size: ButtonSize) -> Self {
479        self.size = size;
480        self
481    }
482
483    fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
484        self.tooltip = Some(Box::new(tooltip));
485        self
486    }
487
488    fn layer(mut self, elevation: ElevationIndex) -> Self {
489        self.layer = Some(elevation);
490        self
491    }
492}
493
494impl VisibleOnHover for ButtonLike {
495    fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
496        self.base = self.base.visible_on_hover(group_name);
497        self
498    }
499}
500
501impl ParentElement for ButtonLike {
502    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
503        self.children.extend(elements)
504    }
505}
506
507impl RenderOnce for ButtonLike {
508    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
509        let style = self
510            .selected_style
511            .filter(|_| self.selected)
512            .unwrap_or(self.style);
513
514        self.base
515            .h_flex()
516            .id(self.id.clone())
517            .font_ui(cx)
518            .group("")
519            .flex_none()
520            .h(self.height.unwrap_or(self.size.rems().into()))
521            .when_some(self.width, |this, width| {
522                this.w(width).justify_center().text_center()
523            })
524            .when_some(self.rounding, |this, rounding| match rounding {
525                ButtonLikeRounding::All => this.rounded_sm(),
526                ButtonLikeRounding::Left => this.rounded_l_sm(),
527                ButtonLikeRounding::Right => this.rounded_r_sm(),
528            })
529            .gap(DynamicSpacing::Base04.rems(cx))
530            .map(|this| match self.size {
531                ButtonSize::Large => this.px(DynamicSpacing::Base06.rems(cx)),
532                ButtonSize::Default | ButtonSize::Compact => {
533                    this.px(DynamicSpacing::Base04.rems(cx))
534                }
535                ButtonSize::None => this,
536            })
537            .bg(style.enabled(self.layer, cx).background)
538            .when(self.disabled, |this| {
539                if self.cursor_style == CursorStyle::PointingHand {
540                    this.cursor_not_allowed()
541                } else {
542                    this.cursor(self.cursor_style)
543                }
544            })
545            .when(!self.disabled, |this| {
546                this.cursor(self.cursor_style)
547                    .hover(|hover| hover.bg(style.hovered(self.layer, cx).background))
548                    .active(|active| active.bg(style.active(cx).background))
549            })
550            .when_some(
551                self.on_right_click.filter(|_| !self.disabled),
552                |this, on_right_click| {
553                    this.on_mouse_down(MouseButton::Right, |_event, window, cx| {
554                        window.prevent_default();
555                        cx.stop_propagation();
556                    })
557                    .on_mouse_up(
558                        MouseButton::Right,
559                        move |event, window, cx| {
560                            cx.stop_propagation();
561                            let click_event = ClickEvent {
562                                down: MouseDownEvent {
563                                    button: MouseButton::Right,
564                                    position: event.position,
565                                    modifiers: event.modifiers,
566                                    click_count: 1,
567                                    first_mouse: false,
568                                },
569                                up: MouseUpEvent {
570                                    button: MouseButton::Right,
571                                    position: event.position,
572                                    modifiers: event.modifiers,
573                                    click_count: 1,
574                                },
575                            };
576                            (on_right_click)(&click_event, window, cx)
577                        },
578                    )
579                },
580            )
581            .when_some(
582                self.on_click.filter(|_| !self.disabled),
583                |this, on_click| {
584                    this.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
585                        .on_click(move |event, window, cx| {
586                            cx.stop_propagation();
587                            (on_click)(event, window, cx)
588                        })
589                },
590            )
591            .when_some(self.tooltip, |this, tooltip| {
592                this.tooltip(move |window, cx| tooltip(window, cx))
593            })
594            .children(self.children)
595    }
596}
597
598impl Component for ButtonLike {
599    fn scope() -> ComponentScope {
600        ComponentScope::Input
601    }
602
603    fn sort_name() -> &'static str {
604        // ButtonLike should be at the bottom of the button list
605        "ButtonZ"
606    }
607
608    fn description() -> Option<&'static str> {
609        Some(ButtonLike::DOCS)
610    }
611
612    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
613        Some(
614            v_flex()
615                .gap_6()
616                .children(vec![
617                    example_group(vec![
618                        single_example(
619                            "Default",
620                            ButtonLike::new("default")
621                                .child(Label::new("Default"))
622                                .into_any_element(),
623                        ),
624                        single_example(
625                            "Filled",
626                            ButtonLike::new("filled")
627                                .style(ButtonStyle::Filled)
628                                .child(Label::new("Filled"))
629                                .into_any_element(),
630                        ),
631                        single_example(
632                            "Subtle",
633                            ButtonLike::new("outline")
634                                .style(ButtonStyle::Subtle)
635                                .child(Label::new("Subtle"))
636                                .into_any_element(),
637                        ),
638                        single_example(
639                            "Tinted",
640                            ButtonLike::new("tinted_accent_style")
641                                .style(ButtonStyle::Tinted(TintColor::Accent))
642                                .child(Label::new("Accent"))
643                                .into_any_element(),
644                        ),
645                        single_example(
646                            "Transparent",
647                            ButtonLike::new("transparent")
648                                .style(ButtonStyle::Transparent)
649                                .child(Label::new("Transparent"))
650                                .into_any_element(),
651                        ),
652                    ]),
653                    example_group_with_title(
654                        "Button Group Constructors",
655                        vec![
656                            single_example(
657                                "Left Rounded",
658                                ButtonLike::new_rounded_left("left_rounded")
659                                    .child(Label::new("Left Rounded"))
660                                    .style(ButtonStyle::Filled)
661                                    .into_any_element(),
662                            ),
663                            single_example(
664                                "Right Rounded",
665                                ButtonLike::new_rounded_right("right_rounded")
666                                    .child(Label::new("Right Rounded"))
667                                    .style(ButtonStyle::Filled)
668                                    .into_any_element(),
669                            ),
670                            single_example(
671                                "Button Group",
672                                h_flex()
673                                    .gap_px()
674                                    .child(
675                                        ButtonLike::new_rounded_left("bg_left")
676                                            .child(Label::new("Left"))
677                                            .style(ButtonStyle::Filled),
678                                    )
679                                    .child(
680                                        ButtonLike::new_rounded_right("bg_right")
681                                            .child(Label::new("Right"))
682                                            .style(ButtonStyle::Filled),
683                                    )
684                                    .into_any_element(),
685                            ),
686                        ],
687                    ),
688                ])
689                .into_any_element(),
690        )
691    }
692}