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, 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    pub(crate) fn disabled(self, cx: &mut WindowContext) -> ButtonLikeStyles {
203        match self {
204            ButtonStyle::Filled => ButtonLikeStyles {
205                background: cx.theme().colors().element_disabled,
206                border_color: cx.theme().colors().border_disabled,
207                label_color: Color::Disabled.color(cx),
208                icon_color: Color::Disabled.color(cx),
209            },
210            ButtonStyle::Tinted => ButtonLikeStyles {
211                background: gpui::red(),
212                border_color: gpui::red(),
213                label_color: gpui::red(),
214                icon_color: gpui::red(),
215            },
216            ButtonStyle::Subtle => ButtonLikeStyles {
217                background: cx.theme().colors().ghost_element_disabled,
218                border_color: cx.theme().colors().border_disabled,
219                label_color: Color::Disabled.color(cx),
220                icon_color: Color::Disabled.color(cx),
221            },
222            ButtonStyle::Transparent => ButtonLikeStyles {
223                background: transparent_black(),
224                border_color: transparent_black(),
225                label_color: Color::Disabled.color(cx),
226                icon_color: Color::Disabled.color(cx),
227            },
228        }
229    }
230}
231
232/// ButtonSize can also be used to help build  non-button elements
233/// that are consistently sized with buttons.
234#[derive(Default, PartialEq, Clone, Copy)]
235pub enum ButtonSize {
236    Large,
237    #[default]
238    Default,
239    Compact,
240    None,
241}
242
243impl ButtonSize {
244    fn height(self) -> Rems {
245        match self {
246            ButtonSize::Large => rems(32. / 16.),
247            ButtonSize::Default => rems(22. / 16.),
248            ButtonSize::Compact => rems(18. / 16.),
249            ButtonSize::None => rems(16. / 16.),
250        }
251    }
252}
253
254/// A button-like element that can be used to create a custom button when
255/// prebuilt buttons are not sufficient. Use this sparingly, as it is
256/// unconstrained and may make the UI feel less consistent.
257///
258/// This is also used to build the prebuilt buttons.
259#[derive(IntoElement)]
260pub struct ButtonLike {
261    base: Div,
262    id: ElementId,
263    pub(super) style: ButtonStyle,
264    pub(super) disabled: bool,
265    pub(super) selected: bool,
266    pub(super) width: Option<DefiniteLength>,
267    size: ButtonSize,
268    rounding: Option<ButtonLikeRounding>,
269    tooltip: Option<Box<dyn Fn(&mut WindowContext) -> AnyView>>,
270    on_click: Option<Box<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
271    children: SmallVec<[AnyElement; 2]>,
272}
273
274impl ButtonLike {
275    pub fn new(id: impl Into<ElementId>) -> Self {
276        Self {
277            base: div(),
278            id: id.into(),
279            style: ButtonStyle::default(),
280            disabled: false,
281            selected: false,
282            width: None,
283            size: ButtonSize::Default,
284            rounding: Some(ButtonLikeRounding::All),
285            tooltip: None,
286            children: SmallVec::new(),
287            on_click: None,
288        }
289    }
290
291    pub(crate) fn rounding(mut self, rounding: impl Into<Option<ButtonLikeRounding>>) -> Self {
292        self.rounding = rounding.into();
293        self
294    }
295}
296
297impl Disableable for ButtonLike {
298    fn disabled(mut self, disabled: bool) -> Self {
299        self.disabled = disabled;
300        self
301    }
302}
303
304impl Selectable for ButtonLike {
305    fn selected(mut self, selected: bool) -> Self {
306        self.selected = selected;
307        self
308    }
309}
310
311impl Clickable for ButtonLike {
312    fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
313        self.on_click = Some(Box::new(handler));
314        self
315    }
316}
317
318impl FixedWidth for ButtonLike {
319    fn width(mut self, width: DefiniteLength) -> Self {
320        self.width = Some(width);
321        self
322    }
323
324    fn full_width(mut self) -> Self {
325        self.width = Some(relative(1.));
326        self
327    }
328}
329
330impl ButtonCommon for ButtonLike {
331    fn id(&self) -> &ElementId {
332        &self.id
333    }
334
335    fn style(mut self, style: ButtonStyle) -> Self {
336        self.style = style;
337        self
338    }
339
340    fn size(mut self, size: ButtonSize) -> Self {
341        self.size = size;
342        self
343    }
344
345    fn tooltip(mut self, tooltip: impl Fn(&mut WindowContext) -> AnyView + 'static) -> Self {
346        self.tooltip = Some(Box::new(tooltip));
347        self
348    }
349}
350
351impl VisibleOnHover for ButtonLike {
352    fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
353        self.base = self.base.visible_on_hover(group_name);
354        self
355    }
356}
357
358impl ParentElement for ButtonLike {
359    fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
360        &mut self.children
361    }
362}
363
364impl RenderOnce for ButtonLike {
365    type Rendered = Stateful<Div>;
366
367    fn render(self, cx: &mut WindowContext) -> Self::Rendered {
368        self.base
369            .h_flex()
370            .id(self.id.clone())
371            .group("")
372            .flex_none()
373            .h(self.size.height())
374            .when_some(self.width, |this, width| this.w(width).justify_center())
375            .when_some(self.rounding, |this, rounding| match rounding {
376                ButtonLikeRounding::All => this.rounded_md(),
377                ButtonLikeRounding::Left => this.rounded_l_md(),
378                ButtonLikeRounding::Right => this.rounded_r_md(),
379            })
380            .gap_1()
381            .map(|this| match self.size {
382                ButtonSize::Large => this.px_2(),
383                ButtonSize::Default | ButtonSize::Compact => this.px_1(),
384                ButtonSize::None => this,
385            })
386            .bg(self.style.enabled(cx).background)
387            .when(self.disabled, |this| this.cursor_not_allowed())
388            .when(!self.disabled, |this| {
389                this.cursor_pointer()
390                    .hover(|hover| hover.bg(self.style.hovered(cx).background))
391                    .active(|active| active.bg(self.style.active(cx).background))
392            })
393            .when_some(
394                self.on_click.filter(|_| !self.disabled),
395                |this, on_click| {
396                    this.on_mouse_down(MouseButton::Left, |_, cx| cx.prevent_default())
397                        .on_click(move |event, cx| {
398                            cx.stop_propagation();
399                            (on_click)(event, cx)
400                        })
401                },
402            )
403            .when_some(self.tooltip, |this, tooltip| {
404                this.tooltip(move |cx| tooltip(cx))
405            })
406            .children(self.children)
407    }
408}