1use crate::color::Color;
2use crate::id::Id;
3
4#[derive(Debug, Clone, Default, PartialEq, Eq)]
6pub enum AccessibilityRole {
7 #[default]
8 None,
9 Button,
11 Link,
12 Heading {
14 level: u8,
15 },
16 Label,
17 StaticText,
18 TextInput,
20 TextArea,
21 Checkbox,
22 RadioButton,
23 Slider,
24 Group,
26 List,
27 ListItem,
28 Menu,
29 MenuItem,
30 MenuBar,
31 Tab,
32 TabList,
33 TabPanel,
34 Dialog,
35 AlertDialog,
36 Toolbar,
37 Image,
39 ProgressBar,
40}
41
42#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
43pub enum LiveRegionMode {
44 #[default]
46 Off,
47 Polite,
49 Assertive,
51}
52
53#[derive(Debug, Clone, Default)]
54pub struct AccessibilityConfig {
55 pub focusable: bool,
56 pub role: AccessibilityRole,
57 pub label: String,
58 pub description: String,
59 pub value: String,
60 pub value_min: Option<f32>,
61 pub value_max: Option<f32>,
62 pub checked: Option<bool>,
63 pub tab_index: Option<i32>,
64 pub focus_right: Option<u32>,
65 pub focus_left: Option<u32>,
66 pub focus_up: Option<u32>,
67 pub focus_down: Option<u32>,
68 pub show_ring: bool,
69 pub ring_color: Option<Color>,
70 pub ring_width: Option<u16>,
71 pub live_region: LiveRegionMode,
72}
73
74impl AccessibilityConfig {
75 pub fn new() -> Self {
76 Self {
77 show_ring: true,
78 ..Default::default()
79 }
80 }
81}
82
83pub struct AccessibilityBuilder {
84 pub(crate) config: AccessibilityConfig,
85}
86
87impl AccessibilityBuilder {
88 pub(crate) fn new() -> Self {
89 Self {
90 config: AccessibilityConfig::new(),
91 }
92 }
93
94 pub fn focusable(&mut self) -> &mut Self {
96 self.config.focusable = true;
97 self
98 }
99
100 pub fn button(&mut self, label: &str) -> &mut Self {
102 self.config.role = AccessibilityRole::Button;
103 self.config.label = label.to_string();
104 self.config.focusable = true;
105 self
106 }
107
108 pub fn heading(&mut self, label: &str, level: u8) -> &mut Self {
110 self.config.role = AccessibilityRole::Heading { level };
111 self.config.label = label.to_string();
112 self
113 }
114
115 pub fn link(&mut self, label: &str) -> &mut Self {
117 self.config.role = AccessibilityRole::Link;
118 self.config.label = label.to_string();
119 self.config.focusable = true;
120 self
121 }
122
123 pub fn static_text(&mut self, label: &str) -> &mut Self {
125 self.config.role = AccessibilityRole::StaticText;
126 self.config.label = label.to_string();
127 self
128 }
129
130 pub fn checkbox(&mut self, label: &str) -> &mut Self {
132 self.config.role = AccessibilityRole::Checkbox;
133 self.config.label = label.to_string();
134 self.config.focusable = true;
135 self
136 }
137
138 pub fn slider(&mut self, label: &str) -> &mut Self {
140 self.config.role = AccessibilityRole::Slider;
141 self.config.label = label.to_string();
142 self.config.focusable = true;
143 self
144 }
145
146 pub fn image(&mut self, alt: &str) -> &mut Self {
148 self.config.role = AccessibilityRole::Image;
149 self.config.label = alt.to_string();
150 self
151 }
152
153 pub fn role(&mut self, role: AccessibilityRole) -> &mut Self {
155 self.config.role = role;
156 self
157 }
158
159 pub fn label(&mut self, label: &str) -> &mut Self {
161 self.config.label = label.to_string();
162 self
163 }
164
165 pub fn description(&mut self, desc: &str) -> &mut Self {
167 self.config.description = desc.to_string();
168 self
169 }
170
171 pub fn value(&mut self, value: &str) -> &mut Self {
173 self.config.value = value.to_string();
174 self
175 }
176
177 pub fn value_min(&mut self, min: f32) -> &mut Self {
179 self.config.value_min = Some(min);
180 self
181 }
182
183 pub fn value_max(&mut self, max: f32) -> &mut Self {
185 self.config.value_max = Some(max);
186 self
187 }
188
189 pub fn checked(&mut self, checked: bool) -> &mut Self {
191 self.config.checked = Some(checked);
192 self
193 }
194
195 pub fn tab_index(&mut self, index: i32) -> &mut Self {
198 self.config.tab_index = Some(index);
199 self
200 }
201
202 pub fn focus_right(&mut self, target: impl Into<Id>) -> &mut Self {
205 self.config.focus_right = Some(target.into().id);
206 self
207 }
208
209 pub fn focus_left(&mut self, target: impl Into<Id>) -> &mut Self {
212 self.config.focus_left = Some(target.into().id);
213 self
214 }
215
216 pub fn focus_up(&mut self, target: impl Into<Id>) -> &mut Self {
219 self.config.focus_up = Some(target.into().id);
220 self
221 }
222
223 pub fn focus_down(&mut self, target: impl Into<Id>) -> &mut Self {
226 self.config.focus_down = Some(target.into().id);
227 self
228 }
229
230 pub fn disable_ring(&mut self) -> &mut Self {
232 self.config.show_ring = false;
233 self
234 }
235
236 pub fn ring_color(&mut self, color: impl Into<Color>) -> &mut Self {
238 self.config.ring_color = Some(color.into());
239 self
240 }
241
242 pub fn ring_width(&mut self, width: u16) -> &mut Self {
244 self.config.ring_width = Some(width);
245 self
246 }
247
248 pub fn live_region_polite(&mut self) -> &mut Self {
250 self.config.live_region = LiveRegionMode::Polite;
251 self
252 }
253
254 pub fn live_region_assertive(&mut self) -> &mut Self {
256 self.config.live_region = LiveRegionMode::Assertive;
257 self
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264
265 #[test]
266 fn builder_button_sets_role_and_focusable() {
267 let mut builder = AccessibilityBuilder::new();
268 builder.button("Submit");
269 assert_eq!(builder.config.role, AccessibilityRole::Button);
270 assert_eq!(builder.config.label, "Submit");
271 assert!(builder.config.focusable);
272 assert!(builder.config.show_ring); }
274
275 #[test]
276 fn builder_heading_sets_level() {
277 let mut builder = AccessibilityBuilder::new();
278 builder.heading("Settings", 2);
279 assert_eq!(
280 builder.config.role,
281 AccessibilityRole::Heading { level: 2 }
282 );
283 assert_eq!(builder.config.label, "Settings");
284 }
285
286 #[test]
287 fn builder_disable_ring() {
288 let mut builder = AccessibilityBuilder::new();
289 builder.focusable().disable_ring();
290 assert!(builder.config.focusable);
291 assert!(!builder.config.show_ring);
292 }
293
294 #[test]
295 fn builder_focus_directions() {
296 let mut builder = AccessibilityBuilder::new();
297 builder
298 .focusable()
299 .focus_right(("next", 0u32))
300 .focus_left(("prev", 0u32))
301 .focus_up(("above", 0u32))
302 .focus_down(("below", 0u32));
303
304 assert_eq!(builder.config.focus_right, Some(Id::from(("next", 0u32)).id));
305 assert_eq!(builder.config.focus_left, Some(Id::from(("prev", 0u32)).id));
306 assert_eq!(builder.config.focus_up, Some(Id::from(("above", 0u32)).id));
307 assert_eq!(builder.config.focus_down, Some(Id::from(("below", 0u32)).id));
308 }
309
310 #[test]
311 fn builder_slider_properties() {
312 let mut builder = AccessibilityBuilder::new();
313 builder
314 .role(AccessibilityRole::Slider)
315 .label("Volume")
316 .description("Adjusts the master volume from 0 to 100")
317 .value("75")
318 .value_min(0.0)
319 .value_max(100.0);
320
321 assert_eq!(builder.config.role, AccessibilityRole::Slider);
322 assert_eq!(builder.config.label, "Volume");
323 assert_eq!(builder.config.value, "75");
324 assert_eq!(builder.config.value_min, Some(0.0));
325 assert_eq!(builder.config.value_max, Some(100.0));
326 }
327
328 #[test]
329 fn builder_ring_styling() {
330 let mut builder = AccessibilityBuilder::new();
331 builder
332 .focusable()
333 .ring_color(Color::rgb(0.0, 120.0, 255.0))
334 .ring_width(3);
335
336 assert!(builder.config.show_ring);
337 assert_eq!(builder.config.ring_color, Some(Color::rgb(0.0, 120.0, 255.0)));
338 assert_eq!(builder.config.ring_width, Some(3));
339 }
340
341 #[test]
342 fn ring_color_accepts_into() {
343 let mut builder = AccessibilityBuilder::new();
344 builder.ring_color((0u8, 120u8, 255u8));
345 assert_eq!(builder.config.ring_color, Some(Color::rgb(0.0, 120.0, 255.0)));
346 }
347
348 #[test]
349 fn ring_defaults_are_none() {
350 let config = AccessibilityConfig::new();
351 assert!(config.show_ring);
352 assert!(config.ring_color.is_none());
353 assert!(config.ring_width.is_none());
354 }
355}