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