button_like.rs

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