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