Skip to main content

ferroui_browser/
browser_platform_settings.rs

1use crate::interop::dom_helper;
2use crate::windowing_platform::BrowserWindowingPlatform;
3use ferroui_base::input::platform::PlatformHotkeyConfiguration;
4use ferroui_base::input::PointerType;
5use ferroui_base::platform::{
6    ColorContrastPreference, DefaultPlatformSettings, IPlatformSettings, PlatformColorValues, PlatformThemeVariant,
7};
8use ferroui_base::reactive::IDisposable;
9use ferroui_base::Size;
10use std::cell::{Cell, RefCell};
11use std::rc::Rc;
12use std::time::Duration;
13
14/// What the page reports: the dark-mode and contrast media queries and the
15/// language of the browser. Replaced by fixed values in the tests.
16trait IPageSettings {
17    fn get_dark_mode(&self) -> Vec<i32>;
18    fn get_navigator_language(&self) -> Option<String>;
19}
20
21struct PageSettings;
22
23impl IPageSettings for PageSettings {
24    fn get_dark_mode(&self) -> Vec<i32> {
25        dom_helper::get_dark_mode(&BrowserWindowingPlatform::global_this())
26    }
27
28    fn get_navigator_language(&self) -> Option<String> {
29        dom_helper::get_navigator_language(&BrowserWindowingPlatform::global_this())
30    }
31}
32
33/// The platform settings of the browser: the colour scheme, the contrast
34/// preference and the language of the page.
35pub struct BrowserPlatformSettings {
36    base: DefaultPlatformSettings,
37    is_dark_mode: Cell<bool>,
38    is_high_contrast: Cell<bool>,
39    is_initialized: Cell<bool>,
40    color_values: Cell<Option<PlatformColorValues>>,
41    last_language: RefCell<Option<String>>,
42    page: Box<dyn IPageSettings>,
43}
44
45impl Default for BrowserPlatformSettings {
46    fn default() -> Self {
47        Self::with_page(Box::new(PageSettings))
48    }
49}
50
51impl BrowserPlatformSettings {
52    /// Creates the settings. The page is asked on first use.
53    pub fn new() -> Rc<Self> {
54        Rc::new(Self::default())
55    }
56
57    fn with_page(page: Box<dyn IPageSettings>) -> Self {
58        Self {
59            base: DefaultPlatformSettings::new(),
60            is_dark_mode: Cell::new(false),
61            is_high_contrast: Cell::new(false),
62            is_initialized: Cell::new(false),
63            color_values: Cell::new(None),
64            last_language: RefCell::new(None),
65            page,
66        }
67    }
68
69    fn build_platform_color_values(is_dark_mode: bool, is_high_contrast: bool) -> PlatformColorValues {
70        PlatformColorValues::new()
71            .with_theme_variant(if is_dark_mode { PlatformThemeVariant::Dark } else { PlatformThemeVariant::Light })
72            .with_contrast_preference(if is_high_contrast {
73                ColorContrastPreference::High
74            } else {
75                ColorContrastPreference::NoPreference
76            })
77    }
78
79    /// The page reported a change of its colour scheme or contrast
80    /// preference.
81    pub fn on_color_values_changed(&self, is_dark_mode: bool, is_high_contrast: bool) {
82        self.is_dark_mode.set(is_dark_mode);
83        self.is_high_contrast.set(is_high_contrast);
84        self.update_color_values();
85    }
86
87    /// The page reported a change of the language of the browser.
88    pub fn on_preferred_language_changed(&self, language: Option<&str>) {
89        if let Some(language) = language {
90            if self.last_language.borrow().as_deref() != Some(language) {
91                *self.last_language.borrow_mut() = Some(language.to_string());
92                self.base.on_preferred_application_language_changed();
93            }
94        }
95    }
96
97    fn ensure_settings(&self) {
98        if !self.is_initialized.get() {
99            // The module is initialized asynchronously. The page cannot be asked right away while
100            // the components are registered.
101            self.is_initialized.set(true);
102            let values = self.page.get_dark_mode();
103            if values.len() == 2 {
104                self.is_dark_mode.set(values[0] > 0);
105                self.is_high_contrast.set(values[1] > 0);
106            }
107
108            *self.last_language.borrow_mut() = self.page.get_navigator_language();
109        }
110    }
111
112    fn update_color_values(&self) {
113        let old_color_values = self.color_values.get();
114        let color_values = Self::build_platform_color_values(self.is_dark_mode.get(), self.is_high_contrast.get());
115
116        if old_color_values != Some(color_values) {
117            self.color_values.set(Some(color_values));
118            self.base.on_color_values_changed(color_values);
119        }
120    }
121}
122
123impl IPlatformSettings for BrowserPlatformSettings {
124    fn get_tap_size(&self, type_: PointerType) -> Size {
125        self.base.get_tap_size(type_)
126    }
127
128    fn get_double_tap_size(&self, type_: PointerType) -> Size {
129        self.base.get_double_tap_size(type_)
130    }
131
132    fn get_double_tap_time(&self, type_: PointerType) -> Duration {
133        self.base.get_double_tap_time(type_)
134    }
135
136    fn hold_wait_duration(&self) -> Duration {
137        self.base.hold_wait_duration()
138    }
139
140    fn hotkey_configuration(&self) -> Rc<PlatformHotkeyConfiguration> {
141        self.base.hotkey_configuration()
142    }
143
144    fn preferred_application_language(&self) -> String {
145        self.ensure_settings();
146
147        match self.last_language.borrow().clone() {
148            Some(language) => language,
149            None => self.base.preferred_application_language(),
150        }
151    }
152
153    fn get_color_values(&self) -> PlatformColorValues {
154        if let Some(color_values) = self.color_values.get() {
155            return color_values;
156        }
157
158        self.ensure_settings();
159        let color_values = Self::build_platform_color_values(self.is_dark_mode.get(), self.is_high_contrast.get());
160        self.color_values.set(Some(color_values));
161        color_values
162    }
163
164    fn color_values_changed(&self, handler: Rc<dyn Fn(&PlatformColorValues)>) -> Rc<dyn IDisposable> {
165        self.ensure_settings();
166        self.base.color_values_changed(handler)
167    }
168
169    fn preferred_application_language_changed(&self, handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
170        self.ensure_settings();
171        self.base.preferred_application_language_changed(handler)
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    struct FixedPage {
180        dark_mode: Vec<i32>,
181        language: Option<&'static str>,
182        queries: Rc<Cell<u32>>,
183    }
184
185    impl IPageSettings for FixedPage {
186        fn get_dark_mode(&self) -> Vec<i32> {
187            self.queries.set(self.queries.get() + 1);
188            self.dark_mode.clone()
189        }
190
191        fn get_navigator_language(&self) -> Option<String> {
192            self.language.map(str::to_string)
193        }
194    }
195
196    fn settings(dark_mode: &[i32], language: Option<&'static str>) -> (BrowserPlatformSettings, Rc<Cell<u32>>) {
197        let queries = Rc::new(Cell::new(0));
198        let page = FixedPage { dark_mode: dark_mode.to_vec(), language, queries: queries.clone() };
199        (BrowserPlatformSettings::with_page(Box::new(page)), queries)
200    }
201
202    #[test]
203    fn the_page_is_asked_once_and_only_when_needed() {
204        let (settings, queries) = settings(&[1, 0], Some("de-DE"));
205        assert_eq!(0, queries.get());
206
207        settings.get_color_values();
208        settings.get_color_values();
209        settings.preferred_application_language();
210
211        assert_eq!(1, queries.get());
212    }
213
214    #[test]
215    fn the_colour_values_follow_the_media_queries() {
216        for (dark_mode, theme, contrast) in [
217            ([0, 0], PlatformThemeVariant::Light, ColorContrastPreference::NoPreference),
218            ([1, 0], PlatformThemeVariant::Dark, ColorContrastPreference::NoPreference),
219            ([0, 1], PlatformThemeVariant::Light, ColorContrastPreference::High),
220            ([1, 1], PlatformThemeVariant::Dark, ColorContrastPreference::High),
221        ] {
222            let (settings, _) = settings(&dark_mode, None);
223
224            let values = settings.get_color_values();
225
226            assert_eq!(theme, values.theme_variant());
227            assert_eq!(contrast, values.contrast_preference());
228        }
229    }
230
231    #[test]
232    fn an_answer_of_the_wrong_shape_leaves_the_defaults() {
233        let (settings, _) = settings(&[1], None);
234
235        assert_eq!(PlatformThemeVariant::Light, settings.get_color_values().theme_variant());
236    }
237
238    #[test]
239    fn a_change_of_the_colour_scheme_is_raised_once_per_change() {
240        let (settings, _) = settings(&[0, 0], None);
241        let seen = Rc::new(RefCell::new(Vec::new()));
242        let log = seen.clone();
243        let subscription =
244            settings.color_values_changed(Rc::new(move |values| log.borrow_mut().push(values.theme_variant())));
245        settings.get_color_values();
246
247        settings.on_color_values_changed(true, false);
248        settings.on_color_values_changed(true, false);
249        settings.on_color_values_changed(false, false);
250        subscription.dispose();
251        settings.on_color_values_changed(true, false);
252
253        assert_eq!(vec![PlatformThemeVariant::Dark, PlatformThemeVariant::Light], *seen.borrow());
254        assert_eq!(PlatformThemeVariant::Dark, settings.get_color_values().theme_variant());
255    }
256
257    #[test]
258    fn the_language_of_the_browser_is_preferred() {
259        let (settings, _) = settings(&[0, 0], Some("pl-PL"));
260
261        assert_eq!("pl-PL", settings.preferred_application_language());
262    }
263
264    #[test]
265    fn a_change_of_the_language_is_raised_once_per_change() {
266        let (settings, _) = settings(&[0, 0], Some("en-US"));
267        let count = Rc::new(Cell::new(0));
268        let seen = count.clone();
269        let _subscription = settings.preferred_application_language_changed(Rc::new(move || seen.set(seen.get() + 1)));
270
271        settings.on_preferred_language_changed(Some("en-US"));
272        settings.on_preferred_language_changed(None);
273        settings.on_preferred_language_changed(Some("fr-FR"));
274        settings.on_preferred_language_changed(Some("fr-FR"));
275
276        assert_eq!(1, count.get());
277        assert_eq!("fr-FR", settings.preferred_application_language());
278    }
279}