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