use egui::{Color32, FontId, Id, TextStyle};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "lowercase"))]
pub enum ThemeVariant {
#[default]
Light,
Dark,
}
pub trait ThemeProvider {
fn to_ds_theme(&self) -> Theme;
}
#[derive(Debug, Clone)]
pub struct Theme {
pub variant: ThemeVariant,
pub primary: Color32,
pub primary_hover: Color32,
pub primary_text: Color32,
pub secondary: Color32,
pub secondary_hover: Color32,
pub secondary_text: Color32,
pub bg_primary: Color32,
pub bg_secondary: Color32,
pub bg_tertiary: Color32,
pub text_primary: Color32,
pub text_secondary: Color32,
pub text_muted: Color32,
pub state_success: Color32,
pub state_warning: Color32,
pub state_danger: Color32,
pub state_info: Color32,
pub state_success_text: Color32,
pub state_warning_text: Color32,
pub state_danger_text: Color32,
pub state_info_text: Color32,
pub state_success_hover: Color32,
pub state_warning_hover: Color32,
pub state_danger_hover: Color32,
pub state_info_hover: Color32,
pub log_debug: Color32,
pub log_info: Color32,
pub log_warn: Color32,
pub log_error: Color32,
pub log_critical: Color32,
pub border: Color32,
pub border_focus: Color32,
pub spacing_xs: f32,
pub spacing_sm: f32,
pub spacing_md: f32,
pub spacing_lg: f32,
pub spacing_xl: f32,
pub radius_sm: f32,
pub radius_md: f32,
pub radius_lg: f32,
pub border_width: f32,
pub stroke_width: f32,
pub font_size_xs: f32,
pub font_size_sm: f32,
pub font_size_md: f32,
pub font_size_lg: f32,
pub font_size_xl: f32,
pub font_size_2xl: f32,
pub font_size_3xl: f32,
pub line_height: f32,
pub overlay_dim: f32,
pub surface_alpha: f32,
pub shadow_blur: Option<f32>,
pub glass_opacity: f32,
pub glass_blur_radius: f32,
pub glass_tint: Option<Color32>,
pub glass_border: bool,
pub titlebar_height: f32,
}
impl Default for Theme {
fn default() -> Self {
Self::light()
}
}
impl Theme {
pub fn light() -> Self {
Self {
variant: ThemeVariant::Light,
primary: Color32::from_rgb(59, 130, 246),
primary_hover: Color32::from_rgb(37, 99, 235),
primary_text: Color32::WHITE,
secondary: Color32::from_rgb(107, 114, 128),
secondary_hover: Color32::from_rgb(75, 85, 99),
secondary_text: Color32::WHITE,
bg_primary: Color32::WHITE,
bg_secondary: Color32::from_rgb(249, 250, 251),
bg_tertiary: Color32::from_rgb(243, 244, 246),
text_primary: Color32::from_rgb(17, 24, 39),
text_secondary: Color32::from_rgb(75, 85, 99),
text_muted: Color32::from_rgb(156, 163, 175),
state_success: Color32::from_rgb(34, 197, 94), state_warning: Color32::from_rgb(245, 158, 11), state_danger: Color32::from_rgb(239, 68, 68), state_info: Color32::from_rgb(14, 165, 233),
state_success_text: Color32::WHITE,
state_warning_text: Color32::WHITE,
state_danger_text: Color32::WHITE,
state_info_text: Color32::WHITE,
state_success_hover: Color32::from_rgb(22, 163, 74), state_warning_hover: Color32::from_rgb(217, 119, 6), state_danger_hover: Color32::from_rgb(220, 38, 38), state_info_hover: Color32::from_rgb(2, 132, 199),
log_debug: Color32::from_rgb(156, 163, 175), log_info: Color32::from_rgb(59, 130, 246), log_warn: Color32::from_rgb(245, 158, 11), log_error: Color32::from_rgb(239, 68, 68), log_critical: Color32::from_rgb(190, 24, 93),
border: Color32::from_rgb(229, 231, 235),
border_focus: Color32::from_rgb(59, 130, 246),
spacing_xs: 6.0,
spacing_sm: 12.0,
spacing_md: 20.0,
spacing_lg: 32.0,
spacing_xl: 48.0,
radius_sm: 4.0,
radius_md: 8.0,
radius_lg: 12.0,
border_width: 1.0,
stroke_width: 1.0,
font_size_xs: 10.0,
font_size_sm: 12.0,
font_size_md: 14.0,
font_size_lg: 16.0,
font_size_xl: 20.0,
font_size_2xl: 24.0,
font_size_3xl: 30.0,
line_height: 1.4,
overlay_dim: 0.5,
surface_alpha: 1.0,
shadow_blur: None,
glass_opacity: 0.6,
glass_blur_radius: 8.0,
glass_tint: None, glass_border: true,
titlebar_height: 32.0,
}
}
pub fn dark() -> Self {
Self {
variant: ThemeVariant::Dark,
primary: Color32::from_rgb(96, 165, 250),
primary_hover: Color32::from_rgb(59, 130, 246),
primary_text: Color32::from_rgb(17, 24, 39),
secondary: Color32::from_rgb(156, 163, 175),
secondary_hover: Color32::from_rgb(107, 114, 128),
secondary_text: Color32::from_rgb(17, 24, 39),
bg_primary: Color32::from_rgb(17, 24, 39),
bg_secondary: Color32::from_rgb(31, 41, 55),
bg_tertiary: Color32::from_rgb(55, 65, 81),
text_primary: Color32::from_rgb(249, 250, 251),
text_secondary: Color32::from_rgb(209, 213, 219),
text_muted: Color32::from_rgb(156, 163, 175),
state_success: Color32::from_rgb(74, 222, 128), state_warning: Color32::from_rgb(251, 191, 36), state_danger: Color32::from_rgb(248, 113, 113), state_info: Color32::from_rgb(56, 189, 248),
state_success_text: Color32::from_rgb(17, 24, 39),
state_warning_text: Color32::from_rgb(17, 24, 39),
state_danger_text: Color32::from_rgb(17, 24, 39),
state_info_text: Color32::from_rgb(17, 24, 39),
state_success_hover: Color32::from_rgb(34, 197, 94), state_warning_hover: Color32::from_rgb(245, 158, 11), state_danger_hover: Color32::from_rgb(239, 68, 68), state_info_hover: Color32::from_rgb(14, 165, 233),
log_debug: Color32::from_rgb(209, 213, 219), log_info: Color32::from_rgb(96, 165, 250), log_warn: Color32::from_rgb(251, 191, 36), log_error: Color32::from_rgb(248, 113, 113), log_critical: Color32::from_rgb(244, 114, 182),
border: Color32::from_rgb(55, 65, 81),
border_focus: Color32::from_rgb(96, 165, 250),
spacing_xs: 6.0,
spacing_sm: 12.0,
spacing_md: 20.0,
spacing_lg: 32.0,
spacing_xl: 48.0,
radius_sm: 4.0,
radius_md: 8.0,
radius_lg: 12.0,
border_width: 1.0,
stroke_width: 1.0,
font_size_xs: 10.0,
font_size_sm: 12.0,
font_size_md: 14.0,
font_size_lg: 16.0,
font_size_xl: 20.0,
font_size_2xl: 24.0,
font_size_3xl: 30.0,
line_height: 1.4,
overlay_dim: 0.7,
surface_alpha: 1.0,
shadow_blur: None,
glass_opacity: 0.7,
glass_blur_radius: 8.0,
glass_tint: None, glass_border: true,
titlebar_height: 32.0,
}
}
const STORAGE_ID: &'static str = "egui_cha_ds_theme";
pub fn current(ctx: &egui::Context) -> Self {
ctx.data(|d| d.get_temp::<Theme>(Id::new(Self::STORAGE_ID)))
.unwrap_or_default()
}
pub fn from_provider(provider: impl ThemeProvider) -> Self {
provider.to_ds_theme()
}
pub fn apply_colors_only(&self, ctx: &egui::Context) {
ctx.data_mut(|d| d.insert_temp(Id::new(Self::STORAGE_ID), self.clone()));
let mut style = (*ctx.style()).clone();
let visuals = &mut style.visuals;
visuals.dark_mode = self.variant == ThemeVariant::Dark;
visuals.panel_fill = self.bg_primary;
visuals.window_fill = self.bg_primary;
visuals.extreme_bg_color = self.bg_secondary;
visuals.faint_bg_color = self.bg_secondary;
visuals.code_bg_color = self.bg_tertiary;
visuals.override_text_color = Some(self.text_primary);
visuals.hyperlink_color = self.primary;
visuals.warn_fg_color = self.state_warning;
visuals.error_fg_color = self.state_danger;
visuals.widgets.noninteractive.bg_fill = self.bg_secondary;
visuals.widgets.noninteractive.weak_bg_fill = self.bg_secondary;
visuals.widgets.noninteractive.bg_stroke.color = self.border;
visuals.widgets.noninteractive.fg_stroke.color = self.text_primary;
visuals.widgets.inactive.bg_fill = self.bg_tertiary;
visuals.widgets.inactive.weak_bg_fill = self.bg_tertiary;
visuals.widgets.inactive.bg_stroke.color = self.border;
visuals.widgets.inactive.fg_stroke.color = self.text_primary;
visuals.widgets.hovered.bg_fill = self.primary_hover;
visuals.widgets.hovered.weak_bg_fill = self.primary_hover;
visuals.widgets.hovered.bg_stroke.color = self.primary;
visuals.widgets.hovered.fg_stroke.color = self.primary_text;
visuals.widgets.active.bg_fill = self.primary;
visuals.widgets.active.weak_bg_fill = self.primary;
visuals.widgets.active.bg_stroke.color = self.primary;
visuals.widgets.active.fg_stroke.color = self.primary_text;
visuals.widgets.open.bg_fill = self.bg_tertiary;
visuals.widgets.open.weak_bg_fill = self.bg_tertiary;
visuals.widgets.open.bg_stroke.color = self.primary;
visuals.widgets.open.fg_stroke.color = self.text_primary;
visuals.selection.bg_fill = self.primary.linear_multiply(0.3);
visuals.selection.stroke.color = self.primary;
visuals.window_stroke.color = self.border;
ctx.set_style(style);
}
pub fn apply(&self, ctx: &egui::Context) {
ctx.data_mut(|d| d.insert_temp(Id::new(Self::STORAGE_ID), self.clone()));
let mut style = (*ctx.style()).clone();
let visuals = &mut style.visuals;
visuals.dark_mode = self.variant == ThemeVariant::Dark;
visuals.panel_fill = self.bg_primary;
visuals.window_fill = self.bg_primary;
visuals.extreme_bg_color = self.bg_secondary;
visuals.faint_bg_color = self.bg_secondary;
visuals.code_bg_color = self.bg_tertiary;
visuals.override_text_color = Some(self.text_primary);
visuals.hyperlink_color = self.primary;
visuals.warn_fg_color = self.state_warning;
visuals.error_fg_color = self.state_danger;
visuals.widgets.noninteractive.bg_fill = self.bg_secondary;
visuals.widgets.noninteractive.weak_bg_fill = self.bg_secondary;
visuals.widgets.noninteractive.bg_stroke.color = self.border;
visuals.widgets.noninteractive.fg_stroke.color = self.text_primary;
visuals.widgets.inactive.bg_fill = self.bg_tertiary;
visuals.widgets.inactive.weak_bg_fill = self.bg_tertiary;
visuals.widgets.inactive.bg_stroke.color = self.border;
visuals.widgets.inactive.fg_stroke.color = self.text_primary;
visuals.widgets.hovered.bg_fill = self.primary_hover;
visuals.widgets.hovered.weak_bg_fill = self.primary_hover;
visuals.widgets.hovered.bg_stroke.color = self.primary;
visuals.widgets.hovered.fg_stroke.color = self.primary_text;
visuals.widgets.active.bg_fill = self.primary;
visuals.widgets.active.weak_bg_fill = self.primary;
visuals.widgets.active.bg_stroke.color = self.primary;
visuals.widgets.active.fg_stroke.color = self.primary_text;
visuals.widgets.open.bg_fill = self.bg_tertiary;
visuals.widgets.open.weak_bg_fill = self.bg_tertiary;
visuals.widgets.open.bg_stroke.color = self.primary;
visuals.widgets.open.fg_stroke.color = self.text_primary;
visuals.selection.bg_fill = self.primary.linear_multiply(0.3);
visuals.selection.stroke.color = self.primary;
visuals.widgets.noninteractive.bg_stroke.width = self.border_width;
visuals.widgets.noninteractive.fg_stroke.width = self.stroke_width;
visuals.widgets.inactive.bg_stroke.width = self.border_width;
visuals.widgets.inactive.fg_stroke.width = self.stroke_width;
visuals.widgets.hovered.bg_stroke.width = self.border_width;
visuals.widgets.hovered.fg_stroke.width = self.stroke_width;
visuals.widgets.active.bg_stroke.width = self.border_width;
visuals.widgets.active.fg_stroke.width = self.stroke_width;
visuals.widgets.open.bg_stroke.width = self.border_width;
visuals.widgets.open.fg_stroke.width = self.stroke_width;
visuals.selection.stroke.width = self.stroke_width;
visuals.window_stroke.color = self.border;
visuals.window_stroke.width = self.border_width;
match self.shadow_blur {
None => {
visuals.window_shadow = egui::Shadow::NONE;
visuals.popup_shadow = egui::Shadow::NONE;
}
Some(blur) => {
let alpha = if self.variant == ThemeVariant::Dark {
60
} else {
30
};
visuals.window_shadow = egui::Shadow {
offset: [0, 2],
blur: blur as u8,
spread: 0,
color: Color32::from_black_alpha(alpha),
};
visuals.popup_shadow = visuals.window_shadow;
}
}
style
.text_styles
.insert(TextStyle::Small, FontId::proportional(self.font_size_sm));
style
.text_styles
.insert(TextStyle::Body, FontId::proportional(self.font_size_md));
style
.text_styles
.insert(TextStyle::Button, FontId::proportional(self.font_size_md));
style
.text_styles
.insert(TextStyle::Heading, FontId::proportional(self.font_size_xl));
style
.text_styles
.insert(TextStyle::Monospace, FontId::monospace(self.font_size_md));
style.spacing.item_spacing = egui::vec2(self.spacing_sm, self.spacing_sm);
style.spacing.window_margin = egui::Margin::same(self.spacing_md as i8);
style.spacing.button_padding = egui::vec2(self.spacing_sm, self.spacing_xs);
style.spacing.menu_margin = egui::Margin::same(self.spacing_sm as i8);
style.spacing.indent = self.spacing_md;
style.spacing.icon_spacing = self.spacing_xs;
style.spacing.icon_width = self.spacing_md;
ctx.set_style(style);
}
pub fn with_scale(mut self, scale: f32) -> Self {
self.spacing_xs *= scale;
self.spacing_sm *= scale;
self.spacing_md *= scale;
self.spacing_lg *= scale;
self.spacing_xl *= scale;
self
}
pub fn with_spacing_scale(mut self, scale: f32) -> Self {
self.spacing_xs *= scale;
self.spacing_sm *= scale;
self.spacing_md *= scale;
self.spacing_lg *= scale;
self.spacing_xl *= scale;
self
}
pub fn with_radius_scale(mut self, scale: f32) -> Self {
self.radius_sm *= scale;
self.radius_md *= scale;
self.radius_lg *= scale;
self
}
pub fn with_font_scale(mut self, scale: f32) -> Self {
self.font_size_xs *= scale;
self.font_size_sm *= scale;
self.font_size_md *= scale;
self.font_size_lg *= scale;
self.font_size_xl *= scale;
self.font_size_2xl *= scale;
self.font_size_3xl *= scale;
self
}
pub fn with_stroke_scale(mut self, scale: f32) -> Self {
self.border_width *= scale;
self.stroke_width *= scale;
self
}
pub fn with_shadow(self) -> Self {
self.with_shadow_blur(4.0)
}
pub fn with_shadow_blur(mut self, blur: f32) -> Self {
self.shadow_blur = Some(blur);
self
}
pub fn pastel() -> Self {
Self {
variant: ThemeVariant::Light,
primary: Color32::from_rgb(167, 139, 250), primary_hover: Color32::from_rgb(139, 92, 246), primary_text: Color32::WHITE,
secondary: Color32::from_rgb(244, 114, 182), secondary_hover: Color32::from_rgb(236, 72, 153), secondary_text: Color32::WHITE,
bg_primary: Color32::from_rgb(255, 251, 245), bg_secondary: Color32::from_rgb(254, 243, 235), bg_tertiary: Color32::from_rgb(253, 235, 223),
text_primary: Color32::from_rgb(64, 57, 72), text_secondary: Color32::from_rgb(107, 98, 116), text_muted: Color32::from_rgb(156, 148, 163),
state_success: Color32::from_rgb(134, 239, 172), state_warning: Color32::from_rgb(253, 224, 71), state_danger: Color32::from_rgb(253, 164, 175), state_info: Color32::from_rgb(147, 197, 253),
state_success_text: Color32::from_rgb(22, 101, 52), state_warning_text: Color32::from_rgb(133, 77, 14), state_danger_text: Color32::from_rgb(159, 18, 57), state_info_text: Color32::from_rgb(30, 64, 175),
state_success_hover: Color32::from_rgb(74, 222, 128), state_warning_hover: Color32::from_rgb(250, 204, 21), state_danger_hover: Color32::from_rgb(251, 113, 133), state_info_hover: Color32::from_rgb(96, 165, 250),
log_debug: Color32::from_rgb(156, 148, 163), log_info: Color32::from_rgb(96, 165, 250), log_warn: Color32::from_rgb(250, 204, 21), log_error: Color32::from_rgb(251, 113, 133), log_critical: Color32::from_rgb(236, 72, 153),
border: Color32::from_rgb(233, 213, 202), border_focus: Color32::from_rgb(167, 139, 250),
spacing_xs: 6.0,
spacing_sm: 12.0,
spacing_md: 20.0,
spacing_lg: 32.0,
spacing_xl: 48.0,
radius_sm: 6.0,
radius_md: 12.0,
radius_lg: 16.0,
border_width: 1.0,
stroke_width: 1.0,
font_size_xs: 10.0,
font_size_sm: 12.0,
font_size_md: 14.0,
font_size_lg: 16.0,
font_size_xl: 20.0,
font_size_2xl: 24.0,
font_size_3xl: 30.0,
line_height: 1.4,
overlay_dim: 0.4,
surface_alpha: 1.0,
shadow_blur: None,
glass_opacity: 0.55,
glass_blur_radius: 10.0,
glass_tint: None,
glass_border: true,
titlebar_height: 32.0,
}
}
pub fn pastel_dark() -> Self {
Self {
variant: ThemeVariant::Dark,
primary: Color32::from_rgb(196, 181, 253), primary_hover: Color32::from_rgb(167, 139, 250), primary_text: Color32::from_rgb(30, 27, 38),
secondary: Color32::from_rgb(249, 168, 212), secondary_hover: Color32::from_rgb(244, 114, 182), secondary_text: Color32::from_rgb(30, 27, 38),
bg_primary: Color32::from_rgb(24, 22, 32), bg_secondary: Color32::from_rgb(32, 29, 43), bg_tertiary: Color32::from_rgb(45, 41, 58),
text_primary: Color32::from_rgb(243, 237, 255), text_secondary: Color32::from_rgb(196, 189, 210),
text_muted: Color32::from_rgb(140, 133, 156),
state_success: Color32::from_rgb(74, 222, 128), state_warning: Color32::from_rgb(250, 204, 21), state_danger: Color32::from_rgb(251, 113, 133), state_info: Color32::from_rgb(96, 165, 250),
state_success_text: Color32::from_rgb(20, 30, 25),
state_warning_text: Color32::from_rgb(35, 30, 15),
state_danger_text: Color32::from_rgb(35, 20, 25),
state_info_text: Color32::from_rgb(20, 25, 35),
state_success_hover: Color32::from_rgb(134, 239, 172),
state_warning_hover: Color32::from_rgb(253, 224, 71),
state_danger_hover: Color32::from_rgb(253, 164, 175),
state_info_hover: Color32::from_rgb(147, 197, 253),
log_debug: Color32::from_rgb(140, 133, 156), log_info: Color32::from_rgb(147, 197, 253), log_warn: Color32::from_rgb(253, 224, 71), log_error: Color32::from_rgb(253, 164, 175), log_critical: Color32::from_rgb(249, 168, 212),
border: Color32::from_rgb(55, 50, 70),
border_focus: Color32::from_rgb(196, 181, 253),
spacing_xs: 6.0,
spacing_sm: 12.0,
spacing_md: 20.0,
spacing_lg: 32.0,
spacing_xl: 48.0,
radius_sm: 6.0,
radius_md: 12.0,
radius_lg: 16.0,
border_width: 1.0,
stroke_width: 1.0,
font_size_xs: 10.0,
font_size_sm: 12.0,
font_size_md: 14.0,
font_size_lg: 16.0,
font_size_xl: 20.0,
font_size_2xl: 24.0,
font_size_3xl: 30.0,
line_height: 1.4,
overlay_dim: 0.6,
surface_alpha: 1.0,
shadow_blur: None,
glass_opacity: 0.65,
glass_blur_radius: 10.0,
glass_tint: None,
glass_border: true,
titlebar_height: 32.0,
}
}
}
#[cfg(feature = "serde")]
#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(default)]
pub struct ThemeConfig {
pub base: Option<String>,
pub primary: Option<String>,
pub primary_hover: Option<String>,
pub primary_text: Option<String>,
pub secondary: Option<String>,
pub secondary_hover: Option<String>,
pub secondary_text: Option<String>,
pub bg_primary: Option<String>,
pub bg_secondary: Option<String>,
pub bg_tertiary: Option<String>,
pub text_primary: Option<String>,
pub text_secondary: Option<String>,
pub text_muted: Option<String>,
pub state_success: Option<String>,
pub state_warning: Option<String>,
pub state_danger: Option<String>,
pub state_info: Option<String>,
pub border: Option<String>,
pub border_focus: Option<String>,
pub spacing_scale: Option<f32>,
pub font_scale: Option<f32>,
pub radius_scale: Option<f32>,
pub stroke_scale: Option<f32>,
pub shadow_blur: Option<f32>,
pub overlay_dim: Option<f32>,
pub surface_alpha: Option<f32>,
pub glass_opacity: Option<f32>,
pub glass_blur_radius: Option<f32>,
pub glass_tint: Option<String>,
pub glass_border: Option<bool>,
pub titlebar_height: Option<f32>,
}
#[cfg(feature = "serde")]
impl ThemeConfig {
pub fn parse_color(s: &str) -> Option<Color32> {
let s = s.trim();
if let Some(hex) = s.strip_prefix('#') {
return match hex.len() {
6 => {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
Some(Color32::from_rgb(r, g, b))
}
8 => {
let r = u8::from_str_radix(&hex[0..2], 16).ok()?;
let g = u8::from_str_radix(&hex[2..4], 16).ok()?;
let b = u8::from_str_radix(&hex[4..6], 16).ok()?;
let a = u8::from_str_radix(&hex[6..8], 16).ok()?;
Some(Color32::from_rgba_unmultiplied(r, g, b, a))
}
_ => None,
};
}
if let Some(inner) = s.strip_prefix("rgb(").and_then(|s| s.strip_suffix(')')) {
let parts: Vec<&str> = inner.split(',').collect();
if parts.len() == 3 {
let r: u8 = parts[0].trim().parse().ok()?;
let g: u8 = parts[1].trim().parse().ok()?;
let b: u8 = parts[2].trim().parse().ok()?;
return Some(Color32::from_rgb(r, g, b));
}
}
if let Some(inner) = s.strip_prefix("rgba(").and_then(|s| s.strip_suffix(')')) {
let parts: Vec<&str> = inner.split(',').collect();
if parts.len() == 4 {
let r: u8 = parts[0].trim().parse().ok()?;
let g: u8 = parts[1].trim().parse().ok()?;
let b: u8 = parts[2].trim().parse().ok()?;
let a_str = parts[3].trim();
let a: u8 = if a_str.contains('.') {
let f: f32 = a_str.parse().ok()?;
(f * 255.0) as u8
} else {
a_str.parse().ok()?
};
return Some(Color32::from_rgba_unmultiplied(r, g, b, a));
}
}
None
}
pub fn color_to_hex(color: Color32) -> String {
let [r, g, b, a] = color.to_srgba_unmultiplied();
if a == 255 {
format!("#{:02X}{:02X}{:02X}", r, g, b)
} else {
format!("#{:02X}{:02X}{:02X}{:02X}", r, g, b, a)
}
}
}
#[cfg(feature = "serde")]
impl Theme {
pub fn from_config(config: &ThemeConfig) -> Self {
let mut theme = match config.base.as_deref() {
Some("dark") => Self::dark(),
Some("pastel") => Self::pastel(),
Some("pastel_dark") => Self::pastel_dark(),
_ => Self::light(), };
macro_rules! apply_color {
($field:ident) => {
if let Some(ref s) = config.$field {
if let Some(c) = ThemeConfig::parse_color(s) {
theme.$field = c;
}
}
};
}
apply_color!(primary);
apply_color!(primary_hover);
apply_color!(primary_text);
apply_color!(secondary);
apply_color!(secondary_hover);
apply_color!(secondary_text);
apply_color!(bg_primary);
apply_color!(bg_secondary);
apply_color!(bg_tertiary);
apply_color!(text_primary);
apply_color!(text_secondary);
apply_color!(text_muted);
apply_color!(state_success);
apply_color!(state_warning);
apply_color!(state_danger);
apply_color!(state_info);
apply_color!(border);
apply_color!(border_focus);
if let Some(scale) = config.spacing_scale {
theme = theme.with_spacing_scale(scale);
}
if let Some(scale) = config.font_scale {
theme = theme.with_font_scale(scale);
}
if let Some(scale) = config.radius_scale {
theme = theme.with_radius_scale(scale);
}
if let Some(scale) = config.stroke_scale {
theme = theme.with_stroke_scale(scale);
}
if let Some(blur) = config.shadow_blur {
theme.shadow_blur = Some(blur);
}
if let Some(dim) = config.overlay_dim {
theme.overlay_dim = dim;
}
if let Some(alpha) = config.surface_alpha {
theme.surface_alpha = alpha;
}
if let Some(opacity) = config.glass_opacity {
theme.glass_opacity = opacity.clamp(0.0, 1.0);
}
if let Some(blur) = config.glass_blur_radius {
theme.glass_blur_radius = blur.max(0.0);
}
if let Some(ref tint) = config.glass_tint {
theme.glass_tint = ThemeConfig::parse_color(tint);
}
if let Some(border) = config.glass_border {
theme.glass_border = border;
}
if let Some(height) = config.titlebar_height {
theme.titlebar_height = height.max(0.0);
}
theme
}
pub fn to_config(&self) -> ThemeConfig {
ThemeConfig {
base: Some(match self.variant {
ThemeVariant::Light => "light".into(),
ThemeVariant::Dark => "dark".into(),
}),
primary: Some(ThemeConfig::color_to_hex(self.primary)),
primary_hover: Some(ThemeConfig::color_to_hex(self.primary_hover)),
primary_text: Some(ThemeConfig::color_to_hex(self.primary_text)),
secondary: Some(ThemeConfig::color_to_hex(self.secondary)),
secondary_hover: Some(ThemeConfig::color_to_hex(self.secondary_hover)),
secondary_text: Some(ThemeConfig::color_to_hex(self.secondary_text)),
bg_primary: Some(ThemeConfig::color_to_hex(self.bg_primary)),
bg_secondary: Some(ThemeConfig::color_to_hex(self.bg_secondary)),
bg_tertiary: Some(ThemeConfig::color_to_hex(self.bg_tertiary)),
text_primary: Some(ThemeConfig::color_to_hex(self.text_primary)),
text_secondary: Some(ThemeConfig::color_to_hex(self.text_secondary)),
text_muted: Some(ThemeConfig::color_to_hex(self.text_muted)),
state_success: Some(ThemeConfig::color_to_hex(self.state_success)),
state_warning: Some(ThemeConfig::color_to_hex(self.state_warning)),
state_danger: Some(ThemeConfig::color_to_hex(self.state_danger)),
state_info: Some(ThemeConfig::color_to_hex(self.state_info)),
border: Some(ThemeConfig::color_to_hex(self.border)),
border_focus: Some(ThemeConfig::color_to_hex(self.border_focus)),
spacing_scale: None, font_scale: None,
radius_scale: None,
stroke_scale: None,
shadow_blur: self.shadow_blur,
overlay_dim: Some(self.overlay_dim),
surface_alpha: Some(self.surface_alpha),
glass_opacity: Some(self.glass_opacity),
glass_blur_radius: Some(self.glass_blur_radius),
glass_tint: self.glass_tint.map(ThemeConfig::color_to_hex),
glass_border: Some(self.glass_border),
titlebar_height: Some(self.titlebar_height),
}
}
pub fn from_toml(toml_str: &str) -> Result<Self, toml::de::Error> {
let config: ThemeConfig = toml::from_str(toml_str)?;
Ok(Self::from_config(&config))
}
pub fn to_toml(&self) -> Result<String, toml::ser::Error> {
toml::to_string_pretty(&self.to_config())
}
pub fn load_toml(path: impl AsRef<std::path::Path>) -> Result<Self, ThemeLoadError> {
let content = std::fs::read_to_string(path)?;
let theme = Self::from_toml(&content)?;
Ok(theme)
}
pub fn save_toml(&self, path: impl AsRef<std::path::Path>) -> Result<(), ThemeSaveError> {
let toml_str = self.to_toml()?;
std::fs::write(path, toml_str)?;
Ok(())
}
}
#[cfg(feature = "serde")]
#[derive(Debug)]
pub enum ThemeLoadError {
Io(std::io::Error),
Parse(toml::de::Error),
}
#[cfg(feature = "serde")]
impl std::fmt::Display for ThemeLoadError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "IO error: {}", e),
Self::Parse(e) => write!(f, "Parse error: {}", e),
}
}
}
#[cfg(feature = "serde")]
impl std::error::Error for ThemeLoadError {}
#[cfg(feature = "serde")]
impl From<std::io::Error> for ThemeLoadError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
#[cfg(feature = "serde")]
impl From<toml::de::Error> for ThemeLoadError {
fn from(e: toml::de::Error) -> Self {
Self::Parse(e)
}
}
#[cfg(feature = "serde")]
#[derive(Debug)]
pub enum ThemeSaveError {
Io(std::io::Error),
Serialize(toml::ser::Error),
}
#[cfg(feature = "serde")]
impl std::fmt::Display for ThemeSaveError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Io(e) => write!(f, "IO error: {}", e),
Self::Serialize(e) => write!(f, "Serialize error: {}", e),
}
}
}
#[cfg(feature = "serde")]
impl std::error::Error for ThemeSaveError {}
#[cfg(feature = "serde")]
impl From<std::io::Error> for ThemeSaveError {
fn from(e: std::io::Error) -> Self {
Self::Io(e)
}
}
#[cfg(feature = "serde")]
impl From<toml::ser::Error> for ThemeSaveError {
fn from(e: toml::ser::Error) -> Self {
Self::Serialize(e)
}
}
pub trait LightweightTheme {
fn primary(&self) -> Color32;
fn background(&self) -> Color32;
fn text(&self) -> Color32;
fn to_theme(&self) -> Theme {
let primary = self.primary();
let bg = self.background();
let text = self.text();
let [r, g, b, _] = bg.to_array();
let luminance = 0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32;
let is_dark = luminance < 128.0;
let mut theme = if is_dark {
Theme::dark()
} else {
Theme::light()
};
theme.primary = primary;
theme.primary_hover = if is_dark {
lighten(primary, 0.15)
} else {
darken(primary, 0.15)
};
theme.primary_text = contrast_text(primary);
theme.border_focus = primary;
theme.bg_primary = bg;
theme.bg_secondary = if is_dark {
lighten(bg, 0.05)
} else {
darken(bg, 0.02)
};
theme.bg_tertiary = if is_dark {
lighten(bg, 0.10)
} else {
darken(bg, 0.05)
};
theme.text_primary = text;
theme.text_secondary = with_alpha(text, 0.7);
theme.text_muted = with_alpha(text, 0.5);
theme.border = if is_dark {
lighten(bg, 0.15)
} else {
darken(bg, 0.10)
};
theme
}
}
fn lighten(color: Color32, amount: f32) -> Color32 {
let [r, g, b, a] = color.to_array();
let f = 1.0 + amount;
Color32::from_rgba_unmultiplied(
((r as f32 * f).min(255.0)) as u8,
((g as f32 * f).min(255.0)) as u8,
((b as f32 * f).min(255.0)) as u8,
a,
)
}
fn darken(color: Color32, amount: f32) -> Color32 {
let [r, g, b, a] = color.to_array();
let f = 1.0 - amount;
Color32::from_rgba_unmultiplied(
(r as f32 * f) as u8,
(g as f32 * f) as u8,
(b as f32 * f) as u8,
a,
)
}
fn with_alpha(color: Color32, alpha: f32) -> Color32 {
let [r, g, b, _] = color.to_array();
Color32::from_rgba_unmultiplied(r, g, b, (alpha * 255.0) as u8)
}
fn contrast_text(bg: Color32) -> Color32 {
let [r, g, b, _] = bg.to_array();
let luminance = 0.299 * r as f32 + 0.587 * g as f32 + 0.114 * b as f32;
if luminance > 128.0 {
Color32::from_rgb(17, 24, 39) } else {
Color32::WHITE }
}
#[cfg(all(test, feature = "serde"))]
mod tests {
use super::*;
#[test]
fn test_parse_hex_color() {
assert_eq!(
ThemeConfig::parse_color("#FF0000"),
Some(Color32::from_rgb(255, 0, 0))
);
assert_eq!(
ThemeConfig::parse_color("#00FF00"),
Some(Color32::from_rgb(0, 255, 0))
);
assert_eq!(
ThemeConfig::parse_color("#0000FF"),
Some(Color32::from_rgb(0, 0, 255))
);
assert_eq!(
ThemeConfig::parse_color("#FF000080"),
Some(Color32::from_rgba_unmultiplied(255, 0, 0, 128))
);
}
#[test]
fn test_parse_rgb_color() {
assert_eq!(
ThemeConfig::parse_color("rgb(255, 0, 0)"),
Some(Color32::from_rgb(255, 0, 0))
);
assert_eq!(
ThemeConfig::parse_color("rgba(255, 0, 0, 128)"),
Some(Color32::from_rgba_unmultiplied(255, 0, 0, 128))
);
assert_eq!(
ThemeConfig::parse_color("rgba(255, 0, 0, 0.5)"),
Some(Color32::from_rgba_unmultiplied(255, 0, 0, 127))
);
}
#[test]
fn test_color_to_hex() {
assert_eq!(
ThemeConfig::color_to_hex(Color32::from_rgb(255, 0, 0)),
"#FF0000"
);
assert_eq!(
ThemeConfig::color_to_hex(Color32::from_rgba_unmultiplied(255, 0, 0, 128)),
"#FF000080"
);
}
#[test]
fn test_theme_from_toml() {
let toml = r##"
base = "dark"
primary = "#8B5CF6"
spacing_scale = 0.85
"##;
let theme = Theme::from_toml(toml).unwrap();
assert_eq!(theme.variant, ThemeVariant::Dark);
assert_eq!(theme.primary, Color32::from_rgb(139, 92, 246));
}
#[test]
fn test_theme_roundtrip() {
let original = Theme::dark();
let toml = original.to_toml().unwrap();
let restored = Theme::from_toml(&toml).unwrap();
assert_eq!(original.variant, restored.variant);
assert_eq!(original.primary, restored.primary);
assert_eq!(original.bg_primary, restored.bg_primary);
}
#[test]
fn test_lightweight_theme() {
struct TestTheme;
impl LightweightTheme for TestTheme {
fn primary(&self) -> Color32 {
Color32::from_rgb(139, 92, 246)
}
fn background(&self) -> Color32 {
Color32::from_rgb(15, 15, 25)
}
fn text(&self) -> Color32 {
Color32::WHITE
}
}
let theme = TestTheme.to_theme();
assert_eq!(theme.primary, Color32::from_rgb(139, 92, 246));
assert_eq!(theme.bg_primary, Color32::from_rgb(15, 15, 25));
assert_eq!(theme.text_primary, Color32::WHITE);
assert_eq!(theme.variant, ThemeVariant::Dark); }
}