use std::cell::RefCell;
use std::rc::Rc;
use serde::{Deserialize, Serialize};
use crate::geometry::Color;
#[derive(Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Palette {
pub accent: Color,
pub accent_hover: Color,
pub accent_active: Color,
pub on_accent: Color,
pub bg: Color,
pub surface: Color,
pub surface_alt: Color,
pub text: Color,
pub text_muted: Color,
pub text_disabled: Color,
pub border: Color,
pub track: Color,
pub placeholder: Color,
pub divider: Color,
pub danger: Color,
}
impl Default for Palette {
fn default() -> Self {
Self {
accent: Color::hex(0x4C8BF5),
accent_hover: Color::hex(0x6BA3FF),
accent_active: Color::hex(0x3A6FD0),
on_accent: Color::WHITE,
bg: Color::hex(0xF3F3F3),
surface: Color::WHITE,
surface_alt: Color::hex(0xF6F8FA),
text: Color::hex(0x2D3436),
text_muted: Color::hex(0x636E72),
text_disabled: Color::hex(0xB0B6BD),
border: Color::hex(0xCFD4DC),
track: Color::hex(0xCFD4DC),
placeholder: Color::hex(0xAAB0B8),
divider: Color::hex(0xE2E6EA),
danger: Color::hex(0xE5484D),
}
}
}
impl Palette {
pub fn dark() -> Self {
Self {
accent: Color::hex(0x4C8BF5),
accent_hover: Color::hex(0x6BA3FF),
accent_active: Color::hex(0x3A6FD0),
on_accent: Color::WHITE,
bg: Color::hex(0x111827),
surface: Color::hex(0x1B2333),
surface_alt: Color::hex(0x232C3E),
text: Color::hex(0xE6E8EC),
text_muted: Color::hex(0x9AA3B2),
text_disabled: Color::hex(0x5A6172),
border: Color::hex(0x2C3650),
track: Color::hex(0x2B3344),
placeholder: Color::hex(0x6B7280),
divider: Color::hex(0x242C3C),
danger: Color::hex(0xE5484D),
}
}
}
#[derive(Clone, Copy)]
pub enum Intent {
Primary,
Neutral,
Danger,
Custom(Color),
}
#[derive(Clone, Copy, Debug)]
pub struct IntentColors {
pub bg: Color,
pub hover: Color,
pub active: Color,
pub fg: Color,
}
impl Intent {
pub fn colors(self, p: &Palette) -> IntentColors {
const L: f32 = 0.10; const D: f32 = 0.10; match self {
Intent::Primary => IntentColors {
bg: p.accent,
hover: p.accent_hover,
active: p.accent_active,
fg: p.on_accent,
},
Intent::Neutral => IntentColors {
bg: p.border,
hover: p.border.darken(D),
active: p.border.darken(D * 2.0),
fg: p.text,
},
Intent::Danger => IntentColors {
bg: p.danger,
hover: p.danger.lighten(L),
active: p.danger.darken(D),
fg: p.danger.pick_fg(p.text, p.on_accent),
},
Intent::Custom(c) => IntentColors {
bg: c,
hover: c.lighten(L),
active: c.darken(D),
fg: c.pick_fg(p.text, p.on_accent),
},
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum Len {
Dp(f32),
Physical { px: f32 },
}
impl Len {
pub fn to_logical(self, scale: f32) -> f32 {
match self {
Len::Dp(v) => v,
Len::Physical { px } => {
if scale > 0.0 {
px / scale
} else {
px
}
}
}
}
}
#[derive(Clone, Serialize, Deserialize)]
#[serde(default)]
pub struct Metrics {
pub corner_sm: f32,
pub corner_md: f32,
pub corner_lg: f32,
pub border_width: Len,
pub border_width_focus: Len,
pub spacing: i32,
pub text_pad: i32,
pub font_sm: f32,
pub font_md: f32,
pub font_lg: f32,
}
impl Default for Metrics {
fn default() -> Self {
Self {
corner_sm: 4.0,
corner_md: 6.0,
corner_lg: 10.0,
border_width: Len::Dp(1.0),
border_width_focus: Len::Dp(1.8),
spacing: 8,
text_pad: 10,
font_sm: 13.0,
font_md: 14.0,
font_lg: 16.0,
}
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ButtonTheme {
pub bg: Option<Color>,
pub hover: Option<Color>,
pub active: Option<Color>,
pub disabled: Option<Color>,
pub fg: Option<Color>,
pub corner: Option<f32>,
}
impl ButtonTheme {
pub fn bg(&self, p: &Palette) -> Color {
self.bg.unwrap_or(p.accent)
}
pub fn hover(&self, p: &Palette) -> Color {
self.hover.unwrap_or(p.accent_hover)
}
pub fn active(&self, p: &Palette) -> Color {
self.active.unwrap_or(p.accent_active)
}
pub fn disabled(&self, p: &Palette) -> Color {
self.disabled.unwrap_or(p.track)
}
pub fn fg(&self, p: &Palette) -> Color {
self.fg.unwrap_or(p.on_accent)
}
pub fn corner(&self, m: &Metrics) -> f32 {
self.corner.unwrap_or(m.corner_md)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct InputTheme {
pub bg: Option<Color>,
pub border: Option<Color>,
pub border_focus: Option<Color>,
pub text: Option<Color>,
pub placeholder: Option<Color>,
pub selection: Option<Color>,
pub cursor: Option<Color>,
pub corner: Option<f32>,
}
impl InputTheme {
pub fn bg(&self, p: &Palette) -> Color {
self.bg.unwrap_or(p.surface)
}
pub fn border(&self, p: &Palette) -> Color {
self.border.unwrap_or(p.border)
}
pub fn border_focus(&self, p: &Palette) -> Color {
self.border_focus.unwrap_or(p.accent)
}
pub fn text(&self, p: &Palette) -> Color {
self.text.unwrap_or(p.text)
}
pub fn placeholder(&self, p: &Palette) -> Color {
self.placeholder.unwrap_or(p.placeholder)
}
pub fn selection(&self, p: &Palette) -> Color {
self.selection
.unwrap_or(Color::rgba(p.accent.r, p.accent.g, p.accent.b, 0x55))
}
pub fn cursor(&self, p: &Palette) -> Color {
self.cursor.unwrap_or(p.text)
}
pub fn corner(&self, m: &Metrics) -> f32 {
self.corner.unwrap_or(m.corner_md)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ToggleTheme {
pub accent: Option<Color>,
pub track: Option<Color>,
pub knob: Option<Color>,
}
impl ToggleTheme {
pub fn accent(&self, p: &Palette) -> Color {
self.accent.unwrap_or(p.accent)
}
pub fn track(&self, p: &Palette) -> Color {
self.track.unwrap_or(p.track)
}
pub fn knob(&self, p: &Palette) -> Color {
self.knob.unwrap_or(p.surface)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct DropdownTheme {
pub bg: Option<Color>,
pub border: Option<Color>,
pub border_focus: Option<Color>,
pub text: Option<Color>,
pub chevron: Option<Color>,
pub corner: Option<f32>,
}
impl DropdownTheme {
pub fn bg(&self, p: &Palette) -> Color {
self.bg.unwrap_or(p.surface)
}
pub fn border(&self, p: &Palette) -> Color {
self.border.unwrap_or(p.border)
}
pub fn border_focus(&self, p: &Palette) -> Color {
self.border_focus.unwrap_or(p.accent)
}
pub fn text(&self, p: &Palette) -> Color {
self.text.unwrap_or(p.text)
}
pub fn chevron(&self, p: &Palette) -> Color {
self.chevron.unwrap_or(p.text_muted)
}
pub fn corner(&self, m: &Metrics) -> f32 {
self.corner.unwrap_or(m.corner_md)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct MenuTheme {
pub bg: Option<Color>,
pub border: Option<Color>,
pub text: Option<Color>,
pub text_disabled: Option<Color>,
pub hover: Option<Color>,
pub accent: Option<Color>,
}
impl MenuTheme {
pub fn bg(&self, p: &Palette) -> Color {
self.bg.unwrap_or(p.surface)
}
pub fn border(&self, p: &Palette) -> Color {
self.border.unwrap_or(p.border)
}
pub fn text(&self, p: &Palette) -> Color {
self.text.unwrap_or(p.text)
}
pub fn text_disabled(&self, p: &Palette) -> Color {
self.text_disabled.unwrap_or(p.text_disabled)
}
pub fn hover(&self, p: &Palette) -> Color {
self.hover
.unwrap_or(Color::rgba(p.accent.r, p.accent.g, p.accent.b, 0x22))
}
pub fn accent(&self, p: &Palette) -> Color {
self.accent.unwrap_or(p.accent)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct TabTheme {
pub accent: Option<Color>,
pub inactive: Option<Color>,
pub hover: Option<Color>,
}
impl TabTheme {
pub fn accent(&self, p: &Palette) -> Color {
self.accent.unwrap_or(p.accent)
}
pub fn inactive(&self, p: &Palette) -> Color {
self.inactive.unwrap_or(p.text_muted)
}
pub fn hover(&self, p: &Palette) -> Color {
self.hover.unwrap_or(p.text)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ProgressTheme {
pub track: Option<Color>,
pub fill: Option<Color>,
}
impl ProgressTheme {
pub fn track(&self, p: &Palette) -> Color {
self.track.unwrap_or(p.track)
}
pub fn fill(&self, p: &Palette) -> Color {
self.fill.unwrap_or(p.accent)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct StepperTheme {
pub bg: Option<Color>,
pub border: Option<Color>,
pub text: Option<Color>,
pub button: Option<Color>,
pub button_hover: Option<Color>,
}
impl StepperTheme {
pub fn bg(&self, p: &Palette) -> Color {
self.bg.unwrap_or(p.surface)
}
pub fn border(&self, p: &Palette) -> Color {
self.border.unwrap_or(p.border)
}
pub fn text(&self, p: &Palette) -> Color {
self.text.unwrap_or(p.text)
}
pub fn button(&self, p: &Palette) -> Color {
self.button.unwrap_or(p.accent)
}
pub fn button_hover(&self, p: &Palette) -> Color {
self.button_hover
.unwrap_or(Color::rgba(p.accent.r, p.accent.g, p.accent.b, 0x18))
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ListTheme {
pub text: Option<Color>,
pub selected_bg: Option<Color>,
pub selected_text: Option<Color>,
pub hover_bg: Option<Color>,
}
impl ListTheme {
pub fn text(&self, p: &Palette) -> Color {
self.text.unwrap_or(p.text)
}
pub fn selected_bg(&self, p: &Palette) -> Color {
self.selected_bg
.unwrap_or(Color::rgba(p.accent.r, p.accent.g, p.accent.b, 0x22))
}
pub fn selected_text(&self, p: &Palette) -> Color {
self.selected_text.unwrap_or(p.accent)
}
pub fn hover_bg(&self, p: &Palette) -> Color {
self.hover_bg.unwrap_or(p.surface_alt)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct LinkTheme {
pub color: Option<Color>,
pub hover: Option<Color>,
pub pressed: Option<Color>,
}
impl LinkTheme {
pub fn color(&self, p: &Palette) -> Color {
self.color.unwrap_or(p.accent)
}
pub fn hover(&self, p: &Palette) -> Color {
self.hover.unwrap_or(p.accent_hover)
}
pub fn pressed(&self, p: &Palette) -> Color {
self.pressed.unwrap_or(p.accent_active)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct SegmentTheme {
pub bg: Option<Color>,
pub border: Option<Color>,
pub border_focus: Option<Color>,
pub selected_bg: Option<Color>,
pub selected_text: Option<Color>,
pub text: Option<Color>,
pub hover_bg: Option<Color>,
pub divider: Option<Color>,
pub corner: Option<f32>,
}
impl SegmentTheme {
pub fn bg(&self, p: &Palette) -> Color {
self.bg.unwrap_or(p.surface)
}
pub fn border(&self, p: &Palette) -> Color {
self.border.unwrap_or(p.border)
}
pub fn border_focus(&self, p: &Palette) -> Color {
self.border_focus.unwrap_or(p.accent)
}
pub fn selected_bg(&self, p: &Palette) -> Color {
self.selected_bg.unwrap_or(p.accent)
}
pub fn selected_text(&self, p: &Palette) -> Color {
self.selected_text.unwrap_or(p.on_accent)
}
pub fn text(&self, p: &Palette) -> Color {
self.text.unwrap_or(p.text_muted)
}
pub fn hover_bg(&self, p: &Palette) -> Color {
self.hover_bg
.unwrap_or(Color::rgba(p.accent.r, p.accent.g, p.accent.b, 0x12))
}
pub fn divider(&self, p: &Palette) -> Color {
self.divider.unwrap_or(p.divider)
}
pub fn corner(&self, m: &Metrics) -> f32 {
self.corner.unwrap_or(m.corner_md)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct TableTheme {
pub sort_asc: Option<String>,
pub sort_desc: Option<String>,
pub sort_size: Option<f32>,
pub sort_color: Option<Color>,
pub sort_slot: Option<i32>,
pub sort_gap: Option<i32>,
pub sort_leading: Option<bool>,
}
impl TableTheme {
pub fn sort_asc(&self) -> &str {
self.sort_asc.as_deref().unwrap_or("\u{25B2}")
}
pub fn sort_desc(&self) -> &str {
self.sort_desc.as_deref().unwrap_or("\u{25BC}")
}
pub fn sort_size(&self) -> f32 {
self.sort_size.unwrap_or(10.0)
}
pub fn sort_slot(&self) -> i32 {
self.sort_slot.unwrap_or(14)
}
pub fn sort_gap(&self) -> i32 {
self.sort_gap.unwrap_or(2)
}
pub fn sort_leading(&self) -> bool {
self.sort_leading.unwrap_or(false)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct NavTheme {
pub text: Option<Color>,
pub hover_bg: Option<Color>,
pub chevron: Option<Color>,
}
impl NavTheme {
pub fn text(&self, p: &Palette) -> Color {
self.text.unwrap_or(p.text)
}
pub fn hover_bg(&self, p: &Palette) -> Color {
self.hover_bg.unwrap_or(p.surface_alt)
}
pub fn chevron(&self, p: &Palette) -> Color {
self.chevron.unwrap_or(p.text_muted)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct AnimTheme {
pub fast: Option<u32>,
pub normal: Option<u32>,
pub slow: Option<u32>,
}
impl AnimTheme {
pub fn fast(&self) -> u32 {
self.fast.unwrap_or(120)
}
pub fn normal(&self) -> u32 {
self.normal.unwrap_or(200)
}
pub fn slow(&self) -> u32 {
self.slow.unwrap_or(300)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct AccordionTheme {
pub border: Option<Color>,
pub corner: Option<f32>,
pub header_bg: Option<Color>,
pub divider: Option<Color>,
}
impl AccordionTheme {
pub fn border(&self, p: &Palette) -> Color {
self.border.unwrap_or(p.border)
}
pub fn corner(&self, m: &Metrics) -> f32 {
self.corner.unwrap_or(m.corner_md)
}
pub fn header_bg(&self, p: &Palette) -> Color {
self.header_bg.unwrap_or(p.surface_alt)
}
pub fn divider(&self, p: &Palette) -> Color {
self.divider.unwrap_or(p.divider)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct TooltipTheme {
pub bg: Option<Color>,
pub text: Option<Color>,
pub corner: Option<f32>,
}
impl TooltipTheme {
pub fn bg(&self, _p: &Palette) -> Color {
self.bg.unwrap_or(Color::hex(0x303033))
}
pub fn text(&self, _p: &Palette) -> Color {
self.text.unwrap_or(Color::WHITE)
}
pub fn corner(&self, m: &Metrics) -> f32 {
self.corner.unwrap_or(m.corner_sm)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct ToastTheme {
pub bg: Option<Color>,
pub text: Option<Color>,
pub success: Option<Color>,
pub error: Option<Color>,
pub corner: Option<f32>,
}
impl ToastTheme {
pub fn bg(&self, _p: &Palette) -> Color {
self.bg.unwrap_or(Color::rgba(0x32, 0x32, 0x35, 235))
}
pub fn text(&self, _p: &Palette) -> Color {
self.text.unwrap_or(Color::WHITE)
}
pub fn info(&self, _p: &Palette) -> Color {
self.text.unwrap_or(Color::WHITE)
}
pub fn success(&self, _p: &Palette) -> Color {
self.success.unwrap_or(Color::hex(0x4ADE80))
}
pub fn error(&self, p: &Palette) -> Color {
self.error.unwrap_or(p.danger)
}
pub fn corner(&self, m: &Metrics) -> f32 {
self.corner.unwrap_or(m.corner_md)
}
}
#[derive(Clone, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct Theme {
pub palette: Palette,
pub metrics: Metrics,
pub button: ButtonTheme,
pub input: InputTheme,
pub toggle: ToggleTheme,
pub dropdown: DropdownTheme,
pub menu: MenuTheme,
pub tab: TabTheme,
pub progress: ProgressTheme,
pub stepper: StepperTheme,
pub list: ListTheme,
pub link: LinkTheme,
pub segment: SegmentTheme,
pub table: TableTheme,
pub nav: NavTheme,
pub accordion: AccordionTheme,
pub anim: AnimTheme,
pub tooltip: TooltipTheme,
pub toast: ToastTheme,
}
impl Theme {
pub fn dark() -> Self {
Self {
palette: Palette::dark(),
..Theme::default()
}
}
pub fn from_toml(s: &str) -> Result<Self, toml::de::Error> {
toml::from_str(s)
}
pub fn to_toml(&self) -> Result<String, toml::ser::Error> {
toml::to_string_pretty(self)
}
}
thread_local! {
static CURRENT: RefCell<Rc<Theme>> = RefCell::new(Rc::new(Theme::default()));
}
pub fn current() -> Rc<Theme> {
CURRENT.with(|c| c.borrow().clone())
}
pub fn set_current(theme: Rc<Theme>) {
CURRENT.with(|c| *c.borrow_mut() = theme);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dark_theme_has_dark_bg_and_light_text() {
let d = Theme::dark();
let lum = |c: Color| c.r as u32 + c.g as u32 + c.b as u32;
assert!(lum(d.palette.bg) < 300, "暗色背景亮度应低");
assert!(lum(d.palette.text) > 500, "暗色文字应亮");
assert_eq!(d.metrics.corner_md, Metrics::default().corner_md);
}
#[test]
fn toml_roundtrip_preserves_palette() {
let mut t = Theme::default();
t.palette.accent = Color::hex(0xFF8800);
let s = t.to_toml().expect("序列化");
let back = Theme::from_toml(&s).expect("反序列化");
assert_eq!(back.palette.accent, Color::hex(0xFF8800));
assert_eq!(back.metrics.corner_md, t.metrics.corner_md);
}
#[test]
fn partial_toml_falls_back_to_defaults() {
let t = Theme::from_toml("[palette]\naccent = \"#112233\"\n").expect("部分 TOML");
assert_eq!(t.palette.accent, Color::hex(0x112233));
assert_eq!(
t.palette.text,
Palette::default().text,
"未指定字段回退默认"
);
assert!(t.button.bg.is_none(), "控件覆盖默认 None");
}
#[test]
fn override_layer_resolves_or_falls_back() {
let p = Palette::default();
let mut bt = ButtonTheme::default();
assert_eq!(bt.bg(&p), p.accent, "无覆盖回退 palette.accent");
bt.bg = Some(Color::hex(0x010203));
assert_eq!(bt.bg(&p), Color::hex(0x010203), "有覆盖取覆盖值");
}
#[test]
fn intent_colors_maps_palette_and_derives_fg() {
let p = Palette::default();
assert_eq!(Intent::Primary.colors(&p).bg, p.accent);
assert_eq!(Intent::Primary.colors(&p).fg, p.on_accent);
assert_eq!(Intent::Neutral.colors(&p).bg, p.border);
assert_eq!(Intent::Danger.colors(&p).bg, p.danger);
let custom = Color::hex(0x2E9E5B);
assert_eq!(
Intent::Custom(custom).colors(&p).bg,
custom,
"Custom 基色直用"
);
assert_eq!(Intent::Custom(Color::hex(0xFFF0A0)).colors(&p).fg, p.text);
}
}