button_like.rs

  1use gpui::{rems, transparent_black, AnyElement, AnyView, ClickEvent, Div, Hsla, Rems, Stateful};
  2use smallvec::SmallVec;
  3
  4use crate::h_stack;
  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    /// Mosty 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 ButtonStyle {
 34    /// A filled button with a solid background color. Provides emphasis versus
 35    /// the more common subtle button.
 36    Filled,
 37
 38    /// 🚧 Under construction 🚧
 39    ///
 40    /// Used to emphasize a button in some way, like a selected state, or a semantic
 41    /// coloring like an error or success button.
 42    Tinted,
 43
 44    /// The default button style, used for most buttons. Has a transparent background,
 45    /// but has a background color to indicate states like hover and active.
 46    #[default]
 47    Subtle,
 48
 49    /// Used for buttons that only change forground color on hover and active states.
 50    ///
 51    /// TODO: Better docs for this.
 52    Transparent,
 53}
 54
 55#[derive(Debug, Clone)]
 56pub(crate) struct ButtonLikeStyles {
 57    pub background: Hsla,
 58    #[allow(unused)]
 59    pub border_color: Hsla,
 60    #[allow(unused)]
 61    pub label_color: Hsla,
 62    #[allow(unused)]
 63    pub icon_color: Hsla,
 64}
 65
 66impl ButtonStyle {
 67    pub(crate) fn enabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
 68        match self {
 69            ButtonStyle::Filled => ButtonLikeStyles {
 70                background: cx.theme().colors().element_background,
 71                border_color: transparent_black(),
 72                label_color: Color::Default.color(cx),
 73                icon_color: Color::Default.color(cx),
 74            },
 75            ButtonStyle::Tinted => ButtonLikeStyles {
 76                background: gpui::red(),
 77                border_color: gpui::red(),
 78                label_color: gpui::red(),
 79                icon_color: gpui::red(),
 80            },
 81            ButtonStyle::Subtle => ButtonLikeStyles {
 82                background: cx.theme().colors().ghost_element_background,
 83                border_color: transparent_black(),
 84                label_color: Color::Default.color(cx),
 85                icon_color: Color::Default.color(cx),
 86            },
 87            ButtonStyle::Transparent => ButtonLikeStyles {
 88                background: transparent_black(),
 89                border_color: transparent_black(),
 90                label_color: Color::Default.color(cx),
 91                icon_color: Color::Default.color(cx),
 92            },
 93        }
 94    }
 95
 96    pub(crate) fn hovered(self, cx: &mut WindowContext) -> ButtonLikeStyles {
 97        match self {
 98            ButtonStyle::Filled => ButtonLikeStyles {
 99                background: cx.theme().colors().element_hover,
100                border_color: transparent_black(),
101                label_color: Color::Default.color(cx),
102                icon_color: Color::Default.color(cx),
103            },
104            ButtonStyle::Tinted => ButtonLikeStyles {
105                background: gpui::red(),
106                border_color: gpui::red(),
107                label_color: gpui::red(),
108                icon_color: gpui::red(),
109            },
110            ButtonStyle::Subtle => ButtonLikeStyles {
111                background: cx.theme().colors().ghost_element_hover,
112                border_color: transparent_black(),
113                label_color: Color::Default.color(cx),
114                icon_color: Color::Default.color(cx),
115            },
116            ButtonStyle::Transparent => ButtonLikeStyles {
117                background: transparent_black(),
118                border_color: transparent_black(),
119                // TODO: These are not great
120                label_color: Color::Muted.color(cx),
121                // TODO: These are not great
122                icon_color: Color::Muted.color(cx),
123            },
124        }
125    }
126
127    pub(crate) fn active(self, cx: &mut WindowContext) -> ButtonLikeStyles {
128        match self {
129            ButtonStyle::Filled => ButtonLikeStyles {
130                background: cx.theme().colors().element_active,
131                border_color: transparent_black(),
132                label_color: Color::Default.color(cx),
133                icon_color: Color::Default.color(cx),
134            },
135            ButtonStyle::Tinted => ButtonLikeStyles {
136                background: gpui::red(),
137                border_color: gpui::red(),
138                label_color: gpui::red(),
139                icon_color: gpui::red(),
140            },
141            ButtonStyle::Subtle => ButtonLikeStyles {
142                background: cx.theme().colors().ghost_element_active,
143                border_color: transparent_black(),
144                label_color: Color::Default.color(cx),
145                icon_color: Color::Default.color(cx),
146            },
147            ButtonStyle::Transparent => ButtonLikeStyles {
148                background: transparent_black(),
149                border_color: transparent_black(),
150                // TODO: These are not great
151                label_color: Color::Muted.color(cx),
152                // TODO: These are not great
153                icon_color: Color::Muted.color(cx),
154            },
155        }
156    }
157
158    #[allow(unused)]
159    pub(crate) fn focused(self, cx: &mut WindowContext) -> ButtonLikeStyles {
160        match self {
161            ButtonStyle::Filled => ButtonLikeStyles {
162                background: cx.theme().colors().element_background,
163                border_color: cx.theme().colors().border_focused,
164                label_color: Color::Default.color(cx),
165                icon_color: Color::Default.color(cx),
166            },
167            ButtonStyle::Tinted => ButtonLikeStyles {
168                background: gpui::red(),
169                border_color: gpui::red(),
170                label_color: gpui::red(),
171                icon_color: gpui::red(),
172            },
173            ButtonStyle::Subtle => ButtonLikeStyles {
174                background: cx.theme().colors().ghost_element_background,
175                border_color: cx.theme().colors().border_focused,
176                label_color: Color::Default.color(cx),
177                icon_color: Color::Default.color(cx),
178            },
179            ButtonStyle::Transparent => ButtonLikeStyles {
180                background: transparent_black(),
181                border_color: cx.theme().colors().border_focused,
182                label_color: Color::Accent.color(cx),
183                icon_color: Color::Accent.color(cx),
184            },
185        }
186    }
187
188    pub(crate) fn disabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
189        match self {
190            ButtonStyle::Filled => ButtonLikeStyles {
191                background: cx.theme().colors().element_disabled,
192                border_color: cx.theme().colors().border_disabled,
193                label_color: Color::Disabled.color(cx),
194                icon_color: Color::Disabled.color(cx),
195            },
196            ButtonStyle::Tinted => ButtonLikeStyles {
197                background: gpui::red(),
198                border_color: gpui::red(),
199                label_color: gpui::red(),
200                icon_color: gpui::red(),
201            },
202            ButtonStyle::Subtle => ButtonLikeStyles {
203                background: cx.theme().colors().ghost_element_disabled,
204                border_color: cx.theme().colors().border_disabled,
205                label_color: Color::Disabled.color(cx),
206                icon_color: Color::Disabled.color(cx),
207            },
208            ButtonStyle::Transparent => ButtonLikeStyles {
209                background: transparent_black(),
210                border_color: transparent_black(),
211                label_color: Color::Disabled.color(cx),
212                icon_color: Color::Disabled.color(cx),
213            },
214        }
215    }
216}
217
218/// ButtonSize can also be used to help build  non-button elements
219/// that are consistently sized with buttons.
220#[derive(Default, PartialEq, Clone, Copy)]
221pub enum ButtonSize {
222    #[default]
223    Default,
224    Compact,
225    None,
226}
227
228impl ButtonSize {
229    fn height(self) -> Rems {
230        match self {
231            ButtonSize::Default => rems(22. / 16.),
232            ButtonSize::Compact => rems(18. / 16.),
233            ButtonSize::None => rems(16. / 16.),
234        }
235    }
236}
237
238/// A button-like element that can be used to create a custom button when
239/// prebuilt buttons are not sufficient. Use this sparingly, as it is
240/// unconstrained and may make the UI feel less consistent.
241///
242/// This is also used to build the prebuilt buttons.
243#[derive(IntoElement)]
244pub struct ButtonLike {
245    id: ElementId,
246    pub(super) style: ButtonStyle,
247    pub(super) disabled: bool,
248    pub(super) selected: bool,
249    size: ButtonSize,
250    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
251    on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
252    children: SmallVec<[AnyElement; 2]>,
253}
254
255impl ButtonLike {
256    pub fn new(id: impl Into<ElementId>) -> Self {
257        Self {
258            id: id.into(),
259            style: ButtonStyle::default(),
260            disabled: false,
261            selected: false,
262            size: ButtonSize::Default,
263            tooltip: None,
264            children: SmallVec::new(),
265            on_click: None,
266        }
267    }
268}
269
270impl Disableable for ButtonLike {
271    fn disabled(mut self, disabled: bool) -> Self {
272        self.disabled = disabled;
273        self
274    }
275}
276
277impl Selectable for ButtonLike {
278    fn selected(mut self, selected: bool) -> Self {
279        self.selected = selected;
280        self
281    }
282}
283
284impl Clickable for ButtonLike {
285    fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
286        self.on_click = Some(Box::new(handler));
287        self
288    }
289}
290
291impl ButtonCommon for ButtonLike {
292    fn id(&self) -> &ElementId {
293        &self.id
294    }
295
296    fn style(mut self, style: ButtonStyle) -> Self {
297        self.style = style;
298        self
299    }
300
301    fn size(mut self, size: ButtonSize) -> Self {
302        self.size = size;
303        self
304    }
305
306    fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
307        self.tooltip = Some(Box::new(tooltip));
308        self
309    }
310}
311
312impl ParentElement for ButtonLike {
313    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
314        &mut self.children
315    }
316}
317
318impl RenderOnce for ButtonLike {
319    type Rendered = Stateful<Div>;
320
321    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
322        h_stack()
323            .id(self.id.clone())
324            .h(self.size.height())
325            .rounded_md()
326            .when(!self.disabled, |el| el.cursor_pointer())
327            .gap_1()
328            .px_1()
329            .bg(self.style.enabled(cx).background)
330            .hover(|hover| {
331                hover.bg(if self.disabled {
332                    self.style.disabled(cx).background
333                } else {
334                    self.style.hovered(cx).background
335                })
336            })
337            .active(|active| {
338                active.bg(if self.disabled {
339                    self.style.disabled(cx).background
340                } else {
341                    self.style.active(cx).background
342                })
343            })
344            .when_some(
345                self.on_click.filter(|_| !self.disabled),
346                |this, on_click| {
347                    this.on_click(move |event, cx| {
348                        cx.stop_propagation();
349                        (on_click)(event, cx)
350                    })
351                },
352            )
353            .when_some(self.tooltip, |this, tooltip| {
354                if !self.selected {
355                    this.tooltip(move |cx| tooltip(cx))
356                } else {
357                    this
358                }
359            })
360            .children(self.children)
361    }
362}