use crate::get_global_color;
use egui::{
Align2, Color32, ColorImage, FontId, Rect, Response, Sense, Stroke, TextureHandle, TextureOptions, Ui, Vec2,
Widget,
};
use std::path::Path;
use std::fs;
use std::collections::HashMap;
use std::sync::Arc;
use std::sync::Mutex;
use resvg::usvg::{Options, Tree};
use resvg::tiny_skia::{Pixmap, Transform};
use resvg::render;
lazy_static::lazy_static! {
static ref SVG_IMAGE_CACHE: Mutex<HashMap<String, Arc<ColorImage>>> = Mutex::new(HashMap::new());
}
#[derive(Clone, Copy, PartialEq)]
pub enum IconButtonVariant {
Standard,
Filled,
FilledTonal,
Outlined,
}
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct MaterialIconButton<'a> {
icon: String,
variant: IconButtonVariant,
selected: Option<&'a mut bool>,
enabled: bool,
size: f32,
container: bool,
svg_path: Option<String>,
svg_data: Option<String>,
icon_color_override: Option<Color32>,
action: Option<Box<dyn Fn() + 'a>>,
}
impl<'a> MaterialIconButton<'a> {
pub fn new(icon: impl Into<String>, variant: IconButtonVariant) -> Self {
Self {
icon: icon.into(),
variant,
selected: None,
enabled: true,
size: 40.0,
container: false, svg_path: None,
svg_data: None,
icon_color_override: None,
action: None,
}
}
pub fn standard(icon: impl Into<String>) -> Self {
Self::new(icon, IconButtonVariant::Standard)
}
pub fn filled(icon: impl Into<String>) -> Self {
Self::new(icon, IconButtonVariant::Filled)
}
pub fn filled_tonal(icon: impl Into<String>) -> Self {
Self::new(icon, IconButtonVariant::FilledTonal)
}
pub fn outlined(icon: impl Into<String>) -> Self {
Self::new(icon, IconButtonVariant::Outlined)
}
pub fn toggle(icon: impl Into<String>, selected: &'a mut bool) -> Self {
let mut button = Self::standard(icon);
button.selected = Some(selected);
button
}
pub fn size(mut self, size: f32) -> Self {
self.size = size;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
self
}
pub fn container(mut self, container: bool) -> Self {
self.container = container;
self
}
pub fn svg(mut self, path: impl Into<String>) -> Self {
self.svg_path = Some(path.into());
self
}
pub fn svg_data(mut self, svg_content: impl Into<String>) -> Self {
self.svg_data = Some(svg_content.into());
self
}
pub fn icon_color(mut self, color: Color32) -> Self {
self.icon_color_override = Some(color);
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 MaterialIconButton<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let desired_size = Vec2::splat(self.size);
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
let is_selected = self.selected.as_ref().map_or(false, |s| **s);
if response.clicked() && self.enabled {
if let Some(selected) = self.selected {
*selected = !*selected;
response.mark_changed();
}
if let Some(action) = self.action {
action();
}
}
let primary_color = get_global_color("primary");
let secondary_container = get_global_color("secondaryContainer");
let on_secondary_container = get_global_color("onSecondaryContainer");
let _surface = get_global_color("surface");
let on_surface = get_global_color("onSurface");
let on_surface_variant = get_global_color("onSurfaceVariant");
let outline = get_global_color("outline");
let (bg_color, icon_color, border_color) = if !self.enabled {
(
get_global_color("surfaceContainer"),
get_global_color("outline"),
Color32::TRANSPARENT,
)
} else {
match self.variant {
IconButtonVariant::Standard => {
if is_selected {
(Color32::TRANSPARENT, primary_color, Color32::TRANSPARENT)
} else if response.hovered() {
(
Color32::from_rgba_premultiplied(
on_surface.r(),
on_surface.g(),
on_surface.b(),
20,
),
on_surface,
Color32::TRANSPARENT,
)
} else {
(
Color32::TRANSPARENT,
on_surface_variant,
Color32::TRANSPARENT,
)
}
}
IconButtonVariant::Filled => {
if is_selected {
(
primary_color,
get_global_color("onPrimary"),
Color32::TRANSPARENT,
)
} else if response.hovered() || response.is_pointer_button_down_on() {
let lighten_amount = if response.is_pointer_button_down_on() { 40 } else { 20 };
(
Color32::from_rgba_premultiplied(
primary_color.r().saturating_add(lighten_amount),
primary_color.g().saturating_add(lighten_amount),
primary_color.b().saturating_add(lighten_amount),
255,
),
get_global_color("onPrimary"),
Color32::TRANSPARENT,
)
} else {
(primary_color, get_global_color("onPrimary"), Color32::TRANSPARENT)
}
}
IconButtonVariant::FilledTonal => {
if is_selected {
(
secondary_container,
on_secondary_container,
Color32::TRANSPARENT,
)
} else if response.hovered() {
(
Color32::from_rgba_premultiplied(
secondary_container.r().saturating_sub(10),
secondary_container.g().saturating_sub(10),
secondary_container.b().saturating_sub(10),
255,
),
on_secondary_container,
Color32::TRANSPARENT,
)
} else {
(
secondary_container,
on_secondary_container,
Color32::TRANSPARENT,
)
}
}
IconButtonVariant::Outlined => {
if is_selected {
(
Color32::from_rgba_premultiplied(
primary_color.r(),
primary_color.g(),
primary_color.b(),
24,
),
primary_color,
primary_color,
)
} else if response.hovered() {
(
Color32::from_rgba_premultiplied(
on_surface.r(),
on_surface.g(),
on_surface.b(),
20,
),
on_surface_variant,
outline,
)
} else {
(Color32::TRANSPARENT, on_surface_variant, outline)
}
}
}
};
let corner_radius = if self.container {
rect.height() * 0.2 } else {
rect.height() / 2.0
};
if bg_color != Color32::TRANSPARENT {
ui.painter().rect_filled(rect, corner_radius, bg_color);
}
if border_color != Color32::TRANSPARENT {
ui.painter().rect_stroke(
rect,
corner_radius,
Stroke::new(1.0, border_color),
egui::epaint::StrokeKind::Outside,
);
}
let icon_size = self.size * 0.6;
let icon_rect = Rect::from_center_size(rect.center(), Vec2::splat(icon_size));
let render_svg = |ui: &mut Ui, bytes: &[u8], cache_key: &str, icon_rect: Rect, icon_size: f32| {
let size_px = (icon_size.max(1.0).ceil() as u32).max(1);
let texture_id = format!("svg_icon:{}:{}", cache_key, size_px);
let color_image = {
let mut cache = SVG_IMAGE_CACHE.lock().unwrap();
if let Some(cached_image) = cache.get(&texture_id) {
Some(cached_image.clone())
} else {
let mut opt = Options::default();
opt.fontdb_mut().load_system_fonts();
if let Ok(tree) = Tree::from_data(bytes, &opt) {
if let Some(mut pixmap) = Pixmap::new(size_px, size_px) {
let tree_size = tree.size();
let scale_x = size_px as f32 / tree_size.width();
let scale_y = size_px as f32 / tree_size.height();
let scale = scale_x.min(scale_y);
let transform = Transform::from_scale(scale, scale);
render(&tree, transform, &mut pixmap.as_mut());
let data = pixmap.data();
let mut rgba: Vec<u8> = Vec::with_capacity((size_px * size_px * 4) as usize);
rgba.extend_from_slice(data);
let img = Arc::new(ColorImage::from_rgba_unmultiplied(
[size_px as usize, size_px as usize],
&rgba
));
cache.insert(texture_id.clone(), img.clone());
Some(img)
} else {
None
}
} else {
None
}
}
};
if let Some(img) = color_image {
let tex: TextureHandle = ui.ctx().load_texture(
texture_id,
(*img).clone(),
TextureOptions::LINEAR,
);
ui.scope_builder(egui::UiBuilder::new().max_rect(icon_rect), |ui| {
ui.image(&tex);
});
}
};
if let Some(svg_content) = &self.svg_data {
let bytes = svg_content.as_bytes();
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
let mut hasher = DefaultHasher::new();
bytes.hash(&mut hasher);
let hash = hasher.finish();
let cache_key = format!("inline_{:x}", hash);
render_svg(ui, bytes, &cache_key, icon_rect, icon_size);
} else if let Some(path) = &self.svg_path {
if Path::new(path).exists() {
if let Ok(bytes) = fs::read(path) {
render_svg(ui, &bytes, path, icon_rect, icon_size);
}
}
} else {
let text = &self.icon;
let font = FontId::proportional(icon_size);
let final_icon_color = self.icon_color_override.unwrap_or(icon_color);
ui.painter().text(icon_rect.center(), Align2::CENTER_CENTER, text, font, final_icon_color);
}
if response.hovered() && self.enabled && self.variant != IconButtonVariant::Filled {
let ripple_color = Color32::from_rgba_premultiplied(
icon_color.r(),
icon_color.g(),
icon_color.b(),
30,
);
ui.painter().rect_filled(rect, corner_radius, ripple_color);
}
response
}
}
pub fn icon_button_standard(icon: impl Into<String>) -> MaterialIconButton<'static> {
MaterialIconButton::standard(icon)
}
pub fn icon_button_filled(icon: impl Into<String>) -> MaterialIconButton<'static> {
MaterialIconButton::filled(icon)
}
pub fn icon_button_filled_tonal(icon: impl Into<String>) -> MaterialIconButton<'static> {
MaterialIconButton::filled_tonal(icon)
}
pub fn icon_button_outlined(icon: impl Into<String>) -> MaterialIconButton<'static> {
MaterialIconButton::outlined(icon)
}
pub fn icon_button_toggle(icon: impl Into<String>, selected: &mut bool) -> MaterialIconButton<'_> {
MaterialIconButton::toggle(icon, selected)
}