gpui_ui_kit/
icon_button.rs

1//! IconButton component
2//!
3//! A button that displays only an icon, with optional tooltip.
4//! Supports both text/emoji icons and custom child elements (like SVG icons).
5
6use gpui::prelude::*;
7use gpui::*;
8
9/// Theme colors for icon button styling
10#[derive(Debug, Clone)]
11pub struct IconButtonTheme {
12    /// Background color for ghost variant
13    pub ghost_bg: Rgba,
14    /// Background color on hover for ghost variant
15    pub ghost_hover_bg: Rgba,
16    /// Background color when selected
17    pub selected_bg: Rgba,
18    /// Background color on hover when selected
19    pub selected_hover_bg: Rgba,
20    /// Filled variant background
21    pub filled_bg: Rgba,
22    /// Filled variant hover background
23    pub filled_hover_bg: Rgba,
24    /// Accent color (for filled selected, outline border)
25    pub accent: Rgba,
26    /// Accent hover color
27    pub accent_hover: Rgba,
28    /// Default text/icon color
29    pub text: Rgba,
30    /// Text color when selected or on accent background
31    pub text_on_accent: Rgba,
32    /// Border color for outline variant
33    pub border: Rgba,
34}
35
36impl Default for IconButtonTheme {
37    fn default() -> Self {
38        Self {
39            ghost_bg: rgba(0x00000000),
40            ghost_hover_bg: rgba(0x3a3a3aff),
41            selected_bg: rgba(0x3a3a3aff),
42            selected_hover_bg: rgba(0x4a4a4aff),
43            filled_bg: rgba(0x3a3a3aff),
44            filled_hover_bg: rgba(0x4a4a4aff),
45            accent: rgba(0x007accff),
46            accent_hover: rgba(0x0098ffff),
47            text: rgba(0xccccccff),
48            text_on_accent: rgba(0xffffffff),
49            border: rgba(0x555555ff),
50        }
51    }
52}
53
54/// IconButton size variants
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
56pub enum IconButtonSize {
57    /// Extra small (16px)
58    Xs,
59    /// Small (20px)
60    Sm,
61    /// Medium (24px, default)
62    #[default]
63    Md,
64    /// Large (32px)
65    Lg,
66    /// Extra large (48px)
67    Xl,
68    /// Custom size in pixels
69    Custom(u32),
70}
71
72impl IconButtonSize {
73    /// Get the size in pixels
74    pub fn size(&self) -> Pixels {
75        match self {
76            IconButtonSize::Xs => px(16.0),
77            IconButtonSize::Sm => px(20.0),
78            IconButtonSize::Md => px(24.0),
79            IconButtonSize::Lg => px(32.0),
80            IconButtonSize::Xl => px(48.0),
81            IconButtonSize::Custom(size) => px(*size as f32),
82        }
83    }
84}
85
86/// IconButton variant
87#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
88pub enum IconButtonVariant {
89    /// Ghost button (transparent, default)
90    #[default]
91    Ghost,
92    /// Filled background
93    Filled,
94    /// Outline border
95    Outline,
96}
97
98/// Icon content - either text/emoji or a custom element
99enum IconContent {
100    Text(SharedString),
101    Element(AnyElement),
102}
103
104/// An icon-only button component
105///
106/// # Examples
107///
108/// ```ignore
109/// // With text/emoji icon
110/// IconButton::new("btn", "🔊")
111///     .variant(IconButtonVariant::Ghost)
112///     .on_click(|window, cx| { /* handle click */ })
113///
114/// // With custom element (e.g., SVG icon)
115/// IconButton::with_child("btn", my_svg_icon)
116///     .size(IconButtonSize::Lg)
117///     .rounded_full()
118///     .theme(my_theme)
119/// ```
120pub struct IconButton {
121    id: ElementId,
122    content: IconContent,
123    size: IconButtonSize,
124    variant: IconButtonVariant,
125    disabled: bool,
126    selected: bool,
127    rounded_full: bool,
128    padding: Option<Pixels>,
129    theme: Option<IconButtonTheme>,
130    on_click: Option<Box<dyn Fn(&mut Window, &mut App) + 'static>>,
131}
132
133impl IconButton {
134    /// Create a new icon button with a text/emoji icon
135    pub fn new(id: impl Into<ElementId>, icon: impl Into<SharedString>) -> Self {
136        Self {
137            id: id.into(),
138            content: IconContent::Text(icon.into()),
139            size: IconButtonSize::default(),
140            variant: IconButtonVariant::default(),
141            disabled: false,
142            selected: false,
143            rounded_full: false,
144            padding: None,
145            theme: None,
146            on_click: None,
147        }
148    }
149
150    /// Create a new icon button with a custom child element (e.g., SVG icon)
151    pub fn with_child(id: impl Into<ElementId>, child: impl IntoElement) -> Self {
152        Self {
153            id: id.into(),
154            content: IconContent::Element(child.into_any_element()),
155            size: IconButtonSize::default(),
156            variant: IconButtonVariant::default(),
157            disabled: false,
158            selected: false,
159            rounded_full: false,
160            padding: None,
161            theme: None,
162            on_click: None,
163        }
164    }
165
166    /// Set the button size
167    pub fn size(mut self, size: IconButtonSize) -> Self {
168        self.size = size;
169        self
170    }
171
172    /// Set the button variant
173    pub fn variant(mut self, variant: IconButtonVariant) -> Self {
174        self.variant = variant;
175        self
176    }
177
178    /// Set disabled state
179    pub fn disabled(mut self, disabled: bool) -> Self {
180        self.disabled = disabled;
181        self
182    }
183
184    /// Set selected state
185    pub fn selected(mut self, selected: bool) -> Self {
186        self.selected = selected;
187        self
188    }
189
190    /// Set click handler
191    pub fn on_click(mut self, handler: impl Fn(&mut Window, &mut App) + 'static) -> Self {
192        self.on_click = Some(Box::new(handler));
193        self
194    }
195
196    /// Use fully rounded corners (circular button)
197    pub fn rounded_full(mut self) -> Self {
198        self.rounded_full = true;
199        self
200    }
201
202    /// Set custom padding (overrides default size-based padding)
203    pub fn padding(mut self, padding: Pixels) -> Self {
204        self.padding = Some(padding);
205        self
206    }
207
208    /// Set the button theme
209    pub fn theme(mut self, theme: IconButtonTheme) -> Self {
210        self.theme = Some(theme);
211        self
212    }
213
214    /// Get the computed colors based on variant and state
215    pub fn compute_colors(&self) -> (Rgba, Rgba, Rgba, Option<Rgba>) {
216        let default_theme = IconButtonTheme::default();
217        let theme = self.theme.as_ref().unwrap_or(&default_theme);
218
219        match self.variant {
220            IconButtonVariant::Ghost => {
221                if self.selected {
222                    (
223                        theme.selected_bg,
224                        theme.selected_hover_bg,
225                        theme.text_on_accent,
226                        None,
227                    )
228                } else {
229                    (theme.ghost_bg, theme.ghost_hover_bg, theme.text, None)
230                }
231            }
232            IconButtonVariant::Filled => {
233                if self.selected {
234                    (theme.accent, theme.accent_hover, theme.text_on_accent, None)
235                } else {
236                    (theme.filled_bg, theme.filled_hover_bg, theme.text, None)
237                }
238            }
239            IconButtonVariant::Outline => {
240                if self.selected {
241                    (
242                        theme.selected_bg,
243                        theme.selected_hover_bg,
244                        theme.text_on_accent,
245                        Some(theme.accent),
246                    )
247                } else {
248                    (
249                        theme.ghost_bg,
250                        theme.ghost_hover_bg,
251                        theme.text,
252                        Some(theme.border),
253                    )
254                }
255            }
256        }
257    }
258
259    /// Build into element
260    pub fn build(self) -> Stateful<Div> {
261        let size = self.size.size();
262        let (bg, bg_hover, text_color, border) = self.compute_colors();
263
264        let mut el = div()
265            .id(self.id)
266            .flex()
267            .items_center()
268            .justify_center()
269            .w(size)
270            .h(size)
271            .bg(bg)
272            .text_color(text_color)
273            .cursor_pointer();
274
275        // Apply padding if specified
276        if let Some(padding) = self.padding {
277            el = el.p(padding);
278        }
279
280        // Apply rounding
281        if self.rounded_full {
282            el = el.rounded_full();
283        } else {
284            el = el.rounded_md();
285        }
286
287        if let Some(border_color) = border {
288            el = el.border_1().border_color(border_color);
289        }
290
291        if self.disabled {
292            el = el.opacity(0.5).cursor_not_allowed();
293        } else {
294            el = el.hover(|s| s.bg(bg_hover));
295
296            if let Some(handler) = self.on_click {
297                el = el.on_mouse_up(MouseButton::Left, move |_event, window, cx| {
298                    handler(window, cx);
299                });
300            }
301        }
302
303        // Add content
304        match self.content {
305            IconContent::Text(text) => el.child(text),
306            IconContent::Element(element) => el.child(element),
307        }
308    }
309}
310
311impl IntoElement for IconButton {
312    type Element = Stateful<Div>;
313
314    fn into_element(self) -> Self::Element {
315        self.build()
316    }
317}