1use ferroui_base::input::platform::PlatformHotkeyConfiguration;
10use ferroui_base::input::PointerType;
11use ferroui_base::media::Color;
12use ferroui_base::platform::{
13 ColorContrastPreference, DefaultPlatformSettings, IPlatformSettings, PlatformColorValues, PlatformThemeVariant,
14};
15use ferroui_base::reactive::IDisposable;
16use ferroui_base::Size;
17use std::cell::{Cell, RefCell};
18use std::rc::Rc;
19use std::time::Duration;
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq)]
23pub(crate) enum AccentColors {
24 System(u32, u32, u32),
26 Theme(u32),
28 None,
30}
31
32#[derive(Clone, Copy, Debug, PartialEq, Eq)]
34pub(crate) struct ColorInputs {
35 pub night: bool,
37 pub high_contrast: bool,
39 pub accents: AccentColors,
40}
41
42#[derive(Clone, Copy, Debug, PartialEq)]
44pub(crate) struct InputConfig {
45 pub long_press_timeout: i32,
47 pub multi_press_timeout: Option<i32>,
49 pub slops: Option<(i32, i32)>,
52 pub density: f32,
54}
55
56pub(crate) trait ISettingsContext {
58 fn color_inputs(&self) -> ColorInputs;
59 fn input_config(&self) -> InputConfig;
60 fn preferred_application_language(&self) -> Option<String>;
62 fn register_configuration_changed_receiver(&self);
66}
67
68ferroui_base::ferro_thread_local! {
69 static INSTANCE: RefCell<Option<Rc<AndroidPlatformSettings>>> = const { RefCell::new(None) };
70}
71
72pub struct AndroidPlatformSettings {
73 base: DefaultPlatformSettings,
74 context: Option<Box<dyn ISettingsContext>>,
75 color_values: Cell<PlatformColorValues>,
76 hold_wait_duration: Cell<Duration>,
77 double_tap_time: Cell<Duration>,
78 double_tap_size: Cell<Size>,
79 tap_size: Cell<Size>,
80 latest_language: RefCell<Option<String>>,
81}
82
83impl AndroidPlatformSettings {
84 pub(crate) fn new(context: Option<Box<dyn ISettingsContext>>) -> Rc<AndroidPlatformSettings> {
87 let base = DefaultPlatformSettings::new();
88 let this = AndroidPlatformSettings {
89 color_values: Cell::new(base.get_color_values()),
90 base,
91 context,
92 hold_wait_duration: Cell::new(Duration::from_millis(300)),
93 double_tap_time: Cell::new(Duration::from_millis(500)),
94 double_tap_size: Cell::new(Size::new(16.0, 16.0)),
95 tap_size: Cell::new(Size::new(10.0, 10.0)),
96 latest_language: RefCell::new(None),
97 };
98
99 if let Some(context) = &this.context {
100 this.update_input_config_values(context.as_ref());
101 this.color_values.set(Self::get_color_values_from_context(context.as_ref()));
102 *this.latest_language.borrow_mut() = Self::query_preferred_application_language(Some(context.as_ref()));
103
104 context.register_configuration_changed_receiver();
105 }
106
107 let this = Rc::new(this);
108 INSTANCE.with(|instance| *instance.borrow_mut() = Some(this.clone()));
110 this
111 }
112
113 fn update_color_values(&self, context: &dyn ISettingsContext) {
114 let old_color_values = self.color_values.get();
115 let color_values = Self::get_color_values_from_context(context);
116
117 if old_color_values != color_values {
118 self.color_values.set(color_values);
119 self.base.on_color_values_changed(color_values);
120 }
121 }
122
123 fn update_preferred_application_language(&self, context: &dyn ISettingsContext) {
124 let old_language = self.latest_language.borrow().clone();
125 let language = Self::query_preferred_application_language(Some(context));
126
127 if old_language != language {
128 *self.latest_language.borrow_mut() = language;
129 self.base.on_preferred_application_language_changed();
130 }
131 }
132
133 pub(crate) fn get_color_values_from_context(context: &dyn ISettingsContext) -> PlatformColorValues {
134 let inputs = context.color_inputs();
135 let system_theme = if inputs.night { PlatformThemeVariant::Dark } else { PlatformThemeVariant::Light };
136 let contrast_preference =
137 if inputs.high_contrast { ColorContrastPreference::High } else { ColorContrastPreference::NoPreference };
138
139 let values =
140 PlatformColorValues::new().with_theme_variant(system_theme).with_contrast_preference(contrast_preference);
141 match inputs.accents {
142 AccentColors::System(accent1, accent2, accent3) => values
143 .with_accent_color1(Color::from_uint32(accent1))
144 .with_accent_color2(Color::from_uint32(accent2))
145 .with_accent_color3(Color::from_uint32(accent3)),
146 AccentColors::Theme(accent) => values.with_accent_color1(Color::from_uint32(accent)),
147 AccentColors::None => values,
148 }
149 }
150
151 fn update_input_config_values(&self, context: &dyn ISettingsContext) {
152 let config = context.input_config();
153 self.hold_wait_duration.set(Duration::from_millis(u64::try_from(config.long_press_timeout).unwrap_or(0)));
154
155 if let Some(multi_press_timeout) = config.multi_press_timeout {
156 self.double_tap_time.set(Duration::from_millis(u64::try_from(multi_press_timeout).unwrap_or(0)));
157 }
158 let scaling = config.density;
159 if let Some((scaled_double_tap_slop, scaled_touch_slop)) = config.slops {
160 let size = f64::from((scaled_double_tap_slop * 2) as f32 / scaling);
163 self.double_tap_size.set(Size::new(size, size));
164 let size = f64::from((scaled_touch_slop * 2) as f32 / scaling);
165 self.tap_size.set(Size::new(size, size));
166 }
167 }
168
169 fn query_preferred_application_language(context: Option<&dyn ISettingsContext>) -> Option<String> {
170 context?.preferred_application_language().filter(|tag| !tag.is_empty())
171 }
172
173 pub(crate) fn on_receive() {
175 let Some(settings) = INSTANCE.with(|instance| instance.borrow().clone()) else {
176 return;
177 };
178 settings.schedule_update();
179 }
180
181 #[cfg(target_os = "android")]
182 fn schedule_update(self: &Rc<Self>) {
183 use ferroui_base::threading::{DispatcherPriority, DispatcherTimer};
184
185 let settings = self.clone();
188 DispatcherTimer::run_once(move || settings.update(), Duration::from_millis(100), DispatcherPriority::NORMAL);
189 }
190
191 #[cfg(not(target_os = "android"))]
192 fn schedule_update(self: &Rc<Self>) {
193 self.update();
195 }
196
197 pub(crate) fn update(&self) {
199 if let Some(context) = &self.context {
200 self.update_input_config_values(context.as_ref());
201 self.update_color_values(context.as_ref());
202 self.update_preferred_application_language(context.as_ref());
203 }
204 }
205}
206
207impl IPlatformSettings for AndroidPlatformSettings {
208 fn get_tap_size(&self, type_: PointerType) -> Size {
209 if type_ == PointerType::Mouse {
210 self.base.get_tap_size(type_)
211 } else {
212 self.tap_size.get()
213 }
214 }
215
216 fn get_double_tap_size(&self, type_: PointerType) -> Size {
217 if type_ == PointerType::Mouse {
218 self.base.get_double_tap_size(type_)
219 } else {
220 self.double_tap_size.get()
221 }
222 }
223
224 fn get_double_tap_time(&self, type_: PointerType) -> Duration {
225 if type_ == PointerType::Mouse {
226 self.base.get_double_tap_time(type_)
227 } else {
228 self.double_tap_time.get()
229 }
230 }
231
232 fn hold_wait_duration(&self) -> Duration {
233 self.hold_wait_duration.get()
234 }
235
236 fn hotkey_configuration(&self) -> Rc<PlatformHotkeyConfiguration> {
237 self.base.hotkey_configuration()
238 }
239
240 fn preferred_application_language(&self) -> String {
241 match &*self.latest_language.borrow() {
242 Some(language) => language.clone(),
243 None => self.base.preferred_application_language(),
244 }
245 }
246
247 fn get_color_values(&self) -> PlatformColorValues {
248 self.color_values.get()
249 }
250
251 fn color_values_changed(&self, handler: Rc<dyn Fn(&PlatformColorValues)>) -> Rc<dyn IDisposable> {
252 self.base.color_values_changed(handler)
253 }
254
255 fn preferred_application_language_changed(&self, handler: Rc<dyn Fn()>) -> Rc<dyn IDisposable> {
256 self.base.preferred_application_language_changed(handler)
257 }
258}
259
260#[cfg(target_os = "android")]
262pub(crate) struct ApplicationSettingsContext {
263 context: crate::interop::java::JavaObject,
264}
265
266#[cfg(target_os = "android")]
267impl ApplicationSettingsContext {
268 pub fn new(context: crate::interop::java::JavaObject) -> Self {
269 Self { context }
270 }
271
272 fn helper() -> crate::interop::java::JavaClass {
273 crate::interop::java::JavaClass::find(crate::interop::natives::PLATFORM_HELPER)
274 }
275}
276
277#[cfg(target_os = "android")]
278impl ISettingsContext for ApplicationSettingsContext {
279 fn color_inputs(&self) -> ColorInputs {
280 use crate::interop::java::{call_static_object, int_array_of, JavaValue};
281
282 let values = call_static_object(
283 &Self::helper(),
284 "getColorValues",
285 "(Landroid/content/Context;)[I",
286 &[JavaValue::Object(Some(&self.context))],
287 )
288 .map(|values| int_array_of(&values))
289 .unwrap_or_default();
290 color_inputs_from_values(&values)
291 }
292
293 fn input_config(&self) -> InputConfig {
294 use crate::interop::java::{call_static_object, float_array_of, JavaValue};
295
296 let values = call_static_object(
297 &Self::helper(),
298 "getInputConfigValues",
299 "(Landroid/content/Context;)[F",
300 &[JavaValue::Object(Some(&self.context))],
301 )
302 .map(|values| float_array_of(&values))
303 .unwrap_or_default();
304 input_config_from_values(&values)
305 }
306
307 fn preferred_application_language(&self) -> Option<String> {
308 use crate::interop::java::{call_static_object, string_of, JavaValue};
309
310 call_static_object(
311 &Self::helper(),
312 "getPreferredApplicationLanguage",
313 "(Landroid/content/Context;)Ljava/lang/String;",
314 &[JavaValue::Object(Some(&self.context))],
315 )
316 .map(|tag| string_of(&tag))
317 }
318
319 fn register_configuration_changed_receiver(&self) {
320 use crate::interop::java::{call_static_void, JavaClass, JavaValue};
321
322 call_static_void(
323 &JavaClass::find(crate::interop::natives::CONFIGURATION_CHANGED_RECEIVER),
324 "register",
325 "(Landroid/content/Context;)V",
326 &[JavaValue::Object(Some(&self.context))],
327 );
328 }
329}
330
331pub(crate) fn color_inputs_from_values(values: &[i32]) -> ColorInputs {
336 let at = |index: usize| values.get(index).copied().unwrap_or(0);
337 let color = |index: usize| at(index) as u32;
338 ColorInputs {
339 night: at(0) != 0,
340 high_contrast: at(1) != 0,
341 accents: match at(2) {
342 3 => AccentColors::System(color(3), color(4), color(5)),
343 1 => AccentColors::Theme(color(3)),
344 _ => AccentColors::None,
345 },
346 }
347}
348
349pub(crate) fn input_config_from_values(values: &[f32]) -> InputConfig {
354 let at = |index: usize| values.get(index).copied().unwrap_or(0.0);
355 InputConfig {
356 long_press_timeout: at(0) as i32,
357 multi_press_timeout: (at(1) >= 0.0).then_some(at(1) as i32),
358 slops: (at(2) != 0.0).then_some((at(3) as i32, at(4) as i32)),
359 density: if values.len() > 5 { at(5) } else { 1.0 },
360 }
361}
362
363#[cfg(test)]
364mod tests {
365 use super::*;
367
368 struct TestContext {
369 colors: Rc<Cell<ColorInputs>>,
370 input: Rc<Cell<InputConfig>>,
371 language: Rc<RefCell<Option<String>>>,
372 registered: Rc<Cell<u32>>,
373 }
374
375 struct Fixture {
376 colors: Rc<Cell<ColorInputs>>,
377 input: Rc<Cell<InputConfig>>,
378 language: Rc<RefCell<Option<String>>>,
379 registered: Rc<Cell<u32>>,
380 settings: Rc<AndroidPlatformSettings>,
381 }
382
383 impl ISettingsContext for TestContext {
384 fn color_inputs(&self) -> ColorInputs {
385 self.colors.get()
386 }
387
388 fn input_config(&self) -> InputConfig {
389 self.input.get()
390 }
391
392 fn preferred_application_language(&self) -> Option<String> {
393 self.language.borrow().clone()
394 }
395
396 fn register_configuration_changed_receiver(&self) {
397 self.registered.set(self.registered.get() + 1);
398 }
399 }
400
401 fn fixture() -> Fixture {
402 let colors = Rc::new(Cell::new(ColorInputs {
403 night: false,
404 high_contrast: false,
405 accents: AccentColors::System(0xff11_2233, 0xff44_5566, 0xff77_8899),
406 }));
407 let input = Rc::new(Cell::new(InputConfig {
408 long_press_timeout: 400,
409 multi_press_timeout: Some(300),
410 slops: Some((262, 21)),
411 density: 2.625,
412 }));
413 let language = Rc::new(RefCell::new(Some("pl-PL".to_string())));
414 let registered = Rc::new(Cell::new(0));
415 let settings = AndroidPlatformSettings::new(Some(Box::new(TestContext {
416 colors: colors.clone(),
417 input: input.clone(),
418 language: language.clone(),
419 registered: registered.clone(),
420 })));
421 Fixture { colors, input, language, registered, settings }
422 }
423
424 #[test]
425 fn without_a_context_the_settings_are_the_defaults() {
426 let settings = AndroidPlatformSettings::new(None);
427 let base = DefaultPlatformSettings::new();
428 assert_eq!(settings.get_color_values(), base.get_color_values());
429 assert_eq!(settings.hold_wait_duration(), Duration::from_millis(300));
430 assert_eq!(settings.get_double_tap_time(PointerType::Touch), Duration::from_millis(500));
431 assert_eq!(settings.get_double_tap_size(PointerType::Touch), Size::new(16.0, 16.0));
432 assert_eq!(settings.get_tap_size(PointerType::Pen), Size::new(10.0, 10.0));
433 assert_eq!(settings.preferred_application_language(), base.preferred_application_language());
434 }
435
436 #[test]
437 fn the_values_of_the_context_are_read_and_the_receiver_is_registered() {
438 let f = fixture();
439 assert_eq!(f.registered.get(), 1);
440
441 let colors = f.settings.get_color_values();
442 assert_eq!(colors.theme_variant(), PlatformThemeVariant::Light);
443 assert_eq!(colors.contrast_preference(), ColorContrastPreference::NoPreference);
444 assert_eq!(colors.accent_color1(), Color::from_argb(0xff, 0x11, 0x22, 0x33));
445 assert_eq!(colors.accent_color2(), Color::from_argb(0xff, 0x44, 0x55, 0x66));
446 assert_eq!(colors.accent_color3(), Color::from_argb(0xff, 0x77, 0x88, 0x99));
447
448 assert_eq!(f.settings.hold_wait_duration(), Duration::from_millis(400));
449 assert_eq!(f.settings.get_double_tap_time(PointerType::Touch), Duration::from_millis(300));
450 let double_tap = f64::from(524.0f32 / 2.625f32);
451 assert_eq!(f.settings.get_double_tap_size(PointerType::Touch), Size::new(double_tap, double_tap));
452 let tap = f64::from(42.0f32 / 2.625f32);
453 assert_eq!(f.settings.get_tap_size(PointerType::Touch), Size::new(tap, tap));
454 assert_eq!(f.settings.preferred_application_language(), "pl-PL");
455 }
456
457 #[test]
458 fn a_mouse_has_the_values_of_the_framework() {
459 let f = fixture();
460 let base = DefaultPlatformSettings::new();
461 assert_eq!(f.settings.get_tap_size(PointerType::Mouse), base.get_tap_size(PointerType::Mouse));
462 assert_eq!(f.settings.get_double_tap_size(PointerType::Mouse), base.get_double_tap_size(PointerType::Mouse));
463 assert_eq!(f.settings.get_double_tap_time(PointerType::Mouse), base.get_double_tap_time(PointerType::Mouse));
464 }
465
466 #[test]
467 fn a_configuration_change_raises_what_changed_and_only_that() {
468 let f = fixture();
469 let color_changes = Rc::new(RefCell::new(Vec::new()));
470 let language_changes = Rc::new(Cell::new(0));
471 let _colors = f.settings.color_values_changed(Rc::new({
472 let color_changes = color_changes.clone();
473 move |values: &PlatformColorValues| color_changes.borrow_mut().push(*values)
474 }));
475 let _language = f.settings.preferred_application_language_changed(Rc::new({
476 let language_changes = language_changes.clone();
477 move || language_changes.set(language_changes.get() + 1)
478 }));
479
480 AndroidPlatformSettings::on_receive();
482 assert!(color_changes.borrow().is_empty());
483 assert_eq!(language_changes.get(), 0);
484
485 f.colors.set(ColorInputs { night: true, high_contrast: true, accents: AccentColors::Theme(0xff00_ff00) });
487 AndroidPlatformSettings::on_receive();
488 assert_eq!(color_changes.borrow().len(), 1);
489 let colors = f.settings.get_color_values();
490 assert_eq!(colors, color_changes.borrow()[0]);
491 assert_eq!(colors.theme_variant(), PlatformThemeVariant::Dark);
492 assert_eq!(colors.contrast_preference(), ColorContrastPreference::High);
493 assert_eq!(colors.accent_color1(), Color::from_argb(0xff, 0, 0xff, 0));
494 assert_eq!(colors.accent_color2(), colors.accent_color1());
496 assert_eq!(language_changes.get(), 0);
497
498 *f.language.borrow_mut() = Some("de-DE".to_string());
500 f.input.set(InputConfig { long_press_timeout: 500, multi_press_timeout: None, slops: None, density: 1.0 });
501 AndroidPlatformSettings::on_receive();
502 assert_eq!(language_changes.get(), 1);
503 assert_eq!(f.settings.preferred_application_language(), "de-DE");
504 assert_eq!(f.settings.hold_wait_duration(), Duration::from_millis(500));
505 assert_eq!(f.settings.get_double_tap_time(PointerType::Touch), Duration::from_millis(300));
507 assert_eq!(color_changes.borrow().len(), 1);
508
509 *f.language.borrow_mut() = Some(String::new());
511 AndroidPlatformSettings::on_receive();
512 assert_eq!(language_changes.get(), 2);
513 assert_eq!(
514 f.settings.preferred_application_language(),
515 DefaultPlatformSettings::new().preferred_application_language()
516 );
517 }
518
519 #[test]
520 fn the_answers_of_the_java_layer_are_read() {
521 assert_eq!(
522 color_inputs_from_values(&[1, 0, 3, 0xff11_2233u32 as i32, 0xff44_5566u32 as i32, 0xff77_8899u32 as i32]),
523 ColorInputs {
524 night: true,
525 high_contrast: false,
526 accents: AccentColors::System(0xff11_2233, 0xff44_5566, 0xff77_8899),
527 }
528 );
529 assert_eq!(
530 color_inputs_from_values(&[0, 1, 1, 0xff00_ff00u32 as i32, 0, 0]),
531 ColorInputs { night: false, high_contrast: true, accents: AccentColors::Theme(0xff00_ff00) }
532 );
533 assert_eq!(
534 color_inputs_from_values(&[]),
535 ColorInputs { night: false, high_contrast: false, accents: AccentColors::None }
536 );
537
538 assert_eq!(
539 input_config_from_values(&[400.0, 300.0, 1.0, 262.0, 21.0, 2.625]),
540 InputConfig { long_press_timeout: 400, multi_press_timeout: Some(300), slops: Some((262, 21)), density: 2.625 }
541 );
542 assert_eq!(
543 input_config_from_values(&[400.0, -1.0, 0.0, 0.0, 0.0, 1.5]),
544 InputConfig { long_press_timeout: 400, multi_press_timeout: None, slops: None, density: 1.5 }
545 );
546 }
547}