Skip to main content

cranpose_services/
theme.rs

1use cranpose_core::{compositionLocalOf, CompositionLocal, CompositionLocalProvider};
2use cranpose_macros::composable;
3use std::cell::RefCell;
4
5#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum SystemTheme {
7    Light,
8    Dark,
9}
10
11pub fn default_system_theme() -> SystemTheme {
12    #[cfg(all(
13        not(target_arch = "wasm32"),
14        not(target_os = "android"),
15        not(target_os = "ios")
16    ))]
17    {
18        match dark_light::detect() {
19            Ok(dark_light::Mode::Dark) => SystemTheme::Dark,
20            _ => SystemTheme::Light,
21        }
22    }
23
24    #[cfg(target_arch = "wasm32")]
25    {
26        web_sys::window()
27            .and_then(|window| {
28                window
29                    .match_media("(prefers-color-scheme: dark)")
30                    .ok()
31                    .flatten()
32            })
33            .map(|query| {
34                if query.matches() {
35                    SystemTheme::Dark
36                } else {
37                    SystemTheme::Light
38                }
39            })
40            .unwrap_or(SystemTheme::Light)
41    }
42
43    #[cfg(any(target_os = "android", target_os = "ios"))]
44    {
45        SystemTheme::Light
46    }
47}
48
49pub fn local_system_theme() -> CompositionLocal<SystemTheme> {
50    thread_local! {
51        static LOCAL_SYSTEM_THEME: RefCell<Option<CompositionLocal<SystemTheme>>> = const { RefCell::new(None) };
52    }
53
54    LOCAL_SYSTEM_THEME.with(|cell| {
55        let mut local = cell.borrow_mut();
56        if local.is_none() {
57            *local = Some(compositionLocalOf(default_system_theme));
58        }
59        local
60            .as_ref()
61            .expect("System theme composition local must be initialized")
62            .clone()
63    })
64}
65
66#[allow(non_snake_case)]
67#[composable]
68pub fn ProvideSystemTheme(theme: SystemTheme, content: impl FnOnce()) {
69    let local = local_system_theme();
70    CompositionLocalProvider(vec![local.provides(theme)], move || {
71        content();
72    });
73}
74
75#[allow(non_snake_case)]
76#[composable]
77pub fn isSystemInDarkTheme() -> bool {
78    matches!(local_system_theme().current(), SystemTheme::Dark)
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84    use crate::run_test_composition;
85    use cranpose_core::CompositionLocalProvider;
86    use std::cell::RefCell;
87    use std::rc::Rc;
88
89    #[test]
90    fn default_system_theme_returns_supported_variant() {
91        assert!(matches!(
92            default_system_theme(),
93            SystemTheme::Light | SystemTheme::Dark
94        ));
95    }
96
97    #[test]
98    fn local_system_theme_can_be_overridden() {
99        let local = local_system_theme();
100        let captured = Rc::new(RefCell::new(None));
101
102        {
103            let captured = Rc::clone(&captured);
104            let local_for_provider = local.clone();
105            let local_for_read = local.clone();
106            run_test_composition(move || {
107                let captured = Rc::clone(&captured);
108                let local_for_read = local_for_read.clone();
109                CompositionLocalProvider(
110                    vec![local_for_provider.provides(SystemTheme::Dark)],
111                    move || {
112                        *captured.borrow_mut() = Some(local_for_read.current());
113                    },
114                );
115            });
116        }
117
118        assert_eq!(*captured.borrow(), Some(SystemTheme::Dark));
119    }
120
121    #[test]
122    fn provide_system_theme_sets_current_theme() {
123        let local = local_system_theme();
124        let captured = Rc::new(RefCell::new(None));
125
126        {
127            let captured = Rc::clone(&captured);
128            let local = local.clone();
129            run_test_composition(move || {
130                let captured = Rc::clone(&captured);
131                let local = local.clone();
132                ProvideSystemTheme(SystemTheme::Dark, move || {
133                    *captured.borrow_mut() = Some(local.current());
134                });
135            });
136        }
137
138        assert_eq!(*captured.borrow(), Some(SystemTheme::Dark));
139    }
140
141    #[test]
142    fn is_system_in_dark_theme_reads_current_theme() {
143        let captured = Rc::new(RefCell::new(None));
144
145        {
146            let captured = Rc::clone(&captured);
147            run_test_composition(move || {
148                let captured = Rc::clone(&captured);
149                ProvideSystemTheme(SystemTheme::Dark, move || {
150                    *captured.borrow_mut() = Some(isSystemInDarkTheme());
151                });
152            });
153        }
154
155        assert_eq!(*captured.borrow(), Some(true));
156    }
157}