1use crate::get_global_color;
2use crate::icon::MaterialIcon;
3use crate::material_symbol::material_symbol_text;
4use egui::{self, Color32, Pos2, Rect, Response, Sense, Ui, Vec2, Widget};
5
6#[derive(Clone, Copy, PartialEq)]
8pub enum FabVariant {
9 Surface,
11 Primary,
13 Secondary,
15 Tertiary,
17 Branded,
19}
20
21#[derive(Clone, Copy, PartialEq, Debug)]
23pub enum FabSize {
24 Small,
26 Regular,
28 Large,
30 Extended,
32}
33
34pub struct MaterialFab<'a> {
69 variant: FabVariant,
71 size: FabSize,
73 icon: Option<String>,
75 text: Option<String>,
77 svg_icon: Option<SvgIcon>,
79 svg_data: Option<String>,
81 enabled: bool,
83 action: Option<Box<dyn Fn() + 'a>>,
85}
86
87#[derive(Clone)]
89pub struct SvgIcon {
90 pub paths: Vec<SvgPath>,
92 pub viewbox_size: Vec2,
94}
95
96#[derive(Clone)]
98pub struct SvgPath {
99 pub path: String,
101 pub fill: Color32,
103}
104
105impl<'a> MaterialFab<'a> {
106 pub fn new(variant: FabVariant) -> Self {
111 Self {
112 variant,
113 size: FabSize::Regular,
114 icon: None,
115 text: None,
116 svg_icon: None,
117 svg_data: None,
118 enabled: true,
119 action: None,
120 }
121 }
122
123 pub fn surface() -> Self {
125 Self::new(FabVariant::Surface)
126 }
127
128 pub fn primary() -> Self {
130 Self::new(FabVariant::Primary)
131 }
132
133 pub fn secondary() -> Self {
135 Self::new(FabVariant::Secondary)
136 }
137
138 pub fn tertiary() -> Self {
140 Self::new(FabVariant::Tertiary)
141 }
142
143 pub fn branded() -> Self {
145 Self::new(FabVariant::Branded)
146 }
147
148 pub fn size(mut self, size: FabSize) -> Self {
150 self.size = size;
151 self
152 }
153
154 pub fn icon(mut self, icon: impl Into<String>) -> Self {
156 self.icon = Some(icon.into());
157 self
158 }
159
160 pub fn text(mut self, text: impl Into<String>) -> Self {
162 self.text = Some(text.into());
163 self.size = FabSize::Extended;
164 self
165 }
166
167 pub fn enabled(mut self, enabled: bool) -> Self {
169 self.enabled = enabled;
170 self
171 }
172
173 pub fn lowered(self, _lowered: bool) -> Self {
175 self
178 }
179
180 pub fn svg_icon(mut self, svg_icon: SvgIcon) -> Self {
182 self.svg_icon = Some(svg_icon);
183 self
184 }
185
186 pub fn svg_data(mut self, svg_data: impl Into<String>) -> Self {
188 self.svg_data = Some(svg_data.into());
189 self
190 }
191
192 pub fn on_click<F>(mut self, f: F) -> Self
194 where
195 F: Fn() + 'a,
196 {
197 self.action = Some(Box::new(f));
198 self
199 }
200}
201
202impl<'a> Widget for MaterialFab<'a> {
203 fn ui(self, ui: &mut Ui) -> Response {
204 let size = match self.size {
205 FabSize::Small => Vec2::splat(40.0),
206 FabSize::Regular => Vec2::splat(56.0),
207 FabSize::Large => Vec2::splat(96.0),
208 FabSize::Extended => {
209 let left_margin = 16.0;
210 let right_margin = 24.0;
211 let icon_width = if self.icon.is_some() || self.svg_icon.is_some() || self.svg_data.is_some() {
212 24.0 + 12.0
213 } else {
214 0.0
215 };
216
217 let text_width = if let Some(ref text) = self.text {
218 let font_id = egui::FontId::proportional(14.0);
219 ui.painter().layout_no_wrap(text.clone(), font_id, Color32::WHITE)
220 .size()
221 .x
222 } else {
223 0.0
224 };
225
226 let total_width = left_margin + icon_width + text_width + right_margin;
227 Vec2::new(total_width.max(80.0), 56.0) }
229 };
230
231 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
232
233 let action = self.action;
235 let enabled = self.enabled;
236 let variant = self.variant;
237 let size_enum = self.size;
238 let icon = self.icon;
239 let text = self.text;
240 let svg_icon = self.svg_icon;
241 let svg_data = self.svg_data;
242
243 let clicked = response.clicked() && enabled;
244
245 if clicked {
246 if let Some(action) = action {
247 action();
248 }
249 }
250
251 let primary_color = get_global_color("primary");
253 let secondary_color = get_global_color("secondary");
254 let tertiary_color = get_global_color("tertiary");
255 let surface = get_global_color("surface");
256 let on_primary = get_global_color("onPrimary");
257 let on_surface = get_global_color("onSurface");
258
259 let (bg_color, icon_color) = if !enabled {
260 (
261 get_global_color("surfaceContainer"),
262 get_global_color("outline"),
263 )
264 } else {
265 match variant {
266 FabVariant::Surface => {
267 if response.is_pointer_button_down_on() {
268 (get_global_color("surfaceContainerHighest"), on_surface)
269 } else if response.hovered() {
270 (get_global_color("surfaceContainerHigh"), on_surface)
271 } else {
272 (surface, on_surface)
273 }
274 }
275 FabVariant::Primary => {
276 if response.hovered() || response.is_pointer_button_down_on() {
277 let lighten_amount = if response.is_pointer_button_down_on() { 40 } else { 20 };
278 (
279 Color32::from_rgba_premultiplied(
280 primary_color.r().saturating_add(lighten_amount),
281 primary_color.g().saturating_add(lighten_amount),
282 primary_color.b().saturating_add(lighten_amount),
283 255,
284 ),
285 on_primary,
286 )
287 } else {
288 (primary_color, on_primary)
289 }
290 }
291 FabVariant::Secondary => {
292 if response.hovered() || response.is_pointer_button_down_on() {
293 let lighten_amount = if response.is_pointer_button_down_on() { 40 } else { 20 };
294 (
295 Color32::from_rgba_premultiplied(
296 secondary_color.r().saturating_add(lighten_amount),
297 secondary_color.g().saturating_add(lighten_amount),
298 secondary_color.b().saturating_add(lighten_amount),
299 255,
300 ),
301 on_primary,
302 )
303 } else {
304 (secondary_color, on_primary)
305 }
306 }
307 FabVariant::Tertiary => {
308 if response.hovered() || response.is_pointer_button_down_on() {
309 let lighten_amount = if response.is_pointer_button_down_on() { 40 } else { 20 };
310 (
311 Color32::from_rgba_premultiplied(
312 tertiary_color.r().saturating_add(lighten_amount),
313 tertiary_color.g().saturating_add(lighten_amount),
314 tertiary_color.b().saturating_add(lighten_amount),
315 255,
316 ),
317 on_primary,
318 )
319 } else {
320 (tertiary_color, on_primary)
321 }
322 }
323 FabVariant::Branded => {
324 let google_brand = Color32::from_rgb(66, 133, 244);
326 if response.hovered() || response.is_pointer_button_down_on() {
327 let lighten_amount = if response.is_pointer_button_down_on() { 40 } else { 20 };
328 (
329 Color32::from_rgba_premultiplied(
330 google_brand.r().saturating_add(lighten_amount),
331 google_brand.g().saturating_add(lighten_amount),
332 google_brand.b().saturating_add(lighten_amount),
333 255,
334 ),
335 on_primary,
336 )
337 } else {
338 (google_brand, on_primary)
339 }
340 }
341 }
342 };
343
344 let corner_radius = match size_enum {
346 FabSize::Small => 12.0,
347 FabSize::Large => 16.0,
348 _ => 14.0,
349 };
350
351 ui.painter().rect_filled(rect, corner_radius, bg_color);
353
354 match size_enum {
356 FabSize::Extended => {
357 let left_margin = 16.0;
359 let _right_margin = 24.0;
360 let icon_text_gap = 12.0;
361 let mut content_x = rect.min.x + left_margin;
362
363 if let Some(ref svg_str) = svg_data {
364 if let Ok(texture) = render_svg_to_texture(ui.ctx(), svg_str, 24) {
366 let icon_rect = Rect::from_center_size(
367 Pos2::new(content_x + 12.0, rect.center().y),
368 Vec2::splat(24.0),
369 );
370 ui.painter().image(
371 texture.id(),
372 icon_rect,
373 Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)),
374 Color32::WHITE,
375 );
376 }
377 content_x += 24.0 + icon_text_gap;
378 } else if let Some(ref icon_name) = icon {
379 let icon_rect = Rect::from_min_size(
380 Pos2::new(content_x, rect.center().y - 12.0),
381 Vec2::splat(24.0),
382 );
383
384 let icon_char = material_symbol_text(icon_name);
386 let icon = MaterialIcon::new(icon_char).size(24.0).color(icon_color);
387 ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
388 ui.add(icon);
389 });
390
391 content_x += 24.0 + icon_text_gap;
392 } else if let Some(ref _svg_icon) = svg_icon {
393 draw_google_logo(ui, Pos2::new(content_x + 12.0, rect.center().y), 24.0);
395 content_x += 24.0 + icon_text_gap;
396 }
397
398 if let Some(ref text) = text {
399 let text_pos = Pos2::new(content_x, rect.center().y);
400 ui.painter().text(
401 text_pos,
402 egui::Align2::LEFT_CENTER,
403 text,
404 egui::FontId::proportional(14.0),
405 icon_color,
406 );
407 }
408 }
409 _ => {
410 if let Some(ref svg_str) = svg_data {
412 let icon_size = match size_enum {
413 FabSize::Small => 18,
414 FabSize::Large => 36,
415 _ => 24,
416 };
417
418 if let Ok(texture) = render_svg_to_texture(ui.ctx(), svg_str, icon_size) {
420 let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size as f32));
421 ui.painter().image(
422 texture.id(),
423 icon_rect,
424 Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)),
425 Color32::WHITE,
426 );
427 }
428 } else if let Some(ref _svg_icon) = svg_icon {
429 let icon_size = match size_enum {
430 FabSize::Small => 18.0,
431 FabSize::Large => 36.0,
432 _ => 24.0,
433 };
434
435 draw_google_logo(ui, rect.center(), icon_size);
437 } else if let Some(ref icon_name) = icon {
438 let icon_size = match size_enum {
439 FabSize::Small => 18.0,
440 FabSize::Large => 36.0,
441 _ => 24.0,
442 };
443
444 let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
445 let icon_char = material_symbol_text(icon_name);
446 let icon = MaterialIcon::new(icon_char)
447 .size(icon_size)
448 .color(icon_color);
449 ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
450 ui.add(icon);
451 });
452 } else {
453 let icon_size = match size_enum {
455 FabSize::Small => 18.0,
456 FabSize::Large => 36.0,
457 _ => 24.0,
458 };
459
460 let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
461 let icon_char = material_symbol_text("add");
462 let icon = MaterialIcon::new(icon_char).size(icon_size).color(icon_color);
463 ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
464 ui.add(icon);
465 });
466 }
467 }
468 }
469
470 response
471 }
472}
473
474fn draw_google_logo(ui: &mut Ui, center: Pos2, size: f32) {
476 let half_size = size / 2.0;
477 let quarter_size = size / 4.0;
478
479 ui.painter().rect_filled(
482 Rect::from_min_size(
483 Pos2::new(center.x, center.y - half_size),
484 Vec2::new(half_size, quarter_size),
485 ),
486 0.0,
487 Color32::from_rgb(52, 168, 83), );
489
490 ui.painter().rect_filled(
492 Rect::from_min_size(
493 Pos2::new(center.x, center.y - quarter_size),
494 Vec2::new(half_size, half_size),
495 ),
496 0.0,
497 Color32::from_rgb(66, 133, 244), );
499
500 ui.painter().rect_filled(
502 Rect::from_min_size(
503 Pos2::new(center.x - half_size, center.y + quarter_size),
504 Vec2::new(half_size, quarter_size),
505 ),
506 0.0,
507 Color32::from_rgb(251, 188, 5), );
509
510 ui.painter().rect_filled(
512 Rect::from_min_size(
513 Pos2::new(center.x - half_size, center.y - half_size),
514 Vec2::new(quarter_size, size),
515 ),
516 0.0,
517 Color32::from_rgb(234, 67, 53), );
519}
520
521fn render_svg_to_texture(
523 ctx: &egui::Context,
524 svg_data: &str,
525 size: u32,
526) -> Result<egui::TextureHandle, String> {
527 use resvg::{usvg, tiny_skia};
528
529 let tree = usvg::Tree::from_str(svg_data, &usvg::Options::default())
530 .map_err(|e| e.to_string())?;
531 let mut pixmap =
532 tiny_skia::Pixmap::new(size, size).ok_or_else(|| "pixmap alloc failed".to_string())?;
533
534 let ts = tree.size();
535 let scale = (size as f32 / ts.width()).min(size as f32 / ts.height());
536 resvg::render(
537 &tree,
538 tiny_skia::Transform::from_scale(scale, scale),
539 &mut pixmap.as_mut(),
540 );
541
542 let color_image = egui::ColorImage::from_rgba_unmultiplied(
543 [size as usize, size as usize],
544 pixmap.data(),
545 );
546
547 use std::collections::hash_map::DefaultHasher;
549 use std::hash::{Hash, Hasher};
550 let mut hasher = DefaultHasher::new();
551 svg_data.hash(&mut hasher);
552 size.hash(&mut hasher);
553 let key = format!("fab_svg_{:x}", hasher.finish());
554
555 Ok(ctx.load_texture(key, color_image, egui::TextureOptions::LINEAR))
556}
557
558pub fn fab_surface() -> MaterialFab<'static> {
559 MaterialFab::surface()
560}
561
562pub fn fab_primary() -> MaterialFab<'static> {
563 MaterialFab::primary()
564}
565
566pub fn fab_secondary() -> MaterialFab<'static> {
567 MaterialFab::secondary()
568}
569
570pub fn fab_tertiary() -> MaterialFab<'static> {
571 MaterialFab::tertiary()
572}
573
574pub fn fab_branded() -> MaterialFab<'static> {
575 MaterialFab::branded()
576}
577
578pub fn google_branded_icon() -> SvgIcon {
580 SvgIcon {
581 paths: vec![
582 SvgPath {
583 path: "M16 16v14h4V20z".to_string(),
584 fill: Color32::from_rgb(52, 168, 83), },
586 SvgPath {
587 path: "M30 16H20l-4 4h14z".to_string(),
588 fill: Color32::from_rgb(66, 133, 244), },
590 SvgPath {
591 path: "M6 16v4h10l4-4z".to_string(),
592 fill: Color32::from_rgb(251, 188, 5), },
594 SvgPath {
595 path: "M20 16V6h-4v14z".to_string(),
596 fill: Color32::from_rgb(234, 67, 53), },
598 ],
599 viewbox_size: Vec2::new(36.0, 36.0),
600 }
601}