Skip to main content

guise/
button.rs

1//! `Button` — the flagship interactive control, with the library's variants,
2//! colors, and sizes.
3
4use gpui::prelude::*;
5use gpui::{div, px, App, ClickEvent, ElementId, FontWeight, IntoElement, SharedString, Window};
6
7use crate::devtools::Probed;
8use crate::input::ClickHandler;
9use crate::style::{surface, ColorValue, Variant};
10use crate::theme::{theme, Size};
11
12/// A clickable button.
13#[derive(IntoElement)]
14pub struct Button {
15    id: ElementId,
16    label: SharedString,
17    variant: Variant,
18    color: ColorValue,
19    size: Size,
20    radius: Option<Size>,
21    full_width: bool,
22    disabled: bool,
23    left_section: Option<gpui::AnyElement>,
24    right_section: Option<gpui::AnyElement>,
25    on_click: Option<ClickHandler>,
26}
27
28impl Button {
29    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
30        Button {
31            id: id.into(),
32            label: label.into(),
33            variant: Variant::Filled,
34            color: ColorValue::default(),
35            size: Size::Sm,
36            radius: None,
37            full_width: false,
38            disabled: false,
39            left_section: None,
40            right_section: None,
41            on_click: None,
42        }
43    }
44
45    pub fn variant(mut self, variant: Variant) -> Self {
46        self.variant = variant;
47        self
48    }
49
50    /// Override the button color. Accepts a palette `ColorName` or any explicit
51    /// color (e.g. `color!(rgba(34, 139, 230, 1))`).
52    pub fn color(mut self, color: impl Into<ColorValue>) -> Self {
53        self.color = color.into();
54        self
55    }
56
57    pub fn size(mut self, size: Size) -> Self {
58        self.size = size;
59        self
60    }
61
62    pub fn radius(mut self, radius: Size) -> Self {
63        self.radius = Some(radius);
64        self
65    }
66
67    pub fn full_width(mut self, full_width: bool) -> Self {
68        self.full_width = full_width;
69        self
70    }
71
72    pub fn disabled(mut self, disabled: bool) -> Self {
73        self.disabled = disabled;
74        self
75    }
76
77    /// Content shown before the label (e.g. an icon).
78    pub fn left_section(mut self, section: impl IntoElement) -> Self {
79        self.left_section = Some(section.into_any_element());
80        self
81    }
82
83    /// Content shown after the label.
84    pub fn right_section(mut self, section: impl IntoElement) -> Self {
85        self.right_section = Some(section.into_any_element());
86        self
87    }
88
89    pub fn on_click(
90        mut self,
91        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
92    ) -> Self {
93        self.on_click = Some(Box::new(handler));
94        self
95    }
96
97    /// (height, horizontal padding, font size) for the size scale.
98    fn metrics(&self) -> (f32, f32, f32) {
99        match self.size {
100            Size::Xs => (30.0, 14.0, 12.0),
101            Size::Sm => (36.0, 18.0, 14.0),
102            Size::Md => (42.0, 22.0, 16.0),
103            Size::Lg => (50.0, 26.0, 18.0),
104            Size::Xl => (60.0, 32.0, 20.0),
105        }
106    }
107}
108
109impl RenderOnce for Button {
110    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
111        let t = theme(cx);
112        let s = surface(t, self.color, self.variant);
113        let (height, pad_x, font) = self.metrics();
114        let radius = t.radius(self.radius.unwrap_or(t.default_radius));
115
116        let mut el = div()
117            .id(self.id)
118            .flex()
119            .items_center()
120            .justify_center()
121            .gap(px(8.0))
122            .h(px(height))
123            .px(px(pad_x))
124            .rounded(px(radius))
125            .bg(s.bg)
126            .text_color(s.fg)
127            .text_size(px(font))
128            .font_weight(FontWeight::SEMIBOLD);
129
130        if let Some(border) = s.border {
131            el = el.border_1().border_color(border);
132        }
133        if self.full_width {
134            el = el.w_full();
135        }
136        if let Some(left) = self.left_section {
137            el = el.child(left);
138        }
139        el = el.child(self.label);
140        if let Some(right) = self.right_section {
141            el = el.child(right);
142        }
143
144        let element = if self.disabled {
145            el.opacity(0.6)
146        } else {
147            let hover_bg = s.bg_hover;
148            el = el.hover(move |st| st.bg(hover_bg));
149            if let Some(handler) = self.on_click {
150                el = el.on_click(handler);
151            }
152            el
153        };
154
155        element
156            .probe("Button")
157            .attr("variant", self.variant.label())
158            .attr("size", self.size.label())
159            .attr_if("disabled", self.disabled)
160    }
161}