1use std::cell::RefCell;
10use std::collections::HashMap;
11use std::mem::ManuallyDrop;
12use std::rc::Rc;
13
14use reactive_core::{RwSignal, signal};
15
16type ApplyMode = Rc<dyn Fn()>;
19
20thread_local! {
21 static ACTIVE_MODE: ManuallyDrop<RwSignal<Option<String>>> = ManuallyDrop::new(signal(None));
24 static MODES: ManuallyDrop<RefCell<HashMap<String, ApplyMode>>> =
25 ManuallyDrop::new(RefCell::new(HashMap::new()));
26}
27
28pub fn register_mode(id: impl Into<String>, apply: impl Fn() + 'static) {
31 MODES.with(|m| m.borrow_mut().insert(id.into(), Rc::new(apply)));
32}
33
34pub fn set_mode(id: impl Into<String>) {
38 let id = id.into();
39 let apply = MODES.with(|m| m.borrow().get(&id).cloned());
40 if let Some(apply) = apply {
41 apply();
42 }
43 ACTIVE_MODE.with(|s| s.set(Some(id)));
44}
45
46pub fn init_mode(default: impl Into<String>) {
49 let already_set = ACTIVE_MODE.with(|s| s.peek().is_some());
50 if !already_set {
51 set_mode(default);
52 }
53}
54
55pub fn use_mode() -> Option<String> {
58 ACTIVE_MODE.with(|s| s.get())
59}
60
61pub fn active_mode() -> Option<String> {
63 ACTIVE_MODE.with(|s| s.peek())
64}
65
66thread_local! {
67 static SCHEME_PAIR: ManuallyDrop<RefCell<Option<(String, String)>>> =
71 ManuallyDrop::new(RefCell::new(None));
72}
73
74pub fn set_light_dark(light: impl Into<String>, dark: impl Into<String>) {
79 SCHEME_PAIR.with(|p| *p.borrow_mut() = Some((light.into(), dark.into())));
80}
81
82pub fn is_dark() -> bool {
85 let active = use_mode();
86 SCHEME_PAIR.with(|p| {
87 p.borrow()
88 .as_ref()
89 .is_some_and(|(_, dark)| active.as_deref() == Some(dark.as_str()))
90 })
91}
92
93pub fn set_dark(on: bool) {
95 let target = SCHEME_PAIR.with(|p| {
96 p.borrow()
97 .as_ref()
98 .map(|(light, dark)| if on { dark.clone() } else { light.clone() })
99 });
100 if let Some(target) = target {
101 set_mode(target);
102 }
103}
104
105pub fn toggle_dark() {
108 let currently_dark = SCHEME_PAIR.with(|p| {
109 p.borrow()
110 .as_ref()
111 .is_some_and(|(_, dark)| active_mode().as_deref() == Some(dark.as_str()))
112 });
113 set_dark(!currently_dark);
114}
115
116thread_local! {
117 static SYSTEM_DARK: ManuallyDrop<RwSignal<bool>> = ManuallyDrop::new(signal(false));
120 static FOLLOW: ManuallyDrop<RefCell<Option<reactive_core::Effect>>> =
123 ManuallyDrop::new(RefCell::new(None));
124}
125
126pub fn set_system_dark(dark: bool) {
129 SYSTEM_DARK.with(|s| s.set(dark));
130}
131
132pub fn follow_system(light: impl Into<String>, dark: impl Into<String>) {
137 let light = light.into();
138 let dark = dark.into();
139 set_light_dark(light.clone(), dark.clone());
140 let eff = reactive_core::effect(move || {
141 let want = if SYSTEM_DARK.with(|s| s.get()) {
142 &dark
143 } else {
144 &light
145 };
146 set_mode(want.clone());
147 });
148 FOLLOW.with(|f| *f.borrow_mut() = Some(eff));
149}
150
151#[cfg(test)]
152mod tests {
153 use super::*;
154 use std::cell::Cell;
155
156 fn reset() {
158 ACTIVE_MODE.with(|s| s.set(None));
159 MODES.with(|m| m.borrow_mut().clear());
160 SCHEME_PAIR.with(|p| *p.borrow_mut() = None);
161 FOLLOW.with(|f| *f.borrow_mut() = None);
163 SYSTEM_DARK.with(|s| s.set(false));
164 }
165
166 #[test]
167 fn set_mode_runs_apply_and_publishes_id() {
168 reset();
169 let hits = Rc::new(Cell::new(0));
170 let h = hits.clone();
171 register_mode("dark", move || h.set(h.get() + 1));
172 set_mode("dark");
173 assert_eq!(hits.get(), 1, "apply closure ran once");
174 assert_eq!(active_mode().as_deref(), Some("dark"));
175 }
176
177 #[test]
178 fn set_mode_publishes_even_without_registration() {
179 reset();
180 set_mode("unregistered");
181 assert_eq!(active_mode().as_deref(), Some("unregistered"));
182 }
183
184 #[test]
185 fn init_mode_does_not_clobber_existing_selection() {
186 reset();
187 set_mode("midnight");
188 init_mode("modern");
189 assert_eq!(
190 active_mode().as_deref(),
191 Some("midnight"),
192 "init must keep a selection already made (e.g. restored across hot reload)"
193 );
194 }
195
196 #[test]
197 fn init_mode_applies_default_when_empty() {
198 reset();
199 init_mode("modern");
200 assert_eq!(active_mode().as_deref(), Some("modern"));
201 }
202
203 #[test]
204 fn set_and_toggle_dark_switch_between_the_pair() {
205 reset();
206 register_mode("day", || {});
207 register_mode("night", || {});
208 set_light_dark("day", "night");
209
210 set_dark(true);
211 assert_eq!(active_mode().as_deref(), Some("night"));
212 set_dark(false);
213 assert_eq!(active_mode().as_deref(), Some("day"));
214
215 toggle_dark();
216 assert_eq!(active_mode().as_deref(), Some("night"));
217 toggle_dark();
218 assert_eq!(active_mode().as_deref(), Some("day"));
219 }
220
221 #[test]
222 fn follow_system_drives_mode_from_os_scheme() {
223 reset();
224 register_mode("day", || {});
225 register_mode("night", || {});
226 follow_system("day", "night");
227 assert_eq!(
228 active_mode().as_deref(),
229 Some("day"),
230 "effect runs once with default SYSTEM_DARK=false → light"
231 );
232 set_system_dark(true);
233 assert_eq!(active_mode().as_deref(), Some("night"));
234 set_system_dark(false);
235 assert_eq!(active_mode().as_deref(), Some("day"));
236 }
237
238 #[test]
239 fn is_dark_false_for_unpaired_third_mode() {
240 reset();
241 set_light_dark("day", "night");
242 set_mode("pastel");
243 assert!(
244 !is_dark(),
245 "a third mode outside the pair is neither dark nor light"
246 );
247 }
248
249 #[test]
250 fn dark_helpers_are_noops_without_a_pair() {
251 reset();
252 set_dark(true);
253 toggle_dark();
254 assert_eq!(active_mode(), None, "no pair set → nothing to switch to");
255 }
256
257 #[test]
258 fn use_mode_is_reactive() {
259 reset();
260 let seen = Rc::new(RefCell::new(Vec::<Option<String>>::new()));
261 let s = seen.clone();
262 let _e = reactive_core::effect(move || s.borrow_mut().push(use_mode()));
263 set_mode("a");
264 set_mode("b");
265 let got = seen.borrow().clone();
266 assert_eq!(
267 got,
268 vec![None, Some("a".into()), Some("b".into())],
269 "effect re-ran on each mode switch"
270 );
271 }
272}