use crate::atoms::icons;
use egui::text::{LayoutJob, TextFormat};
use egui::{Color32, FontId, RichText, Stroke, Ui};
use egui_cha::ViewCtx;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ButtonStyle {
#[default]
Icon,
Text,
Both,
}
pub struct SemanticButton {
icon: &'static str,
label: &'static str,
style: ButtonStyle,
variant: SemanticVariant,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum SemanticVariant {
#[default]
Primary,
Secondary,
Danger,
Success,
}
impl SemanticButton {
fn new(icon: &'static str, label: &'static str, style: ButtonStyle) -> Self {
Self {
icon,
label,
style,
variant: SemanticVariant::Primary,
}
}
fn with_variant(mut self, variant: SemanticVariant) -> Self {
self.variant = variant;
self
}
pub fn on_click<Msg>(self, ctx: &mut ViewCtx<'_, Msg>, msg: Msg) -> bool {
let clicked = self.show(ctx.ui);
if clicked {
ctx.emit(msg);
}
clicked
}
pub fn show(self, ui: &mut Ui) -> bool {
let is_dark = ui.ctx().style().visuals.dark_mode;
let (fill, text_color, stroke) = self.variant_style(is_dark);
let button = match self.style {
ButtonStyle::Icon => {
let content = RichText::new(self.icon)
.family(egui::FontFamily::Name("icons".into()))
.color(text_color);
egui::Button::new(content).fill(fill)
}
ButtonStyle::Text => {
let content = RichText::new(self.label).color(text_color);
egui::Button::new(content).fill(fill)
}
ButtonStyle::Both => {
let mut job = LayoutJob::default();
job.append(
self.icon,
0.0,
TextFormat {
font_id: FontId::new(14.0, egui::FontFamily::Name("icons".into())),
color: text_color,
..Default::default()
},
);
job.append(
" ",
0.0,
TextFormat {
font_id: FontId::default(),
color: text_color,
..Default::default()
},
);
job.append(
self.label,
0.0,
TextFormat {
font_id: FontId::default(),
color: text_color,
..Default::default()
},
);
egui::Button::new(job).fill(fill)
}
};
let mut button = button;
if let Some(s) = stroke {
button = button.stroke(s);
}
ui.add(button).clicked()
}
fn variant_style(&self, is_dark: bool) -> (Color32, Color32, Option<Stroke>) {
match self.variant {
SemanticVariant::Primary => {
let bg = if is_dark {
Color32::from_rgb(96, 165, 250)
} else {
Color32::from_rgb(59, 130, 246)
};
let fg = if is_dark {
Color32::from_rgb(17, 24, 39)
} else {
Color32::WHITE
};
(bg, fg, None)
}
SemanticVariant::Secondary => {
let bg = if is_dark {
Color32::from_rgb(55, 65, 81)
} else {
Color32::from_rgb(107, 114, 128)
};
let fg = if is_dark {
Color32::from_rgb(249, 250, 251)
} else {
Color32::WHITE
};
(bg, fg, None)
}
SemanticVariant::Danger => {
let bg = if is_dark {
Color32::from_rgb(248, 113, 113)
} else {
Color32::from_rgb(239, 68, 68)
};
let fg = if is_dark {
Color32::from_rgb(17, 24, 39)
} else {
Color32::WHITE
};
(bg, fg, None)
}
SemanticVariant::Success => {
let bg = if is_dark {
Color32::from_rgb(74, 222, 128)
} else {
Color32::from_rgb(34, 197, 94)
};
let fg = if is_dark {
Color32::from_rgb(17, 24, 39)
} else {
Color32::WHITE
};
(bg, fg, None)
}
}
}
}
pub fn save(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::FLOPPY_DISK, "Save", style)
}
pub fn close(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::X, "Close", style).with_variant(SemanticVariant::Secondary)
}
pub fn delete(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::TRASH, "Delete", style).with_variant(SemanticVariant::Danger)
}
pub fn edit(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::PENCIL_SIMPLE, "Edit", style)
}
pub fn add(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::PLUS, "Add", style)
}
pub fn remove(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::MINUS, "Remove", style).with_variant(SemanticVariant::Danger)
}
pub fn search(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::MAGNIFYING_GLASS, "Search", style)
}
pub fn refresh(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::ARROWS_CLOCKWISE, "Refresh", style)
.with_variant(SemanticVariant::Secondary)
}
pub fn settings(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::GEAR, "Settings", style).with_variant(SemanticVariant::Secondary)
}
pub fn play(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::PLAY, "Play", style).with_variant(SemanticVariant::Success)
}
pub fn pause(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::PAUSE, "Pause", style).with_variant(SemanticVariant::Secondary)
}
pub fn stop(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::STOP, "Stop", style).with_variant(SemanticVariant::Danger)
}
pub fn copy(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::COPY, "Copy", style).with_variant(SemanticVariant::Secondary)
}
pub fn back(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::ARROW_LEFT, "Back", style).with_variant(SemanticVariant::Secondary)
}
pub fn forward(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::ARROW_RIGHT, "Forward", style)
.with_variant(SemanticVariant::Secondary)
}
pub fn confirm(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::CHECK, "Confirm", style).with_variant(SemanticVariant::Success)
}
pub fn cancel(style: ButtonStyle) -> SemanticButton {
SemanticButton::new(icons::X, "Cancel", style).with_variant(SemanticVariant::Secondary)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_button_style_text_generation() {
let btn = save(ButtonStyle::Icon);
assert_eq!(btn.icon, icons::FLOPPY_DISK);
assert_eq!(btn.label, "Save");
let btn = delete(ButtonStyle::Text);
assert_eq!(btn.label, "Delete");
let btn = edit(ButtonStyle::Both);
assert_eq!(btn.icon, icons::PENCIL_SIMPLE);
assert_eq!(btn.label, "Edit");
}
#[test]
fn test_semantic_variants() {
assert_eq!(save(ButtonStyle::Icon).variant, SemanticVariant::Primary);
assert_eq!(delete(ButtonStyle::Icon).variant, SemanticVariant::Danger);
assert_eq!(close(ButtonStyle::Icon).variant, SemanticVariant::Secondary);
}
}