use crate::compat::HashMap;
use crate::core::{Color, Font};
#[cfg(not(alloc_frugal))]
use serde::{Deserialize, Serialize};
use crate::style::Shadow;
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Theme {
pub name: String,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub appearance: AppearanceMode,
pub colors: Colors,
pub fonts: Fonts,
pub spacing: Spacing,
pub borders: Borders,
pub overrides: ThemeOverrides,
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AppearanceMode {
#[default]
Light,
Dark,
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum WidgetRole {
#[default]
Surface,
Primary,
Text,
Input,
Accent,
Choice,
Danger,
}
impl WidgetRole {
pub fn for_kind_name(kind_name: &str) -> Self {
let normalized: String = kind_name
.chars()
.filter(|c| !matches!(c, '_' | '-' | ' '))
.flat_map(char::to_lowercase)
.collect();
match normalized.as_str() {
"button" | "toggle" | "togglebutton" => Self::Primary,
"label" | "floatinglabel" | "text" | "heading" | "breadcrumb" | "commandlink"
| "lcdnumber" | "fontpreview" => Self::Text,
"lineedit"
| "textedit"
| "textareainput"
| "textarea"
| "spinbox"
| "doublespinbox"
| "combobox"
| "editablecombobox"
| "fontcombobox"
| "autocompleteedit"
| "maskededit"
| "multiselectcombobox"
| "searchbar"
| "searchbox"
| "taginput"
| "codeeditor"
| "richedit"
| "dateedit"
| "timeedit"
| "datetimeedit"
| "inplaceeditor"
| "shortcuteditor"
| "coloreditor" => Self::Input,
"slider" | "rangeslider" | "progressbar" | "progresscircle" | "activityindicator"
| "meter" | "lcdprogress" | "sparkline" => Self::Accent,
"checkbox" | "radiobutton" | "switch" | "checklistbox" | "rating" | "stepper" => {
Self::Choice
}
"errordialog" | "trash" | "deletebutton" => Self::Danger,
_ => Self::Surface,
}
}
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Colors {
pub background: Color,
pub foreground: Color,
pub primary: Color,
pub secondary: Color,
pub accent: Color,
pub error: Color,
pub warning: Color,
pub success: Color,
pub disabled: Color,
#[cfg_attr(not(alloc_frugal), serde(default = "default_info_color"))]
pub info: Color,
}
#[cfg(not(alloc_frugal))]
const fn default_info_color() -> Color {
Color::INFO
}
impl Color {
pub fn from_hex(hex: &str) -> Result<Self, String> {
Self::parse_hex(hex).ok_or_else(|| format!("Invalid hex color string: '{hex}'"))
}
pub fn to_hex(&self) -> String {
self.to_hex_rgba()
}
pub fn dark_variant(&self, factor: f32) -> Self {
let f = factor.clamp(0.0, 1.0);
Self::rgba(
(self.r as f32 * (1.0 - f)).round().clamp(0.0, 255.0) as u8,
(self.g as f32 * (1.0 - f)).round().clamp(0.0, 255.0) as u8,
(self.b as f32 * (1.0 - f)).round().clamp(0.0, 255.0) as u8,
self.a,
)
}
pub fn light_variant(&self, factor: f32) -> Self {
let f = factor.clamp(0.0, 1.0);
Self::rgba(
(self.r as f32 + (255.0 - self.r as f32) * f).round().clamp(0.0, 255.0) as u8,
(self.g as f32 + (255.0 - self.g as f32) * f).round().clamp(0.0, 255.0) as u8,
(self.b as f32 + (255.0 - self.b as f32) * f).round().clamp(0.0, 255.0) as u8,
self.a,
)
}
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Fonts {
pub regular: Font,
pub bold: Font,
pub italic: Font,
pub monospace: Font,
#[cfg_attr(not(alloc_frugal), serde(default = "default_caption_font"))]
pub caption: Font,
#[cfg_attr(not(alloc_frugal), serde(default = "default_body_font"))]
pub body: Font,
#[cfg_attr(not(alloc_frugal), serde(default = "default_title_font"))]
pub title: Font,
#[cfg_attr(not(alloc_frugal), serde(default = "default_headline_font"))]
pub headline: Font,
#[cfg_attr(not(alloc_frugal), serde(default = "default_display_font"))]
pub display: Font,
}
#[cfg(not(alloc_frugal))]
fn default_caption_font() -> Font {
Font::simple("Arial", 11.0)
}
#[cfg(not(alloc_frugal))]
fn default_body_font() -> Font {
Font::simple("Arial", 14.0)
}
#[cfg(not(alloc_frugal))]
fn default_title_font() -> Font {
Font::bold("Arial", 16.0)
}
#[cfg(not(alloc_frugal))]
fn default_headline_font() -> Font {
Font::bold("Arial", 20.0)
}
#[cfg(not(alloc_frugal))]
fn default_display_font() -> Font {
Font::bold("Arial", 28.0)
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Spacing {
pub small: u32,
pub medium: u32,
pub large: u32,
pub extra_large: u32,
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct Borders {
pub width: u32,
pub radius: u32,
pub shadow: bool,
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone)]
pub struct ThemeOverrides {
pub styles: HashMap<String, ThemeStyleToken>,
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ThemeStyleToken {
pub background: Option<Color>,
pub foreground: Option<Color>,
pub border: Option<Color>,
pub border_width: Option<u32>,
pub radius: Option<u32>,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub font: Option<Font>,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub opacity: Option<f32>,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub shadow: ShadowOverride,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub touch_target: Option<[u32; 2]>,
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ShadowOverride {
#[default]
Inherit,
None,
Set(ShadowToken),
}
impl ShadowOverride {
pub fn apply(self, current: Option<Shadow>) -> Option<Shadow> {
match self {
Self::Inherit => current,
Self::None => None,
Self::Set(token) => Some(token.into()),
}
}
}
#[cfg_attr(not(alloc_frugal), derive(Serialize, Deserialize))]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ShadowToken {
#[cfg_attr(not(alloc_frugal), serde(default))]
pub x: i32,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub y: i32,
#[cfg_attr(not(alloc_frugal), serde(default))]
pub blur: u32,
pub color: Color,
}
impl From<ShadowToken> for Shadow {
fn from(token: ShadowToken) -> Self {
Self { x: token.x, y: token.y, blur: token.blur, color: token.color }
}
}