cranpose_services/
theme.rs1use 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}