1use std::cell::RefCell;
23use std::hash::{Hash, Hasher};
24use std::rc::Rc;
25use std::sync::{Arc, Mutex};
26use std::time::Duration;
27
28use crate::callback::Callback;
29use crate::component::request_rebuild;
30use crate::locale::Locale;
31use crate::localizations::Localizations;
32use crate::media::Brightness;
33use crate::scrollposition::ScrollTo;
34use crate::sheet::SheetTo;
35use crate::theme::{Theme, ThemeMode};
36
37struct Settings {
42 title: String,
43 theme: Theme,
44 dark_theme: Option<Theme>,
45 theme_mode: ThemeMode,
46 density: f32,
47 locale: Option<Locale>,
48 supported_locales: Vec<Locale>,
49 localizations: Option<Rc<dyn Localizations>>,
50}
51
52impl Default for Settings {
53 fn default() -> Self {
54 Self {
55 title: "frus".to_string(),
56 theme: Theme::light(),
57 dark_theme: None,
58 theme_mode: ThemeMode::System,
59 density: 1.0,
60 locale: None,
61 supported_locales: Vec::new(),
62 localizations: None,
63 }
64 }
65}
66
67thread_local! {
68 static SETTINGS: RefCell<Settings> = RefCell::new(Settings::default());
69 static EFFECTS: RefCell<Vec<Effect>> = const { RefCell::new(Vec::new()) };
70}
71
72#[derive(Clone, Copy, Default, Debug)]
79pub struct App;
80
81pub fn app() -> App {
83 App
84}
85
86macro_rules! setting {
87 ($(#[$doc:meta])* $get:ident, $set:ident: $ty:ty, $field:ident) => {
88 $(#[$doc])*
89 pub fn $get(&self) -> $ty {
90 SETTINGS.with(|s| s.borrow().$field.clone())
91 }
92
93 #[doc = concat!("Changes [`App::", stringify!($get), "`].")]
94 pub fn $set(&self, value: $ty) {
95 SETTINGS.with(|s| s.borrow_mut().$field = value);
96 request_rebuild();
97 }
98 };
99}
100
101impl App {
102 setting!(
103 title, set_title: String, title
105 );
106 setting!(
107 theme, set_theme: Theme, theme
109 );
110 setting!(
111 dark_theme, set_dark_theme: Option<Theme>, dark_theme
113 );
114 setting!(
115 theme_mode, set_theme_mode: ThemeMode, theme_mode
117 );
118 setting!(
119 density, set_density: f32, density
121 );
122 setting!(
123 locale, set_locale: Option<Locale>, locale
125 );
126 setting!(
127 supported_locales, set_supported_locales: Vec<Locale>, supported_locales
129 );
130 setting!(
131 localizations, set_localizations: Option<Rc<dyn Localizations>>, localizations
134 );
135
136 pub fn resolved_theme(&self, brightness: Brightness) -> Theme {
139 let dark = match self.theme_mode() {
140 ThemeMode::Light => false,
141 ThemeMode::Dark => true,
142 ThemeMode::System => brightness == Brightness::Dark,
143 };
144 match (dark, self.dark_theme()) {
145 (true, Some(theme)) => theme,
146 _ => self.theme(),
147 }
148 }
149}
150
151pub enum Effect {
157 Focus(u64),
159 Scroll(u64, ScrollTo),
161 Sheet(u64, SheetTo),
163 Spawn(Box<dyn FnOnce() -> Option<Callback> + Send>),
166 After(Duration, Callback),
168}
169
170pub fn key_hash(key: impl Hash) -> u64 {
172 let mut hasher = std::collections::hash_map::DefaultHasher::new();
173 key.hash(&mut hasher);
174 hasher.finish()
175}
176
177fn push(effect: Effect) {
178 EFFECTS.with(|effects| effects.borrow_mut().push(effect));
179 request_rebuild();
181}
182
183pub fn take_effects() -> Vec<Effect> {
186 EFFECTS.with(|effects| std::mem::take(&mut *effects.borrow_mut()))
187}
188
189pub fn focus(key: impl Hash) {
192 push(Effect::Focus(key_hash(key)));
193}
194
195pub fn scroll_to(key: impl Hash, to: ScrollTo) {
197 push(Effect::Scroll(key_hash(key), to));
198}
199
200pub fn sheet_to(key: impl Hash, to: SheetTo) {
202 push(Effect::Sheet(key_hash(key), to));
203}
204
205pub fn spawn<R: Send + 'static>(
212 work: impl FnOnce() -> R + Send + 'static,
213 then: impl Fn(R) + 'static,
214) {
215 let answer: Arc<Mutex<Option<R>>> = Arc::default();
216 let deliver = {
217 let answer = answer.clone();
218 Callback::new(move || {
219 let made = answer.lock().ok().and_then(|mut slot| slot.take());
220 if let Some(made) = made {
221 then(made);
222 }
223 })
224 };
225 push(Effect::Spawn(Box::new(move || {
226 let made = work();
227 *answer.lock().ok()? = Some(made);
228 Some(deliver)
229 })));
230}
231
232pub fn after(delay: Duration, then: impl Fn() + 'static) {
234 push(Effect::After(delay, Callback::new(then)));
235}
236
237#[cfg(test)]
238mod tests {
239 use super::*;
240
241 #[test]
242 fn a_setting_is_read_back_and_asks_for_a_rebuild() {
243 crate::take_rebuild_request();
244 app().set_theme_mode(ThemeMode::Dark);
245 assert_eq!(app().theme_mode(), ThemeMode::Dark);
246 assert!(crate::rebuild_requested());
247 app().set_title("hello".into());
248 assert_eq!(app().title(), "hello");
249 }
250
251 #[test]
252 fn the_mode_chooses_between_the_two_themes() {
253 app().set_theme(Theme::light());
254 app().set_dark_theme(Some(Theme::dark()));
255 let light = Theme::light().background;
256 app().set_theme_mode(ThemeMode::Light);
257 assert_eq!(app().resolved_theme(Brightness::Dark).background, light);
258 app().set_theme_mode(ThemeMode::Dark);
259 assert_ne!(app().resolved_theme(Brightness::Light).background, light);
260 app().set_theme_mode(ThemeMode::System);
261 assert_eq!(app().resolved_theme(Brightness::Light).background, light);
262 assert_ne!(app().resolved_theme(Brightness::Dark).background, light);
263 }
264
265 #[test]
266 fn a_dark_mode_with_no_dark_theme_stays_light() {
267 app().set_theme(Theme::light());
268 app().set_dark_theme(None);
269 app().set_theme_mode(ThemeMode::Dark);
270 assert_eq!(
271 app().resolved_theme(Brightness::Dark).background,
272 Theme::light().background
273 );
274 }
275
276 #[test]
277 fn effects_are_kept_in_order_and_taken_once() {
278 let _ = take_effects();
279 focus("name");
280 scroll_to("list", ScrollTo::start());
281 after(Duration::from_millis(5), || {});
282 let effects = take_effects();
283 assert_eq!(effects.len(), 3);
284 assert!(matches!(effects[0], Effect::Focus(h) if h == key_hash("name")));
285 assert!(matches!(effects[1], Effect::Scroll(h, _) if h == key_hash("list")));
286 assert!(matches!(effects[2], Effect::After(d, _) if d == Duration::from_millis(5)));
287 assert!(take_effects().is_empty(), "taken once");
288 }
289
290 #[test]
291 fn spawned_work_comes_back_as_a_callback_that_hands_over_the_answer() {
292 let _ = take_effects();
293 let seen = Rc::new(RefCell::new(None));
294 let put = seen.clone();
295 spawn(|| 6 * 7, move |answer| *put.borrow_mut() = Some(answer));
296 let mut effects = take_effects();
297 let Some(Effect::Spawn(task)) = effects.pop() else {
298 panic!("a spawn is an effect");
299 };
300 let callback = std::thread::spawn(task)
302 .join()
303 .expect("the work ran")
304 .expect("and answered");
305 assert_eq!(*seen.borrow(), None, "nothing runs until it is delivered");
306 callback.call();
307 assert_eq!(*seen.borrow(), Some(42));
308 callback.call();
309 assert_eq!(
310 *seen.borrow(),
311 Some(42),
312 "and it hands the answer over once"
313 );
314 }
315}