1use std::cell::RefCell;
23use std::future::Future;
24use std::hash::{Hash, Hasher};
25use std::pin::Pin;
26use std::rc::Rc;
27use std::sync::{Arc, Mutex};
28use std::time::Duration;
29
30use crate::callback::Callback;
31use crate::component::request_rebuild;
32use crate::locale::Locale;
33use crate::localizations::Localizations;
34use crate::media::Brightness;
35use crate::scrollposition::ScrollTo;
36use crate::sheet::SheetTo;
37use crate::theme::{Theme, ThemeMode};
38
39struct Settings {
44 title: String,
45 theme: Theme,
46 dark_theme: Option<Theme>,
47 theme_mode: ThemeMode,
48 density: f32,
49 locale: Option<Locale>,
50 supported_locales: Vec<Locale>,
51 localizations: Option<Rc<dyn Localizations>>,
52}
53
54impl Default for Settings {
55 fn default() -> Self {
56 Self {
57 title: "frus".to_string(),
58 theme: Theme::light(),
59 dark_theme: None,
60 theme_mode: ThemeMode::System,
61 density: 1.0,
62 locale: None,
63 supported_locales: Vec::new(),
64 localizations: None,
65 }
66 }
67}
68
69thread_local! {
70 static SETTINGS: RefCell<Settings> = RefCell::new(Settings::default());
71 static EFFECTS: RefCell<Vec<Effect>> = const { RefCell::new(Vec::new()) };
72}
73
74#[derive(Clone, Copy, Default, Debug)]
81pub struct App;
82
83pub fn app() -> App {
85 App
86}
87
88macro_rules! setting {
89 ($(#[$doc:meta])* $get:ident, $set:ident: $ty:ty, $field:ident) => {
90 $(#[$doc])*
91 #[inline(never)]
92 pub fn $get(&self) -> $ty {
93 SETTINGS.with(|s| s.borrow().$field.clone())
94 }
95
96 #[doc = concat!("Changes [`App::", stringify!($get), "`].")]
97 #[inline(never)]
98 pub fn $set(&self, value: $ty) {
99 SETTINGS.with(|s| s.borrow_mut().$field = value);
100 request_rebuild();
101 }
102 };
103}
104
105impl App {
106 setting!(
107 title, set_title: String, title
109 );
110 setting!(
111 theme, set_theme: Theme, theme
113 );
114 setting!(
115 dark_theme, set_dark_theme: Option<Theme>, dark_theme
117 );
118 setting!(
119 theme_mode, set_theme_mode: ThemeMode, theme_mode
121 );
122 setting!(
123 density, set_density: f32, density
125 );
126 setting!(
127 locale, set_locale: Option<Locale>, locale
129 );
130 setting!(
131 supported_locales, set_supported_locales: Vec<Locale>, supported_locales
133 );
134 setting!(
135 localizations, set_localizations: Option<Rc<dyn Localizations>>, localizations
138 );
139
140 pub fn resolved_theme(&self, brightness: Brightness) -> Theme {
143 let dark = match self.theme_mode() {
144 ThemeMode::Light => false,
145 ThemeMode::Dark => true,
146 ThemeMode::System => brightness == Brightness::Dark,
147 };
148 match (dark, self.dark_theme()) {
149 (true, Some(theme)) => theme,
150 _ => self.theme(),
151 }
152 }
153}
154
155pub enum Effect {
161 Focus(u64),
163 Scroll(u64, ScrollTo),
165 Sheet(u64, SheetTo),
167 Spawn(Box<dyn FnOnce() -> Option<Callback> + Send>),
170 After(Duration, Callback),
172 Async(Task),
174}
175
176#[cfg(not(target_arch = "wasm32"))]
179pub type Task = Pin<Box<dyn Future<Output = Option<Callback>> + Send>>;
180
181#[cfg(target_arch = "wasm32")]
184pub type Task = Pin<Box<dyn Future<Output = Option<Callback>>>>;
185
186pub fn key_hash(key: impl Hash) -> u64 {
188 let mut hasher = std::collections::hash_map::DefaultHasher::new();
189 key.hash(&mut hasher);
190 hasher.finish()
191}
192
193#[inline(never)]
194fn push(effect: Effect) {
195 EFFECTS.with(|effects| effects.borrow_mut().push(effect));
196 request_rebuild();
198}
199
200#[inline(never)]
203pub fn take_effects() -> Vec<Effect> {
204 EFFECTS.with(|effects| std::mem::take(&mut *effects.borrow_mut()))
205}
206
207pub fn focus(key: impl Hash) {
210 push(Effect::Focus(key_hash(key)));
211}
212
213pub fn scroll_to(key: impl Hash, to: ScrollTo) {
215 push(Effect::Scroll(key_hash(key), to));
216}
217
218pub fn sheet_to(key: impl Hash, to: SheetTo) {
220 push(Effect::Sheet(key_hash(key), to));
221}
222
223pub fn spawn<R: Send + 'static>(
230 work: impl FnOnce() -> R + Send + 'static,
231 then: impl Fn(R) + 'static,
232) {
233 let answer: Arc<Mutex<Option<R>>> = Arc::default();
234 let deliver = {
235 let answer = answer.clone();
236 Callback::new(move || {
237 let made = answer.lock().ok().and_then(|mut slot| slot.take());
238 if let Some(made) = made {
239 then(made);
240 }
241 })
242 };
243 push(Effect::Spawn(Box::new(move || {
244 let made = work();
245 *answer.lock().ok()? = Some(made);
246 Some(deliver)
247 })));
248}
249
250#[cfg(not(target_arch = "wasm32"))]
258pub fn spawn_async<R: Send + 'static>(
259 work: impl Future<Output = R> + Send + 'static,
260 then: impl Fn(R) + 'static,
261) {
262 let (deliver, answer) = delivery(then);
263 push(Effect::Async(Box::pin(async move {
264 let made = work.await;
265 *answer.lock().ok()? = Some(made);
266 Some(deliver)
267 })));
268}
269
270#[cfg(target_arch = "wasm32")]
276pub fn spawn_async<R: 'static>(
277 work: impl Future<Output = R> + 'static,
278 then: impl Fn(R) + 'static,
279) {
280 let (deliver, answer) = delivery(then);
281 push(Effect::Async(Box::pin(async move {
282 let made = work.await;
283 *answer.lock().ok()? = Some(made);
284 Some(deliver)
285 })));
286}
287
288fn delivery<R: 'static>(then: impl Fn(R) + 'static) -> (Callback, Arc<Mutex<Option<R>>>) {
290 let answer: Arc<Mutex<Option<R>>> = Arc::default();
291 let deliver = {
292 let answer = answer.clone();
293 Callback::new(move || {
294 let made = answer.lock().ok().and_then(|mut slot| slot.take());
295 if let Some(made) = made {
296 then(made);
297 }
298 })
299 };
300 (deliver, answer)
301}
302
303pub fn after(delay: Duration, then: impl Fn() + 'static) {
305 push(Effect::After(delay, Callback::new(then)));
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 #[test]
315 fn an_awaited_answer_reaches_then_once() {
316 let _ = take_effects();
317 let seen = Rc::new(RefCell::new(Vec::new()));
318 let record = seen.clone();
319 spawn_async(async { 7 }, move |value| record.borrow_mut().push(value));
320 let mut effects = take_effects();
321 assert_eq!(effects.len(), 1);
322 let Some(Effect::Async(mut task)) = effects.pop() else {
323 panic!("spawn_async asks for an awaited effect");
324 };
325 assert!(
326 seen.borrow().is_empty(),
327 "nothing before the work is awaited"
328 );
329 let mut context = std::task::Context::from_waker(std::task::Waker::noop());
330 let std::task::Poll::Ready(Some(deliver)) = task.as_mut().poll(&mut context) else {
331 panic!("a ready work answers with its delivery");
332 };
333 assert!(seen.borrow().is_empty(), "nothing before the delivery runs");
334 deliver.call();
335 deliver.call();
336 assert_eq!(*seen.borrow(), vec![7]);
337 }
338
339 #[test]
340 fn a_setting_is_read_back_and_asks_for_a_rebuild() {
341 crate::take_rebuild_request();
342 app().set_theme_mode(ThemeMode::Dark);
343 assert_eq!(app().theme_mode(), ThemeMode::Dark);
344 assert!(crate::rebuild_requested());
345 app().set_title("hello".into());
346 assert_eq!(app().title(), "hello");
347 }
348
349 #[test]
350 fn the_mode_chooses_between_the_two_themes() {
351 app().set_theme(Theme::light());
352 app().set_dark_theme(Some(Theme::dark()));
353 let light = Theme::light().background;
354 app().set_theme_mode(ThemeMode::Light);
355 assert_eq!(app().resolved_theme(Brightness::Dark).background, light);
356 app().set_theme_mode(ThemeMode::Dark);
357 assert_ne!(app().resolved_theme(Brightness::Light).background, light);
358 app().set_theme_mode(ThemeMode::System);
359 assert_eq!(app().resolved_theme(Brightness::Light).background, light);
360 assert_ne!(app().resolved_theme(Brightness::Dark).background, light);
361 }
362
363 #[test]
364 fn a_dark_mode_with_no_dark_theme_stays_light() {
365 app().set_theme(Theme::light());
366 app().set_dark_theme(None);
367 app().set_theme_mode(ThemeMode::Dark);
368 assert_eq!(
369 app().resolved_theme(Brightness::Dark).background,
370 Theme::light().background
371 );
372 }
373
374 #[test]
375 fn effects_are_kept_in_order_and_taken_once() {
376 let _ = take_effects();
377 focus("name");
378 scroll_to("list", ScrollTo::start());
379 after(Duration::from_millis(5), || {});
380 let effects = take_effects();
381 assert_eq!(effects.len(), 3);
382 assert!(matches!(effects[0], Effect::Focus(h) if h == key_hash("name")));
383 assert!(matches!(effects[1], Effect::Scroll(h, _) if h == key_hash("list")));
384 assert!(matches!(effects[2], Effect::After(d, _) if d == Duration::from_millis(5)));
385 assert!(take_effects().is_empty(), "taken once");
386 }
387
388 #[test]
389 fn spawned_work_comes_back_as_a_callback_that_hands_over_the_answer() {
390 let _ = take_effects();
391 let seen = Rc::new(RefCell::new(None));
392 let put = seen.clone();
393 spawn(|| 6 * 7, move |answer| *put.borrow_mut() = Some(answer));
394 let mut effects = take_effects();
395 let Some(Effect::Spawn(task)) = effects.pop() else {
396 panic!("a spawn is an effect");
397 };
398 let callback = std::thread::spawn(task)
400 .join()
401 .expect("the work ran")
402 .expect("and answered");
403 assert_eq!(*seen.borrow(), None, "nothing runs until it is delivered");
404 callback.call();
405 assert_eq!(*seen.borrow(), Some(42));
406 callback.call();
407 assert_eq!(
408 *seen.borrow(),
409 Some(42),
410 "and it hands the answer over once"
411 );
412 }
413}