gpui_ui_kit/
button.rs

1//! Button component with variants and sizes
2//!
3//! Provides a flexible button component with different visual styles.
4
5use gpui::prelude::*;
6use gpui::*;
7
8/// Button visual variant
9#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
10pub enum ButtonVariant {
11    /// Primary action button (accent color)
12    #[default]
13    Primary,
14    /// Secondary action button (muted)
15    Secondary,
16    /// Destructive action (red)
17    Destructive,
18    /// Ghost button (transparent until hover)
19    Ghost,
20    /// Outline button (border only)
21    Outline,
22}
23
24/// Button size
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
26pub enum ButtonSize {
27    /// Extra small button
28    Xs,
29    /// Small button
30    Sm,
31    /// Medium button (default)
32    #[default]
33    Md,
34    /// Large button
35    Lg,
36}
37
38/// Theme colors for button styling
39#[derive(Debug, Clone)]
40pub struct ButtonTheme {
41    pub accent: Rgba,
42    pub accent_hover: Rgba,
43    pub surface: Rgba,
44    pub surface_hover: Rgba,
45    pub text_primary: Rgba,
46    pub text_secondary: Rgba,
47    pub error: Rgba,
48    pub border: Rgba,
49}
50
51impl Default for ButtonTheme {
52    fn default() -> Self {
53        Self {
54            accent: rgb(0x007acc),
55            accent_hover: rgb(0x0098ff),
56            surface: rgb(0x3c3c3c),
57            surface_hover: rgb(0x4a4a4a),
58            text_primary: rgb(0xffffff),
59            text_secondary: rgb(0xcccccc),
60            error: rgb(0xcc3333),
61            border: rgb(0x555555),
62        }
63    }
64}
65
66/// A styled button component
67#[derive(IntoElement)]
68pub struct Button {
69    id: ElementId,
70    label: SharedString,
71    variant: ButtonVariant,
72    size: ButtonSize,
73    disabled: bool,
74    selected: bool,
75    full_width: bool,
76    icon_left: Option<SharedString>,
77    icon_right: Option<SharedString>,
78    theme: Option<ButtonTheme>,
79    on_click: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
80}
81
82impl Button {
83    /// Create a new button with a label
84    pub fn new(id: impl Into<ElementId>, label: impl Into<SharedString>) -> Self {
85        Self {
86            id: id.into(),
87            label: label.into(),
88            variant: ButtonVariant::default(),
89            size: ButtonSize::default(),
90            disabled: false,
91            selected: false,
92            full_width: false,
93            icon_left: None,
94            icon_right: None,
95            theme: None,
96            on_click: None,
97        }
98    }
99
100    /// Set the button variant
101    pub fn variant(mut self, variant: ButtonVariant) -> Self {
102        self.variant = variant;
103        self
104    }
105
106    /// Set the button size
107    pub fn size(mut self, size: ButtonSize) -> Self {
108        self.size = size;
109        self
110    }
111
112    /// Disable the button
113    pub fn disabled(mut self, disabled: bool) -> Self {
114        self.disabled = disabled;
115        self
116    }
117
118    /// Set button selected state (for toggle buttons)
119    pub fn selected(mut self, selected: bool) -> Self {
120        self.selected = selected;
121        self
122    }
123
124    /// Make button full width
125    pub fn full_width(mut self, full_width: bool) -> Self {
126        self.full_width = full_width;
127        self
128    }
129
130    /// Add an icon to the left of the label
131    pub fn icon_left(mut self, icon: impl Into<SharedString>) -> Self {
132        self.icon_left = Some(icon.into());
133        self
134    }
135
136    /// Add an icon to the right of the label
137    pub fn icon_right(mut self, icon: impl Into<SharedString>) -> Self {
138        self.icon_right = Some(icon.into());
139        self
140    }
141
142    /// Set custom theme colors
143    pub fn theme(mut self, theme: ButtonTheme) -> Self {
144        self.theme = Some(theme);
145        self
146    }
147
148    /// Set the click handler (for standalone use without cx.listener)
149    pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
150        self.on_click = Some(Box::new(handler));
151        self
152    }
153
154    /// Build the button into a Stateful<Div> that can have additional handlers added
155    /// Use this when you need to add a cx.listener() handler
156    pub fn build(self) -> Stateful<Div> {
157        let theme = self.theme.unwrap_or_default();
158
159        // Handle selected state for Primary variant
160        let (bg, bg_hover, text_color, border_color) = if self.selected {
161            (
162                theme.accent,
163                theme.accent_hover,
164                theme.text_primary,
165                theme.accent,
166            )
167        } else {
168            match self.variant {
169                ButtonVariant::Primary => (
170                    theme.accent,
171                    theme.accent_hover,
172                    theme.text_primary,
173                    theme.accent,
174                ),
175                ButtonVariant::Secondary => (
176                    theme.surface,
177                    theme.surface_hover,
178                    theme.text_secondary,
179                    theme.surface,
180                ),
181                ButtonVariant::Destructive => {
182                    (theme.error, rgb(0xe64545), theme.text_primary, theme.error)
183                }
184                ButtonVariant::Ghost => (
185                    rgba(0x00000000),
186                    theme.surface_hover,
187                    theme.text_secondary,
188                    rgba(0x00000000),
189                ),
190                ButtonVariant::Outline => (
191                    rgba(0x00000000),
192                    theme.surface,
193                    theme.text_secondary,
194                    theme.border,
195                ),
196            }
197        };
198
199        let (px_val, py_val) = match self.size {
200            ButtonSize::Xs => (px(6.0), px(2.0)),
201            ButtonSize::Sm => (px(8.0), px(4.0)),
202            ButtonSize::Md => (px(12.0), px(6.0)),
203            ButtonSize::Lg => (px(24.0), px(12.0)),
204        };
205
206        let mut el = div()
207            .id(self.id)
208            .flex()
209            .items_center()
210            .justify_center()
211            .gap_2()
212            .px(px_val)
213            .py(py_val)
214            .rounded_md()
215            .bg(bg)
216            .text_color(text_color)
217            .border_1()
218            .border_color(border_color);
219
220        // Apply text size based on button size
221        el = match self.size {
222            ButtonSize::Xs => el.text_xs(),
223            ButtonSize::Sm => el.text_xs(),
224            ButtonSize::Md => el.text_sm(),
225            ButtonSize::Lg => el.text_lg(),
226        };
227
228        // Apply full width
229        if self.full_width {
230            el = el.w_full();
231        }
232
233        if self.disabled {
234            el = el.opacity(0.5).cursor_not_allowed();
235        } else {
236            el = el.cursor_pointer().hover(|style| style.bg(bg_hover));
237        }
238
239        // Add icon left
240        if let Some(icon) = self.icon_left {
241            el = el.child(div().child(icon));
242        }
243
244        // Add label
245        el = el.child(self.label);
246
247        // Add icon right
248        if let Some(icon) = self.icon_right {
249            el = el.child(div().child(icon));
250        }
251
252        el
253    }
254}
255
256impl RenderOnce for Button {
257    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
258        let theme = self.theme.unwrap_or_default();
259
260        // Handle selected state for Primary variant
261        let (bg, bg_hover, text_color, border_color) = if self.selected {
262            (
263                theme.accent,
264                theme.accent_hover,
265                theme.text_primary,
266                theme.accent,
267            )
268        } else {
269            match self.variant {
270                ButtonVariant::Primary => (
271                    theme.accent,
272                    theme.accent_hover,
273                    theme.text_primary,
274                    theme.accent,
275                ),
276                ButtonVariant::Secondary => (
277                    theme.surface,
278                    theme.surface_hover,
279                    theme.text_secondary,
280                    theme.surface,
281                ),
282                ButtonVariant::Destructive => {
283                    (theme.error, rgb(0xe64545), theme.text_primary, theme.error)
284                }
285                ButtonVariant::Ghost => (
286                    rgba(0x00000000),
287                    theme.surface_hover,
288                    theme.text_secondary,
289                    rgba(0x00000000),
290                ),
291                ButtonVariant::Outline => (
292                    rgba(0x00000000),
293                    theme.surface,
294                    theme.text_secondary,
295                    theme.border,
296                ),
297            }
298        };
299
300        let (px_val, py_val) = match self.size {
301            ButtonSize::Xs => (px(6.0), px(2.0)),
302            ButtonSize::Sm => (px(8.0), px(4.0)),
303            ButtonSize::Md => (px(12.0), px(6.0)),
304            ButtonSize::Lg => (px(24.0), px(12.0)),
305        };
306
307        let mut el = div()
308            .id(self.id)
309            .flex()
310            .items_center()
311            .justify_center()
312            .gap_2()
313            .px(px_val)
314            .py(py_val)
315            .rounded_md()
316            .bg(bg)
317            .text_color(text_color)
318            .border_1()
319            .border_color(border_color)
320            .cursor_pointer();
321
322        // Apply text size based on button size
323        el = match self.size {
324            ButtonSize::Xs => el.text_xs(),
325            ButtonSize::Sm => el.text_xs(),
326            ButtonSize::Md => el.text_sm(),
327            ButtonSize::Lg => el.text_lg(),
328        };
329
330        // Apply full width
331        if self.full_width {
332            el = el.w_full();
333        }
334
335        if self.disabled {
336            el = el.opacity(0.5).cursor_not_allowed();
337        } else {
338            el = el.hover(|style| style.bg(bg_hover));
339
340            if let Some(handler) = self.on_click {
341                el = el.on_mouse_up(MouseButton::Left, move |_event, window, cx| {
342                    handler(window, cx);
343                });
344            }
345        }
346
347        // Add icon left
348        if let Some(icon) = self.icon_left {
349            el = el.child(div().child(icon));
350        }
351
352        // Add label
353        el = el.child(self.label);
354
355        // Add icon right
356        if let Some(icon) = self.icon_right {
357            el = el.child(div().child(icon));
358        }
359
360        el
361    }
362}