egui_material3/
fab.rs

1use eframe::egui::{self, Color32, Pos2, Rect, Response, Sense, Ui, Vec2, Widget};
2use crate::get_global_color;
3use crate::icon::MaterialIcon;
4
5/// Material Design FAB (Floating Action Button) variants
6#[derive(Clone, Copy, PartialEq)]
7pub enum FabVariant {
8    /// Surface FAB - uses surface colors for neutral actions
9    Surface,
10    /// Primary FAB - uses primary colors for main actions (most common)
11    Primary,
12    /// Secondary FAB - uses secondary colors for secondary actions
13    Secondary, 
14    /// Tertiary FAB - uses tertiary colors for alternative actions
15    Tertiary,
16    /// Branded FAB - uses custom brand colors
17    Branded,
18}
19
20/// Material Design FAB sizes following Material Design 3 specifications
21#[derive(Clone, Copy, PartialEq, Debug)]
22pub enum FabSize {
23    /// Small FAB - 40x40dp, used in compact layouts
24    Small,
25    /// Regular FAB - 56x56dp, the standard size
26    Regular,
27    /// Large FAB - 96x96dp, used for prominent actions
28    Large,
29    /// Extended FAB - Variable width with text, at least 80dp wide
30    Extended,
31}
32
33/// Material Design Floating Action Button (FAB) component
34///
35/// FABs help users take primary actions within an app. They appear in front of all screen content,
36/// typically as a circular button with an icon in the center.
37///
38/// ## Usage Examples
39/// ```rust
40/// # egui::__run_test_ui(|ui| {
41/// // Primary FAB with add icon
42/// ui.add(MaterialFab::primary()
43///     .icon("add")
44///     .action(|| println!("Add clicked")));
45/// 
46/// // Extended FAB with text
47/// ui.add(MaterialFab::primary()
48///     .size(FabSize::Extended)
49///     .icon("edit")
50///     .text("Compose")
51///     .action(|| println!("Compose clicked")));
52/// 
53/// // Large FAB for prominent action
54/// ui.add(MaterialFab::primary()
55///     .size(FabSize::Large)
56///     .icon("camera")
57///     .action(|| println!("Camera clicked")));
58/// # });
59/// ```
60///
61/// ## Material Design Spec
62/// - Elevation: 6dp (raised above content)
63/// - Corner radius: 50% (fully rounded)
64/// - Sizes: Small (40dp), Regular (56dp), Large (96dp), Extended (≥80dp)
65/// - Icon size: 24dp for regular, 32dp for large
66/// - Placement: 16dp from screen edge, above navigation bars
67pub struct MaterialFab<'a> {
68    /// Color variant of the FAB
69    variant: FabVariant,
70    /// Size of the FAB
71    size: FabSize,
72    /// Material Design icon name
73    icon: Option<String>,
74    /// Text content (for extended FABs)
75    text: Option<String>,
76    /// Custom SVG icon data
77    svg_icon: Option<SvgIcon>,
78    /// Whether the FAB is interactive
79    enabled: bool,
80    /// Action callback when FAB is pressed
81    action: Option<Box<dyn Fn() + 'a>>,
82}
83
84/// SVG icon data for custom FAB icons
85#[derive(Clone)]
86pub struct SvgIcon {
87    /// Vector of SVG paths that make up the icon
88    pub paths: Vec<SvgPath>,
89    /// Viewbox dimensions of the SVG
90    pub viewbox_size: Vec2,
91}
92
93/// Individual SVG path with styling
94#[derive(Clone)]
95pub struct SvgPath {
96    /// SVG path data string
97    pub path: String,
98    /// Fill color for the path
99    pub fill: Color32,
100}
101
102impl<'a> MaterialFab<'a> {
103    /// Create a new FAB with the specified variant
104    ///
105    /// ## Parameters
106    /// - `variant`: The color variant to use for the FAB
107    pub fn new(variant: FabVariant) -> Self {
108        Self {
109            variant,
110            size: FabSize::Regular,
111            icon: None,
112            text: None,
113            svg_icon: None,
114            enabled: true,
115            action: None,
116        }
117    }
118
119    /// Create a surface FAB
120    pub fn surface() -> Self {
121        Self::new(FabVariant::Surface)
122    }
123
124    /// Create a primary FAB
125    pub fn primary() -> Self {
126        Self::new(FabVariant::Primary)
127    }
128
129    /// Create a secondary FAB
130    pub fn secondary() -> Self {
131        Self::new(FabVariant::Secondary)
132    }
133
134    /// Create a tertiary FAB
135    pub fn tertiary() -> Self {
136        Self::new(FabVariant::Tertiary)
137    }
138
139    /// Create a branded FAB
140    pub fn branded() -> Self {
141        Self::new(FabVariant::Branded)
142    }
143
144    /// Set the size of the FAB
145    pub fn size(mut self, size: FabSize) -> Self {
146        self.size = size;
147        self
148    }
149
150    /// Set the icon of the FAB
151    pub fn icon(mut self, icon: impl Into<String>) -> Self {
152        self.icon = Some(icon.into());
153        self
154    }
155
156    /// Set the text of the FAB (for extended FABs)
157    pub fn text(mut self, text: impl Into<String>) -> Self {
158        self.text = Some(text.into());
159        self.size = FabSize::Extended;
160        self
161    }
162
163    /// Enable or disable the FAB
164    pub fn enabled(mut self, enabled: bool) -> Self {
165        self.enabled = enabled;
166        self
167    }
168
169    /// Set the lowered state of the FAB (elevation effect)
170    pub fn lowered(self, _lowered: bool) -> Self {
171        // Placeholder for lowered state (elevation effect)
172        // In a real implementation, this would affect the visual elevation
173        self
174    }
175
176    /// Set a custom SVG icon for the FAB
177    pub fn svg_icon(mut self, svg_icon: SvgIcon) -> Self {
178        self.svg_icon = Some(svg_icon);
179        self
180    }
181
182    /// Set the action to perform when the FAB is clicked
183    pub fn on_click<F>(mut self, f: F) -> Self
184    where
185        F: Fn() + 'a,
186    {
187        self.action = Some(Box::new(f));
188        self
189    }
190}
191
192impl<'a> Widget for MaterialFab<'a> {
193    fn ui(self, ui: &mut Ui) -> Response {
194        let size = match self.size {
195            FabSize::Small => Vec2::splat(40.0),
196            FabSize::Regular => Vec2::splat(56.0),
197            FabSize::Large => Vec2::splat(96.0),
198            FabSize::Extended => {
199                let left_margin = 16.0;
200                let right_margin = 24.0;
201                let icon_width = if self.icon.is_some() || self.svg_icon.is_some() { 24.0 + 12.0 } else { 0.0 };
202                
203                let text_width = if let Some(ref text) = self.text {
204                    ui.fonts(|fonts| {
205                        let font_id = egui::FontId::proportional(14.0);
206                        fonts.layout_no_wrap(text.clone(), font_id, Color32::WHITE).size().x
207                    })
208                } else {
209                    0.0
210                };
211                
212                let total_width = left_margin + icon_width + text_width + right_margin;
213                Vec2::new(total_width.max(80.0), 56.0) // Minimum width of 80px
214            }
215        };
216
217        let (rect, response) = ui.allocate_exact_size(size, Sense::click());
218
219        // Extract all needed data before partial move
220        let action = self.action;
221        let enabled = self.enabled;
222        let variant = self.variant;
223        let size_enum = self.size;
224        let icon = self.icon;
225        let text = self.text;
226        let svg_icon = self.svg_icon;
227        
228        let clicked = response.clicked() && enabled;
229        
230        if clicked {
231            if let Some(action) = action {
232                action();
233            }
234        }
235
236        // Material Design colors
237        let primary_color = get_global_color("primary");
238        let secondary_color = get_global_color("secondary");
239        let tertiary_color = get_global_color("tertiary");
240        let surface = get_global_color("surface");
241        let on_primary = get_global_color("onPrimary");
242        let on_surface = get_global_color("onSurface");
243
244        let (bg_color, icon_color) = if !enabled {
245            (
246                get_global_color("surfaceContainer"),
247                get_global_color("outline"),
248            )
249        } else {
250            match variant {
251                FabVariant::Surface => {
252                    if response.hovered() {
253                        (
254                            get_global_color("surfaceContainerHigh"),
255                            on_surface,
256                        )
257                    } else {
258                        (surface, on_surface)
259                    }
260                }
261                FabVariant::Primary => {
262                    if response.hovered() {
263                        (
264                            Color32::from_rgba_premultiplied(
265                                primary_color.r().saturating_add(20),
266                                primary_color.g().saturating_add(20),
267                                primary_color.b().saturating_add(20),
268                                255,
269                            ),
270                            on_primary,
271                        )
272                    } else {
273                        (primary_color, on_primary)
274                    }
275                }
276                FabVariant::Secondary => {
277                    if response.hovered() {
278                        (
279                            Color32::from_rgba_premultiplied(
280                                secondary_color.r().saturating_add(20),
281                                secondary_color.g().saturating_add(20),
282                                secondary_color.b().saturating_add(20),
283                                255,
284                            ),
285                            on_primary,
286                        )
287                    } else {
288                        (secondary_color, on_primary)
289                    }
290                }
291                FabVariant::Tertiary => {
292                    if response.hovered() {
293                        (
294                            Color32::from_rgba_premultiplied(
295                                tertiary_color.r().saturating_add(20),
296                                tertiary_color.g().saturating_add(20),
297                                tertiary_color.b().saturating_add(20),
298                                255,
299                            ),
300                            on_primary,
301                        )
302                    } else {
303                        (tertiary_color, on_primary)
304                    }
305                }
306                FabVariant::Branded => {
307                    // Google brand colors
308                    let google_brand = Color32::from_rgb(66, 133, 244);
309                    if response.hovered() {
310                        (
311                            Color32::from_rgba_premultiplied(
312                                google_brand.r().saturating_add(20),
313                                google_brand.g().saturating_add(20),
314                                google_brand.b().saturating_add(20),
315                                255,
316                            ),
317                            on_primary,
318                        )
319                    } else {
320                        (google_brand, on_primary)
321                    }
322                }
323            }
324        };
325
326        // Calculate corner radius for FAB
327        let corner_radius = match size_enum {
328            FabSize::Small => 12.0,
329            FabSize::Large => 16.0, 
330            _ => 14.0,
331        };
332
333        // Draw FAB background with less rounded corners
334        ui.painter().rect_filled(
335            rect,
336            corner_radius,
337            bg_color,
338        );
339
340        // Draw content
341        match size_enum {
342            FabSize::Extended => {
343                // Draw icon and text with proper spacing
344                let left_margin = 16.0;
345                let _right_margin = 24.0;
346                let icon_text_gap = 12.0;
347                let mut content_x = rect.min.x + left_margin;
348                
349                if let Some(ref icon_name) = icon {
350                    let icon_rect = Rect::from_min_size(
351                        Pos2::new(content_x, rect.center().y - 12.0),
352                        Vec2::splat(24.0),
353                    );
354                    
355                    // Draw material icon
356                    let icon = MaterialIcon::new(icon_name).size(24.0).color(icon_color);
357                    ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
358                        ui.add(icon);
359                    });
360                    
361                    content_x += 24.0 + icon_text_gap;
362                } else if let Some(ref _svg_icon) = svg_icon {
363                    // Render simplified Google logo for branded FAB
364                    draw_google_logo(ui, Pos2::new(content_x + 12.0, rect.center().y), 24.0);
365                    content_x += 24.0 + icon_text_gap;
366                }
367
368                if let Some(ref text) = text {
369                    let text_pos = Pos2::new(content_x, rect.center().y);
370                    ui.painter().text(
371                        text_pos,
372                        egui::Align2::LEFT_CENTER,
373                        text,
374                        egui::FontId::proportional(14.0),
375                        icon_color,
376                    );
377                }
378            }
379            _ => {
380                // Draw centered icon
381                if let Some(ref _svg_icon) = svg_icon {
382                    let icon_size = match size_enum {
383                        FabSize::Small => 18.0,
384                        FabSize::Large => 36.0,
385                        _ => 24.0,
386                    };
387                    
388                    // Render simplified Google logo for branded FAB
389                    draw_google_logo(ui, rect.center(), icon_size);
390                } else if let Some(ref icon_name) = icon {
391                    let icon_size = match size_enum {
392                        FabSize::Small => 18.0,
393                        FabSize::Large => 36.0,
394                        _ => 24.0,
395                    };
396                    
397                    let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
398                    let icon = MaterialIcon::new(icon_name).size(icon_size).color(icon_color);
399                    ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
400                        ui.add(icon);
401                    });
402                } else {
403                    // Default add icon
404                    let icon_size = match size_enum {
405                        FabSize::Small => 18.0,
406                        FabSize::Large => 36.0,
407                        _ => 24.0,
408                    };
409                    
410                    let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
411                    let icon = MaterialIcon::new("add").size(icon_size).color(icon_color);
412                    ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
413                        ui.add(icon);
414                    });
415                }
416            }
417        }
418
419        // Add ripple effect on click
420        if response.hovered() && enabled {
421            let ripple_color = Color32::from_rgba_premultiplied(icon_color.r(), icon_color.g(), icon_color.b(), 30);
422            ui.painter().rect_filled(
423                rect,
424                corner_radius,
425                ripple_color,
426            );
427        }
428
429        response
430    }
431}
432
433// Helper function to draw Google logo
434fn draw_google_logo(ui: &mut Ui, center: Pos2, size: f32) {
435        let half_size = size / 2.0;
436        let quarter_size = size / 4.0;
437
438        // Google 4-color logo - simplified version
439        // Green (top right)
440        ui.painter().rect_filled(
441            Rect::from_min_size(
442                Pos2::new(center.x, center.y - half_size),
443                Vec2::new(half_size, quarter_size),
444            ),
445            0.0,
446            Color32::from_rgb(52, 168, 83), // Green #34A853
447        );
448
449        // Blue (right)
450        ui.painter().rect_filled(
451            Rect::from_min_size(
452                Pos2::new(center.x, center.y - quarter_size),
453                Vec2::new(half_size, half_size),
454            ),
455            0.0,
456            Color32::from_rgb(66, 133, 244), // Blue #4285F4
457        );
458
459        // Yellow (bottom left)
460        ui.painter().rect_filled(
461            Rect::from_min_size(
462                Pos2::new(center.x - half_size, center.y + quarter_size),
463                Vec2::new(half_size, quarter_size),
464            ),
465            0.0,
466            Color32::from_rgb(251, 188, 5), // Yellow #FBBC05
467        );
468
469        // Red (left)
470        ui.painter().rect_filled(
471            Rect::from_min_size(
472                Pos2::new(center.x - half_size, center.y - half_size),
473                Vec2::new(quarter_size, size),
474            ),
475            0.0,
476            Color32::from_rgb(234, 67, 53), // Red #EA4335
477        );
478}
479
480pub fn fab_surface() -> MaterialFab<'static> {
481    MaterialFab::surface()
482}
483
484pub fn fab_primary() -> MaterialFab<'static> {
485    MaterialFab::primary()
486}
487
488pub fn fab_secondary() -> MaterialFab<'static> {
489    MaterialFab::secondary()
490}
491
492pub fn fab_tertiary() -> MaterialFab<'static> {
493    MaterialFab::tertiary()
494}
495
496pub fn fab_branded() -> MaterialFab<'static> {
497    MaterialFab::branded()
498}
499
500/// Create Google branded icon (4-color logo)
501pub fn google_branded_icon() -> SvgIcon {
502    SvgIcon {
503        paths: vec![
504            SvgPath {
505                path: "M16 16v14h4V20z".to_string(),
506                fill: Color32::from_rgb(52, 168, 83), // Green #34A853
507            },
508            SvgPath {
509                path: "M30 16H20l-4 4h14z".to_string(),
510                fill: Color32::from_rgb(66, 133, 244), // Blue #4285F4
511            },
512            SvgPath {
513                path: "M6 16v4h10l4-4z".to_string(),
514                fill: Color32::from_rgb(251, 188, 5), // Yellow #FBBC05
515            },
516            SvgPath {
517                path: "M20 16V6h-4v14z".to_string(),
518                fill: Color32::from_rgb(234, 67, 53), // Red #EA4335
519            },
520        ],
521        viewbox_size: Vec2::new(36.0, 36.0),
522    }
523}