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| this.cursor_not_allowed())
539            .when(!self.disabled, |this| {
540                this.cursor_pointer()
541                    .hover(|hover| hover.bg(style.hovered(self.layer, cx).background))
542                    .active(|active| active.bg(style.active(cx).background))
543            })
544            .when_some(
545                self.on_right_click.filter(|_| !self.disabled),
546                |this, on_right_click| {
547                    this.on_mouse_down(MouseButton::Right, |_event, window, cx| {
548                        window.prevent_default();
549                        cx.stop_propagation();
550                    })
551                    .on_mouse_up(
552                        MouseButton::Right,
553                        move |event, window, cx| {
554                            cx.stop_propagation();
555                            let click_event = ClickEvent {
556                                down: MouseDownEvent {
557                                    button: MouseButton::Right,
558                                    position: event.position,
559                                    modifiers: event.modifiers,
560                                    click_count: 1,
561                                    first_mouse: false,
562                                },
563                                up: MouseUpEvent {
564                                    button: MouseButton::Right,
565                                    position: event.position,
566                                    modifiers: event.modifiers,
567                                    click_count: 1,
568                                },
569                            };
570                            (on_right_click)(&click_event, window, cx)
571                        },
572                    )
573                },
574            )
575            .when_some(
576                self.on_click.filter(|_| !self.disabled),
577                |this, on_click| {
578                    this.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
579                        .on_click(move |event, window, cx| {
580                            cx.stop_propagation();
581                            (on_click)(event, window, cx)
582                        })
583                },
584            )
585            .when_some(self.tooltip, |this, tooltip| {
586                this.tooltip(move |window, cx| tooltip(window, cx))
587            })
588            .children(self.children)
589    }
590}
591
592impl Component for ButtonLike {
593    fn scope() -> ComponentScope {
594        ComponentScope::Input
595    }
596
597    fn sort_name() -> &'static str {
598        // ButtonLike should be at the bottom of the button list
599        "ButtonZ"
600    }
601
602    fn description() -> Option<&'static str> {
603        Some(ButtonLike::DOCS)
604    }
605
606    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
607        Some(
608            v_flex()
609                .gap_6()
610                .children(vec![
611                    example_group(vec![
612                        single_example(
613                            "Default",
614                            ButtonLike::new("default")
615                                .child(Label::new("Default"))
616                                .into_any_element(),
617                        ),
618                        single_example(
619                            "Filled",
620                            ButtonLike::new("filled")
621                                .style(ButtonStyle::Filled)
622                                .child(Label::new("Filled"))
623                                .into_any_element(),
624                        ),
625                        single_example(
626                            "Subtle",
627                            ButtonLike::new("outline")
628                                .style(ButtonStyle::Subtle)
629                                .child(Label::new("Subtle"))
630                                .into_any_element(),
631                        ),
632                        single_example(
633                            "Tinted",
634                            ButtonLike::new("tinted_accent_style")
635                                .style(ButtonStyle::Tinted(TintColor::Accent))
636                                .child(Label::new("Accent"))
637                                .into_any_element(),
638                        ),
639                        single_example(
640                            "Transparent",
641                            ButtonLike::new("transparent")
642                                .style(ButtonStyle::Transparent)
643                                .child(Label::new("Transparent"))
644                                .into_any_element(),
645                        ),
646                    ]),
647                    example_group_with_title(
648                        "Button Group Constructors",
649                        vec![
650                            single_example(
651                                "Left Rounded",
652                                ButtonLike::new_rounded_left("left_rounded")
653                                    .child(Label::new("Left Rounded"))
654                                    .style(ButtonStyle::Filled)
655                                    .into_any_element(),
656                            ),
657                            single_example(
658                                "Right Rounded",
659                                ButtonLike::new_rounded_right("right_rounded")
660                                    .child(Label::new("Right Rounded"))
661                                    .style(ButtonStyle::Filled)
662                                    .into_any_element(),
663                            ),
664                            single_example(
665                                "Button Group",
666                                h_flex()
667                                    .gap_px()
668                                    .child(
669                                        ButtonLike::new_rounded_left("bg_left")
670                                            .child(Label::new("Left"))
671                                            .style(ButtonStyle::Filled),
672                                    )
673                                    .child(
674                                        ButtonLike::new_rounded_right("bg_right")
675                                            .child(Label::new("Right"))
676                                            .style(ButtonStyle::Filled),
677                                    )
678                                    .into_any_element(),
679                            ),
680                        ],
681                    ),
682                ])
683                .into_any_element(),
684        )
685    }
686}