use crate::theme::{ColorScheme, ThemeBundle};
use crate::tokens::*;
use blinc_animation::{AnimatedValue, AnimationScheduler, SchedulerHandle, SpringConfig};
use blinc_core::Color;
use rustc_hash::FxHashMap;
use std::collections::HashMap;
use std::sync::{atomic::AtomicBool, atomic::Ordering, Arc, Mutex, OnceLock, RwLock};
static THEME_STATE: OnceLock<ThemeState> = OnceLock::new();
static REDRAW_CALLBACK: Mutex<Option<fn()>> = Mutex::new(None);
pub fn set_redraw_callback(callback: fn()) {
*REDRAW_CALLBACK.lock().unwrap() = Some(callback);
}
fn trigger_redraw() {
if let Some(callback) = *REDRAW_CALLBACK.lock().unwrap() {
callback();
}
}
#[derive(Default)]
struct ThemeTransition {
progress: Option<AnimatedValue>,
from_colors: Option<ColorTokens>,
to_colors: Option<ColorTokens>,
}
pub struct ThemeState {
bundle: ThemeBundle,
scheme: RwLock<ColorScheme>,
colors: RwLock<ColorTokens>,
shadows: RwLock<ShadowTokens>,
spacing: RwLock<SpacingTokens>,
typography: RwLock<TypographyTokens>,
radii: RwLock<RadiusTokens>,
animations: RwLock<AnimationTokens>,
color_overrides: RwLock<FxHashMap<ColorToken, Color>>,
spacing_overrides: RwLock<FxHashMap<SpacingToken, f32>>,
radius_overrides: RwLock<FxHashMap<RadiusToken, f32>>,
needs_repaint: AtomicBool,
needs_layout: AtomicBool,
scheduler_handle: RwLock<Option<SchedulerHandle>>,
transition: Mutex<ThemeTransition>,
}
impl ThemeState {
pub fn init(bundle: ThemeBundle, scheme: ColorScheme) {
let theme = bundle.for_scheme(scheme);
let state = ThemeState {
bundle,
scheme: RwLock::new(scheme),
colors: RwLock::new(theme.colors().clone()),
shadows: RwLock::new(theme.shadows().clone()),
spacing: RwLock::new(theme.spacing().clone()),
typography: RwLock::new(theme.typography().clone()),
radii: RwLock::new(theme.radii().clone()),
animations: RwLock::new(theme.animations().clone()),
color_overrides: RwLock::new(FxHashMap::default()),
spacing_overrides: RwLock::new(FxHashMap::default()),
radius_overrides: RwLock::new(FxHashMap::default()),
needs_repaint: AtomicBool::new(false),
needs_layout: AtomicBool::new(false),
scheduler_handle: RwLock::new(None),
transition: Mutex::new(ThemeTransition::default()),
};
let _ = THEME_STATE.set(state);
}
pub fn set_scheduler(&self, scheduler: &Arc<Mutex<AnimationScheduler>>) {
let handle = scheduler.lock().unwrap().handle();
*self.scheduler_handle.write().unwrap() = Some(handle);
}
pub fn init_default() {
use crate::platform::detect_system_color_scheme;
use crate::themes::platform::platform_theme_bundle;
let bundle = platform_theme_bundle();
let scheme = detect_system_color_scheme();
Self::init(bundle, scheme);
}
pub fn get() -> &'static ThemeState {
THEME_STATE
.get()
.expect("ThemeState not initialized. Call ThemeState::init() at app startup.")
}
pub fn try_get() -> Option<&'static ThemeState> {
THEME_STATE.get()
}
pub fn scheme(&self) -> ColorScheme {
*self.scheme.read().unwrap()
}
pub fn set_scheme(&self, scheme: ColorScheme) {
let mut current = self.scheme.write().unwrap();
if *current != scheme {
tracing::debug!(
"ThemeState::set_scheme - switching from {:?} to {:?}",
*current,
scheme
);
let old_colors = self.colors.read().unwrap().clone();
*current = scheme;
drop(current);
let theme = self.bundle.for_scheme(scheme);
let new_colors = theme.colors().clone();
*self.shadows.write().unwrap() = theme.shadows().clone();
*self.spacing.write().unwrap() = theme.spacing().clone();
*self.typography.write().unwrap() = theme.typography().clone();
*self.radii.write().unwrap() = theme.radii().clone();
*self.animations.write().unwrap() = theme.animations().clone();
let handle_opt = self.scheduler_handle.read().unwrap().clone();
if let Some(handle) = handle_opt {
let mut transition = self.transition.lock().unwrap();
transition.from_colors = Some(old_colors.clone());
transition.to_colors = Some(new_colors.clone());
let mut progress = AnimatedValue::new(handle, 0.0, SpringConfig::gentle());
progress.set_target(100.0);
transition.progress = Some(progress);
drop(transition);
*self.colors.write().unwrap() = old_colors;
} else {
*self.colors.write().unwrap() = new_colors;
}
self.needs_repaint.store(true, Ordering::SeqCst);
self.needs_layout.store(true, Ordering::SeqCst);
trigger_redraw();
}
}
pub fn tick(&self) -> bool {
let mut transition = self.transition.lock().unwrap();
let progress_opt = transition.progress.as_ref();
if progress_opt.is_none() {
return false;
}
let progress_anim = transition.progress.as_ref().unwrap();
let raw_progress = progress_anim.get();
let progress = (raw_progress / 100.0).clamp(0.0, 1.0);
let at_target = (raw_progress - 100.0).abs() < 1.0;
tracing::trace!(
"Theme tick: raw={:.1}, progress={:.3}, at_target={}",
raw_progress,
progress,
at_target
);
if let (Some(ref from), Some(ref to)) = (&transition.from_colors, &transition.to_colors) {
let interpolated = interpolate_color_tokens(from, to, progress);
drop(transition);
*self.colors.write().unwrap() = interpolated;
if at_target {
let mut transition = self.transition.lock().unwrap();
transition.progress = None;
transition.from_colors = None;
transition.to_colors = None;
return false;
}
trigger_redraw();
return true;
}
transition.progress = None;
false
}
pub fn is_animating(&self) -> bool {
let transition = self.transition.lock().unwrap();
transition
.progress
.as_ref()
.map(|p| p.is_animating())
.unwrap_or(false)
}
pub fn toggle_scheme(&self) {
let current = self.scheme();
self.set_scheme(current.toggle());
}
pub fn color(&self, token: ColorToken) -> Color {
if let Some(color) = self.color_overrides.read().unwrap().get(&token) {
return *color;
}
self.colors.read().unwrap().get(token)
}
pub fn colors(&self) -> ColorTokens {
self.colors.read().unwrap().clone()
}
pub fn set_color_override(&self, token: ColorToken, color: Color) {
self.color_overrides.write().unwrap().insert(token, color);
self.needs_repaint.store(true, Ordering::SeqCst);
trigger_redraw();
}
pub fn remove_color_override(&self, token: ColorToken) {
self.color_overrides.write().unwrap().remove(&token);
self.needs_repaint.store(true, Ordering::SeqCst);
trigger_redraw();
}
pub fn to_css_variable_map(&self) -> HashMap<String, String> {
fn hex(c: Color) -> String {
if c.a < 1.0 {
format!(
"rgba({},{},{},{})",
(c.r * 255.0) as u8,
(c.g * 255.0) as u8,
(c.b * 255.0) as u8,
c.a
)
} else {
format!(
"#{:02x}{:02x}{:02x}",
(c.r * 255.0) as u8,
(c.g * 255.0) as u8,
(c.b * 255.0) as u8
)
}
}
let mut vars = HashMap::with_capacity(44);
vars.insert("primary".into(), hex(self.color(ColorToken::Primary)));
vars.insert(
"primary-hover".into(),
hex(self.color(ColorToken::PrimaryHover)),
);
vars.insert(
"primary-active".into(),
hex(self.color(ColorToken::PrimaryActive)),
);
vars.insert("secondary".into(), hex(self.color(ColorToken::Secondary)));
vars.insert(
"secondary-hover".into(),
hex(self.color(ColorToken::SecondaryHover)),
);
vars.insert(
"secondary-active".into(),
hex(self.color(ColorToken::SecondaryActive)),
);
vars.insert("success".into(), hex(self.color(ColorToken::Success)));
vars.insert("success-bg".into(), hex(self.color(ColorToken::SuccessBg)));
vars.insert("warning".into(), hex(self.color(ColorToken::Warning)));
vars.insert("warning-bg".into(), hex(self.color(ColorToken::WarningBg)));
vars.insert("error".into(), hex(self.color(ColorToken::Error)));
vars.insert("error-bg".into(), hex(self.color(ColorToken::ErrorBg)));
vars.insert("info".into(), hex(self.color(ColorToken::Info)));
vars.insert("info-bg".into(), hex(self.color(ColorToken::InfoBg)));
vars.insert("background".into(), hex(self.color(ColorToken::Background)));
vars.insert("surface".into(), hex(self.color(ColorToken::Surface)));
vars.insert(
"surface-elevated".into(),
hex(self.color(ColorToken::SurfaceElevated)),
);
vars.insert(
"surface-overlay".into(),
hex(self.color(ColorToken::SurfaceOverlay)),
);
vars.insert(
"text-primary".into(),
hex(self.color(ColorToken::TextPrimary)),
);
vars.insert(
"text-secondary".into(),
hex(self.color(ColorToken::TextSecondary)),
);
vars.insert(
"text-tertiary".into(),
hex(self.color(ColorToken::TextTertiary)),
);
vars.insert(
"text-inverse".into(),
hex(self.color(ColorToken::TextInverse)),
);
vars.insert("text-link".into(), hex(self.color(ColorToken::TextLink)));
vars.insert("border".into(), hex(self.color(ColorToken::Border)));
vars.insert(
"border-secondary".into(),
hex(self.color(ColorToken::BorderSecondary)),
);
vars.insert(
"border-hover".into(),
hex(self.color(ColorToken::BorderHover)),
);
vars.insert(
"border-focus".into(),
hex(self.color(ColorToken::BorderFocus)),
);
vars.insert(
"border-error".into(),
hex(self.color(ColorToken::BorderError)),
);
vars.insert("input-bg".into(), hex(self.color(ColorToken::InputBg)));
vars.insert(
"input-bg-hover".into(),
hex(self.color(ColorToken::InputBgHover)),
);
vars.insert(
"input-bg-focus".into(),
hex(self.color(ColorToken::InputBgFocus)),
);
vars.insert(
"input-bg-disabled".into(),
hex(self.color(ColorToken::InputBgDisabled)),
);
vars.insert("selection".into(), hex(self.color(ColorToken::Selection)));
vars.insert(
"selection-text".into(),
hex(self.color(ColorToken::SelectionText)),
);
vars.insert("accent".into(), hex(self.color(ColorToken::Accent)));
vars.insert(
"accent-subtle".into(),
hex(self.color(ColorToken::AccentSubtle)),
);
vars.insert(
"tooltip-bg".into(),
hex(self.color(ColorToken::TooltipBackground)),
);
vars.insert(
"tooltip-text".into(),
hex(self.color(ColorToken::TooltipText)),
);
vars
}
pub fn spacing_value(&self, token: SpacingToken) -> f32 {
if let Some(value) = self.spacing_overrides.read().unwrap().get(&token) {
return *value;
}
self.spacing.read().unwrap().get(token)
}
pub fn spacing(&self) -> SpacingTokens {
self.spacing.read().unwrap().clone()
}
pub fn set_spacing_override(&self, token: SpacingToken, value: f32) {
self.spacing_overrides.write().unwrap().insert(token, value);
self.needs_layout.store(true, Ordering::SeqCst);
trigger_redraw();
}
pub fn remove_spacing_override(&self, token: SpacingToken) {
self.spacing_overrides.write().unwrap().remove(&token);
self.needs_layout.store(true, Ordering::SeqCst);
trigger_redraw();
}
pub fn typography(&self) -> TypographyTokens {
self.typography.read().unwrap().clone()
}
pub fn radius(&self, token: RadiusToken) -> f32 {
if let Some(value) = self.radius_overrides.read().unwrap().get(&token) {
return *value;
}
self.radii.read().unwrap().get(token)
}
pub fn radii(&self) -> RadiusTokens {
self.radii.read().unwrap().clone()
}
pub fn set_radius_override(&self, token: RadiusToken, value: f32) {
self.radius_overrides.write().unwrap().insert(token, value);
self.needs_repaint.store(true, Ordering::SeqCst);
trigger_redraw();
}
pub fn shadows(&self) -> ShadowTokens {
self.shadows.read().unwrap().clone()
}
pub fn animations(&self) -> AnimationTokens {
self.animations.read().unwrap().clone()
}
pub fn needs_repaint(&self) -> bool {
self.needs_repaint.load(Ordering::SeqCst)
}
pub fn clear_repaint(&self) {
self.needs_repaint.store(false, Ordering::SeqCst);
}
pub fn needs_layout(&self) -> bool {
self.needs_layout.load(Ordering::SeqCst)
}
pub fn clear_layout(&self) {
self.needs_layout.store(false, Ordering::SeqCst);
}
pub fn clear_overrides(&self) {
self.color_overrides.write().unwrap().clear();
self.spacing_overrides.write().unwrap().clear();
self.radius_overrides.write().unwrap().clear();
self.needs_repaint.store(true, Ordering::SeqCst);
self.needs_layout.store(true, Ordering::SeqCst);
trigger_redraw();
}
}
fn interpolate_color_tokens(from: &ColorTokens, to: &ColorTokens, t: f32) -> ColorTokens {
ColorTokens::lerp(from, to, t)
}