use std::{cell::RefCell, rc::Rc};
use crate::element::style::Color;
#[derive(Clone, Debug, PartialEq)]
pub struct Theme {
pub colors: ColorTokens,
pub chrome: WidgetChromeTokens,
pub typography: TypographyTokens,
pub spacing: SpacingTokens,
pub radius: RadiusTokens,
}
impl Default for Theme {
fn default() -> Self {
Self::default_dark()
}
}
impl Theme {
pub fn default_light() -> Self {
Self {
colors: ColorTokens {
background: Color::rgb8(248, 250, 252),
surface: Color::rgb8(255, 255, 255),
surface_hover: Color::rgb8(241, 245, 249),
surface_pressed: Color::rgb8(226, 232, 240),
foreground: Color::rgb8(15, 23, 42),
foreground_secondary: Color::rgb8(71, 85, 105),
foreground_disabled: Color::rgb8(148, 163, 184),
on_accent: Color::rgb8(255, 255, 255),
accent: Color::rgb8(59, 130, 246),
accent_hover: Color::rgb8(37, 99, 235),
accent_pressed: Color::rgb8(29, 78, 216),
error: Color::rgb8(220, 38, 38),
border: Color::rgb8(203, 213, 225),
},
chrome: WidgetChromeTokens {
scrollbar_track: Color::rgb8(229, 231, 235),
scrollbar_thumb: Color::rgb8(156, 163, 175),
caret: Color::rgb8(15, 23, 42),
focus_ring: Color::rgb8(59, 130, 246),
},
typography: TypographyTokens {
font_size_sm: 12.0,
font_size_md: 14.0,
font_size_lg: 18.0,
font_size_xl: 24.0,
font_family: "system-ui".to_string(),
line_height: 1.5,
letter_spacing: 0.0,
},
spacing: SpacingTokens {
xs: 4.0,
sm: 8.0,
md: 12.0,
lg: 16.0,
xl: 24.0,
},
radius: RadiusTokens {
sm: 4.0,
md: 8.0,
lg: 12.0,
},
}
}
pub fn default_dark() -> Self {
Self {
colors: ColorTokens {
background: Color::rgb8(0, 0, 0),
surface: Color::rgb8(35, 35, 52),
surface_hover: Color::rgb8(45, 45, 66),
surface_pressed: Color::rgb8(55, 55, 78),
foreground: Color::rgb8(255, 255, 255),
foreground_secondary: Color::rgb8(156, 163, 175),
foreground_disabled: Color::rgb8(107, 114, 128),
on_accent: Color::rgb8(255, 255, 255),
accent: Color::rgb8(59, 130, 246),
accent_hover: Color::rgb8(37, 99, 235),
accent_pressed: Color::rgb8(29, 78, 216),
error: Color::rgb8(248, 113, 113),
border: Color::rgb8(80, 80, 110),
},
chrome: WidgetChromeTokens {
scrollbar_track: Color::rgb8(30, 30, 40),
scrollbar_thumb: Color::rgb8(120, 120, 140),
caret: Color::rgb8(235, 235, 240),
focus_ring: Color::rgb8(59, 130, 246),
},
typography: TypographyTokens {
font_size_sm: 12.0,
font_size_md: 14.0,
font_size_lg: 18.0,
font_size_xl: 24.0,
font_family: "system-ui".to_string(),
line_height: 1.5,
letter_spacing: 0.0,
},
spacing: SpacingTokens {
xs: 4.0,
sm: 8.0,
md: 12.0,
lg: 16.0,
xl: 24.0,
},
radius: RadiusTokens {
sm: 4.0,
md: 8.0,
lg: 12.0,
},
}
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ColorTokens {
pub background: Color,
pub surface: Color,
pub surface_hover: Color,
pub surface_pressed: Color,
pub foreground: Color,
pub foreground_secondary: Color,
pub foreground_disabled: Color,
pub on_accent: Color,
pub accent: Color,
pub accent_hover: Color,
pub accent_pressed: Color,
pub error: Color,
pub border: Color,
}
#[derive(Clone, Debug, PartialEq)]
pub struct WidgetChromeTokens {
pub scrollbar_track: Color,
pub scrollbar_thumb: Color,
pub caret: Color,
pub focus_ring: Color,
}
#[derive(Clone, Debug, PartialEq)]
pub struct TypographyTokens {
pub font_size_sm: f32,
pub font_size_md: f32,
pub font_size_lg: f32,
pub font_size_xl: f32,
pub font_family: String,
pub line_height: f32,
pub letter_spacing: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct SpacingTokens {
pub xs: f32,
pub sm: f32,
pub md: f32,
pub lg: f32,
pub xl: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct RadiusTokens {
pub sm: f32,
pub md: f32,
pub lg: f32,
}
thread_local! {
static ACTIVE_THEME: RefCell<Rc<Theme>> = RefCell::new(Rc::new(Theme::default_dark()));
}
pub fn set_active_theme(theme: Theme) {
ACTIVE_THEME.with(|active| {
*active.borrow_mut() = Rc::new(theme);
});
}
pub fn current_theme() -> Theme {
ACTIVE_THEME.with(|active| active.borrow().as_ref().clone())
}
#[cfg(test)]
mod tests {
use super::{current_theme, set_active_theme, Theme, WidgetChromeTokens};
use crate::element::style::Color;
#[test]
fn active_theme_defaults_to_dark_on_new_thread() {
let from_thread = std::thread::spawn(current_theme)
.join()
.expect("theme thread should join");
assert_eq!(from_thread, Theme::default_dark());
}
#[test]
fn active_theme_roundtrip_works() {
let previous = current_theme();
let dark = Theme::default_dark();
set_active_theme(dark.clone());
assert_eq!(current_theme(), dark);
set_active_theme(previous);
}
#[test]
fn default_light_chrome_tokens_are_set() {
let theme = Theme::default_light();
assert_ne!(theme.colors.surface_hover, theme.colors.surface);
assert_ne!(theme.colors.surface_pressed, theme.colors.surface);
assert_ne!(theme.colors.accent_pressed, theme.colors.accent);
assert_ne!(theme.colors.foreground_disabled, theme.colors.foreground);
assert_eq!(theme.chrome.focus_ring, Color::rgb8(59, 130, 246));
assert_eq!(theme.chrome.caret, Color::rgb8(15, 23, 42));
assert_ne!(theme.chrome.scrollbar_track, theme.chrome.scrollbar_thumb);
}
#[test]
fn default_dark_chrome_tokens_are_set() {
let theme = Theme::default_dark();
assert_ne!(theme.colors.surface_hover, theme.colors.surface);
assert_ne!(theme.colors.surface_pressed, theme.colors.surface);
assert_ne!(theme.colors.accent_pressed, theme.colors.accent);
assert_ne!(theme.colors.foreground_disabled, theme.colors.foreground);
assert_eq!(theme.chrome.focus_ring, Color::rgb8(59, 130, 246));
assert_eq!(theme.chrome.caret, Color::rgb8(235, 235, 240));
assert_ne!(theme.chrome.scrollbar_track, theme.chrome.scrollbar_thumb);
}
#[test]
fn widget_chrome_tokens_can_be_overridden_independently() {
let previous = current_theme();
let mut custom = Theme::default_light();
custom.chrome = WidgetChromeTokens {
scrollbar_track: Color::rgb8(1, 2, 3),
scrollbar_thumb: Color::rgb8(4, 5, 6),
caret: Color::rgb8(7, 8, 9),
focus_ring: Color::rgb8(10, 11, 12),
};
set_active_theme(custom.clone());
let active = current_theme();
assert_eq!(active.chrome.scrollbar_track, Color::rgb8(1, 2, 3));
assert_eq!(active.chrome.scrollbar_thumb, Color::rgb8(4, 5, 6));
assert_eq!(active.chrome.caret, Color::rgb8(7, 8, 9));
assert_eq!(active.chrome.focus_ring, Color::rgb8(10, 11, 12));
assert_eq!(active.colors, custom.colors);
set_active_theme(previous);
}
}