button_like.rs

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