button_like.rs

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