gpui_kit_material/components/
icon_button.rs1use super::{
11 ButtonShape, ButtonSize, ButtonVariant, IconButtonWidth, InteractionPreview, MaterialButton,
12 button::{Form, Palette, Roles},
13};
14use crate::{color::ColorRole, icons::Icon};
15use gpui::{prelude::*, *};
16
17#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
19pub enum IconButtonVariant {
20 #[default]
22 Standard,
23 Filled,
24 Tonal,
25 Outlined,
26}
27
28impl IconButtonVariant {
29 pub const ALL: [Self; 4] = [Self::Standard, Self::Filled, Self::Tonal, Self::Outlined];
30 pub const fn name(self) -> &'static str {
31 match self {
32 Self::Standard => "standard",
33 Self::Filled => "filled",
34 Self::Tonal => "tonal",
35 Self::Outlined => "outlined",
36 }
37 }
38 fn button_variant(self) -> ButtonVariant {
41 match self {
42 Self::Standard => ButtonVariant::Text,
43 Self::Filled => ButtonVariant::Filled,
44 Self::Tonal => ButtonVariant::Tonal,
45 Self::Outlined => ButtonVariant::Outlined,
46 }
47 }
48}
49
50const STANDARD: Palette = Palette {
51 rest: Roles {
52 container: None,
53 content: ColorRole::ON_SURFACE_VARIANT,
54 },
55 selected: Some(Roles {
56 container: None,
57 content: ColorRole::PRIMARY,
58 }),
59 disabled_container: false,
60 elevation: (0, 0),
61};
62
63type ChangeHandler = Box<dyn Fn(bool, &ClickEvent, &mut Window, &mut App)>;
64type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App)>;
65
66#[derive(gpui::IntoElement)]
68pub struct IconButton {
69 id: ElementId,
70 icon: Icon,
71 selected_icon: Option<Icon>,
72 label: SharedString,
73 variant: IconButtonVariant,
74 size: ButtonSize,
75 width: IconButtonWidth,
76 shape: ButtonShape,
77 toggle: bool,
78 selected: bool,
79 disabled: bool,
80 preview: Option<InteractionPreview>,
81 focus: Option<FocusHandle>,
82 tab_stop: Option<bool>,
83 on_click: Option<ClickHandler>,
84 on_change: Option<ChangeHandler>,
85 style: StyleRefinement,
86}
87
88impl IconButton {
89 pub fn new(
90 id: impl Into<ElementId>,
91 icon: impl Into<Icon>,
92 label: impl Into<SharedString>,
93 ) -> Self {
94 Self {
95 id: id.into(),
96 icon: icon.into(),
97 selected_icon: None,
98 label: label.into(),
99 variant: IconButtonVariant::Standard,
100 size: ButtonSize::Small,
101 width: IconButtonWidth::Default,
102 shape: ButtonShape::Round,
103 toggle: false,
104 selected: false,
105 disabled: false,
106 preview: None,
107 focus: None,
108 tab_stop: None,
109 on_click: None,
110 on_change: None,
111 style: StyleRefinement::default(),
112 }
113 }
114 pub fn variant(mut self, variant: IconButtonVariant) -> Self {
115 self.variant = variant;
116 self
117 }
118 pub fn button_size(mut self, size: ButtonSize) -> Self {
119 self.size = size;
120 self
121 }
122 pub fn width(mut self, width: IconButtonWidth) -> Self {
123 self.width = width;
124 self
125 }
126 pub fn shape(mut self, shape: ButtonShape) -> Self {
127 self.shape = shape;
128 self
129 }
130 pub fn toggle(mut self, toggle: bool) -> Self {
132 self.toggle = toggle;
133 self
134 }
135 pub fn selected(mut self, selected: bool) -> Self {
136 self.selected = selected;
137 self
138 }
139 pub fn selected_icon(mut self, icon: impl Into<Icon>) -> Self {
141 self.selected_icon = Some(icon.into());
142 self
143 }
144 pub fn disabled(mut self, disabled: bool) -> Self {
145 self.disabled = disabled;
146 self
147 }
148 pub fn preview_state(mut self, state: InteractionPreview) -> Self {
150 self.preview = Some(state);
151 self
152 }
153 pub fn track_focus(mut self, focus: &FocusHandle) -> Self {
154 self.focus = Some(focus.clone());
155 self
156 }
157 pub fn tab_stop(mut self, stop: bool) -> Self {
158 self.tab_stop = Some(stop);
159 self
160 }
161 pub fn on_click(
162 mut self,
163 callback: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
164 ) -> Self {
165 self.on_click = Some(Box::new(callback));
166 self
167 }
168 pub fn on_change(
170 mut self,
171 callback: impl Fn(bool, &ClickEvent, &mut Window, &mut App) + 'static,
172 ) -> Self {
173 self.on_change = Some(Box::new(callback));
174 self
175 }
176
177 pub(crate) fn into_button(self) -> MaterialButton {
178 let selected = self.toggle && self.selected;
179 let icon = match self.selected_icon {
180 Some(icon) if selected => icon,
181 _ => self.icon,
182 };
183 let mut button = MaterialButton::new(self.id)
184 .variant(self.variant.button_variant())
185 .form(Form::Icon(self.width))
186 .button_size(self.size)
187 .shape(self.shape)
188 .icon(icon)
189 .accessibility_label(self.label)
190 .toggle(self.toggle)
191 .selected(self.selected)
192 .disabled(self.disabled);
193 if self.variant == IconButtonVariant::Standard {
194 button = button.palette(STANDARD);
195 }
196 if let Some(preview) = self.preview {
197 button = button.preview_state(preview);
198 }
199 if let Some(focus) = &self.focus {
200 button = button.track_focus(focus);
201 }
202 if let Some(stop) = self.tab_stop {
203 button = button.tab_stop(stop);
204 }
205 if let Some(callback) = self.on_click {
206 button = button.on_click(callback);
207 }
208 if let Some(callback) = self.on_change {
209 button = button.on_change(callback);
210 }
211 *button.style() = self.style;
212 button
213 }
214}
215
216impl From<IconButton> for MaterialButton {
217 fn from(button: IconButton) -> Self {
218 button.into_button()
219 }
220}
221
222impl Styled for IconButton {
223 fn style(&mut self) -> &mut StyleRefinement {
224 &mut self.style
225 }
226}
227
228impl RenderOnce for IconButton {
229 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
230 self.into_button()
231 }
232}
233
234#[cfg(test)]
235mod tests {
236 use super::{
237 ButtonSize, ButtonVariant, IconButton, IconButtonVariant, IconButtonWidth, STANDARD,
238 };
239 use crate::{color::ColorRole, icons::IconName};
240 use gpui::{
241 Context, InteractiveElement as _, IntoElement, ParentElement as _, Render, Styled as _,
242 TestAppContext, Window, div, px,
243 };
244
245 struct Harness {
246 variant: IconButtonVariant,
247 size: ButtonSize,
248 width: IconButtonWidth,
249 }
250 impl Render for Harness {
251 fn render(&mut self, window: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
252 window.set_rem_size(px(14.));
253 div().flex().items_start().p(px(40.)).child(
254 div()
255 .flex()
256 .debug_selector(|| "icon-button-slot".into())
257 .child(
258 IconButton::new("icon", IconName::Check, "Check")
259 .variant(self.variant)
260 .button_size(self.size)
261 .width(self.width),
262 ),
263 )
264 }
265 }
266
267 #[gpui::test]
268 fn icon_buttons_take_their_token_box(cx: &mut TestAppContext) {
269 cx.update(|cx| {
270 gpui_base::init(cx);
271 cx.set_reduce_motion(true);
272 });
273 for (size, width, expected) in [
274 (ButtonSize::ExtraSmall, IconButtonWidth::Narrow, (28., 32.)),
275 (ButtonSize::Small, IconButtonWidth::Default, (40., 40.)),
276 (ButtonSize::Medium, IconButtonWidth::Wide, (72., 56.)),
277 (ButtonSize::ExtraLarge, IconButtonWidth::Wide, (184., 136.)),
278 ] {
279 let (_, cx) = cx.add_window_view(|_, _| Harness {
280 variant: IconButtonVariant::Outlined,
281 size,
282 width,
283 });
284 cx.run_until_parked();
285 let bounds = cx
286 .debug_bounds("icon-button-slot")
287 .expect("the icon button renders");
288 assert_eq!(
289 (
290 f32::from(bounds.size.width).round(),
291 f32::from(bounds.size.height).round()
292 ),
293 expected,
294 "{size:?} {width:?}"
295 );
296 }
297 }
298
299 #[core::prelude::v1::test]
300 fn standard_style_has_no_container_and_selects_primary() {
301 assert_eq!(STANDARD.rest.container, None);
302 assert_eq!(STANDARD.selected.unwrap().content, ColorRole::PRIMARY);
303 assert_eq!(
304 IconButtonVariant::Filled.button_variant(),
305 ButtonVariant::Filled
306 );
307 }
308}