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