cranpose_services/
accessibility_options.rs1use std::cell::{Cell, RefCell};
7
8use cranpose_core::{CompositionLocal, CompositionLocalProvider, compositionLocalOf};
9use cranpose_macros::composable;
10
11#[derive(Clone, Copy, Debug, PartialEq)]
13pub struct AccessibilityOptions {
14 pub font_scale: f32,
18 pub reduce_motion: bool,
21 pub reduce_transparency: bool,
24 pub increase_contrast: bool,
27 pub bold_text: bool,
30 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 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
73pub 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
85pub fn platform_accessibility_options() -> AccessibilityOptions {
87 PLATFORM_ACCESSIBILITY_OPTIONS.with(|cell| cell.get())
88}
89
90pub 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#[allow(non_snake_case)]
109#[composable]
110pub fn ProvideAccessibilityOptions(options: AccessibilityOptions, content: impl FnOnce()) {
111 let provided = local_accessibility_options().provides(options.normalized());
112 CompositionLocalProvider(vec![provided], move || content());
113}
114
115#[cfg(test)]
116mod tests {
117 use std::{cell::Cell, rc::Rc};
118
119 use super::*;
120 use crate::captured_in_composition;
121
122 fn large_and_still() -> AccessibilityOptions {
123 AccessibilityOptions {
124 font_scale: 1.5,
125 reduce_motion: true,
126 ..AccessibilityOptions::default()
127 }
128 }
129
130 #[test]
131 fn the_platform_options_report_a_change_once_and_mend_a_bad_scale() {
132 set_platform_accessibility_options(AccessibilityOptions::default());
133 assert!(set_platform_accessibility_options(large_and_still()));
134 assert!(!set_platform_accessibility_options(large_and_still()));
135 assert_eq!(platform_accessibility_options(), large_and_still());
136 let bad_scale = AccessibilityOptions {
137 font_scale: 0.0,
138 ..AccessibilityOptions::default()
139 };
140 assert!(set_platform_accessibility_options(bad_scale));
141 assert_eq!(platform_accessibility_options().font_scale, 1.0);
142 }
143
144 #[test]
145 fn a_composable_reads_the_platform_options_and_a_provider_wins_over_them() {
146 set_platform_accessibility_options(large_and_still());
147 let seen = captured_in_composition(|| local_accessibility_options().current());
148 set_platform_accessibility_options(AccessibilityOptions::default());
149 assert_eq!(seen, Some(large_and_still()));
150
151 let under_provider = captured_in_composition(|| {
152 let slot = Rc::new(Cell::new(None));
153 let sink = Rc::clone(&slot);
154 let local = local_accessibility_options();
155 ProvideAccessibilityOptions(large_and_still(), move || sink.set(Some(local.current())));
156 slot.get()
157 });
158 assert_eq!(under_provider, Some(Some(large_and_still())));
159 }
160}