Skip to main content

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;