Skip to main content

telar_theme_core/
context.rs

1use std::mem::ManuallyDrop;
2use std::rc::Rc;
3
4use geometry_core::Color;
5use reactive_core::{RwSignal, signal};
6
7// Flexible theme contract: users define their own tokens with whatever names they want. `as_any` is the only requirement so `use_theme` can downcast back to the concrete type.
8pub trait Theme: 'static {
9    fn as_any(&self) -> &dyn std::any::Any;
10}
11
12// Opt-in semantic-token contract the built-in component catalogue reads through. A theme implements this so a component can resolve semantic colors without knowing the concrete theme type. Only the two primary tokens are mandatory; the rest carry defaults so a theme can omit them.
13pub trait ThemeTokens: 'static {
14    fn primary(&self) -> Color;
15    fn on_primary(&self) -> Color;
16
17    fn muted(&self) -> Color {
18        Color::rgba(0.5, 0.5, 0.6, 0.6)
19    }
20    fn scrollbar(&self) -> Color {
21        Color::rgba(0.5, 0.5, 0.6, 0.6)
22    }
23
24    /// Primary text ink for component labels/titles/values. Defaults to a near-black; a theme should override
25    /// it (e.g. a dark theme returns a light ink) so component text stays legible on its surface.
26    fn ink(&self) -> Color {
27        Color::rgba(0.15, 0.15, 0.2, 1.0)
28    }
29    /// A quiet, low-contrast surface tone for chip/tag backgrounds. Defaults to a faint neutral wash.
30    fn surface_alt(&self) -> Color {
31        Color::rgba(0.5, 0.5, 0.55, 0.1)
32    }
33    /// Hairline border/divider tone. Defaults to a faint neutral.
34    fn border(&self) -> Color {
35        Color::rgba(0.5, 0.5, 0.55, 0.35)
36    }
37
38    /// Semantic status colours. Defaults are conventional hues; a theme should override to match its palette.
39    fn success(&self) -> Color {
40        Color::rgba(0.4, 0.7, 0.4, 1.0)
41    }
42    fn warning(&self) -> Color {
43        Color::rgba(0.9, 0.75, 0.4, 1.0)
44    }
45    fn error(&self) -> Color {
46        Color::rgba(0.8, 0.35, 0.4, 1.0)
47    }
48    fn info(&self) -> Color {
49        Color::rgba(0.4, 0.6, 0.8, 1.0)
50    }
51
52    /// Three progressively stronger highlight/elevation tints for hover, selection, and pressed states.
53    /// Defaults to faint neutral washes a theme can override with palette-specific tones.
54    fn highlight_low(&self) -> Color {
55        Color::rgba(0.5, 0.5, 0.55, 0.06)
56    }
57    fn highlight_med(&self) -> Color {
58        Color::rgba(0.5, 0.5, 0.55, 0.12)
59    }
60    fn highlight_high(&self) -> Color {
61        Color::rgba(0.5, 0.5, 0.55, 0.20)
62    }
63}
64
65thread_local! {
66    // ManuallyDrop suppresses RwSignal's Drop impl so no TLS destructor is registered. Cleanup happens via reset_runtime() which drops the entire Runtime (and its signals slab).
67    static THEME: ManuallyDrop<RwSignal<Option<Rc<dyn Theme>>>> =
68        ManuallyDrop::new(signal(None));
69    static THEME_TOKENS: ManuallyDrop<RwSignal<Option<Rc<dyn ThemeTokens>>>> =
70        ManuallyDrop::new(signal(None));
71}
72
73pub fn set_theme<T: Theme + ThemeTokens + Clone + 'static>(theme: T) {
74    let theme = Rc::new(theme);
75    let as_theme: Rc<dyn Theme> = theme.clone();
76    let as_tokens: Rc<dyn ThemeTokens> = theme;
77    THEME.with(|s| s.set(Some(as_theme)));
78    THEME_TOKENS.with(|s| s.set(Some(as_tokens)));
79}
80
81pub fn use_theme<T: Theme + Clone + 'static>() -> T {
82    THEME.with(|s| {
83        let theme = s.get().unwrap_or_else(|| {
84            panic!(
85                "use_theme::<{}> called but no theme has been set; call set_theme first",
86                std::any::type_name::<T>()
87            )
88        });
89        theme
90            .as_any()
91            .downcast_ref::<T>()
92            .unwrap_or_else(|| {
93                panic!(
94                    "use_theme::<{}> called but a theme of a different type is set",
95                    std::any::type_name::<T>()
96                )
97            })
98            .clone()
99    })
100}
101
102pub fn use_theme_tokens() -> Option<Rc<dyn ThemeTokens>> {
103    THEME_TOKENS.with(|s| s.get())
104}