use crate::{get_global_color, image_utils};
use egui::{
self, Color32, Pos2, Rect, Response, Sense, Stroke, TextureHandle, Ui, Vec2, Widget,
};
#[derive(Clone, Copy, PartialEq)]
pub enum ChipVariant {
Assist,
Filter,
Input,
Suggestion,
}
#[derive(Clone)]
pub enum IconType {
MaterialIcon(String),
SvgData(String),
PngBytes(Vec<u8>),
Texture(TextureHandle),
}
pub struct MaterialChip<'a> {
text: String,
variant: ChipVariant,
selected: Option<&'a mut bool>,
enabled: bool,
soft_disabled: bool,
elevated: bool,
removable: bool,
leading_icon: Option<IconType>,
avatar: bool,
is_small: bool,
action: Option<Box<dyn Fn() + 'a>>,
}
impl<'a> MaterialChip<'a> {
pub fn new(text: impl Into<String>, variant: ChipVariant) -> Self {
Self {
text: text.into(),
variant,
selected: None,
enabled: true,
soft_disabled: false,
elevated: false,
removable: false,
leading_icon: None,
avatar: false, is_small: false,
action: None,
}
}
pub fn assist(text: impl Into<String>) -> Self {
Self::new(text, ChipVariant::Assist)
}
pub fn filter(text: impl Into<String>, selected: &'a mut bool) -> Self {
let mut chip = Self::new(text, ChipVariant::Filter);
chip.selected = Some(selected);
chip
}
pub fn input(text: impl Into<String>) -> Self {
Self::new(text, ChipVariant::Input)
}
pub fn suggestion(text: impl Into<String>) -> Self {
Self::new(text, ChipVariant::Suggestion)
}
pub fn elevated(mut self, elevated: bool) -> Self {
self.elevated = elevated;
self
}
pub fn enabled(mut self, enabled: bool) -> Self {
self.enabled = enabled;
if enabled {
self.soft_disabled = false; }
self
}
pub fn soft_disabled(mut self, soft_disabled: bool) -> Self {
self.soft_disabled = soft_disabled;
if soft_disabled {
self.enabled = false; }
self
}
pub fn small(mut self) -> Self {
self.is_small = true;
self
}
pub fn removable(mut self, removable: bool) -> Self {
self.removable = removable;
self
}
pub fn leading_icon(mut self, icon: impl Into<String>) -> Self {
self.leading_icon = Some(IconType::MaterialIcon(icon.into()));
self
}
pub fn leading_icon_svg(mut self, svg_data: impl Into<String>) -> Self {
self.leading_icon = Some(IconType::SvgData(svg_data.into()));
self
}
pub fn leading_icon_png(mut self, png_bytes: Vec<u8>) -> Self {
self.leading_icon = Some(IconType::PngBytes(png_bytes));
self
}
pub fn leading_icon_texture(mut self, texture: TextureHandle) -> Self {
self.leading_icon = Some(IconType::Texture(texture));
self
}
pub fn avatar(mut self, avatar: bool) -> Self {
self.avatar = avatar;
self
}
pub fn on_click<F>(mut self, f: F) -> Self
where
F: Fn() + 'a,
{
self.action = Some(Box::new(f));
self
}
}
struct ChipColors {
bg: Color32,
border: Color32,
text: Color32,
icon: Color32,
delete_icon: Color32,
state_layer: Color32,
}
fn resolve_chip_colors(
variant: ChipVariant,
is_selected: bool,
enabled: bool,
soft_disabled: bool,
elevated: bool,
is_hovered: bool,
is_pressed: bool,
) -> ChipColors {
let on_surface = get_global_color("onSurface"); let on_surface_variant = get_global_color("onSurfaceVariant"); let outline_variant = get_global_color("outlineVariant"); let surface_container_low = get_global_color("surfaceContainerLow"); let secondary_container = get_global_color("secondaryContainer"); let on_secondary_container = get_global_color("onSecondaryContainer"); let primary = get_global_color("primary");
if !enabled {
let (bg, border, text) = if soft_disabled {
(
on_surface.linear_multiply(0.12),
Color32::TRANSPARENT,
on_surface.linear_multiply(0.60),
)
} else {
(
on_surface.linear_multiply(0.12),
on_surface.linear_multiply(0.12),
on_surface.linear_multiply(0.38),
)
};
return ChipColors {
bg,
border,
text,
icon: text,
delete_icon: text,
state_layer: Color32::TRANSPARENT,
};
}
let state_layer_base = if is_selected {
on_secondary_container } else {
on_surface_variant };
let state_layer = if is_pressed {
state_layer_base.linear_multiply(0.12)
} else if is_hovered {
state_layer_base.linear_multiply(0.08)
} else {
Color32::TRANSPARENT
};
if variant == ChipVariant::Filter && is_selected {
return ChipColors {
bg: secondary_container, border: Color32::TRANSPARENT, text: on_secondary_container, icon: primary, delete_icon: on_secondary_container, state_layer,
};
}
if elevated {
return ChipColors {
bg: surface_container_low, border: Color32::TRANSPARENT, text: on_surface_variant, icon: primary, delete_icon: on_surface_variant, state_layer,
};
}
ChipColors {
bg: Color32::TRANSPARENT, border: outline_variant, text: on_surface_variant, icon: primary, delete_icon: on_surface_variant, state_layer,
}
}
impl<'a> Widget for MaterialChip<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let is_selected = self.selected.as_ref().is_some_and(|s| **s);
let text_width = ui.painter().layout_no_wrap(
self.text.clone(),
egui::FontId::default(),
egui::Color32::WHITE,
).rect.width();
let has_leading = self.leading_icon.is_some()
|| (self.variant == ChipVariant::Filter && is_selected);
let height = if self.is_small { 24.0 } else { 32.0 };
let icon_size = if self.is_small { 18.0 } else { 24.0 };
let icon_width = if has_leading { icon_size } else { 0.0 };
let remove_width = if self.removable { icon_size } else { 0.0 };
let padding = if self.is_small { 12.0 } else { 16.0 };
let desired_size = Vec2::new(
(text_width + icon_width + remove_width + padding).min(ui.available_width()),
height,
);
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
let is_pressed = response.is_pointer_button_down_on();
let is_hovered = response.hovered();
let colors = resolve_chip_colors(
self.variant,
is_selected,
self.enabled,
self.soft_disabled,
self.elevated,
is_hovered,
is_pressed,
);
let corner_radius = 8.0;
if self.elevated && self.enabled {
let shadow_rect = rect.translate(Vec2::new(0.0, 2.0));
ui.painter().rect_filled(
shadow_rect,
corner_radius,
Color32::from_rgba_unmultiplied(0, 0, 0, 30),
);
}
ui.painter().rect_filled(rect, corner_radius, colors.bg);
if colors.state_layer != Color32::TRANSPARENT {
ui.painter()
.rect_filled(rect, corner_radius, colors.state_layer);
}
if colors.border != Color32::TRANSPARENT {
ui.painter().rect_stroke(
rect,
corner_radius,
Stroke::new(1.0, colors.border),
egui::epaint::StrokeKind::Outside,
);
}
let mut content_x = rect.min.x + 8.0;
if let Some(icon) = &self.leading_icon {
let icon_display_size = icon_size * 0.833; let icon_rect = Rect::from_min_size(
Pos2::new(content_x, rect.center().y - icon_display_size / 2.0),
Vec2::splat(icon_display_size),
);
match icon {
IconType::MaterialIcon(icon_str) => {
let font_size = if self.is_small { 14.0 } else { 16.0 };
ui.painter().text(
icon_rect.center(),
egui::Align2::CENTER_CENTER,
icon_str,
egui::FontId::proportional(font_size),
colors.icon,
);
}
IconType::SvgData(svg_data) => {
if let Ok(texture) = image_utils::create_texture_from_svg(
ui.ctx(),
svg_data,
&format!("chip_svg_{}", svg_data.len()),
) {
ui.painter().image(
texture.id(),
icon_rect,
Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)),
Color32::WHITE,
);
}
}
IconType::PngBytes(png_bytes) => {
if let Ok(texture) = image_utils::create_texture_from_png_bytes(
ui.ctx(),
png_bytes,
&format!("chip_png_{}", png_bytes.len()),
) {
ui.painter().image(
texture.id(),
icon_rect,
Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)),
Color32::WHITE,
);
}
}
IconType::Texture(texture) => {
ui.painter().image(
texture.id(),
icon_rect,
Rect::from_min_max(Pos2::ZERO, Pos2::new(1.0, 1.0)),
Color32::WHITE,
);
}
}
content_x += icon_size;
} else if self.variant == ChipVariant::Filter && is_selected {
let icon_display_size = icon_size * 0.833; let icon_rect = Rect::from_min_size(
Pos2::new(content_x, rect.center().y - icon_display_size / 2.0),
Vec2::splat(icon_display_size),
);
let center = icon_rect.center();
let checkmark_size = if self.is_small { 10.0 } else { 12.0 };
let start = Pos2::new(center.x - checkmark_size * 0.3, center.y);
let middle = Pos2::new(
center.x - checkmark_size * 0.1,
center.y + checkmark_size * 0.2,
);
let end = Pos2::new(
center.x + checkmark_size * 0.3,
center.y - checkmark_size * 0.2,
);
let stroke_width = if self.is_small { 1.5 } else { 2.0 };
ui.painter()
.line_segment([start, middle], Stroke::new(stroke_width, colors.icon));
ui.painter()
.line_segment([middle, end], Stroke::new(stroke_width, colors.icon));
content_x += icon_size;
}
let text_pos = Pos2::new(content_x, rect.center().y + 2.0);
ui.painter().text(
text_pos,
egui::Align2::LEFT_CENTER,
&self.text,
egui::FontId::default(),
colors.text,
);
if self.removable {
let icon_display_size = icon_size * 0.833; let remove_rect = Rect::from_min_size(
Pos2::new(rect.max.x - icon_size, rect.center().y - icon_display_size / 2.0),
Vec2::splat(icon_display_size),
);
let center = remove_rect.center();
let cross_size = if self.is_small { 6.0 } else { 8.0 };
let stroke_width = if self.is_small { 1.2 } else { 1.5 };
ui.painter().line_segment(
[
Pos2::new(center.x - cross_size / 2.0, center.y - cross_size / 2.0),
Pos2::new(center.x + cross_size / 2.0, center.y + cross_size / 2.0),
],
Stroke::new(stroke_width, colors.delete_icon),
);
ui.painter().line_segment(
[
Pos2::new(center.x + cross_size / 2.0, center.y - cross_size / 2.0),
Pos2::new(center.x - cross_size / 2.0, center.y + cross_size / 2.0),
],
Stroke::new(stroke_width, colors.delete_icon),
);
}
if response.clicked() && self.enabled {
match self.variant {
ChipVariant::Filter => {
if let Some(selected) = self.selected {
*selected = !*selected;
response.mark_changed();
}
}
_ => {
if let Some(action) = self.action {
action();
}
}
}
}
response
}
}
pub fn assist_chip(text: impl Into<String>) -> MaterialChip<'static> {
MaterialChip::assist(text)
}
pub fn filter_chip(text: impl Into<String>, selected: &mut bool) -> MaterialChip<'_> {
MaterialChip::filter(text, selected)
}
pub fn input_chip(text: impl Into<String>) -> MaterialChip<'static> {
MaterialChip::input(text)
}
pub fn suggestion_chip(text: impl Into<String>) -> MaterialChip<'static> {
MaterialChip::suggestion(text)
}