use std::cell::Cell;
use crate::reactive::{create_root, Scope, Signal};
use crate::theme::{default_theme, themes_by_mode, Theme, ThemeMode};
thread_local! {
static THEME_SIGNAL: Cell<Option<Signal<&'static Theme>>> = const { Cell::new(None) };
static LAST_BY_MODE: Cell<[Option<&'static Theme>; 2]> = const { Cell::new([None, None]) };
}
fn mode_slot(mode: ThemeMode) -> usize {
match mode {
ThemeMode::Dark => 0,
ThemeMode::Light => 1,
}
}
fn remember(theme: &'static Theme) {
LAST_BY_MODE.with(|slot| {
let mut last = slot.get();
last[mode_slot(theme.mode())] = Some(theme);
slot.set(last);
});
}
fn theme_signal() -> Signal<&'static Theme> {
THEME_SIGNAL.with(|slot| {
if let Some(sig) = slot.get() {
return sig;
}
let (root, sig) = create_root(|cx| cx.signal(default_theme()));
std::mem::forget(root);
slot.set(Some(sig));
sig
})
}
pub fn use_theme(_cx: Scope) -> Signal<&'static Theme> {
theme_signal()
}
pub fn current_theme() -> &'static Theme {
theme_signal().get_untracked()
}
pub fn set_theme(theme: &'static Theme) -> &'static Theme {
remember(theme);
let sig = theme_signal();
let prev = sig.get_untracked();
sig.set(theme);
prev
}
pub fn toggle_mode() -> &'static Theme {
let target = current_theme().mode().other();
let remembered = LAST_BY_MODE.with(|slot| slot.get()[mode_slot(target)]);
let next = remembered.unwrap_or_else(|| {
themes_by_mode(target)
.first()
.copied()
.expect("both modes carry built-in themes (registry invariant)")
});
set_theme(next);
next
}
pub fn set_theme_by_id(id: &str) -> bool {
match crate::theme::get(id) {
Some(t) => {
set_theme(t);
true
}
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::reactive::create_root;
#[test]
fn theme_defaults_and_switches_reactively() {
assert_eq!(current_theme().id, crate::theme::DEFAULT_THEME_ID);
let runs = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
let r2 = runs.clone();
let (_root, ()) = create_root(|cx| {
let theme = use_theme(cx);
cx.effect(move || r2.borrow_mut().push(theme.get().id));
});
let target = crate::theme::get("nord").expect("nord registered");
let prev = set_theme(target);
assert_eq!(runs.borrow().last().copied(), Some("nord"));
set_theme(prev);
assert_eq!(runs.borrow().len(), 3, "initial + switch + restore");
}
#[test]
fn set_theme_by_id_rejects_unknown() {
assert!(!set_theme_by_id("no-such-theme"));
assert_eq!(current_theme().id, crate::theme::DEFAULT_THEME_ID);
}
#[test]
fn toggle_mode_defaults_to_the_house_theme_then_remembers_choices() {
let anchor = current_theme();
assert!(set_theme_by_id("nord")); let lit = toggle_mode();
assert_eq!(
lit.id, "abstract-light",
"never-visited light mode falls back to the house palette \
(first of mode in the documented ordering)"
);
assert_eq!(current_theme().id, "abstract-light");
assert_eq!(toggle_mode().id, "nord");
assert!(set_theme_by_id("catppuccin-latte"));
assert_eq!(toggle_mode().id, "nord");
assert_eq!(toggle_mode().id, "catppuccin-latte");
set_theme(anchor); }
}