Skip to main content

incular_platform/
system_environment.rs

1//! Stable operating-system environment preference contracts.
2//!
3//! The desktop shell combines these application-level preferences with
4//! per-window state (metrics, Winit theme, focus, occlusion) to create a full
5//! [`RuntimeEnvironment`]. Unsupported values are represented by `None`; when
6//! applied they intentionally reset to Incular's documented defaults rather
7//! than preserving a stale value from an earlier snapshot.
8
9use incular_config::{EdgeInsets, InputCapabilities, Locale, RuntimeEnvironment};
10use std::sync::{Arc, RwLock};
11
12/// Application-level system preferences observable by a native adapter.
13#[derive(Clone, Debug, Default, PartialEq)]
14pub struct SystemEnvironmentPreferences {
15    /// Ordered OS locale preferences. Order is significant.
16    pub locales: Option<Vec<Locale>>,
17    /// Accessibility text scaling when the platform exposes a meaningful
18    /// application text-scale value distinct from window DPI.
19    pub text_scale: Option<f32>,
20    pub reduced_motion: Option<bool>,
21    pub high_contrast: Option<bool>,
22    pub always_use_24_hour_format: Option<bool>,
23    pub accessible_navigation: Option<bool>,
24    pub invert_colors: Option<bool>,
25    pub on_off_switch_labels: Option<bool>,
26    pub bold_text: Option<bool>,
27    pub input: Option<InputCapabilities>,
28    /// Desktop platforms normally leave these unsupported (`None`), but the
29    /// contract is explicit for shells with desktop safe/view insets.
30    ///
31    /// Inset ownership: `safe_insets` is the currently usable margin and the
32    /// shell reduces covered edges while transient UI obscures them;
33    /// `view_padding` is the persistent obstruction margin the shell never
34    /// reduces under occlusion; `view_insets` is the transient occlusion
35    /// itself (for example the keyboard). Maintained safe-area padding reads
36    /// `view_padding`, never `view_insets`.
37    pub safe_insets: Option<EdgeInsets>,
38    pub view_padding: Option<EdgeInsets>,
39    pub view_insets: Option<EdgeInsets>,
40}
41
42impl SystemEnvironmentPreferences {
43    /// Applies this complete preference snapshot. Every unsupported field is
44    /// reset to the stable `RuntimeEnvironment::default()` value so a backend
45    /// losing support cannot leave old native state cached indefinitely.
46    pub fn apply_to(&self, environment: &mut RuntimeEnvironment) {
47        let defaults = RuntimeEnvironment::default();
48        environment.locales = self.locales.clone().unwrap_or(defaults.locales);
49        environment.text_scale = self.text_scale.unwrap_or(defaults.text_scale);
50        environment.reduced_motion = self.reduced_motion.unwrap_or(defaults.reduced_motion);
51        environment.high_contrast = self.high_contrast.unwrap_or(defaults.high_contrast);
52        environment.always_use_24_hour_format = self
53            .always_use_24_hour_format
54            .unwrap_or(defaults.always_use_24_hour_format);
55        environment.accessible_navigation = self
56            .accessible_navigation
57            .unwrap_or(defaults.accessible_navigation);
58        environment.invert_colors = self.invert_colors.unwrap_or(defaults.invert_colors);
59        environment.on_off_switch_labels = self
60            .on_off_switch_labels
61            .unwrap_or(defaults.on_off_switch_labels);
62        environment.bold_text = self.bold_text.unwrap_or(defaults.bold_text);
63        environment.input = self.input.unwrap_or(defaults.input);
64        environment.safe_insets = self.safe_insets.unwrap_or(defaults.safe_insets);
65        environment.view_padding = self.view_padding.unwrap_or(defaults.view_padding);
66        environment.view_insets = self.view_insets.unwrap_or(defaults.view_insets);
67    }
68}
69
70/// Snapshot source used by platform adapters and deterministic tests.
71pub trait SystemEnvironmentProvider {
72    fn snapshot(&self) -> SystemEnvironmentPreferences;
73}
74
75/// Thread-safe deterministic provider for headless hosts and tests.
76#[derive(Clone, Default)]
77pub struct MemorySystemEnvironmentProvider {
78    snapshot: Arc<RwLock<SystemEnvironmentPreferences>>,
79}
80
81impl MemorySystemEnvironmentProvider {
82    #[must_use]
83    pub fn new(snapshot: SystemEnvironmentPreferences) -> Self {
84        Self {
85            snapshot: Arc::new(RwLock::new(snapshot)),
86        }
87    }
88
89    /// Atomically replaces the complete preference snapshot and returns
90    /// whether any semantic value changed.
91    pub fn replace(&self, snapshot: SystemEnvironmentPreferences) -> bool {
92        let mut current = self.snapshot.write().expect("environment provider lock");
93        if *current == snapshot {
94            return false;
95        }
96        *current = snapshot;
97        true
98    }
99}
100
101impl SystemEnvironmentProvider for MemorySystemEnvironmentProvider {
102    fn snapshot(&self) -> SystemEnvironmentPreferences {
103        self.snapshot
104            .read()
105            .expect("environment provider lock")
106            .clone()
107    }
108}
109
110/// Parses OS-provided BCP-47 locale strings through ICU4X, preserving order
111/// while dropping malformed and duplicate canonical locales.
112#[must_use]
113pub fn canonicalize_system_locales(
114    locales: impl IntoIterator<Item = impl AsRef<str>>,
115) -> Vec<Locale> {
116    let mut result = Vec::new();
117    for value in locales {
118        let Ok(locale) = value.as_ref().parse::<Locale>() else {
119            continue;
120        };
121        if !result.contains(&locale) {
122            result.push(locale);
123        }
124    }
125    result
126}