Skip to main content

ui/components/button/
icon_button.rs

1use gpui::{AnyView, DefiniteLength, Hsla};
2
3use super::button_like::{ButtonCommon, ButtonLike, ButtonSize, ButtonStyle};
4use crate::{
5    ElevationIndex, Icon, IconWithIndicator, Indicator, SelectableButton, TintColor, prelude::*,
6};
7use crate::{IconName, IconSize};
8
9/// The shape of an [`IconButton`].
10#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
11pub enum IconButtonShape {
12    Square,
13    Wide,
14}
15
16#[derive(IntoElement, RegisterComponent)]
17pub struct IconButton {
18    base: ButtonLike,
19    shape: IconButtonShape,
20    icon: IconName,
21    icon_size: IconSize,
22    icon_color: Color,
23    selected_icon: Option<IconName>,
24    selected_icon_color: Option<Color>,
25    selected_style: Option<ButtonStyle>,
26    indicator: Option<Indicator>,
27    indicator_border_color: Option<Hsla>,
28    alpha: Option<f32>,
29}
30
31impl IconButton {
32    pub fn new(id: impl Into<ElementId>, icon: IconName) -> Self {
33        let mut this = Self {
34            base: ButtonLike::new(id),
35            shape: IconButtonShape::Wide,
36            icon,
37            icon_size: IconSize::default(),
38            icon_color: Color::Default,
39            selected_icon: None,
40            selected_icon_color: None,
41            selected_style: None,
42            indicator: None,
43            indicator_border_color: None,
44            alpha: None,
45        };
46        this.base.base = this.base.base.debug_selector(|| format!("ICON-{:?}", icon));
47        this
48    }
49
50    pub fn shape(mut self, shape: IconButtonShape) -> Self {
51        self.shape = shape;
52        self
53    }
54
55    pub fn icon_size(mut self, icon_size: IconSize) -> Self {
56        self.icon_size = icon_size;
57        self
58    }
59
60    pub fn icon_color(mut self, icon_color: Color) -> Self {
61        self.icon_color = icon_color;
62        self
63    }
64
65    pub fn alpha(mut self, alpha: f32) -> Self {
66        self.alpha = Some(alpha);
67        self
68    }
69
70    pub fn selected_icon(mut self, icon: impl Into<Option<IconName>>) -> Self {
71        self.selected_icon = icon.into();
72        self
73    }
74
75    pub fn on_right_click(
76        mut self,
77        handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
78    ) -> Self {
79        self.base = self.base.on_right_click(handler);
80        self
81    }
82
83    /// Sets the icon color used when the button is in a selected state.
84    pub fn selected_icon_color(mut self, color: impl Into<Option<Color>>) -> Self {
85        self.selected_icon_color = color.into();
86        self
87    }
88
89    pub fn indicator(mut self, indicator: Indicator) -> Self {
90        self.indicator = Some(indicator);
91        self
92    }
93
94    pub fn indicator_border_color(mut self, color: Option<Hsla>) -> Self {
95        self.indicator_border_color = color;
96
97        self
98    }
99}
100
101impl Disableable for IconButton {
102    fn disabled(mut self, disabled: bool) -> Self {
103        self.base = self.base.disabled(disabled);
104        self
105    }
106}
107
108impl Toggleable for IconButton {
109    fn toggle_state(mut self, selected: bool) -> Self {
110        self.base = self.base.toggle_state(selected);
111        self
112    }
113}
114
115impl SelectableButton for IconButton {
116    fn selected_style(mut self, style: ButtonStyle) -> Self {
117        self.selected_style = Some(style);
118        self.base = self.base.selected_style(style);
119        self
120    }
121}
122
123impl Clickable for IconButton {
124    fn on_click(
125        mut self,
126        handler: impl Fn(&gpui::ClickEvent, &mut Window, &mut App) + 'static,
127    ) -> Self {
128        self.base = self.base.on_click(handler);
129        self
130    }
131
132    fn cursor_style(mut self, cursor_style: gpui::CursorStyle) -> Self {
133        self.base = self.base.cursor_style(cursor_style);
134        self
135    }
136}
137
138impl FixedWidth for IconButton {
139    fn width(mut self, width: impl Into<DefiniteLength>) -> Self {
140        self.base = self.base.width(width);
141        self
142    }
143
144    fn full_width(mut self) -> Self {
145        self.base = self.base.full_width();
146        self
147    }
148}
149
150impl ButtonCommon for IconButton {
151    fn id(&self) -> &ElementId {
152        self.base.id()
153    }
154
155    fn style(mut self, style: ButtonStyle) -> Self {
156        self.base = self.base.style(style);
157        self
158    }
159
160    fn size(mut self, size: ButtonSize) -> Self {
161        self.base = self.base.size(size);
162        self
163    }
164
165    fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
166        self.base = self.base.tooltip(tooltip);
167        self
168    }
169
170    fn tab_index(mut self, tab_index: impl Into<isize>) -> Self {
171        self.base = self.base.tab_index(tab_index);
172        self
173    }
174
175    fn layer(mut self, elevation: ElevationIndex) -> Self {
176        self.base = self.base.layer(elevation);
177        self
178    }
179
180    fn track_focus(mut self, focus_handle: &gpui::FocusHandle) -> Self {
181        self.base = self.base.track_focus(focus_handle);
182        self
183    }
184}
185
186impl VisibleOnHover for IconButton {
187    fn visible_on_hover(mut self, group_name: impl Into<SharedString>) -> Self {
188        self.base = self.base.visible_on_hover(group_name);
189        self
190    }
191}
192
193impl RenderOnce for IconButton {
194    #[allow(refining_impl_trait)]
195    fn render(self, window: &mut Window, cx: &mut App) -> ButtonLike {
196        let is_disabled = self.base.disabled;
197        let is_selected = self.base.selected;
198
199        let icon = self
200            .selected_icon
201            .filter(|_| is_selected)
202            .unwrap_or(self.icon);
203
204        let icon_color = if is_disabled {
205            Color::Disabled
206        } else if self.selected_style.is_some() && is_selected {
207            self.selected_style.unwrap().into()
208        } else if is_selected {
209            self.selected_icon_color.unwrap_or(Color::Selected)
210        } else if matches!(self.base.style, ButtonStyle::Tinted(_)) {
211            // Solid Tinted backgrounds need a light icon for contrast.
212            Color::Custom(gpui::white().opacity(self.alpha.unwrap_or(1.0)))
213        } else {
214            let base_color = self.icon_color.color(cx);
215            Color::Custom(base_color.opacity(self.alpha.unwrap_or(1.0)))
216        };
217
218        let icon_element = Icon::new(icon).size(self.icon_size).color(icon_color);
219
220        self.base
221            .map(|this| match self.shape {
222                IconButtonShape::Square => {
223                    let size = self.icon_size.square(window, cx);
224                    this.width(size).height(size.into())
225                }
226                IconButtonShape::Wide => this,
227            })
228            .child(match self.indicator {
229                Some(indicator) => IconWithIndicator::new(icon_element, Some(indicator))
230                    .indicator_border_color(self.indicator_border_color)
231                    .into_any_element(),
232                None => icon_element.into_any_element(),
233            })
234    }
235}
236
237impl Component for IconButton {
238    fn scope() -> ComponentScope {
239        ComponentScope::Input
240    }
241
242    fn sort_name() -> &'static str {
243        "ButtonB"
244    }
245
246    fn preview(_window: &mut Window, _cx: &mut App) -> Option<AnyElement> {
247        Some(
248            v_flex()
249                .gap_6()
250                .children(vec![
251                    example_group_with_title(
252                        "Icon Button Styles",
253                        vec![
254                            single_example(
255                                "Default",
256                                IconButton::new("default", IconName::Check)
257                                    .layer(ElevationIndex::Background)
258                                    .into_any_element(),
259                            ),
260                            single_example(
261                                "Filled",
262                                IconButton::new("filled", IconName::Check)
263                                    .layer(ElevationIndex::Background)
264                                    .style(ButtonStyle::Filled)
265                                    .into_any_element(),
266                            ),
267                            single_example(
268                                "Subtle",
269                                IconButton::new("subtle", IconName::Check)
270                                    .layer(ElevationIndex::Background)
271                                    .style(ButtonStyle::Subtle)
272                                    .into_any_element(),
273                            ),
274                            single_example(
275                                "Tinted",
276                                IconButton::new("tinted", IconName::Check)
277                                    .layer(ElevationIndex::Background)
278                                    .style(ButtonStyle::Tinted(TintColor::Accent))
279                                    .into_any_element(),
280                            ),
281                            single_example(
282                                "Transparent",
283                                IconButton::new("transparent", IconName::Check)
284                                    .layer(ElevationIndex::Background)
285                                    .style(ButtonStyle::Transparent)
286                                    .into_any_element(),
287                            ),
288                        ],
289                    ),
290                    example_group_with_title(
291                        "Icon Button Shapes",
292                        vec![
293                            single_example(
294                                "Square",
295                                IconButton::new("square", IconName::Check)
296                                    .shape(IconButtonShape::Square)
297                                    .style(ButtonStyle::Filled)
298                                    .layer(ElevationIndex::Background)
299                                    .into_any_element(),
300                            ),
301                            single_example(
302                                "Wide",
303                                IconButton::new("wide", IconName::Check)
304                                    .shape(IconButtonShape::Wide)
305                                    .style(ButtonStyle::Filled)
306                                    .layer(ElevationIndex::Background)
307                                    .into_any_element(),
308                            ),
309                        ],
310                    ),
311                    example_group_with_title(
312                        "Icon Button Sizes",
313                        vec![
314                            single_example(
315                                "XSmall",
316                                IconButton::new("xsmall", IconName::Check)
317                                    .icon_size(IconSize::XSmall)
318                                    .style(ButtonStyle::Filled)
319                                    .layer(ElevationIndex::Background)
320                                    .into_any_element(),
321                            ),
322                            single_example(
323                                "Small",
324                                IconButton::new("small", IconName::Check)
325                                    .icon_size(IconSize::Small)
326                                    .style(ButtonStyle::Filled)
327                                    .layer(ElevationIndex::Background)
328                                    .into_any_element(),
329                            ),
330                            single_example(
331                                "Medium",
332                                IconButton::new("medium", IconName::Check)
333                                    .icon_size(IconSize::Medium)
334                                    .style(ButtonStyle::Filled)
335                                    .layer(ElevationIndex::Background)
336                                    .into_any_element(),
337                            ),
338                            single_example(
339                                "XLarge",
340                                IconButton::new("xlarge", IconName::Check)
341                                    .icon_size(IconSize::XLarge)
342                                    .style(ButtonStyle::Filled)
343                                    .layer(ElevationIndex::Background)
344                                    .into_any_element(),
345                            ),
346                        ],
347                    ),
348                    example_group_with_title(
349                        "Special States",
350                        vec![
351                            single_example(
352                                "Disabled",
353                                IconButton::new("disabled", IconName::Check)
354                                    .disabled(true)
355                                    .style(ButtonStyle::Filled)
356                                    .layer(ElevationIndex::Background)
357                                    .into_any_element(),
358                            ),
359                            single_example(
360                                "Selected",
361                                IconButton::new("selected", IconName::Check)
362                                    .toggle_state(true)
363                                    .style(ButtonStyle::Filled)
364                                    .layer(ElevationIndex::Background)
365                                    .into_any_element(),
366                            ),
367                            single_example(
368                                "With Indicator",
369                                IconButton::new("indicator", IconName::Check)
370                                    .indicator(Indicator::dot().color(Color::Success))
371                                    .style(ButtonStyle::Filled)
372                                    .layer(ElevationIndex::Background)
373                                    .into_any_element(),
374                            ),
375                        ],
376                    ),
377                    example_group_with_title(
378                        "Custom Colors",
379                        vec![
380                            single_example(
381                                "Custom Icon Color",
382                                IconButton::new("custom_color", IconName::Check)
383                                    .icon_color(Color::Accent)
384                                    .style(ButtonStyle::Filled)
385                                    .layer(ElevationIndex::Background)
386                                    .into_any_element(),
387                            ),
388                            single_example(
389                                "With Alpha",
390                                IconButton::new("alpha", IconName::Check)
391                                    .alpha(0.5)
392                                    .style(ButtonStyle::Filled)
393                                    .layer(ElevationIndex::Background)
394                                    .into_any_element(),
395                            ),
396                        ],
397                    ),
398                ])
399                .into_any_element(),
400        )
401    }
402}