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, Clone)]
 63pub(crate) struct ButtonLikeStyles {
 64    pub background: Hsla,
 65    #[allow(unused)]
 66    pub border_color: Hsla,
 67    #[allow(unused)]
 68    pub label_color: Hsla,
 69    #[allow(unused)]
 70    pub icon_color: Hsla,
 71}
 72
 73impl ButtonStyle {
 74    pub(crate) fn enabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
 75        match self {
 76            ButtonStyle::Filled => ButtonLikeStyles {
 77                background: cx.theme().colors().element_background,
 78                border_color: transparent_black(),
 79                label_color: Color::Default.color(cx),
 80                icon_color: Color::Default.color(cx),
 81            },
 82            ButtonStyle::Tinted => ButtonLikeStyles {
 83                background: gpui::red(),
 84                border_color: gpui::red(),
 85                label_color: gpui::red(),
 86                icon_color: gpui::red(),
 87            },
 88            ButtonStyle::Subtle => ButtonLikeStyles {
 89                background: cx.theme().colors().ghost_element_background,
 90                border_color: transparent_black(),
 91                label_color: Color::Default.color(cx),
 92                icon_color: Color::Default.color(cx),
 93            },
 94            ButtonStyle::Transparent => ButtonLikeStyles {
 95                background: transparent_black(),
 96                border_color: transparent_black(),
 97                label_color: Color::Default.color(cx),
 98                icon_color: Color::Default.color(cx),
 99            },
100        }
101    }
102
103    pub(crate) fn hovered(self, cx: &mut WindowContext) -> ButtonLikeStyles {
104        match self {
105            ButtonStyle::Filled => ButtonLikeStyles {
106                background: cx.theme().colors().element_hover,
107                border_color: transparent_black(),
108                label_color: Color::Default.color(cx),
109                icon_color: Color::Default.color(cx),
110            },
111            ButtonStyle::Tinted => ButtonLikeStyles {
112                background: gpui::red(),
113                border_color: gpui::red(),
114                label_color: gpui::red(),
115                icon_color: gpui::red(),
116            },
117            ButtonStyle::Subtle => ButtonLikeStyles {
118                background: cx.theme().colors().ghost_element_hover,
119                border_color: transparent_black(),
120                label_color: Color::Default.color(cx),
121                icon_color: Color::Default.color(cx),
122            },
123            ButtonStyle::Transparent => ButtonLikeStyles {
124                background: transparent_black(),
125                border_color: transparent_black(),
126                // TODO: These are not great
127                label_color: Color::Muted.color(cx),
128                // TODO: These are not great
129                icon_color: Color::Muted.color(cx),
130            },
131        }
132    }
133
134    pub(crate) fn active(self, cx: &mut WindowContext) -> ButtonLikeStyles {
135        match self {
136            ButtonStyle::Filled => ButtonLikeStyles {
137                background: cx.theme().colors().element_active,
138                border_color: transparent_black(),
139                label_color: Color::Default.color(cx),
140                icon_color: Color::Default.color(cx),
141            },
142            ButtonStyle::Tinted => ButtonLikeStyles {
143                background: gpui::red(),
144                border_color: gpui::red(),
145                label_color: gpui::red(),
146                icon_color: gpui::red(),
147            },
148            ButtonStyle::Subtle => ButtonLikeStyles {
149                background: cx.theme().colors().ghost_element_active,
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                // TODO: These are not great
158                label_color: Color::Muted.color(cx),
159                // TODO: These are not great
160                icon_color: Color::Muted.color(cx),
161            },
162        }
163    }
164
165    #[allow(unused)]
166    pub(crate) fn focused(self, cx: &mut WindowContext) -> ButtonLikeStyles {
167        match self {
168            ButtonStyle::Filled => ButtonLikeStyles {
169                background: cx.theme().colors().element_background,
170                border_color: cx.theme().colors().border_focused,
171                label_color: Color::Default.color(cx),
172                icon_color: Color::Default.color(cx),
173            },
174            ButtonStyle::Tinted => ButtonLikeStyles {
175                background: gpui::red(),
176                border_color: gpui::red(),
177                label_color: gpui::red(),
178                icon_color: gpui::red(),
179            },
180            ButtonStyle::Subtle => ButtonLikeStyles {
181                background: cx.theme().colors().ghost_element_background,
182                border_color: cx.theme().colors().border_focused,
183                label_color: Color::Default.color(cx),
184                icon_color: Color::Default.color(cx),
185            },
186            ButtonStyle::Transparent => ButtonLikeStyles {
187                background: transparent_black(),
188                border_color: cx.theme().colors().border_focused,
189                label_color: Color::Accent.color(cx),
190                icon_color: Color::Accent.color(cx),
191            },
192        }
193    }
194
195    pub(crate) fn disabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
196        match self {
197            ButtonStyle::Filled => ButtonLikeStyles {
198                background: cx.theme().colors().element_disabled,
199                border_color: cx.theme().colors().border_disabled,
200                label_color: Color::Disabled.color(cx),
201                icon_color: Color::Disabled.color(cx),
202            },
203            ButtonStyle::Tinted => ButtonLikeStyles {
204                background: gpui::red(),
205                border_color: gpui::red(),
206                label_color: gpui::red(),
207                icon_color: gpui::red(),
208            },
209            ButtonStyle::Subtle => ButtonLikeStyles {
210                background: cx.theme().colors().ghost_element_disabled,
211                border_color: cx.theme().colors().border_disabled,
212                label_color: Color::Disabled.color(cx),
213                icon_color: Color::Disabled.color(cx),
214            },
215            ButtonStyle::Transparent => ButtonLikeStyles {
216                background: transparent_black(),
217                border_color: transparent_black(),
218                label_color: Color::Disabled.color(cx),
219                icon_color: Color::Disabled.color(cx),
220            },
221        }
222    }
223}
224
225/// ButtonSize can also be used to help build  non-button elements
226/// that are consistently sized with buttons.
227#[derive(Default, PartialEq, Clone, Copy)]
228pub enum ButtonSize {
229    #[default]
230    Default,
231    Compact,
232    None,
233}
234
235impl ButtonSize {
236    fn height(self) -> Rems {
237        match self {
238            ButtonSize::Default => rems(22. / 16.),
239            ButtonSize::Compact => rems(18. / 16.),
240            ButtonSize::None => rems(16. / 16.),
241        }
242    }
243}
244
245/// A button-like element that can be used to create a custom button when
246/// prebuilt buttons are not sufficient. Use this sparingly, as it is
247/// unconstrained and may make the UI feel less consistent.
248///
249/// This is also used to build the prebuilt buttons.
250#[derive(IntoElement)]
251pub struct ButtonLike {
252    base: Div,
253    id: ElementId,
254    pub(super) style: ButtonStyle,
255    pub(super) disabled: bool,
256    pub(super) selected: bool,
257    pub(super) width: Option<DefiniteLength>,
258    size: ButtonSize,
259    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
260    on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
261    children: SmallVec<[AnyElement; 2]>,
262}
263
264impl ButtonLike {
265    pub fn new(id: impl Into<ElementId>) -> Self {
266        Self {
267            base: div(),
268            id: id.into(),
269            style: ButtonStyle::default(),
270            disabled: false,
271            selected: false,
272            width: None,
273            size: ButtonSize::Default,
274            tooltip: None,
275            children: SmallVec::new(),
276            on_click: None,
277        }
278    }
279}
280
281impl Disableable for ButtonLike {
282    fn disabled(mut self, disabled: bool) -> Self {
283        self.disabled = disabled;
284        self
285    }
286}
287
288impl Selectable for ButtonLike {
289    fn selected(mut self, selected: bool) -> Self {
290        self.selected = selected;
291        self
292    }
293}
294
295impl Clickable for ButtonLike {
296    fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
297        self.on_click = Some(Box::new(handler));
298        self
299    }
300}
301
302impl FixedWidth for ButtonLike {
303    fn width(mut self, width: DefiniteLength) -> Self {
304        self.width = Some(width);
305        self
306    }
307
308    fn full_width(mut self) -> Self {
309        self.width = Some(relative(1.));
310        self
311    }
312}
313
314impl ButtonCommon for ButtonLike {
315    fn id(&self) -> &ElementId {
316        &self.id
317    }
318
319    fn style(mut self, style: ButtonStyle) -> Self {
320        self.style = style;
321        self
322    }
323
324    fn size(mut self, size: ButtonSize) -> Self {
325        self.size = size;
326        self
327    }
328
329    fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
330        self.tooltip = Some(Box::new(tooltip));
331        self
332    }
333}
334
335impl VisibleOnHover for ButtonLike {
336    fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
337        self.base = self.base.visible_on_hover(group_name);
338        self
339    }
340}
341
342impl ParentElement for ButtonLike {
343    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
344        &mut self.children
345    }
346}
347
348impl RenderOnce for ButtonLike {
349    type Rendered = Stateful<Div>;
350
351    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
352        self.base
353            .h_flex()
354            .id(self.id.clone())
355            .group("")
356            .flex_none()
357            .h(self.size.height())
358            .when_some(self.width, |this, width| this.w(width).justify_center())
359            .rounded_md()
360            .gap_1()
361            .map(|this| match self.size {
362                ButtonSize::Default | ButtonSize::Compact => this.px_1(),
363                ButtonSize::None => this,
364            })
365            .bg(self.style.enabled(cx).background)
366            .when(self.disabled, |this| this.cursor_not_allowed())
367            .when(!self.disabled, |this| {
368                this.cursor_pointer()
369                    .hover(|hover| hover.bg(self.style.hovered(cx).background))
370                    .active(|active| active.bg(self.style.active(cx).background))
371            })
372            .when_some(
373                self.on_click.filter(|_| !self.disabled),
374                |this, on_click| {
375                    this.on_mouse_down(MouseButton::Left, |_, cx| cx.prevent_default())
376                        .on_click(move |event, cx| {
377                            cx.stop_propagation();
378                            (on_click)(event, cx)
379                        })
380                },
381            )
382            .when_some(self.tooltip, |this, tooltip| {
383                this.tooltip(move |cx| tooltip(cx))
384            })
385            .children(self.children)
386    }
387}