1use eframe::egui::{self, Color32, Pos2, Rect, Response, Sense, Ui, Vec2, Widget};
2use crate::get_global_color;
3use crate::icon::MaterialIcon;
4
5#[derive(Clone, Copy, PartialEq)]
7pub enum FabVariant {
8 Surface,
10 Primary,
12 Secondary,
14 Tertiary,
16 Branded,
18}
19
20#[derive(Clone, Copy, PartialEq, Debug)]
22pub enum FabSize {
23 Small,
25 Regular,
27 Large,
29 Extended,
31}
32
33pub struct MaterialFab<'a> {
68 variant: FabVariant,
70 size: FabSize,
72 icon: Option<String>,
74 text: Option<String>,
76 svg_icon: Option<SvgIcon>,
78 enabled: bool,
80 action: Option<Box<dyn Fn() + 'a>>,
82}
83
84#[derive(Clone)]
86pub struct SvgIcon {
87 pub paths: Vec<SvgPath>,
89 pub viewbox_size: Vec2,
91}
92
93#[derive(Clone)]
95pub struct SvgPath {
96 pub path: String,
98 pub fill: Color32,
100}
101
102impl<'a> MaterialFab<'a> {
103 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 pub fn surface() -> Self {
121 Self::new(FabVariant::Surface)
122 }
123
124 pub fn primary() -> Self {
126 Self::new(FabVariant::Primary)
127 }
128
129 pub fn secondary() -> Self {
131 Self::new(FabVariant::Secondary)
132 }
133
134 pub fn tertiary() -> Self {
136 Self::new(FabVariant::Tertiary)
137 }
138
139 pub fn branded() -> Self {
141 Self::new(FabVariant::Branded)
142 }
143
144 pub fn size(mut self, size: FabSize) -> Self {
146 self.size = size;
147 self
148 }
149
150 pub fn icon(mut self, icon: impl Into<String>) -> Self {
152 self.icon = Some(icon.into());
153 self
154 }
155
156 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 pub fn enabled(mut self, enabled: bool) -> Self {
165 self.enabled = enabled;
166 self
167 }
168
169 pub fn lowered(self, _lowered: bool) -> Self {
171 self
174 }
175
176 pub fn svg_icon(mut self, svg_icon: SvgIcon) -> Self {
178 self.svg_icon = Some(svg_icon);
179 self
180 }
181
182 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) }
215 };
216
217 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
218
219 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 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 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 let corner_radius = match size_enum {
328 FabSize::Small => 12.0,
329 FabSize::Large => 16.0,
330 _ => 14.0,
331 };
332
333 ui.painter().rect_filled(
335 rect,
336 corner_radius,
337 bg_color,
338 );
339
340 match size_enum {
342 FabSize::Extended => {
343 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 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 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 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 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 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 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
433fn 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 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), );
448
449 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), );
458
459 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), );
468
469 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), );
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
500pub 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), },
508 SvgPath {
509 path: "M30 16H20l-4 4h14z".to_string(),
510 fill: Color32::from_rgb(66, 133, 244), },
512 SvgPath {
513 path: "M6 16v4h10l4-4z".to_string(),
514 fill: Color32::from_rgb(251, 188, 5), },
516 SvgPath {
517 path: "M20 16V6h-4v14z".to_string(),
518 fill: Color32::from_rgb(234, 67, 53), },
520 ],
521 viewbox_size: Vec2::new(36.0, 36.0),
522 }
523}