cranpose_services/accessibility_options.rs
1//! The display options a person set in the system's accessibility settings:
2//! larger text, less motion, less transparency, more contrast, bold text,
3//! inverted colors. The framework applies them on its own; an app reads them
4//! for what it draws itself.
5
6use std::cell::{Cell, RefCell};
7
8use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOf};
9use cranpose_macros::composable;
10
11/// What the person set in the system's accessibility settings.
12#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct AccessibilityOptions {
14 /// The text size setting as a multiplier of the default, 1.0 when the
15 /// person left it alone. Dynamic Type on iOS, the font size on Android
16 /// and the web, the text scale of the desktop.
17 pub font_scale: f32,
18 /// Whether the person asked for less motion. Every animation then ends on
19 /// its first frame.
20 pub reduce_motion: bool,
21 /// Whether the person asked for less transparency. Glass then draws as a
22 /// flat surface with no blur.
23 pub reduce_transparency: bool,
24 /// Whether the person asked for more contrast. Secondary text, separators
25 /// and fills then draw darker on light and lighter on dark.
26 pub increase_contrast: bool,
27 /// Whether the person asked for bold text. Every text style then gains a
28 /// weight step.
29 pub bold_text: bool,
30 /// Whether the system inverts colors and leaves this app's window to
31 /// invert its own, the way iOS Smart Invert does. The theme then swaps to
32 /// its other palette and pictures stay as they are. A system that inverts
33 /// the whole screen itself reports false.
34 pub invert_colors: bool,
35}
36
37impl Default for AccessibilityOptions {
38 fn default() -> Self {
39 Self {
40 font_scale: 1.0,
41 reduce_motion: false,
42 reduce_transparency: false,
43 increase_contrast: false,
44 bold_text: false,
45 invert_colors: false,
46 }
47 }
48}
49
50impl AccessibilityOptions {
51 /// The options with the font scale made a plain number: finite and above
52 /// zero, else 1.0.
53 pub fn normalized(mut self) -> Self {
54 if !(self.font_scale.is_finite() && self.font_scale > 0.0) {
55 self.font_scale = 1.0;
56 }
57 self
58 }
59}
60
61thread_local! {
62 static PLATFORM_ACCESSIBILITY_OPTIONS: Cell<AccessibilityOptions> =
63 const { Cell::new(AccessibilityOptions {
64 font_scale: 1.0,
65 reduce_motion: false,
66 reduce_transparency: false,
67 increase_contrast: false,
68 bold_text: false,
69 invert_colors: false,
70 }) };
71}
72
73/// Installs what the platform reports. A backend calls this at start and on
74/// every change, and forces a root render when the answer is true, so the
75/// theme, the glass and the animations read the new options.
76pub fn set_platform_accessibility_options(options: AccessibilityOptions) -> bool {
77 let options = options.normalized();
78 PLATFORM_ACCESSIBILITY_OPTIONS.with(|cell| {
79 let changed = cell.get() != options;
80 cell.set(options);
81 changed
82 })
83}
84
85/// What the platform last reported.
86pub fn platform_accessibility_options() -> AccessibilityOptions {
87 PLATFORM_ACCESSIBILITY_OPTIONS.with(Cell::get)
88}
89
90/// The options a composable reads: what the platform reported, unless a
91/// [`ProvideAccessibilityOptions`] above it says otherwise.
92pub fn local_accessibility_options() -> CompositionLocal<AccessibilityOptions> {
93 thread_local! {
94 static LOCAL: RefCell<Option<CompositionLocal<AccessibilityOptions>>> =
95 const { RefCell::new(None) };
96 }
97
98 LOCAL.with(|cell| {
99 let mut local = cell.borrow_mut();
100 local
101 .get_or_insert_with(|| compositionLocalOf(platform_accessibility_options))
102 .clone()
103 })
104}
105
106/// Gives the content below it fixed options, for a preview or a test that
107/// wants to see the app with larger text, no motion or more contrast.
108#[composable]
109pub fn ProvideAccessibilityOptions(options: AccessibilityOptions, content: impl FnOnce()) {
110 let provided = local_accessibility_options().provides(options.normalized());
111 CompositionLocalProvider(vec![provided], move || content());
112}
113
114#[cfg(test)]
115#[path = "tests/accessibility_options_tests.rs"]
116mod tests;