Skip to main content

ferroui_native/
native_platform_settings.rs

1use crate::frn_string::frn_string_to_string;
2use crate::helpers::ComResultExt;
3use crate::interop::*;
4use ferroui_base::input::platform::PlatformHotkeyConfiguration;
5use ferroui_base::input::PointerType;
6use ferroui_base::media::Color;
7use ferroui_base::platform::{
8    ColorContrastPreference, DefaultPlatformSettings, IPlatformSettings, PlatformColorValues, PlatformThemeVariant,
9};
10use ferroui_base::reactive::IDisposable;
11use ferroui_base::Size;
12use ferroui_microcom::ComPtr;
13use std::cell::{Cell, RefCell};
14use std::rc::{Rc, Weak};
15use std::time::Duration;
16
17/// Maps the native theme to the theme variant and contrast preference;
18/// `None` for a value the toolkit does not know.
19fn to_theme(theme: FrnPlatformThemeVariant) -> Option<(PlatformThemeVariant, ColorContrastPreference)> {
20    if theme == FrnPlatformThemeVariant::Dark {
21        Some((PlatformThemeVariant::Dark, ColorContrastPreference::NoPreference))
22    } else if theme == FrnPlatformThemeVariant::Light {
23        Some((PlatformThemeVariant::Light, ColorContrastPreference::NoPreference))
24    } else if theme == FrnPlatformThemeVariant::HighContrastDark {
25        Some((PlatformThemeVariant::Dark, ColorContrastPreference::High))
26    } else if theme == FrnPlatformThemeVariant::HighContrastLight {
27        Some((PlatformThemeVariant::Light, ColorContrastPreference::High))
28    } else {
29        None
30    }
31}
32
33fn to_color_values(theme: FrnPlatformThemeVariant, accent_color: u32) -> PlatformColorValues {
34    let Some((theme_variant, contrast_preference)) = to_theme(theme) else {
35        panic!("Specified argument was out of the range of valid values: {theme:?}");
36    };
37    let color_values =
38        PlatformColorValues::new().with_theme_variant(theme_variant).with_contrast_preference(contrast_preference);
39
40    if accent_color > 0 {
41        color_values.with_accent_color1(Color::from_uint32(accent_color))
42    } else {
43        color_values
44    }
45}
46
47/// The platform settings of the macOS backend.
48pub struct NativePlatformSettings {
49    base: DefaultPlatformSettings,
50    platform_settings: ComPtr<IFrnPlatformSettings>,
51    color_values: Cell<Option<PlatformColorValues>>,
52    last_language: RefCell<Option<String>>,
53}
54
55struct ColorsChangeCallback {
56    settings: Weak<NativePlatformSettings>,
57}
58
59impl IFrnActionCallbackImpl for ColorsChangeCallback {
60    fn run(&self) {
61        crate::callback_base::guard((), || {
62            if let Some(settings) = self.settings.upgrade() {
63                settings.on_color_values_changed();
64            }
65        })
66    }
67}
68
69struct LanguageChangeCallback {
70    settings: Weak<NativePlatformSettings>,
71}
72
73impl IFrnActionCallbackImpl for LanguageChangeCallback {
74    fn run(&self) {
75        crate::callback_base::guard((), || {
76            if let Some(settings) = self.settings.upgrade() {
77                settings.on_preferred_language_changed();
78            }
79        })
80    }
81}
82
83impl NativePlatformSettings {
84    pub fn new(platform_settings: ComPtr<IFrnPlatformSettings>) -> Rc<NativePlatformSettings> {
85        let this = Rc::new(NativePlatformSettings {
86            base: DefaultPlatformSettings::new(),
87            platform_settings,
88            color_values: Cell::new(None),
89            last_language: RefCell::new(None),
90        });
91        let colors_change = IFrnActionCallback::from_impl(ColorsChangeCallback { settings: Rc::downgrade(&this) });
92        this.platform_settings.register_colors_change(Some(&colors_change));
93        let language_change =
94            IFrnActionCallback::from_impl(LanguageChangeCallback { settings: Rc::downgrade(&this) });
95        this.platform_settings.register_language_change(Some(&language_change));
96        this
97    }
98
99    fn get_uncached_color_values(&self) -> PlatformColorValues {
100        to_color_values(self.platform_settings.get_platform_theme(), self.platform_settings.get_accent_color())
101    }
102
103    pub fn on_color_values_changed(&self) {
104        let old_color_values = self.color_values.get();
105        let color_values = self.get_uncached_color_values();
106
107        if old_color_values != Some(color_values) {
108            self.color_values.set(Some(color_values));
109            self.base.on_color_values_changed(color_values);
110        }
111    }
112
113    fn on_preferred_language_changed(&self) {
114        let old_language = self.last_language.borrow_mut().take();
115
116        if old_language != Some(self.preferred_application_language()) {
117            self.base.on_preferred_application_language_changed();
118        }
119    }
120
121    fn query_preferred_application_language(&self) -> String {
122        let language = self.platform_settings.get_preferred_language().check();
123        match language.and_then(|language| frn_string_to_string(&language)) {
124            Some(value) if !value.is_empty() => value,
125            _ => self.base.preferred_application_language(),
126        }
127    }
128}
129
130impl IPlatformSettings for NativePlatformSettings {
131    fn get_tap_size(&self, type_: PointerType) -> Size {
132        self.base.get_tap_size(type_)
133    }
134
135    fn get_double_tap_size(&self, type_: PointerType) -> Size {
136        self.base.get_double_tap_size(type_)
137    }
138
139    fn get_double_tap_time(&self, type_: PointerType) -> Duration {
140        self.base.get_double_tap_time(type_)
141    }
142
143    fn hold_wait_duration(&self) -> Duration {
144        self.base.hold_wait_duration()
145    }
146
147    fn hotkey_configuration(&self) -> Rc<PlatformHotkeyConfiguration> {
148        self.base.hotkey_configuration()
149    }
150
151    fn preferred_application_language(&self) -> String {
152        if let Some(language) = self.last_language.borrow().clone() {
153            return language;
154        }
155        let language = self.query_preferred_application_language();
156        *self.last_language.borrow_mut() = Some(language.clone());
157        language
158    }
159
160    fn get_color_values(&self) -> PlatformColorValues {
161        match self.color_values.get() {
162            Some(color_values) => color_values,
163            None => {
164                let color_values = self.get_uncached_color_values();
165                self.color_values.set(Some(color_values));
166                color_values
167            }
168        }
169    }
170
171    fn color_values_changed(&self, handler: Rc<dyn Fn(&PlatformColorValues)>) -> Rc<dyn IDisposable> {
172        self.base.color_values_changed(handler)
173    }
174
175    fn preferred_application_language_changed(&self, handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
176        self.base.preferred_application_language_changed(handler)
177    }
178}
179
180#[cfg(test)]
181mod tests {
182    use super::*;
183
184    #[test]
185    fn themes() {
186        use ColorContrastPreference::*;
187        use PlatformThemeVariant::*;
188        assert_eq!(to_theme(FrnPlatformThemeVariant::Light), Some((Light, NoPreference)));
189        assert_eq!(to_theme(FrnPlatformThemeVariant::Dark), Some((Dark, NoPreference)));
190        assert_eq!(to_theme(FrnPlatformThemeVariant::HighContrastLight), Some((Light, High)));
191        assert_eq!(to_theme(FrnPlatformThemeVariant::HighContrastDark), Some((Dark, High)));
192        assert_eq!(to_theme(FrnPlatformThemeVariant(9)), None);
193    }
194
195    #[test]
196    fn accent_color_is_only_taken_when_the_system_reports_one() {
197        let values = to_color_values(FrnPlatformThemeVariant::HighContrastDark, 0xFF336699);
198        assert_eq!(values.theme_variant(), PlatformThemeVariant::Dark);
199        assert_eq!(values.contrast_preference(), ColorContrastPreference::High);
200        assert_eq!(values.accent_color1(), Color::from_uint32(0xFF336699));
201
202        let values = to_color_values(FrnPlatformThemeVariant::Light, 0);
203        assert_eq!(values, PlatformColorValues::new());
204    }
205
206    /// The settings object over the real native settings: none of this
207    /// needs a window server.
208    #[test]
209    fn native_settings_report_the_system_values() {
210        let factory = create_ferro_native().expect("factory");
211        let Ok(Some(native)) = factory.create_platform_settings() else {
212            eprintln!("skipped: the native platform settings are not available");
213            return;
214        };
215        let settings = NativePlatformSettings::new(native);
216
217        // Cached: asking twice gives the same values.
218        assert_eq!(settings.get_color_values(), settings.get_color_values());
219        assert_eq!(settings.preferred_application_language(), settings.preferred_application_language());
220        assert_eq!(settings.get_double_tap_time(PointerType::Mouse), Duration::from_millis(500));
221    }
222}