button_like.rs

  1use gpui::{relative, CursorStyle, DefiniteLength, MouseButton};
  2use gpui::{transparent_black, AnyElement, AnyView, ClickEvent, Hsla, Rems};
  3use smallvec::SmallVec;
  4
  5use crate::{prelude::*, DynamicSpacing, Elevation};
  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 elevation(self, elevation: Elevation) -> 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<Elevation>, cx: &mut App) -> Hsla {
156    match elevation {
157        Some(Elevation::Background) => cx.theme().colors().element_background,
158        Some(Elevation::ElevatedSurface) => cx.theme().colors().elevated_surface_background,
159        Some(Elevation::Surface) => cx.theme().colors().surface_background,
160        Some(Elevation::ModalSurface) => cx.theme().colors().background,
161        _ => cx.theme().colors().element_background,
162    }
163}
164
165impl ButtonStyle {
166    pub(crate) fn enabled(self, elevation: Option<Elevation>, cx: &mut App) -> ButtonLikeStyles {
167        match self {
168            ButtonStyle::Filled => ButtonLikeStyles {
169                background: element_bg_from_elevation(elevation, cx),
170                border_color: transparent_black(),
171                label_color: Color::Default.color(cx),
172                icon_color: Color::Default.color(cx),
173            },
174            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
175            ButtonStyle::Subtle => ButtonLikeStyles {
176                background: cx.theme().colors().ghost_element_background,
177                border_color: transparent_black(),
178                label_color: Color::Default.color(cx),
179                icon_color: Color::Default.color(cx),
180            },
181            ButtonStyle::Transparent => ButtonLikeStyles {
182                background: transparent_black(),
183                border_color: transparent_black(),
184                label_color: Color::Default.color(cx),
185                icon_color: Color::Default.color(cx),
186            },
187        }
188    }
189
190    pub(crate) fn hovered(self, elevation: Option<Elevation>, cx: &mut App) -> ButtonLikeStyles {
191        match self {
192            ButtonStyle::Filled => {
193                let mut filled_background = element_bg_from_elevation(elevation, cx);
194                filled_background.fade_out(0.92);
195
196                ButtonLikeStyles {
197                    background: filled_background,
198                    border_color: transparent_black(),
199                    label_color: Color::Default.color(cx),
200                    icon_color: Color::Default.color(cx),
201                }
202            }
203            ButtonStyle::Tinted(tint) => {
204                let mut styles = tint.button_like_style(cx);
205                let theme = cx.theme();
206                styles.background = theme.darken(styles.background, 0.05, 0.2);
207                styles
208            }
209            ButtonStyle::Subtle => ButtonLikeStyles {
210                background: cx.theme().colors().ghost_element_hover,
211                border_color: transparent_black(),
212                label_color: Color::Default.color(cx),
213                icon_color: Color::Default.color(cx),
214            },
215            ButtonStyle::Transparent => ButtonLikeStyles {
216                background: transparent_black(),
217                border_color: transparent_black(),
218                // TODO: These are not great
219                label_color: Color::Muted.color(cx),
220                // TODO: These are not great
221                icon_color: Color::Muted.color(cx),
222            },
223        }
224    }
225
226    pub(crate) fn active(self, cx: &mut App) -> ButtonLikeStyles {
227        match self {
228            ButtonStyle::Filled => ButtonLikeStyles {
229                background: cx.theme().colors().element_active,
230                border_color: transparent_black(),
231                label_color: Color::Default.color(cx),
232                icon_color: Color::Default.color(cx),
233            },
234            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
235            ButtonStyle::Subtle => ButtonLikeStyles {
236                background: cx.theme().colors().ghost_element_active,
237                border_color: transparent_black(),
238                label_color: Color::Default.color(cx),
239                icon_color: Color::Default.color(cx),
240            },
241            ButtonStyle::Transparent => ButtonLikeStyles {
242                background: transparent_black(),
243                border_color: transparent_black(),
244                // TODO: These are not great
245                label_color: Color::Muted.color(cx),
246                // TODO: These are not great
247                icon_color: Color::Muted.color(cx),
248            },
249        }
250    }
251
252    #[allow(unused)]
253    pub(crate) fn focused(self, window: &mut Window, cx: &mut App) -> ButtonLikeStyles {
254        match self {
255            ButtonStyle::Filled => ButtonLikeStyles {
256                background: cx.theme().colors().element_background,
257                border_color: cx.theme().colors().border_focused,
258                label_color: Color::Default.color(cx),
259                icon_color: Color::Default.color(cx),
260            },
261            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
262            ButtonStyle::Subtle => ButtonLikeStyles {
263                background: cx.theme().colors().ghost_element_background,
264                border_color: cx.theme().colors().border_focused,
265                label_color: Color::Default.color(cx),
266                icon_color: Color::Default.color(cx),
267            },
268            ButtonStyle::Transparent => ButtonLikeStyles {
269                background: transparent_black(),
270                border_color: cx.theme().colors().border_focused,
271                label_color: Color::Accent.color(cx),
272                icon_color: Color::Accent.color(cx),
273            },
274        }
275    }
276
277    #[allow(unused)]
278    pub(crate) fn disabled(
279        self,
280        elevation: Option<Elevation>,
281        window: &mut Window,
282        cx: &mut App,
283    ) -> ButtonLikeStyles {
284        match self {
285            ButtonStyle::Filled => ButtonLikeStyles {
286                background: cx.theme().colors().element_disabled,
287                border_color: cx.theme().colors().border_disabled,
288                label_color: Color::Disabled.color(cx),
289                icon_color: Color::Disabled.color(cx),
290            },
291            ButtonStyle::Tinted(tint) => tint.button_like_style(cx),
292            ButtonStyle::Subtle => ButtonLikeStyles {
293                background: cx.theme().colors().ghost_element_disabled,
294                border_color: cx.theme().colors().border_disabled,
295                label_color: Color::Disabled.color(cx),
296                icon_color: Color::Disabled.color(cx),
297            },
298            ButtonStyle::Transparent => ButtonLikeStyles {
299                background: transparent_black(),
300                border_color: transparent_black(),
301                label_color: Color::Disabled.color(cx),
302                icon_color: Color::Disabled.color(cx),
303            },
304        }
305    }
306}
307
308/// The height of a button.
309///
310/// Can also be used to size non-button elements to align with [`Button`]s.
311#[derive(Default, PartialEq, Clone, Copy)]
312pub enum ButtonSize {
313    Large,
314    #[default]
315    Default,
316    Compact,
317    None,
318}
319
320impl ButtonSize {
321    pub fn rems(self) -> Rems {
322        match self {
323            ButtonSize::Large => rems_from_px(32.),
324            ButtonSize::Default => rems_from_px(22.),
325            ButtonSize::Compact => rems_from_px(18.),
326            ButtonSize::None => rems_from_px(16.),
327        }
328    }
329}
330
331/// A button-like element that can be used to create a custom button when
332/// prebuilt buttons are not sufficient. Use this sparingly, as it is
333/// unconstrained and may make the UI feel less consistent.
334///
335/// This is also used to build the prebuilt buttons.
336#[derive(IntoElement)]
337pub struct ButtonLike {
338    pub(super) base: Div,
339    id: ElementId,
340    pub(super) style: ButtonStyle,
341    pub(super) disabled: bool,
342    pub(super) selected: bool,
343    pub(super) selected_style: Option<ButtonStyle>,
344    pub(super) width: Option<DefiniteLength>,
345    pub(super) height: Option<DefiniteLength>,
346    pub(super) layer: Option<Elevation>,
347    size: ButtonSize,
348    rounding: Option<ButtonLikeRounding>,
349    tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView>>,
350    cursor_style: CursorStyle,
351    on_click: Option<Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>>,
352    children: SmallVec<[AnyElement; 2]>,
353}
354
355impl ButtonLike {
356    pub fn new(id: impl Into<ElementId>) -> Self {
357        Self {
358            base: div(),
359            id: id.into(),
360            style: ButtonStyle::default(),
361            disabled: false,
362            selected: false,
363            selected_style: None,
364            width: None,
365            height: None,
366            size: ButtonSize::Default,
367            rounding: Some(ButtonLikeRounding::All),
368            tooltip: None,
369            children: SmallVec::new(),
370            cursor_style: CursorStyle::PointingHand,
371            on_click: None,
372            layer: None,
373        }
374    }
375
376    pub fn new_rounded_left(id: impl Into<ElementId>) -> Self {
377        Self::new(id).rounding(ButtonLikeRounding::Left)
378    }
379
380    pub fn new_rounded_right(id: impl Into<ElementId>) -> Self {
381        Self::new(id).rounding(ButtonLikeRounding::Right)
382    }
383
384    pub fn opacity(mut self, opacity: f32) -> Self {
385        self.base = self.base.opacity(opacity);
386        self
387    }
388
389    pub(crate) fn height(mut self, height: DefiniteLength) -> Self {
390        self.height = Some(height);
391        self
392    }
393
394    pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
395        self.rounding = rounding.into();
396        self
397    }
398}
399
400impl Disableable for ButtonLike {
401    fn disabled(mut self, disabled: bool) -> Self {
402        self.disabled = disabled;
403        self
404    }
405}
406
407impl Toggleable for ButtonLike {
408    fn toggle_state(mut self, selected: bool) -> Self {
409        self.selected = selected;
410        self
411    }
412}
413
414impl SelectableButton for ButtonLike {
415    fn selected_style(mut self, style: ButtonStyle) -> Self {
416        self.selected_style = Some(style);
417        self
418    }
419}
420
421impl Clickable for ButtonLike {
422    fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static) -> Self {
423        self.on_click = Some(Box::new(handler));
424        self
425    }
426
427    fn cursor_style(mut self, cursor_style: CursorStyle) -> Self {
428        self.cursor_style = cursor_style;
429        self
430    }
431}
432
433impl FixedWidth for ButtonLike {
434    fn width(mut self, width: DefiniteLength) -> Self {
435        self.width = Some(width);
436        self
437    }
438
439    fn full_width(mut self) -> Self {
440        self.width = Some(relative(1.));
441        self
442    }
443}
444
445impl ButtonCommon for ButtonLike {
446    fn id(&self) -> &ElementId {
447        &self.id
448    }
449
450    fn style(mut self, style: ButtonStyle) -> Self {
451        self.style = style;
452        self
453    }
454
455    fn size(mut self, size: ButtonSize) -> Self {
456        self.size = size;
457        self
458    }
459
460    fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
461        self.tooltip = Some(Box::new(tooltip));
462        self
463    }
464
465    fn elevation(mut self, elevation: Elevation) -> Self {
466        self.layer = Some(elevation);
467        self
468    }
469}
470
471impl VisibleOnHover for ButtonLike {
472    fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
473        self.base = self.base.visible_on_hover(group_name);
474        self
475    }
476}
477
478impl ParentElement for ButtonLike {
479    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
480        self.children.extend(elements)
481    }
482}
483
484impl RenderOnce for ButtonLike {
485    fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
486        let style = self
487            .selected_style
488            .filter(|_| self.selected)
489            .unwrap_or(self.style);
490
491        self.base
492            .h_flex()
493            .id(self.id.clone())
494            .font_ui(cx)
495            .group("")
496            .flex_none()
497            .h(self.height.unwrap_or(self.size.rems().into()))
498            .when_some(self.width, |this, width| {
499                this.w(width).justify_center().text_center()
500            })
501            .when_some(self.rounding, |this, rounding| match rounding {
502                ButtonLikeRounding::All => this.rounded_sm(),
503                ButtonLikeRounding::Left => this.rounded_l_sm(),
504                ButtonLikeRounding::Right => this.rounded_r_sm(),
505            })
506            .gap(DynamicSpacing::Base04.rems(cx))
507            .map(|this| match self.size {
508                ButtonSize::Large => this.px(DynamicSpacing::Base06.rems(cx)),
509                ButtonSize::Default | ButtonSize::Compact => {
510                    this.px(DynamicSpacing::Base04.rems(cx))
511                }
512                ButtonSize::None => this,
513            })
514            .bg(style.enabled(self.layer, cx).background)
515            .when(self.disabled, |this| this.cursor_not_allowed())
516            .when(!self.disabled, |this| {
517                this.cursor_pointer()
518                    .hover(|hover| hover.bg(style.hovered(self.layer, cx).background))
519                    .active(|active| active.bg(style.active(cx).background))
520            })
521            .when_some(
522                self.on_click.filter(|_| !self.disabled),
523                |this, on_click| {
524                    this.on_mouse_down(MouseButton::Left, |_, window, _| window.prevent_default())
525                        .on_click(move |event, window, cx| {
526                            cx.stop_propagation();
527                            (on_click)(event, window, cx)
528                        })
529                },
530            )
531            .when_some(self.tooltip, |this, tooltip| {
532                this.tooltip(move |window, cx| tooltip(window, cx))
533            })
534            .children(self.children)
535    }
536}