gpui_ui_kit/
icon_button.rs

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