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