1use argui_core::ColorScheme;
2
3#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
4pub enum PreferenceSource {
5 Override,
6 System,
7 #[default]
8 Default,
9}
10
11#[derive(Clone, Copy, Debug, Eq, PartialEq)]
12pub struct ResolvedPreference<T> {
13 pub value: T,
14 pub source: PreferenceSource,
15}
16
17impl<T: Default> Default for ResolvedPreference<T> {
18 fn default() -> Self {
19 Self {
20 value: T::default(),
21 source: PreferenceSource::Default,
22 }
23 }
24}
25
26#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
27pub struct PreferenceOverrides {
28 pub color_scheme: Option<ColorScheme>,
29 pub reduced_motion: Option<bool>,
30 pub high_contrast: Option<bool>,
31}
32
33#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
34pub struct SystemPreferences {
35 pub color_scheme: ResolvedPreference<ColorScheme>,
36 pub reduced_motion: ResolvedPreference<bool>,
37 pub high_contrast: ResolvedPreference<bool>,
38}
39
40impl SystemPreferences {
41 #[must_use]
42 pub fn detect(overrides: PreferenceOverrides) -> Self {
43 let system = system_preferences();
44 Self {
45 color_scheme: resolve(overrides.color_scheme, system.0),
46 reduced_motion: resolve(overrides.reduced_motion, system.1),
47 high_contrast: resolve(overrides.high_contrast, system.2),
48 }
49 }
50
51 #[cfg(not(target_arch = "wasm32"))]
53 #[cfg_attr(coverage_nightly, coverage(off))]
54 pub fn watch(overrides: PreferenceOverrides, mut publish: impl FnMut(Self) + Send + 'static) {
55 let current = Self::detect(overrides);
56 publish(current);
57 #[cfg(target_os = "linux")]
58 if overrides.color_scheme.is_none() {
59 use ashpd::desktop::settings::{ColorScheme as PortalScheme, Settings};
60 use futures_util::StreamExt;
61
62 let mut current = current;
63 pollster::block_on(async move {
64 let Ok(settings) = Settings::new().await else {
65 return;
66 };
67 let Ok(mut changes) = settings.receive_color_scheme_changed().await else {
68 return;
69 };
70 while let Some(scheme) = changes.next().await {
71 let value = match scheme {
72 PortalScheme::PreferDark => ColorScheme::Dark,
73 PortalScheme::PreferLight | PortalScheme::NoPreference => {
74 ColorScheme::Light
75 }
76 };
77 current.color_scheme = ResolvedPreference {
78 value,
79 source: PreferenceSource::System,
80 };
81 publish(current);
82 }
83 });
84 }
85 }
86}
87
88fn resolve<T: Copy + Default>(
89 override_value: Option<T>,
90 system: Option<T>,
91) -> ResolvedPreference<T> {
92 if let Some(value) = override_value {
93 ResolvedPreference {
94 value,
95 source: PreferenceSource::Override,
96 }
97 } else if let Some(value) = system {
98 ResolvedPreference {
99 value,
100 source: PreferenceSource::System,
101 }
102 } else {
103 ResolvedPreference::default()
104 }
105}
106
107#[cfg(target_os = "linux")]
108#[cfg_attr(coverage_nightly, coverage(off))]
109fn system_preferences() -> (Option<ColorScheme>, Option<bool>, Option<bool>) {
110 use ashpd::desktop::settings::{
111 ColorScheme as PortalScheme, Contrast, ReducedMotion, Settings,
112 };
113
114 pollster::block_on(async {
115 let Ok(settings) = Settings::new().await else {
116 return (None, None, None);
117 };
118 let scheme = settings
119 .color_scheme()
120 .await
121 .ok()
122 .and_then(|value| match value {
123 PortalScheme::PreferDark => Some(ColorScheme::Dark),
124 PortalScheme::PreferLight => Some(ColorScheme::Light),
125 PortalScheme::NoPreference => None,
126 });
127 let motion = settings
128 .reduced_motion()
129 .await
130 .ok()
131 .map(|value| value == ReducedMotion::ReducedMotion);
132 let contrast = settings
133 .contrast()
134 .await
135 .ok()
136 .map(|value| value == Contrast::High);
137 (scheme, motion, contrast)
138 })
139}
140
141#[cfg(target_os = "windows")]
142fn system_preferences() -> (Option<ColorScheme>, Option<bool>, Option<bool>) {
143 use windows::UI::ViewManagement::{AccessibilitySettings, UISettings};
144
145 let motion = UISettings::new()
146 .and_then(|settings| settings.AnimationsEnabled())
147 .ok()
148 .map(|enabled| !enabled);
149 let contrast = AccessibilitySettings::new()
150 .and_then(|settings| settings.HighContrast())
151 .ok();
152 (None, motion, contrast)
153}
154
155#[cfg(target_os = "macos")]
156fn system_preferences() -> (Option<ColorScheme>, Option<bool>, Option<bool>) {
157 let workspace = objc2_app_kit::NSWorkspace::sharedWorkspace();
158 (
159 None,
160 Some(workspace.accessibilityDisplayShouldReduceMotion()),
161 Some(workspace.accessibilityDisplayShouldIncreaseContrast()),
162 )
163}
164
165#[cfg(target_arch = "wasm32")]
166fn system_preferences() -> (Option<ColorScheme>, Option<bool>, Option<bool>) {
167 let Some(window) = web_sys::window() else {
168 return (None, None, None);
169 };
170 let query = |value: &str| {
171 window
172 .match_media(value)
173 .ok()
174 .flatten()
175 .map(|media| media.matches())
176 };
177 (
178 query("(prefers-color-scheme: dark)").map(|dark| {
179 if dark {
180 ColorScheme::Dark
181 } else {
182 ColorScheme::Light
183 }
184 }),
185 query("(prefers-reduced-motion: reduce)"),
186 query("(prefers-contrast: more)"),
187 )
188}
189
190#[cfg(not(any(
191 target_os = "linux",
192 target_os = "windows",
193 target_os = "macos",
194 target_arch = "wasm32"
195)))]
196fn system_preferences() -> (Option<ColorScheme>, Option<bool>, Option<bool>) {
197 (None, None, None)
198}