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