use crate::get_global_color;
use crate::icon::MaterialIcon;
use crate::material_symbol::material_symbol_text;
use egui::{self, Color32, Pos2, Rect, Response, Sense, Ui, Vec2, Widget};
#[derive(Clone, Copy, PartialEq)]
pub enum FabVariant {
Surface,
Primary,
Secondary,
Tertiary,
Branded,
}
#[derive(Clone, Copy, PartialEq, Debug)]
pub enum FabSize {
Small,
Regular,
Large,
Extended,
}
pub struct MaterialFab<'a> {
variant: FabVariant,
size: FabSize,
icon: Option<String>,
text: Option<String>,
svg_icon: Option<SvgIcon>,
svg_data: Option<String>,
enabled: bool,
action: Option<Box<dyn Fn() + 'a>>,
}
#[derive(Clone)]
pub struct SvgIcon {
pub paths: Vec<SvgPath>,
pub viewbox_size: Vec2,
}
#[derive(Clone)]
pub struct SvgPath {
pub path: String,
pub fill: Color32,
}
impl<'a> MaterialFab<'a> {
pub fn new(variant: FabVariant) -> Self {
Self {
variant,
size: FabSize::Regular,
icon: None,
text: None,
svg_icon: None,
svg_data: None,
enabled: true,
action: None,
}
}
pub fn surface() -> Self {
Self::new(FabVariant::Surface)
}
pub fn primary() -> Self {
Self::new(FabVariant::Primary)
}
pub fn secondary() -> Self {
Self::new(FabVariant::Secondary)
}
pub fn tertiary() -> Self {
Self::new(FabVariant::Tertiary)
}
pub fn branded() -> Self {
Self::new(FabVariant::Branded)
}
pub fn size(mut self, size: FabSize) -> Self {
self.size = size;
self
}
pub fn icon(mut self, icon: impl Into<String>) -> Self {
self.icon = Some(icon.into());
self
}
pub fn text(mut self, text: impl Into<String>) -> Self {
self.text = Some(text.into());
self.size = FabSize::Extended;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn lowered(self, _lowered: bool) -> Self {
self
}
pub fn svg_icon(mut self, svg_icon: SvgIcon) -> Self {
self.svg_icon = Some(svg_icon);
self
}
pub fn svg_data(mut self, svg_data: impl Into<String>) -> Self {
self.svg_data = Some(svg_data.into());
self
}
pub fn on_click<F>(mut self, f: F) -> Self
where
F: Fn() + 'a,
{
self.action = Some(Box::new(f));
self
}
}
impl<'a> Widget for MaterialFab<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let size = match self.size {
FabSize::Small => Vec2::splat(40.0),
FabSize::Regular => Vec2::splat(56.0),
FabSize::Large => Vec2::splat(96.0),
FabSize::Extended => {
let left_margin = 16.0;
let right_margin = 24.0;
let icon_width = if self.icon.is_some() || self.svg_icon.is_some() || self.svg_data.is_some() {
24.0 + 12.0
} else {
0.0
};
let text_width = if let Some(ref text) = self.text {
let font_id = egui::FontId::proportional(14.0);
ui.painter().layout_no_wrap(text.clone(), font_id, Color32::WHITE)
.size()
.x
} else {
0.0
};
let total_width = left_margin + icon_width + text_width + right_margin;
Vec2::new(total_width.max(80.0), 56.0) }
};
let (rect, response) = ui.allocate_exact_size(size, Sense::click());
let action = self.action;
let enabled = self.enabled;
let variant = self.variant;
let size_enum = self.size;
let icon = self.icon;
let text = self.text;
let svg_icon = self.svg_icon;
let svg_data = self.svg_data;
let clicked = response.clicked() && enabled;
if clicked {
if let Some(action) = action {
action();
}
}
let primary = get_global_color("primary"); let on_primary = get_global_color("onPrimary"); let secondary = get_global_color("secondary"); let on_secondary = get_global_color("onSecondary"); let tertiary = get_global_color("tertiary"); let on_tertiary = get_global_color("onTertiary"); let surface = get_global_color("surface"); let on_surface = get_global_color("onSurface"); let surface_container = get_global_color("surfaceContainer"); let surface_container_high = get_global_color("surfaceContainerHigh"); let surface_container_highest = get_global_color("surfaceContainerHighest"); let outline = get_global_color("outline");
let (bg_color, icon_color) = if !enabled {
(
surface_container,
outline.linear_multiply(0.38),
)
} else {
match variant {
FabVariant::Surface => {
if response.is_pointer_button_down_on() {
(surface_container_highest, on_surface)
} else if response.hovered() {
(surface_container_high, on_surface)
} else {
(surface, on_surface)
}
}
FabVariant::Primary => {
let base_color = primary;
let content_color = on_primary;
if response.is_pointer_button_down_on() {
(blend_state_layer(base_color, content_color, 0.12), content_color)
} else if response.hovered() {
(blend_state_layer(base_color, content_color, 0.08), content_color)
} else {
(base_color, content_color)
}
}
FabVariant::Secondary => {
let base_color = secondary;
let content_color = on_secondary;
if response.is_pointer_button_down_on() {
(blend_state_layer(base_color, content_color, 0.12), content_color)
} else if response.hovered() {
(blend_state_layer(base_color, content_color, 0.08), content_color)
} else {
(base_color, content_color)
}
}
FabVariant::Tertiary => {
let base_color = tertiary;
let content_color = on_tertiary;
if response.is_pointer_button_down_on() {
(blend_state_layer(base_color, content_color, 0.12), content_color)
} else if response.hovered() {
(blend_state_layer(base_color, content_color, 0.08), content_color)
} else {
(base_color, content_color)
}
}
FabVariant::Branded => {
let google_brand = Color32::from_rgb(66, 133, 244);
let content_color = Color32::WHITE; if response.is_pointer_button_down_on() {
(blend_state_layer(google_brand, content_color, 0.12), content_color)
} else if response.hovered() {
(blend_state_layer(google_brand, content_color, 0.08), content_color)
} else {
(google_brand, content_color)
}
}
}
};
let corner_radius = match size_enum {
FabSize::Small => 12.0,
FabSize::Large => 16.0,
_ => 14.0,
};
ui.painter().rect_filled(rect, corner_radius, bg_color);
match size_enum {
FabSize::Extended => {
let left_margin = 16.0;
let _right_margin = 24.0;
let icon_text_gap = 12.0;
let mut content_x = rect.min.x + left_margin;
if let Some(ref svg_str) = svg_data {
if let Ok(texture) = render_svg_to_texture(ui.ctx(), svg_str, 24) {
let icon_rect = Rect::from_center_size(
Pos2::new(content_x + 12.0, rect.center().y),
Vec2::splat(24.0),
);
ui.painter().image(
texture.id(),
icon_rect,
Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)),
Color32::WHITE,
);
}
content_x += 24.0 + icon_text_gap;
} else if let Some(ref icon_name) = icon {
let icon_rect = Rect::from_min_size(
Pos2::new(content_x, rect.center().y - 12.0),
Vec2::splat(24.0),
);
let icon_char = material_symbol_text(icon_name);
let icon = MaterialIcon::new(icon_char).size(24.0).color(icon_color);
ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
ui.add(icon);
});
content_x += 24.0 + icon_text_gap;
} else if let Some(ref _svg_icon) = svg_icon {
draw_google_logo(ui, Pos2::new(content_x + 12.0, rect.center().y), 24.0);
content_x += 24.0 + icon_text_gap;
}
if let Some(ref text) = text {
let text_pos = Pos2::new(content_x, rect.center().y);
ui.painter().text(
text_pos,
egui::Align2::LEFT_CENTER,
text,
egui::FontId::proportional(14.0),
icon_color,
);
}
}
_ => {
if let Some(ref svg_str) = svg_data {
let icon_size = match size_enum {
FabSize::Small => 18,
FabSize::Large => 36,
_ => 24,
};
if let Ok(texture) = render_svg_to_texture(ui.ctx(), svg_str, icon_size) {
let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size as f32));
ui.painter().image(
texture.id(),
icon_rect,
Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)),
Color32::WHITE,
);
}
} else if let Some(ref _svg_icon) = svg_icon {
let icon_size = match size_enum {
FabSize::Small => 18.0,
FabSize::Large => 36.0,
_ => 24.0,
};
draw_google_logo(ui, rect.center(), icon_size);
} else if let Some(ref icon_name) = icon {
let icon_size = match size_enum {
FabSize::Small => 18.0,
FabSize::Large => 36.0,
_ => 24.0,
};
let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
let icon_char = material_symbol_text(icon_name);
let icon = MaterialIcon::new(icon_char)
.size(icon_size)
.color(icon_color);
ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
ui.add(icon);
});
} else {
let icon_size = match size_enum {
FabSize::Small => 18.0,
FabSize::Large => 36.0,
_ => 24.0,
};
let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
let icon_char = material_symbol_text("add");
let icon = MaterialIcon::new(icon_char).size(icon_size).color(icon_color);
ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
ui.add(icon);
});
}
}
}
response
}
}
fn blend_state_layer(base: Color32, overlay: Color32, opacity: f32) -> Color32 {
let alpha = (opacity * 255.0) as u8;
let overlay_with_alpha = Color32::from_rgba_unmultiplied(overlay.r(), overlay.g(), overlay.b(), alpha);
let inv_alpha = 255 - alpha;
Color32::from_rgba_unmultiplied(
((base.r() as u16 * inv_alpha as u16 + overlay_with_alpha.r() as u16 * alpha as u16) / 255) as u8,
((base.g() as u16 * inv_alpha as u16 + overlay_with_alpha.g() as u16 * alpha as u16) / 255) as u8,
((base.b() as u16 * inv_alpha as u16 + overlay_with_alpha.b() as u16 * alpha as u16) / 255) as u8,
base.a(),
)
}
fn draw_google_logo(ui: &mut Ui, center: Pos2, size: f32) {
let half_size = size / 2.0;
let quarter_size = size / 4.0;
ui.painter().rect_filled(
Rect::from_min_size(
Pos2::new(center.x, center.y - half_size),
Vec2::new(half_size, quarter_size),
),
0.0,
Color32::from_rgb(52, 168, 83), );
ui.painter().rect_filled(
Rect::from_min_size(
Pos2::new(center.x, center.y - quarter_size),
Vec2::new(half_size, half_size),
),
0.0,
Color32::from_rgb(66, 133, 244), );
ui.painter().rect_filled(
Rect::from_min_size(
Pos2::new(center.x - half_size, center.y + quarter_size),
Vec2::new(half_size, quarter_size),
),
0.0,
Color32::from_rgb(251, 188, 5), );
ui.painter().rect_filled(
Rect::from_min_size(
Pos2::new(center.x - half_size, center.y - half_size),
Vec2::new(quarter_size, size),
),
0.0,
Color32::from_rgb(234, 67, 53), );
}
fn render_svg_to_texture(
ctx: &egui::Context,
svg_data: &str,
size: u32,
) -> Result<egui::TextureHandle, String> {
use resvg::{usvg, tiny_skia};
let tree = usvg::Tree::from_str(svg_data, &usvg::Options::default())
.map_err(|e| e.to_string())?;
let mut pixmap =
tiny_skia::Pixmap::new(size, size).ok_or_else(|| "pixmap alloc failed".to_string())?;
let ts = tree.size();
let scale = (size as f32 / ts.width()).min(size as f32 / ts.height());
resvg::render(
&tree,
tiny_skia::Transform::from_scale(scale, scale),
&mut pixmap.as_mut(),
);
let color_image = egui::ColorImage::from_rgba_unmultiplied(
[size as usize, size as usize],
pixmap.data(),
);
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
svg_data.hash(&mut hasher);
size.hash(&mut hasher);
let key = format!("fab_svg_{:x}", hasher.finish());
Ok(ctx.load_texture(key, color_image, egui::TextureOptions::LINEAR))
}
pub fn fab_surface() -> MaterialFab<'static> {
MaterialFab::surface()
}
pub fn fab_primary() -> MaterialFab<'static> {
MaterialFab::primary()
}
pub fn fab_secondary() -> MaterialFab<'static> {
MaterialFab::secondary()
}
pub fn fab_tertiary() -> MaterialFab<'static> {
MaterialFab::tertiary()
}
pub fn fab_branded() -> MaterialFab<'static> {
MaterialFab::branded()
}
pub fn google_branded_icon() -> SvgIcon {
SvgIcon {
paths: vec![
SvgPath {
path: "M16 16v14h4V20z".to_string(),
fill: Color32::from_rgb(52, 168, 83), },
SvgPath {
path: "M30 16H20l-4 4h14z".to_string(),
fill: Color32::from_rgb(66, 133, 244), },
SvgPath {
path: "M6 16v4h10l4-4z".to_string(),
fill: Color32::from_rgb(251, 188, 5), },
SvgPath {
path: "M20 16V6h-4v14z".to_string(),
fill: Color32::from_rgb(234, 67, 53), },
],
viewbox_size: Vec2::new(36.0, 36.0),
}
}