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
14trait 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
33pub 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 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 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 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 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}