Skip to main content

argui_platform/
preferences.rs

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    /// Publishes the current preferences and subsequent native system changes.
52    #[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}