cranpose_services/
theme.rs1use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOf};
2use cranpose_macros::composable;
3
4#[derive(Clone, Copy, Debug, PartialEq, Eq)]
6pub enum SystemTheme {
7 Light,
9 Dark,
11}
12
13pub fn set_platform_system_theme(theme: SystemTheme) {
17 crate::thread_state::with(|state| state.platform_system_theme.set(Some(theme)));
18}
19
20pub fn clear_platform_system_theme() {
22 crate::thread_state::with(|state| state.platform_system_theme.set(None));
23}
24
25pub fn known_system_theme() -> Option<SystemTheme> {
28 crate::thread_state::with(|state| {
29 state
30 .platform_system_theme
31 .get()
32 .or_else(|| state.detected_system_theme.get())
33 })
34}
35
36pub fn default_system_theme() -> SystemTheme {
40 known_system_theme().unwrap_or_else(|| {
41 let theme = detect_system_theme_uncached();
42 crate::thread_state::with(|state| state.detected_system_theme.set(Some(theme)));
43 theme
44 })
45}
46
47fn detect_system_theme_uncached() -> SystemTheme {
48 #[cfg(all(
49 not(target_arch = "wasm32"),
50 not(target_os = "android"),
51 not(target_os = "ios"),
52 feature = "system-theme"
53 ))]
54 {
55 detect_native_system_theme().unwrap_or(SystemTheme::Light)
56 }
57
58 #[cfg(all(target_arch = "wasm32", feature = "system-theme-web"))]
59 {
60 web_sys::window()
61 .and_then(|window| {
62 window
63 .match_media("(prefers-color-scheme: dark)")
64 .ok()
65 .flatten()
66 })
67 .map_or(SystemTheme::Light, |query| {
68 if query.matches() {
69 SystemTheme::Dark
70 } else {
71 SystemTheme::Light
72 }
73 })
74 }
75
76 #[cfg(any(
77 target_os = "android",
78 target_os = "ios",
79 all(
80 not(target_arch = "wasm32"),
81 not(target_os = "android"),
82 not(target_os = "ios"),
83 not(feature = "system-theme")
84 ),
85 all(target_arch = "wasm32", not(feature = "system-theme-web"))
86 ))]
87 {
88 SystemTheme::Light
89 }
90}
91
92#[cfg(all(
93 not(target_arch = "wasm32"),
94 not(target_os = "android"),
95 not(target_os = "ios"),
96 feature = "system-theme"
97))]
98fn detect_native_system_theme() -> Option<SystemTheme> {
99 detect_env_theme().or_else(detect_platform_theme)
100}
101
102#[cfg(all(
103 not(target_arch = "wasm32"),
104 not(target_os = "android"),
105 not(target_os = "ios"),
106 feature = "system-theme"
107))]
108fn detect_env_theme() -> Option<SystemTheme> {
109 ["GTK_THEME", "QT_STYLE_OVERRIDE", "XDG_CURRENT_DESKTOP"]
110 .into_iter()
111 .filter_map(|key| std::env::var(key).ok())
112 .find_map(|value| theme_from_text(&value))
113}
114
115#[cfg(all(
116 target_os = "linux",
117 not(target_arch = "wasm32"),
118 feature = "system-theme"
119))]
120fn detect_platform_theme() -> Option<SystemTheme> {
121 command_stdout(
122 "gsettings",
123 &["get", "org.gnome.desktop.interface", "color-scheme"],
124 )
125 .and_then(|value| theme_from_text(&value))
126 .or_else(|| {
127 command_stdout(
128 "gsettings",
129 &["get", "org.gnome.desktop.interface", "gtk-theme"],
130 )
131 .and_then(|value| theme_from_text(&value))
132 })
133 .or_else(|| {
134 command_stdout(
135 "kreadconfig6",
136 &["--group", "General", "--key", "ColorScheme"],
137 )
138 .and_then(|value| theme_from_text(&value))
139 })
140 .or_else(|| {
141 command_stdout(
142 "kreadconfig5",
143 &["--group", "General", "--key", "ColorScheme"],
144 )
145 .and_then(|value| theme_from_text(&value))
146 })
147}
148
149#[cfg(all(target_os = "macos", feature = "system-theme"))]
150fn detect_platform_theme() -> Option<SystemTheme> {
151 command_stdout("defaults", &["read", "-g", "AppleInterfaceStyle"])
152 .and_then(|value| theme_from_text(&value))
153}
154
155#[cfg(all(target_os = "windows", feature = "system-theme"))]
156fn detect_platform_theme() -> Option<SystemTheme> {
157 command_stdout(
158 "reg",
159 &[
160 "query",
161 r"HKCU\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize",
162 "/v",
163 "AppsUseLightTheme",
164 ],
165 )
166 .and_then(|value| theme_from_windows_registry(&value))
167}
168
169#[cfg(all(
170 not(target_os = "linux"),
171 not(target_os = "macos"),
172 not(target_os = "windows"),
173 not(target_arch = "wasm32"),
174 not(target_os = "android"),
175 not(target_os = "ios"),
176 feature = "system-theme"
177))]
178fn detect_platform_theme() -> Option<SystemTheme> {
179 None
180}
181
182#[cfg(all(
183 not(target_arch = "wasm32"),
184 not(target_os = "android"),
185 not(target_os = "ios"),
186 feature = "system-theme"
187))]
188fn command_stdout(program: &str, args: &[&str]) -> Option<String> {
189 let output = crate::windowless_command(program)
190 .args(args)
191 .output()
192 .ok()?;
193 if !output.status.success() {
194 return None;
195 }
196 String::from_utf8(output.stdout).ok()
197}
198
199#[cfg(all(
200 not(target_arch = "wasm32"),
201 not(target_os = "android"),
202 not(target_os = "ios"),
203 feature = "system-theme"
204))]
205fn theme_from_text(value: &str) -> Option<SystemTheme> {
206 let value = value.to_ascii_lowercase();
207 if value.contains("dark") {
208 Some(SystemTheme::Dark)
209 } else if value.contains("light") || value.contains("default") {
210 Some(SystemTheme::Light)
211 } else {
212 None
213 }
214}
215
216#[cfg(all(target_os = "windows", feature = "system-theme"))]
217fn theme_from_windows_registry(value: &str) -> Option<SystemTheme> {
218 value.lines().find_map(|line| {
219 if !line.contains("AppsUseLightTheme") {
220 return None;
221 }
222 if line.contains("0x0") {
223 Some(SystemTheme::Dark)
224 } else if line.contains("0x1") {
225 Some(SystemTheme::Light)
226 } else {
227 None
228 }
229 })
230}
231
232pub fn local_system_theme() -> CompositionLocal<SystemTheme> {
235 crate::composition_locals::cached_local(
236 |locals| &locals.theme,
237 || compositionLocalOf(default_system_theme),
238 )
239}
240
241#[composable]
243pub fn ProvideSystemTheme(theme: SystemTheme, content: impl FnOnce()) {
244 let local = local_system_theme();
245 CompositionLocalProvider([local.provides(theme)], move || {
246 content();
247 });
248}
249
250#[composable]
252pub fn isSystemInDarkTheme() -> bool {
253 matches!(local_system_theme().current(), SystemTheme::Dark)
254}
255
256#[cfg(test)]
257#[path = "tests/theme_tests.rs"]
258mod tests;