1use crate::get_global_color;
2use crate::icon::MaterialIcon;
3use crate::material_symbol::material_symbol_text;
4use eframe::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 enabled: bool,
81 action: Option<Box<dyn Fn() + 'a>>,
83}
84
85#[derive(Clone)]
87pub struct SvgIcon {
88 pub paths: Vec<SvgPath>,
90 pub viewbox_size: Vec2,
92}
93
94#[derive(Clone)]
96pub struct SvgPath {
97 pub path: String,
99 pub fill: Color32,
101}
102
103impl<'a> MaterialFab<'a> {
104 pub fn new(variant: FabVariant) -> Self {
109 Self {
110 variant,
111 size: FabSize::Regular,
112 icon: None,
113 text: None,
114 svg_icon: None,
115 enabled: true,
116 action: None,
117 }
118 }
119
120 pub fn surface() -> Self {
122 Self::new(FabVariant::Surface)
123 }
124
125 pub fn primary() -> Self {
127 Self::new(FabVariant::Primary)
128 }
129
130 pub fn secondary() -> Self {
132 Self::new(FabVariant::Secondary)
133 }
134
135 pub fn tertiary() -> Self {
137 Self::new(FabVariant::Tertiary)
138 }
139
140 pub fn branded() -> Self {
142 Self::new(FabVariant::Branded)
143 }
144
145 pub fn size(mut self, size: FabSize) -> Self {
147 self.size = size;
148 self
149 }
150
151 pub fn icon(mut self, icon: impl Into<String>) -> Self {
153 self.icon = Some(icon.into());
154 self
155 }
156
157 pub fn text(mut self, text: impl Into<String>) -> Self {
159 self.text = Some(text.into());
160 self.size = FabSize::Extended;
161 self
162 }
163
164 pub fn enabled(mut self, enabled: bool) -> Self {
166 self.enabled = enabled;
167 self
168 }
169
170 pub fn lowered(self, _lowered: bool) -> Self {
172 self
175 }
176
177 pub fn svg_icon(mut self, svg_icon: SvgIcon) -> Self {
179 self.svg_icon = Some(svg_icon);
180 self
181 }
182
183 pub fn on_click<F>(mut self, f: F) -> Self
185 where
186 F: Fn() + 'a,
187 {
188 self.action = Some(Box::new(f));
189 self
190 }
191}
192
193impl<'a> Widget for MaterialFab<'a> {
194 fn ui(self, ui: &mut Ui) -> Response {
195 let size = match self.size {
196 FabSize::Small => Vec2::splat(40.0),
197 FabSize::Regular => Vec2::splat(56.0),
198 FabSize::Large => Vec2::splat(96.0),
199 FabSize::Extended => {
200 let left_margin = 16.0;
201 let right_margin = 24.0;
202 let icon_width = if self.icon.is_some() || self.svg_icon.is_some() {
203 24.0 + 12.0
204 } else {
205 0.0
206 };
207
208 let text_width = if let Some(ref text) = self.text {
209 ui.fonts(|fonts| {
210 let font_id = egui::FontId::proportional(14.0);
211 fonts
212 .layout_no_wrap(text.clone(), font_id, Color32::WHITE)
213 .size()
214 .x
215 })
216 } else {
217 0.0
218 };
219
220 let total_width = left_margin + icon_width + text_width + right_margin;
221 Vec2::new(total_width.max(80.0), 56.0) }
223 };
224
225 let (rect, response) = ui.allocate_exact_size(size, Sense::click());
226
227 let action = self.action;
229 let enabled = self.enabled;
230 let variant = self.variant;
231 let size_enum = self.size;
232 let icon = self.icon;
233 let text = self.text;
234 let svg_icon = self.svg_icon;
235
236 let clicked = response.clicked() && enabled;
237
238 if clicked {
239 if let Some(action) = action {
240 action();
241 }
242 }
243
244 let primary_color = get_global_color("primary");
246 let secondary_color = get_global_color("secondary");
247 let tertiary_color = get_global_color("tertiary");
248 let surface = get_global_color("surface");
249 let on_primary = get_global_color("onPrimary");
250 let on_surface = get_global_color("onSurface");
251
252 let (bg_color, icon_color) = if !enabled {
253 (
254 get_global_color("surfaceContainer"),
255 get_global_color("outline"),
256 )
257 } else {
258 match variant {
259 FabVariant::Surface => {
260 if response.is_pointer_button_down_on() {
261 (get_global_color("surfaceContainerHighest"), on_surface)
262 } else if response.hovered() {
263 (get_global_color("surfaceContainerHigh"), on_surface)
264 } else {
265 (surface, on_surface)
266 }
267 }
268 FabVariant::Primary => {
269 if response.hovered() || response.is_pointer_button_down_on() {
270 let lighten_amount = if response.is_pointer_button_down_on() { 40 } else { 20 };
271 (
272 Color32::from_rgba_premultiplied(
273 primary_color.r().saturating_add(lighten_amount),
274 primary_color.g().saturating_add(lighten_amount),
275 primary_color.b().saturating_add(lighten_amount),
276 255,
277 ),
278 on_primary,
279 )
280 } else {
281 (primary_color, on_primary)
282 }
283 }
284 FabVariant::Secondary => {
285 if response.hovered() || response.is_pointer_button_down_on() {
286 let lighten_amount = if response.is_pointer_button_down_on() { 40 } else { 20 };
287 (
288 Color32::from_rgba_premultiplied(
289 secondary_color.r().saturating_add(lighten_amount),
290 secondary_color.g().saturating_add(lighten_amount),
291 secondary_color.b().saturating_add(lighten_amount),
292 255,
293 ),
294 on_primary,
295 )
296 } else {
297 (secondary_color, on_primary)
298 }
299 }
300 FabVariant::Tertiary => {
301 if response.hovered() || response.is_pointer_button_down_on() {
302 let lighten_amount = if response.is_pointer_button_down_on() { 40 } else { 20 };
303 (
304 Color32::from_rgba_premultiplied(
305 tertiary_color.r().saturating_add(lighten_amount),
306 tertiary_color.g().saturating_add(lighten_amount),
307 tertiary_color.b().saturating_add(lighten_amount),
308 255,
309 ),
310 on_primary,
311 )
312 } else {
313 (tertiary_color, on_primary)
314 }
315 }
316 FabVariant::Branded => {
317 let google_brand = Color32::from_rgb(66, 133, 244);
319 if response.hovered() || response.is_pointer_button_down_on() {
320 let lighten_amount = if response.is_pointer_button_down_on() { 40 } else { 20 };
321 (
322 Color32::from_rgba_premultiplied(
323 google_brand.r().saturating_add(lighten_amount),
324 google_brand.g().saturating_add(lighten_amount),
325 google_brand.b().saturating_add(lighten_amount),
326 255,
327 ),
328 on_primary,
329 )
330 } else {
331 (google_brand, on_primary)
332 }
333 }
334 }
335 };
336
337 let corner_radius = match size_enum {
339 FabSize::Small => 12.0,
340 FabSize::Large => 16.0,
341 _ => 14.0,
342 };
343
344 ui.painter().rect_filled(rect, corner_radius, bg_color);
346
347 match size_enum {
349 FabSize::Extended => {
350 let left_margin = 16.0;
352 let _right_margin = 24.0;
353 let icon_text_gap = 12.0;
354 let mut content_x = rect.min.x + left_margin;
355
356 if let Some(ref icon_name) = icon {
357 let icon_rect = Rect::from_min_size(
358 Pos2::new(content_x, rect.center().y - 12.0),
359 Vec2::splat(24.0),
360 );
361
362 let icon_char = material_symbol_text(icon_name);
364 let icon = MaterialIcon::new(icon_char).size(24.0).color(icon_color);
365 ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
366 ui.add(icon);
367 });
368
369 content_x += 24.0 + icon_text_gap;
370 } else if let Some(ref _svg_icon) = svg_icon {
371 draw_google_logo(ui, Pos2::new(content_x + 12.0, rect.center().y), 24.0);
373 content_x += 24.0 + icon_text_gap;
374 }
375
376 if let Some(ref text) = text {
377 let text_pos = Pos2::new(content_x, rect.center().y);
378 ui.painter().text(
379 text_pos,
380 egui::Align2::LEFT_CENTER,
381 text,
382 egui::FontId::proportional(14.0),
383 icon_color,
384 );
385 }
386 }
387 _ => {
388 if let Some(ref _svg_icon) = svg_icon {
390 let icon_size = match size_enum {
391 FabSize::Small => 18.0,
392 FabSize::Large => 36.0,
393 _ => 24.0,
394 };
395
396 draw_google_logo(ui, rect.center(), icon_size);
398 } else if let Some(ref icon_name) = icon {
399 let icon_size = match size_enum {
400 FabSize::Small => 18.0,
401 FabSize::Large => 36.0,
402 _ => 24.0,
403 };
404
405 let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
406 let icon_char = material_symbol_text(icon_name);
407 let icon = MaterialIcon::new(icon_char)
408 .size(icon_size)
409 .color(icon_color);
410 ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
411 ui.add(icon);
412 });
413 } else {
414 let icon_size = match size_enum {
416 FabSize::Small => 18.0,
417 FabSize::Large => 36.0,
418 _ => 24.0,
419 };
420
421 let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
422 let icon_char = material_symbol_text("add");
423 let icon = MaterialIcon::new(icon_char).size(icon_size).color(icon_color);
424 ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
425 ui.add(icon);
426 });
427 }
428 }
429 }
430
431 response
432 }
433}
434
435fn draw_google_logo(ui: &mut Ui, center: Pos2, size: f32) {
437 let half_size = size / 2.0;
438 let quarter_size = size / 4.0;
439
440 ui.painter().rect_filled(
443 Rect::from_min_size(
444 Pos2::new(center.x, center.y - half_size),
445 Vec2::new(half_size, quarter_size),
446 ),
447 0.0,
448 Color32::from_rgb(52, 168, 83), );
450
451 ui.painter().rect_filled(
453 Rect::from_min_size(
454 Pos2::new(center.x, center.y - quarter_size),
455 Vec2::new(half_size, half_size),
456 ),
457 0.0,
458 Color32::from_rgb(66, 133, 244), );
460
461 ui.painter().rect_filled(
463 Rect::from_min_size(
464 Pos2::new(center.x - half_size, center.y + quarter_size),
465 Vec2::new(half_size, quarter_size),
466 ),
467 0.0,
468 Color32::from_rgb(251, 188, 5), );
470
471 ui.painter().rect_filled(
473 Rect::from_min_size(
474 Pos2::new(center.x - half_size, center.y - half_size),
475 Vec2::new(quarter_size, size),
476 ),
477 0.0,
478 Color32::from_rgb(234, 67, 53), );
480}
481
482pub fn fab_surface() -> MaterialFab<'static> {
483 MaterialFab::surface()
484}
485
486pub fn fab_primary() -> MaterialFab<'static> {
487 MaterialFab::primary()
488}
489
490pub fn fab_secondary() -> MaterialFab<'static> {
491 MaterialFab::secondary()
492}
493
494pub fn fab_tertiary() -> MaterialFab<'static> {
495 MaterialFab::tertiary()
496}
497
498pub fn fab_branded() -> MaterialFab<'static> {
499 MaterialFab::branded()
500}
501
502pub fn google_branded_icon() -> SvgIcon {
504 SvgIcon {
505 paths: vec![
506 SvgPath {
507 path: "M16 16v14h4V20z".to_string(),
508 fill: Color32::from_rgb(52, 168, 83), },
510 SvgPath {
511 path: "M30 16H20l-4 4h14z".to_string(),
512 fill: Color32::from_rgb(66, 133, 244), },
514 SvgPath {
515 path: "M6 16v4h10l4-4z".to_string(),
516 fill: Color32::from_rgb(251, 188, 5), },
518 SvgPath {
519 path: "M20 16V6h-4v14z".to_string(),
520 fill: Color32::from_rgb(234, 67, 53), },
522 ],
523 viewbox_size: Vec2::new(36.0, 36.0),
524 }
525}