Skip to main content

ferroui_base/platform/
default_platform_settings.rs

1use super::{IPlatformSettings, PlatformColorValues, PlatformThemeVariant};
2use crate::input::platform::PlatformHotkeyConfiguration;
3use crate::input::PointerType;
4use crate::reactive::{Disposable, IDisposable};
5use crate::utilities::HandlerList;
6use crate::{FerroLocator, LocatorExtensions, Size};
7use std::rc::Rc;
8use std::time::Duration;
9
10const TOUCH_TAP_SIZE: f64 = 10.0;
11/// The default double tap distance for touch input on Windows.
12const TOUCH_DOUBLE_TAP_SIZE: f64 = 50.0;
13
14/// A default implementation of [`IPlatformSettings`] for platforms that do
15/// not provide settings of their own, and the base the settings of the
16/// platform backends are built on: a backend embeds one, delegates what it
17/// does not replace and raises the change events through
18/// [`on_color_values_changed`](Self::on_color_values_changed) and
19/// [`on_preferred_application_language_changed`](Self::on_preferred_application_language_changed).
20#[derive(Default)]
21pub struct DefaultPlatformSettings {
22    color_values_changed: Rc<HandlerList<dyn Fn(&PlatformColorValues)>>,
23    preferred_application_language_changed: Rc<HandlerList<dyn Fn()>>,
24}
25
26impl DefaultPlatformSettings {
27    /// Creates the default settings.
28    pub fn new() -> Self {
29        Self::default()
30    }
31
32    /// Raises [`IPlatformSettings::color_values_changed`].
33    ///
34    /// The settings belong to the UI thread: a backend that learns about
35    /// the change on another thread marshals to the UI thread first.
36    pub fn on_color_values_changed(&self, color_values: PlatformColorValues) {
37        if self.color_values_changed.is_empty() {
38            return;
39        }
40        for (_, handler) in self.color_values_changed.snapshot().iter() {
41            handler(&color_values);
42        }
43    }
44
45    /// Raises [`IPlatformSettings::preferred_application_language_changed`].
46    ///
47    /// The settings belong to the UI thread: a backend that learns about
48    /// the change on another thread marshals to the UI thread first.
49    pub fn on_preferred_application_language_changed(&self) {
50        if self.preferred_application_language_changed.is_empty() {
51            return;
52        }
53        for (_, handler) in self.preferred_application_language_changed.snapshot().iter() {
54            handler();
55        }
56    }
57
58    /// The language tag from a POSIX locale name: `en_US.UTF-8` gives
59    /// `en-US`. The `C` and `POSIX` locales give the empty (invariant) tag.
60    fn language_tag_from_locale(locale: &str) -> String {
61        let name = locale.split(['.', '@']).next().unwrap_or("");
62        if name.is_empty() || name == "C" || name == "POSIX" {
63            return String::new();
64        }
65        name.replace('_', "-")
66    }
67}
68
69impl IPlatformSettings for DefaultPlatformSettings {
70    fn get_tap_size(&self, type_: PointerType) -> Size {
71        match type_ {
72            PointerType::Touch | PointerType::Pen => Size::new(TOUCH_TAP_SIZE, TOUCH_TAP_SIZE),
73            PointerType::Mouse => Size::new(4.0, 4.0),
74        }
75    }
76
77    fn get_double_tap_size(&self, type_: PointerType) -> Size {
78        match type_ {
79            PointerType::Touch | PointerType::Pen => Size::new(TOUCH_DOUBLE_TAP_SIZE, TOUCH_DOUBLE_TAP_SIZE),
80            PointerType::Mouse => Size::new(4.0, 4.0),
81        }
82    }
83
84    fn get_double_tap_time(&self, _type: PointerType) -> Duration {
85        Duration::from_millis(500)
86    }
87
88    fn hold_wait_duration(&self) -> Duration {
89        Duration::from_millis(300)
90    }
91
92    /// The hotkey configuration registered with the locator.
93    ///
94    /// Panics if none is registered.
95    fn hotkey_configuration(&self) -> Rc<PlatformHotkeyConfiguration> {
96        FerroLocator::current().get_required_service::<PlatformHotkeyConfiguration>()
97    }
98
99    /// The language of the user interface the environment asks for
100    /// (`LC_ALL`, `LC_MESSAGES`, `LANG`, in this order), or the empty
101    /// (invariant) tag when it does not say.
102    fn preferred_application_language(&self) -> String {
103        ["LC_ALL", "LC_MESSAGES", "LANG"]
104            .iter()
105            .filter_map(|name| std::env::var(name).ok())
106            .find(|value| !value.is_empty())
107            .map(|value| Self::language_tag_from_locale(&value))
108            .unwrap_or_default()
109    }
110
111    fn get_color_values(&self) -> PlatformColorValues {
112        PlatformColorValues::new().with_theme_variant(PlatformThemeVariant::Light)
113    }
114
115    fn color_values_changed(&self, handler: Rc<dyn Fn(&PlatformColorValues)>) -> Rc<dyn IDisposable> {
116        let token = self.color_values_changed.add(handler);
117        let handlers = self.color_values_changed.clone();
118        Disposable::create(move || {
119            handlers.remove(token);
120        })
121    }
122
123    fn preferred_application_language_changed(&self, handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
124        let token = self.preferred_application_language_changed.add(handler);
125        let handlers = self.preferred_application_language_changed.clone();
126        Disposable::create(move || {
127            handlers.remove(token);
128        })
129    }
130}
131
132#[cfg(test)]
133mod tests {
134    use super::*;
135    use std::cell::RefCell;
136
137    #[test]
138    fn default_color_values_are_light() {
139        let settings = DefaultPlatformSettings::new();
140        assert_eq!(settings.get_color_values(), PlatformColorValues::new());
141        assert_eq!(settings.get_color_values().theme_variant(), PlatformThemeVariant::Light);
142    }
143
144    #[test]
145    fn change_events_reach_subscribers_until_disposed() {
146        let settings = DefaultPlatformSettings::new();
147        let log = Rc::new(RefCell::new(Vec::new()));
148
149        let colors = {
150            let log = log.clone();
151            settings.color_values_changed(Rc::new(move |values: &PlatformColorValues| {
152                log.borrow_mut().push(format!("{:?}", values.theme_variant()))
153            }))
154        };
155        let language = {
156            let log = log.clone();
157            settings.preferred_application_language_changed(Rc::new(move || log.borrow_mut().push("language".to_string())))
158        };
159
160        settings.on_color_values_changed(PlatformColorValues::new().with_theme_variant(PlatformThemeVariant::Dark));
161        settings.on_preferred_application_language_changed();
162        assert_eq!(*log.borrow(), ["Dark", "language"]);
163
164        colors.dispose();
165        language.dispose();
166        settings.on_color_values_changed(PlatformColorValues::new());
167        settings.on_preferred_application_language_changed();
168        assert_eq!(log.borrow().len(), 2);
169    }
170
171    #[test]
172    fn language_tag_is_derived_from_the_locale_name() {
173        assert_eq!(DefaultPlatformSettings::language_tag_from_locale("en_US.UTF-8"), "en-US");
174        assert_eq!(DefaultPlatformSettings::language_tag_from_locale("sr_RS@latin"), "sr-RS");
175        assert_eq!(DefaultPlatformSettings::language_tag_from_locale("pl"), "pl");
176        assert_eq!(DefaultPlatformSettings::language_tag_from_locale("C"), "");
177        assert_eq!(DefaultPlatformSettings::language_tag_from_locale("POSIX"), "");
178        assert_eq!(DefaultPlatformSettings::language_tag_from_locale(""), "");
179    }
180}