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 new_rounded_all(id: impl Into<ElementId>) -> Self {
400        Self::new(id).rounding(ButtonLikeRounding::All)
401    }
402
403    pub fn opacity(mut self, opacity: f32) -> Self {
404        self.base = self.base.opacity(opacity);
405        self
406    }
407
408    pub fn height(mut self, height: DefiniteLength) -> Self {
409        self.height = Some(height);
410        self
411    }
412
413    pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
414        self.rounding = rounding.into();
415        self
416    }
417
418    pub fn on_right_click(
419        mut self,
420        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
421    ) -> Self {
422        self.on_right_click = Some(Box::new(handler));
423        self
424    }
425}
426
427impl Disableable for ButtonLike {
428    fn disabled(mut self, disabled: bool) -> Self {
429        self.disabled = disabled;
430        self
431    }
432}
433
434impl Toggleable for ButtonLike {
435    fn toggle_state(mut self, selected: bool) -> Self {
436        self.selected = selected;
437        self
438    }
439}
440
441impl SelectableButton for ButtonLike {
442    fn selected_style(mut self, style: ButtonStyle) -> Self {
443        self.selected_style = Some(style);
444        self
445    }
446}
447
448impl Clickable for ButtonLike {
449    fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
450        self.on_click = Some(Box::new(handler));
451        self
452    }
453
454    fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
455        self.cursor_style = cursor_style;
456        self
457    }
458}
459
460impl FixedWidth for ButtonLike {
461    fn width(mut self, width: DefiniteLength) -> Self {
462        self.width = Some(width);
463        self
464    }
465
466    fn full_width(mut self) -> Self {
467        self.width = Some(relative(1.));
468        self
469    }
470}
471
472impl ButtonCommon for ButtonLike {
473    fn id(&self) -> &ElementId {
474        &self.id
475    }
476
477    fn style(mut self, style: ButtonStyle) -> Self {
478        self.style = style;
479        self
480    }
481
482    fn size(mut self, size: ButtonSize) -> Self {
483        self.size = size;
484        self
485    }
486
487    fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
488        self.tooltip = Some(Box::new(tooltip));
489        self
490    }
491
492    fn layer(mut self, elevation: ElevationIndex) -> Self {
493        self.layer = Some(elevation);
494        self
495    }
496}
497
498impl VisibleOnHover for ButtonLike {
499    fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
500        self.base = self.base.visible_on_hover(group_name);
501        self
502    }
503}
504
505impl ParentElement for ButtonLike {
506    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
507        self.children.extend(elements)
508    }
509}
510
511impl RenderOnce for ButtonLike {
512    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
513        let style = self
514            .selected_style
515            .filter(|_| self.selected)
516            .unwrap_or(self.style);
517
518        self.base
519            .h_flex()
520            .id(self.id.clone())
521            .font_ui(cx)
522            .group("")
523            .flex_none()
524            .h(self.height.unwrap_or(self.size.rems().into()))
525            .when_some(self.width, |this, width| {
526                this.w(width).justify_center().text_center()
527            })
528            .when_some(self.rounding, |this, rounding| match rounding {
529                ButtonLikeRounding::All => this.rounded_sm(),
530                ButtonLikeRounding::Left => this.rounded_l_sm(),
531                ButtonLikeRounding::Right => this.rounded_r_sm(),
532            })
533            .gap(DynamicSpacing::Base04.rems(cx))
534            .map(|this| match self.size {
535                ButtonSize::Large => this.px(DynamicSpacing::Base06.rems(cx)),
536                ButtonSize::Default | ButtonSize::Compact => {
537                    this.px(DynamicSpacing::Base04.rems(cx))
538                }
539                ButtonSize::None => this,
540            })
541            .bg(style.enabled(self.layer, cx).background)
542            .when(self.disabled, |this| {
543                if self.cursor_style == CursorStyle::PointingHand {
544                    this.cursor_not_allowed()
545                } else {
546                    this.cursor(self.cursor_style)
547                }
548            })
549            .when(!self.disabled, |this| {
550                this.cursor(self.cursor_style)
551                    .hover(|hover| hover.bg(style.hovered(self.layer, cx).background))
552                    .active(|active| active.bg(style.active(cx).background))
553            })
554            .when_some(
555                self.on_right_click.filter(|_| !self.disabled),
556                |this, on_right_click| {
557                    this.on_mouse_down(MouseButton::Right, |_event, window, cx| {
558                        window.prevent_default();
559                        cx.stop_propagation();
560                    })
561                    .on_mouse_up(
562                        MouseButton::Right,
563                        move |event, window, cx| {
564                            cx.stop_propagation();
565                            let click_event = ClickEvent {
566                                down: MouseDownEvent {
567                                    button: MouseButton::Right,
568                                    position: event.position,
569                                    modifiers: event.modifiers,
570                                    click_count: 1,
571                                    first_mouse: false,
572                                },
573                                up: MouseUpEvent {
574                                    button: MouseButton::Right,
575                                    position: event.position,
576                                    modifiers: event.modifiers,
577                                    click_count: 1,
578                                },
579                            };
580                            (on_right_click)(&click_event, window, cx)
581                        },
582                    )
583                },
584            )
585            .when_some(
586                self.on_click.filter(|_| !self.disabled),
587                |this, on_click| {
588                    this.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
589                        .on_click(move |event, window, cx| {
590                            cx.stop_propagation();
591                            (on_click)(event, window, cx)
592                        })
593                },
594            )
595            .when_some(self.tooltip, |this, tooltip| {
596                this.tooltip(move |window, cx| tooltip(window, cx))
597            })
598            .children(self.children)
599    }
600}
601
602impl Component for ButtonLike {
603    fn scope() -> ComponentScope {
604        ComponentScope::Input
605    }
606
607    fn sort_name() -> &'static str {
608        // ButtonLike should be at the bottom of the button list
609        "ButtonZ"
610    }
611
612    fn description() -> Option<&'static str> {
613        Some(ButtonLike::DOCS)
614    }
615
616    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
617        Some(
618            v_flex()
619                .gap_6()
620                .children(vec![
621                    example_group(vec![
622                        single_example(
623                            "Default",
624                            ButtonLike::new("default")
625                                .child(Label::new("Default"))
626                                .into_any_element(),
627                        ),
628                        single_example(
629                            "Filled",
630                            ButtonLike::new("filled")
631                                .style(ButtonStyle::Filled)
632                                .child(Label::new("Filled"))
633                                .into_any_element(),
634                        ),
635                        single_example(
636                            "Subtle",
637                            ButtonLike::new("outline")
638                                .style(ButtonStyle::Subtle)
639                                .child(Label::new("Subtle"))
640                                .into_any_element(),
641                        ),
642                        single_example(
643                            "Tinted",
644                            ButtonLike::new("tinted_accent_style")
645                                .style(ButtonStyle::Tinted(TintColor::Accent))
646                                .child(Label::new("Accent"))
647                                .into_any_element(),
648                        ),
649                        single_example(
650                            "Transparent",
651                            ButtonLike::new("transparent")
652                                .style(ButtonStyle::Transparent)
653                                .child(Label::new("Transparent"))
654                                .into_any_element(),
655                        ),
656                    ]),
657                    example_group_with_title(
658                        "Button Group Constructors",
659                        vec![
660                            single_example(
661                                "Left Rounded",
662                                ButtonLike::new_rounded_left("left_rounded")
663                                    .child(Label::new("Left Rounded"))
664                                    .style(ButtonStyle::Filled)
665                                    .into_any_element(),
666                            ),
667                            single_example(
668                                "Right Rounded",
669                                ButtonLike::new_rounded_right("right_rounded")
670                                    .child(Label::new("Right Rounded"))
671                                    .style(ButtonStyle::Filled)
672                                    .into_any_element(),
673                            ),
674                            single_example(
675                                "Button Group",
676                                h_flex()
677                                    .gap_px()
678                                    .child(
679                                        ButtonLike::new_rounded_left("bg_left")
680                                            .child(Label::new("Left"))
681                                            .style(ButtonStyle::Filled),
682                                    )
683                                    .child(
684                                        ButtonLike::new_rounded_right("bg_right")
685                                            .child(Label::new("Right"))
686                                            .style(ButtonStyle::Filled),
687                                    )
688                                    .into_any_element(),
689                            ),
690                        ],
691                    ),
692                ])
693                .into_any_element(),
694        )
695    }
696}