Skip to main content

cranpose_services/
accessibility_state.rs

1//! What the platform says about the assistive technology in use, for an app
2//! that speaks guidance, slows an automatic step down or drops a gesture-only
3//! path while a screen reader is on.
4
5use std::cell::{Cell, RefCell};
6
7use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOf};
8use cranpose_macros::composable;
9
10/// What the platform reports about the assistive technology in use.
11#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
12pub struct AccessibilityState {
13    /// Whether a screen reader is on: VoiceOver on iOS, a service such as
14    /// TalkBack on Android, or a reader that connected to the app through
15    /// accesskit on the desktop. The web has no such signal and says no.
16    pub screen_reader_on: bool,
17}
18
19thread_local! {
20    static PLATFORM_ACCESSIBILITY_STATE: Cell<AccessibilityState> =
21        const { Cell::new(AccessibilityState { screen_reader_on: false }) };
22}
23
24/// Installs what the platform reports. A backend calls this on every check
25/// and forces a root render when the answer is true, so composition reads
26/// the new state.
27pub fn set_platform_accessibility_state(state: AccessibilityState) -> bool {
28    PLATFORM_ACCESSIBILITY_STATE.with(|cell| {
29        let changed = cell.get() != state;
30        cell.set(state);
31        changed
32    })
33}
34
35/// What the platform last reported.
36pub fn platform_accessibility_state() -> AccessibilityState {
37    PLATFORM_ACCESSIBILITY_STATE.with(|cell| cell.get())
38}
39
40/// The state a composable reads: what the platform reported, unless a
41/// [`ProvideAccessibilityState`] above it says otherwise.
42pub fn local_accessibility_state() -> CompositionLocal<AccessibilityState> {
43    thread_local! {
44        static LOCAL: RefCell<Option<CompositionLocal<AccessibilityState>>> =
45            const { RefCell::new(None) };
46    }
47
48    LOCAL.with(|cell| {
49        let mut local = cell.borrow_mut();
50        local
51            .get_or_insert_with(|| compositionLocalOf(platform_accessibility_state))
52            .clone()
53    })
54}
55
56/// Gives the content below it a fixed state, for a preview or a test that
57/// wants to see the app as a screen reader user does.
58#[allow(non_snake_case)]
59#[composable]
60pub fn ProvideAccessibilityState(state: AccessibilityState, content: impl FnOnce()) {
61    let local = local_accessibility_state();
62    CompositionLocalProvider(vec![local.provides(state)], move || {
63        content();
64    });
65}
66
67#[cfg(test)]
68mod tests {
69    use std::{cell::RefCell, rc::Rc};
70
71    use super::*;
72    use crate::run_test_composition;
73
74    const READER_ON: AccessibilityState = AccessibilityState {
75        screen_reader_on: true,
76    };
77
78    #[test]
79    fn the_platform_state_reports_a_change_once() {
80        set_platform_accessibility_state(AccessibilityState::default());
81        assert!(set_platform_accessibility_state(READER_ON));
82        assert!(!set_platform_accessibility_state(READER_ON));
83        assert_eq!(platform_accessibility_state(), READER_ON);
84        assert!(set_platform_accessibility_state(
85            AccessibilityState::default()
86        ));
87    }
88
89    #[test]
90    fn a_composable_reads_what_the_platform_reported() {
91        set_platform_accessibility_state(READER_ON);
92        let captured = Rc::new(RefCell::new(None));
93        {
94            let captured = Rc::clone(&captured);
95            run_test_composition(move || {
96                *captured.borrow_mut() = Some(local_accessibility_state().current());
97            });
98        }
99        set_platform_accessibility_state(AccessibilityState::default());
100
101        assert_eq!(*captured.borrow(), Some(READER_ON));
102    }
103
104    #[test]
105    fn a_provider_overrides_the_platform_state() {
106        set_platform_accessibility_state(AccessibilityState::default());
107        let local = local_accessibility_state();
108        let captured = Rc::new(RefCell::new(None));
109        {
110            let captured = Rc::clone(&captured);
111            let local = local.clone();
112            run_test_composition(move || {
113                let captured = Rc::clone(&captured);
114                let local = local.clone();
115                ProvideAccessibilityState(READER_ON, move || {
116                    *captured.borrow_mut() = Some(local.current());
117                });
118            });
119        }
120
121        assert_eq!(*captured.borrow(), Some(READER_ON));
122    }
123}