egui_material3/
iconbutton.rs

1use eframe::egui::{Color32, Rect, Response, Sense, Stroke, Ui, Vec2, Widget};
2use crate::get_global_color;
3
4/// Visual variants for the icon button component.
5#[derive(Clone, Copy, PartialEq)]
6pub enum IconButtonVariant {
7    /// Standard icon button (minimal visual emphasis)
8    Standard,
9    /// Filled icon button (high emphasis with filled background)
10    Filled,
11    /// Filled tonal icon button (medium emphasis with tonal background)
12    FilledTonal,
13    /// Outlined icon button (medium emphasis with border)
14    Outlined,
15}
16
17/// Material Design icon button component.
18///
19/// Icon buttons help users take supplementary actions with a single tap.
20/// They're used when a compact button is required.
21///
22/// # Example
23/// ```rust
24/// # egui::__run_test_ui(|ui| {
25/// // Standard icon button
26/// if ui.add(MaterialIconButton::standard("favorite")).clicked() {
27///     println!("Favorite clicked!");
28/// }
29///
30/// // Filled icon button with toggle state
31/// let mut liked = false;
32/// ui.add(MaterialIconButton::filled("favorite")
33///     .toggle(&mut liked)
34///     .size(48.0));
35/// # });
36/// ```
37#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
38pub struct MaterialIconButton<'a> {
39    /// Icon identifier (e.g., "favorite", "settings", "delete")
40    icon: String,
41    /// Visual variant of the button
42    variant: IconButtonVariant,
43    /// Optional toggle state for the button
44    selected: Option<&'a mut bool>,
45    /// Whether the button is enabled for interaction
46    enabled: bool,
47    /// Size of the button (width and height)
48    size: f32,
49    /// Whether to use rectangular container (true) or circular (false)
50    container: bool,
51    /// Optional callback to execute when clicked
52    action: Option<Box<dyn Fn() + 'a>>,
53}
54
55impl<'a> MaterialIconButton<'a> {
56    /// Create a new icon button with the specified variant.
57    ///
58    /// # Arguments
59    /// * `icon` - Icon identifier (e.g., "home", "settings", "delete")
60    /// * `variant` - Visual variant of the button
61    ///
62    /// # Example
63    /// ```rust
64    /// # egui::__run_test_ui(|ui| {
65    /// let button = MaterialIconButton::new("settings", IconButtonVariant::Outlined);
66    /// # });
67    /// ```
68    pub fn new(icon: impl Into<String>, variant: IconButtonVariant) -> Self {
69        Self {
70            icon: icon.into(),
71            variant,
72            selected: None,
73            enabled: true,
74            size: 40.0,
75            container: false, // circular by default
76            action: None,
77        }
78    }
79
80    /// Create a standard icon button (minimal visual emphasis).
81    ///
82    /// # Arguments
83    /// * `icon` - Icon identifier
84    ///
85    /// # Example
86    /// ```rust
87    /// # egui::__run_test_ui(|ui| {
88    /// ui.add(MaterialIconButton::standard("menu"));
89    /// # });
90    /// ```
91    pub fn standard(icon: impl Into<String>) -> Self {
92        Self::new(icon, IconButtonVariant::Standard)
93    }
94
95    /// Create a filled icon button (high emphasis with filled background).
96    ///
97    /// # Arguments
98    /// * `icon` - Icon identifier
99    ///
100    /// # Example
101    /// ```rust
102    /// # egui::__run_test_ui(|ui| {
103    /// ui.add(MaterialIconButton::filled("add"));
104    /// # });
105    /// ```
106    pub fn filled(icon: impl Into<String>) -> Self {
107        Self::new(icon, IconButtonVariant::Filled)
108    }
109
110    /// Create a filled tonal icon button (medium emphasis with tonal background).
111    ///
112    /// # Arguments
113    /// * `icon` - Icon identifier
114    ///
115    /// # Example
116    /// ```rust
117    /// # egui::__run_test_ui(|ui| {
118    /// ui.add(MaterialIconButton::filled_tonal("edit"));
119    /// # });
120    /// ```
121    pub fn filled_tonal(icon: impl Into<String>) -> Self {
122        Self::new(icon, IconButtonVariant::FilledTonal)
123    }
124
125    /// Create an outlined icon button (medium emphasis with border).
126    ///
127    /// # Arguments
128    /// * `icon` - Icon identifier
129    ///
130    /// # Example
131    /// ```rust
132    /// # egui::__run_test_ui(|ui| {
133    /// ui.add(MaterialIconButton::outlined("delete"));
134    /// # });
135    /// ```
136    pub fn outlined(icon: impl Into<String>) -> Self {
137        Self::new(icon, IconButtonVariant::Outlined)
138    }
139
140    /// Create a toggleable icon button.
141    ///
142    /// The button's appearance will change based on the `selected` state.
143    ///
144    /// # Arguments
145    /// * `icon` - Icon identifier
146    /// * `selected` - Mutable reference to the toggle state
147    ///
148    /// # Example
149    /// ```rust
150    /// # egui::__run_test_ui(|ui| {
151    /// let mut is_favorite = false;
152    /// ui.add(MaterialIconButton::toggle("favorite", &mut is_favorite));
153    /// # });
154    /// ```
155    pub fn toggle(icon: impl Into<String>, selected: &'a mut bool) -> Self {
156        let mut button = Self::standard(icon);
157        button.selected = Some(selected);
158        button
159    }
160
161    /// Set the size of the icon button.
162    ///
163    /// # Arguments
164    /// * `size` - Desired size (width and height) of the button
165    ///
166    /// # Example
167    /// ```rust
168    /// # egui::__run_test_ui(|ui| {
169    /// ui.add(MaterialIconButton::standard("settings").size(48.0));
170    /// # });
171    /// ```
172    pub fn size(mut self, size: f32) -> Self {
173        self.size = size;
174        self
175    }
176
177    /// Enable or disable the icon button.
178    ///
179    /// # Arguments
180    /// * `enabled` - `true` to enable the button, `false` to disable
181    ///
182    /// # Example
183    /// ```rust
184    /// # egui::__run_test_ui(|ui| {
185    /// ui.add(MaterialIconButton::standard("download").enabled(false));
186    /// # });
187    /// ```
188    pub fn enabled(mut self, enabled: bool) -> Self {
189        self.enabled = enabled;
190        self
191    }
192
193    /// Set the container style of the icon button.
194    ///
195    /// # Arguments
196    /// * `container` - `true` for rectangular container, `false` for circular
197    ///
198    /// # Example
199    /// ```rust
200    /// # egui::__run_test_ui(|ui| {
201    /// ui.add(MaterialIconButton::standard("share").container(true));
202    /// # });
203    /// ```
204    pub fn container(mut self, container: bool) -> Self {
205        self.container = container;
206        self
207    }
208
209    /// Set the click action for the icon button.
210    ///
211    /// # Arguments
212    /// * `f` - Function to execute when the button is clicked
213    ///
214    /// # Example
215    /// ```rust
216    /// # egui::__run_test_ui(|ui| {
217    /// ui.add(MaterialIconButton::standard("info").on_click(|| {
218    ///     println!("Info button clicked!");
219    /// }));
220    /// # });
221    /// ```
222    pub fn on_click<F>(mut self, f: F) -> Self
223    where
224        F: Fn() + 'a,
225    {
226        self.action = Some(Box::new(f));
227        self
228    }
229}
230
231impl<'a> Widget for MaterialIconButton<'a> {
232    fn ui(self, ui: &mut Ui) -> Response {
233        let desired_size = Vec2::splat(self.size);
234        let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
235
236        let is_selected = self.selected.as_ref().map_or(false, |s| **s);
237
238        if response.clicked() && self.enabled {
239            if let Some(selected) = self.selected {
240                *selected = !*selected;
241                response.mark_changed();
242            }
243            if let Some(action) = self.action {
244                action();
245            }
246        }
247
248        // Material Design colors
249        let primary_color = get_global_color("primary");
250        let secondary_container = get_global_color("secondaryContainer");
251        let on_secondary_container = get_global_color("onSecondaryContainer");
252        let _surface = get_global_color("surface");
253        let on_surface = get_global_color("onSurface");
254        let on_surface_variant = get_global_color("onSurfaceVariant");
255        let outline = get_global_color("outline");
256
257        let (bg_color, icon_color, border_color) = if !self.enabled {
258            (
259                get_global_color("surfaceContainer"),
260                get_global_color("outline"),
261                Color32::TRANSPARENT,
262            )
263        } else {
264            match self.variant {
265                IconButtonVariant::Standard => {
266                    if is_selected {
267                        (Color32::TRANSPARENT, primary_color, Color32::TRANSPARENT)
268                    } else if response.hovered() {
269                        (
270                            Color32::from_rgba_premultiplied(on_surface.r(), on_surface.g(), on_surface.b(), 20),
271                            on_surface,
272                            Color32::TRANSPARENT,
273                        )
274                    } else {
275                        (Color32::TRANSPARENT, on_surface_variant, Color32::TRANSPARENT)
276                    }
277                }
278                IconButtonVariant::Filled => {
279                    if is_selected {
280                        (primary_color, get_global_color("onPrimary"), Color32::TRANSPARENT)
281                    } else if response.hovered() {
282                        (
283                            Color32::from_rgba_premultiplied(
284                                primary_color.r().saturating_add(20),
285                                primary_color.g().saturating_add(20),
286                                primary_color.b().saturating_add(20),
287                                255,
288                            ),
289                            get_global_color("onPrimary"),
290                            Color32::TRANSPARENT,
291                        )
292                    } else {
293                        (primary_color, Color32::WHITE, Color32::TRANSPARENT)
294                    }
295                }
296                IconButtonVariant::FilledTonal => {
297                    if is_selected {
298                        (secondary_container, on_secondary_container, Color32::TRANSPARENT)
299                    } else if response.hovered() {
300                        (
301                            Color32::from_rgba_premultiplied(
302                                secondary_container.r().saturating_sub(10),
303                                secondary_container.g().saturating_sub(10),
304                                secondary_container.b().saturating_sub(10),
305                                255,
306                            ),
307                            on_secondary_container,
308                            Color32::TRANSPARENT,
309                        )
310                    } else {
311                        (secondary_container, on_secondary_container, Color32::TRANSPARENT)
312                    }
313                }
314                IconButtonVariant::Outlined => {
315                    if is_selected {
316                        (
317                            Color32::from_rgba_premultiplied(primary_color.r(), primary_color.g(), primary_color.b(), 24),
318                            primary_color,
319                            primary_color,
320                        )
321                    } else if response.hovered() {
322                        (
323                            Color32::from_rgba_premultiplied(on_surface.r(), on_surface.g(), on_surface.b(), 20),
324                            on_surface_variant,
325                            outline,
326                        )
327                    } else {
328                        (Color32::TRANSPARENT, on_surface_variant, outline)
329                    }
330                }
331            }
332        };
333
334        // Calculate corner radius based on container style
335        let corner_radius = if self.container {
336            // Rectangular container: smaller radius for more rectangular shape
337            rect.height() * 0.2 // About 8px for 40px button
338        } else {
339            // Circular container: full radius
340            rect.height() / 2.0
341        };
342
343        // Draw background
344        if bg_color != Color32::TRANSPARENT {
345            ui.painter().rect_filled(
346                rect,
347                corner_radius,
348                bg_color,
349            );
350        }
351
352        // Draw border for outlined variant
353        if border_color != Color32::TRANSPARENT {
354            ui.painter().rect_stroke(
355                rect,
356                corner_radius,
357                Stroke::new(1.0, border_color),
358                egui::epaint::StrokeKind::Outside,
359            );
360        }
361
362        // Draw icon using our icon system
363        let icon_size = self.size * 0.6;
364        let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
365        
366        let icon_widget = crate::icon::MaterialIcon::new(&self.icon)
367            .size(icon_size)
368            .color(icon_color);
369        
370        ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
371            ui.add(icon_widget);
372        });
373
374        // Add ripple effect on hover
375        if response.hovered() && self.enabled {
376            let ripple_color = Color32::from_rgba_premultiplied(icon_color.r(), icon_color.g(), icon_color.b(), 30);
377            ui.painter().rect_filled(
378                rect,
379                corner_radius,
380                ripple_color,
381            );
382        }
383
384        response
385    }
386}
387
388/// Convenience function to create a standard icon button.
389///
390/// # Arguments
391/// * `icon` - Icon identifier
392///
393/// # Example
394/// ```rust
395/// # egui::__run_test_ui(|ui| {
396/// ui.add(icon_button_standard("menu"));
397/// # });
398/// ```
399pub fn icon_button_standard(icon: impl Into<String>) -> MaterialIconButton<'static> {
400    MaterialIconButton::standard(icon)
401}
402
403/// Convenience function to create a filled icon button.
404///
405/// # Arguments
406/// * `icon` - Icon identifier
407///
408/// # Example
409/// ```rust
410/// # egui::__run_test_ui(|ui| {
411/// ui.add(icon_button_filled("add"));
412/// # });
413/// ```
414pub fn icon_button_filled(icon: impl Into<String>) -> MaterialIconButton<'static> {
415    MaterialIconButton::filled(icon)
416}
417
418/// Convenience function to create a filled tonal icon button.
419///
420/// # Arguments
421/// * `icon` - Icon identifier
422///
423/// # Example
424/// ```rust
425/// # egui::__run_test_ui(|ui| {
426/// ui.add(icon_button_filled_tonal("edit"));
427/// # });
428/// ```
429pub fn icon_button_filled_tonal(icon: impl Into<String>) -> MaterialIconButton<'static> {
430    MaterialIconButton::filled_tonal(icon)
431}
432
433/// Convenience function to create an outlined icon button.
434///
435/// # Arguments
436/// * `icon` - Icon identifier
437///
438/// # Example
439/// ```rust
440/// # egui::__run_test_ui(|ui| {
441/// ui.add(icon_button_outlined("delete"));
442/// # });
443/// ```
444pub fn icon_button_outlined(icon: impl Into<String>) -> MaterialIconButton<'static> {
445    MaterialIconButton::outlined(icon)
446}
447
448/// Convenience function to create a toggleable icon button.
449///
450/// # Arguments
451/// * `icon` - Icon identifier
452/// * `selected` - Mutable reference to the toggle state
453///
454/// # Example
455/// ```rust
456/// # egui::__run_test_ui(|ui| {
457/// let mut is_liked = false;
458/// ui.add(icon_button_toggle("favorite", &mut is_liked));
459/// # });
460/// ```
461pub fn icon_button_toggle(icon: impl Into<String>, selected: &mut bool) -> MaterialIconButton<'_> {
462    MaterialIconButton::toggle(icon, selected)
463}