button_like.rs

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