Skip to main content

oxiui_theme/
manager.rs

1//! [`ThemeManager`] — runtime theme switching with observer notifications.
2//!
3//! ```rust
4//! use oxiui_theme::{CooljapanTheme, dark, light};
5//! use oxiui_theme::manager::ThemeManager;
6//! use std::sync::{Arc, Mutex};
7//!
8//! // Wrap the initial theme (must be a concrete Clone-able type).
9//! let initial = oxiui_theme::cooljapan_default();
10//! ```
11
12use crate::CooljapanTheme;
13use std::sync::atomic::{AtomicU64, Ordering};
14
15/// A callback invoked whenever the active theme changes.
16pub type ThemeListener = Box<dyn Fn(&CooljapanTheme) + Send + Sync>;
17
18/// A unique handle returned by [`ThemeManager::subscribe`].
19///
20/// Pass this to [`ThemeManager::unsubscribe`] to stop receiving notifications.
21pub type ListenerId = u64;
22
23static NEXT_LISTENER_ID: AtomicU64 = AtomicU64::new(1);
24
25/// Runtime theme manager with observer notifications.
26///
27/// Holds one active [`CooljapanTheme`] and notifies all registered listeners
28/// whenever [`set_theme`](ThemeManager::set_theme) is called.
29///
30/// # Example
31/// ```rust
32/// use oxiui_core::{Color, FontSpec, Palette};
33/// use oxiui_theme::{CooljapanTheme};
34/// use oxiui_theme::manager::ThemeManager;
35/// use std::sync::{Arc, Mutex};
36///
37/// let initial = CooljapanTheme::new(
38///     Palette {
39///         background: Color(0, 0, 0, 255),
40///         surface: Color(10, 10, 26, 255),
41///         primary: Color(255, 255, 0, 255),
42///         on_primary: Color(0, 0, 0, 255),
43///         text: Color(255, 255, 255, 255),
44///         muted: Color(200, 200, 200, 255),
45///     },
46///     FontSpec::new("Inter", 14.0, 400),
47/// );
48/// let mut manager = ThemeManager::new(initial.clone());
49/// let called = Arc::new(Mutex::new(false));
50/// let c = called.clone();
51/// manager.subscribe(Box::new(move |_| { *c.lock().unwrap() = true; }));
52/// manager.set_theme(initial);
53/// assert!(*called.lock().unwrap());
54/// ```
55pub struct ThemeManager {
56    active: CooljapanTheme,
57    listeners: Vec<(ListenerId, ThemeListener)>,
58}
59
60impl ThemeManager {
61    /// Construct a manager starting with `initial` as the active theme.
62    pub fn new(initial: CooljapanTheme) -> Self {
63        Self {
64            active: initial,
65            listeners: Vec::new(),
66        }
67    }
68
69    /// Return a reference to the currently active theme.
70    pub fn theme(&self) -> &CooljapanTheme {
71        &self.active
72    }
73
74    /// Switch the active theme and notify every registered listener.
75    pub fn set_theme(&mut self, theme: CooljapanTheme) {
76        self.active = theme;
77        for (_, listener) in &self.listeners {
78            listener(&self.active);
79        }
80    }
81
82    /// Register a listener and return its [`ListenerId`].
83    ///
84    /// The listener is called synchronously inside [`set_theme`](ThemeManager::set_theme)
85    /// with a reference to the new theme.
86    pub fn subscribe(&mut self, f: ThemeListener) -> ListenerId {
87        let id = NEXT_LISTENER_ID.fetch_add(1, Ordering::Relaxed);
88        self.listeners.push((id, f));
89        id
90    }
91
92    /// Remove the listener registered with `id`.
93    ///
94    /// If `id` is not found, this is a no-op.
95    pub fn unsubscribe(&mut self, id: ListenerId) {
96        self.listeners.retain(|(lid, _)| *lid != id);
97    }
98
99    /// Returns the number of currently registered listeners.
100    pub fn listener_count(&self) -> usize {
101        self.listeners.len()
102    }
103}
104
105#[cfg(test)]
106mod tests {
107    use super::*;
108    use oxiui_core::{Color, FontSpec, Palette};
109    use std::sync::{Arc, Mutex};
110
111    fn make_theme(bg: u8) -> CooljapanTheme {
112        CooljapanTheme::new(
113            Palette {
114                background: Color(bg, bg, bg, 255),
115                surface: Color(bg, bg, bg, 255),
116                primary: Color(0, 0, 200, 255),
117                on_primary: Color(255, 255, 255, 255),
118                text: Color(0, 0, 0, 255),
119                muted: Color(60, 60, 60, 255),
120            },
121            FontSpec::new("Inter", 14.0, 400),
122        )
123    }
124
125    #[test]
126    fn theme_manager_set_fires_listeners() {
127        let mut manager = ThemeManager::new(make_theme(0));
128        let called = Arc::new(Mutex::new(0u32));
129        let c = called.clone();
130        manager.subscribe(Box::new(move |_| {
131            *c.lock().unwrap() += 1;
132        }));
133        manager.set_theme(make_theme(255));
134        assert_eq!(
135            *called.lock().unwrap(),
136            1,
137            "listener should be called exactly once"
138        );
139    }
140
141    #[test]
142    fn theme_manager_unsubscribe() {
143        let mut manager = ThemeManager::new(make_theme(0));
144        let called = Arc::new(Mutex::new(0u32));
145        let c = called.clone();
146        let id = manager.subscribe(Box::new(move |_| {
147            *c.lock().unwrap() += 1;
148        }));
149        manager.unsubscribe(id);
150        manager.set_theme(make_theme(128));
151        assert_eq!(
152            *called.lock().unwrap(),
153            0,
154            "unsubscribed listener must not be called"
155        );
156    }
157
158    #[test]
159    fn theme_manager_multiple_listeners() {
160        let mut manager = ThemeManager::new(make_theme(0));
161        let counts: Vec<Arc<Mutex<u32>>> = (0..3).map(|_| Arc::new(Mutex::new(0u32))).collect();
162        for c in &counts {
163            let c = c.clone();
164            manager.subscribe(Box::new(move |_| {
165                *c.lock().unwrap() += 1;
166            }));
167        }
168        manager.set_theme(make_theme(42));
169        for (i, c) in counts.iter().enumerate() {
170            assert_eq!(*c.lock().unwrap(), 1, "listener {i} must be called once");
171        }
172    }
173
174    #[test]
175    fn theme_manager_theme_getter() {
176        use oxiui_core::Theme;
177        let theme = make_theme(100);
178        let manager = ThemeManager::new(theme.clone());
179        let active = manager.theme();
180        assert_eq!(active.palette().background, Color(100, 100, 100, 255));
181    }
182}