button_like.rs

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