button_like.rs

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