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