button_like.rs

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