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}