1#![allow(dead_code)]
10
11use alloc::{string::String, vec::Vec};
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum A11yRole {
20 Window,
22 Button,
24 Label,
26 TextInput,
28 Menu,
30 MenuItem,
32 Toolbar,
34 Scrollbar,
36 List,
38 ListItem,
40 Dialog,
42 Alert,
44 Separator,
46 Image,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum A11yAction {
53 Click,
55 Focus,
57 Expand,
59 Collapse,
61 ScrollUp,
63 ScrollDown,
65}
66
67pub type A11yNodeId = u32;
73
74#[derive(Debug, Clone)]
76pub struct A11yNode {
77 pub id: A11yNodeId,
79 pub role: A11yRole,
81 pub name: String,
83 pub description: String,
85 pub bounds_x: i32,
87 pub bounds_y: i32,
88 pub bounds_w: u32,
89 pub bounds_h: u32,
90 pub children: Vec<A11yNodeId>,
92 pub focusable: bool,
94 pub focused: bool,
96 pub actions: Vec<A11yAction>,
98 pub value: String,
100 pub expanded: bool,
102}
103
104impl A11yNode {
105 pub fn new(id: A11yNodeId, role: A11yRole, name: &str) -> Self {
107 Self {
108 id,
109 role,
110 name: String::from(name),
111 description: String::new(),
112 bounds_x: 0,
113 bounds_y: 0,
114 bounds_w: 0,
115 bounds_h: 0,
116 children: Vec::new(),
117 focusable: matches!(
118 role,
119 A11yRole::Button | A11yRole::TextInput | A11yRole::MenuItem | A11yRole::ListItem
120 ),
121 focused: false,
122 actions: Vec::new(),
123 value: String::new(),
124 expanded: false,
125 }
126 }
127
128 pub fn set_bounds(&mut self, x: i32, y: i32, w: u32, h: u32) {
130 self.bounds_x = x;
131 self.bounds_y = y;
132 self.bounds_w = w;
133 self.bounds_h = h;
134 }
135
136 pub fn add_child(&mut self, child_id: A11yNodeId) {
138 self.children.push(child_id);
139 }
140
141 pub fn add_action(&mut self, action: A11yAction) {
143 if !self.actions.contains(&action) {
144 self.actions.push(action);
145 }
146 }
147}
148
149#[derive(Debug)]
155pub struct A11yTree {
156 nodes: Vec<A11yNode>,
158 root_id: Option<A11yNodeId>,
160 focused_node_id: Option<A11yNodeId>,
162 next_id: A11yNodeId,
164}
165
166impl Default for A11yTree {
167 fn default() -> Self {
168 Self::new()
169 }
170}
171
172impl A11yTree {
173 pub fn new() -> Self {
175 Self {
176 nodes: Vec::new(),
177 root_id: None,
178 focused_node_id: None,
179 next_id: 1,
180 }
181 }
182
183 pub fn add_node(&mut self, mut node: A11yNode) -> A11yNodeId {
185 let id = self.next_id;
186 self.next_id += 1;
187 node.id = id;
188 if self.root_id.is_none() {
189 self.root_id = Some(id);
190 }
191 self.nodes.push(node);
192 id
193 }
194
195 pub fn find_by_id(&self, id: A11yNodeId) -> Option<&A11yNode> {
197 self.nodes.iter().find(|n| n.id == id)
198 }
199
200 pub fn find_by_id_mut(&mut self, id: A11yNodeId) -> Option<&mut A11yNode> {
202 self.nodes.iter_mut().find(|n| n.id == id)
203 }
204
205 pub fn focused_node(&self) -> Option<&A11yNode> {
207 self.focused_node_id.and_then(|id| self.find_by_id(id))
208 }
209
210 pub fn set_focus(&mut self, id: A11yNodeId) -> bool {
212 if let Some(old_id) = self.focused_node_id {
214 if let Some(node) = self.find_by_id_mut(old_id) {
215 node.focused = false;
216 }
217 }
218
219 if let Some(node) = self.find_by_id_mut(id) {
221 if node.focusable {
222 node.focused = true;
223 self.focused_node_id = Some(id);
224 return true;
225 }
226 }
227 false
228 }
229
230 pub fn next_focusable(&mut self) -> Option<A11yNodeId> {
232 let focusable: Vec<A11yNodeId> = self
233 .nodes
234 .iter()
235 .filter(|n| n.focusable)
236 .map(|n| n.id)
237 .collect();
238
239 if focusable.is_empty() {
240 return None;
241 }
242
243 let current_idx = self
244 .focused_node_id
245 .and_then(|id| focusable.iter().position(|&fid| fid == id))
246 .unwrap_or(focusable.len().wrapping_sub(1));
247
248 let next_idx = (current_idx + 1) % focusable.len();
249 let next_id = focusable[next_idx];
250 self.set_focus(next_id);
251 Some(next_id)
252 }
253
254 pub fn prev_focusable(&mut self) -> Option<A11yNodeId> {
256 let focusable: Vec<A11yNodeId> = self
257 .nodes
258 .iter()
259 .filter(|n| n.focusable)
260 .map(|n| n.id)
261 .collect();
262
263 if focusable.is_empty() {
264 return None;
265 }
266
267 let current_idx = self
268 .focused_node_id
269 .and_then(|id| focusable.iter().position(|&fid| fid == id))
270 .unwrap_or(1);
271
272 let prev_idx = if current_idx == 0 {
273 focusable.len() - 1
274 } else {
275 current_idx - 1
276 };
277 let prev_id = focusable[prev_idx];
278 self.set_focus(prev_id);
279 Some(prev_id)
280 }
281
282 pub fn node_count(&self) -> usize {
284 self.nodes.len()
285 }
286
287 pub fn build_from_windows(windows: &[(&str, i32, i32, u32, u32)]) -> Self {
289 let mut tree = Self::new();
290
291 for (name, x, y, w, h) in windows {
292 let mut node = A11yNode::new(0, A11yRole::Window, name);
293 node.set_bounds(*x, *y, *w, *h);
294 node.focusable = true;
295 node.add_action(A11yAction::Focus);
296 tree.add_node(node);
297 }
298
299 tree
300 }
301}
302
303#[derive(Debug)]
309pub struct ScreenReader {
310 announcements: Vec<String>,
312 pub enabled: bool,
314 max_queue: usize,
316}
317
318impl Default for ScreenReader {
319 fn default() -> Self {
320 Self::new()
321 }
322}
323
324impl ScreenReader {
325 pub fn new() -> Self {
327 Self {
328 announcements: Vec::new(),
329 enabled: false,
330 max_queue: 64,
331 }
332 }
333
334 pub fn announce(&mut self, message: &str) {
336 if !self.enabled {
337 return;
338 }
339 if self.announcements.len() < self.max_queue {
340 self.announcements.push(String::from(message));
341 }
342 }
343
344 pub fn read_focused(&mut self, tree: &A11yTree) {
346 if !self.enabled {
347 return;
348 }
349 if let Some(node) = tree.focused_node() {
350 let role_str = match node.role {
351 A11yRole::Window => "window",
352 A11yRole::Button => "button",
353 A11yRole::Label => "label",
354 A11yRole::TextInput => "text input",
355 A11yRole::Menu => "menu",
356 A11yRole::MenuItem => "menu item",
357 A11yRole::Toolbar => "toolbar",
358 A11yRole::Scrollbar => "scrollbar",
359 A11yRole::List => "list",
360 A11yRole::ListItem => "list item",
361 A11yRole::Dialog => "dialog",
362 A11yRole::Alert => "alert",
363 A11yRole::Separator => "separator",
364 A11yRole::Image => "image",
365 };
366 let mut msg = String::new();
367 msg.push_str(&node.name);
368 msg.push_str(", ");
369 msg.push_str(role_str);
370 if !node.value.is_empty() {
371 msg.push_str(", value: ");
372 msg.push_str(&node.value);
373 }
374 self.announce(&msg);
375 }
376 }
377
378 pub fn read_all_children(&mut self, tree: &A11yTree, parent_id: A11yNodeId) {
380 if !self.enabled {
381 return;
382 }
383 if let Some(parent) = tree.find_by_id(parent_id) {
384 let children = parent.children.clone();
385 for child_id in &children {
386 if let Some(child) = tree.find_by_id(*child_id) {
387 self.announce(&child.name);
388 }
389 }
390 }
391 }
392
393 pub fn drain_announcements(&mut self) -> Vec<String> {
395 let mut drained = Vec::new();
396 core::mem::swap(&mut self.announcements, &mut drained);
397 drained
398 }
399
400 pub fn pending_count(&self) -> usize {
402 self.announcements.len()
403 }
404}
405
406#[derive(Debug, Clone, Copy, PartialEq, Eq)]
412pub struct HighContrastTheme {
413 pub bg_color: u32,
415 pub fg_color: u32,
417 pub accent_color: u32,
419 pub border_color: u32,
421}
422
423impl HighContrastTheme {
424 pub const DARK: Self = Self {
426 bg_color: 0xFF000000,
427 fg_color: 0xFFFFFFFF,
428 accent_color: 0xFF00FFFF,
429 border_color: 0xFFFFFF00,
430 };
431
432 pub const LIGHT: Self = Self {
434 bg_color: 0xFFFFFFFF,
435 fg_color: 0xFF000000,
436 accent_color: 0xFF0000FF,
437 border_color: 0xFF000000,
438 };
439
440 pub fn apply_background(&self, buf: &mut [u32], start: usize, count: usize) {
442 let end = (start + count).min(buf.len());
443 for px in &mut buf[start..end] {
444 *px = self.bg_color;
445 }
446 }
447}
448
449#[derive(Debug, Clone)]
455pub struct AccessibilitySettings {
456 pub screen_reader_enabled: bool,
458 pub high_contrast: bool,
460 pub contrast_theme: HighContrastTheme,
462 pub large_text: bool,
464 pub reduce_motion: bool,
466 pub sticky_keys: bool,
468 pub key_repeat_delay: u32,
470 pub key_repeat_rate: u32,
472}
473
474impl Default for AccessibilitySettings {
475 fn default() -> Self {
476 Self {
477 screen_reader_enabled: false,
478 high_contrast: false,
479 contrast_theme: HighContrastTheme::DARK,
480 large_text: false,
481 reduce_motion: false,
482 sticky_keys: false,
483 key_repeat_delay: 500,
484 key_repeat_rate: 50,
485 }
486 }
487}
488
489impl AccessibilitySettings {
490 pub fn any_active(&self) -> bool {
492 self.screen_reader_enabled
493 || self.high_contrast
494 || self.large_text
495 || self.reduce_motion
496 || self.sticky_keys
497 }
498}
499
500#[derive(Debug, Clone)]
506pub struct NavigationArea {
507 pub name: String,
509 pub node_ids: Vec<A11yNodeId>,
511 pub focus_index: usize,
513}
514
515impl NavigationArea {
516 pub fn new(name: &str) -> Self {
518 Self {
519 name: String::from(name),
520 node_ids: Vec::new(),
521 focus_index: 0,
522 }
523 }
524
525 pub fn add_node(&mut self, id: A11yNodeId) {
527 self.node_ids.push(id);
528 }
529}
530
531#[derive(Debug)]
537pub struct KeyboardNavigator {
538 pub areas: Vec<NavigationArea>,
540 pub current_area: usize,
542}
543
544impl Default for KeyboardNavigator {
545 fn default() -> Self {
546 Self::new()
547 }
548}
549
550#[derive(Debug, Clone, Copy, PartialEq, Eq)]
552pub enum NavKey {
553 CycleArea,
555 Tab,
557 ShiftTab,
559 Up,
561 Down,
563 Enter,
565 Escape,
567}
568
569#[derive(Debug, Clone, Copy, PartialEq, Eq)]
571pub enum NavResult {
572 FocusChanged(A11yNodeId),
574 Activated(A11yNodeId),
576 Cancelled,
578 NoOp,
580}
581
582impl KeyboardNavigator {
583 pub fn new() -> Self {
585 Self {
586 areas: Vec::new(),
587 current_area: 0,
588 }
589 }
590
591 pub fn add_area(&mut self, area: NavigationArea) {
593 self.areas.push(area);
594 }
595
596 pub fn cycle_area(&mut self) -> NavResult {
598 if self.areas.is_empty() {
599 return NavResult::NoOp;
600 }
601 self.current_area = (self.current_area + 1) % self.areas.len();
602 self.current_focused_node()
603 .map(NavResult::FocusChanged)
604 .unwrap_or(NavResult::NoOp)
605 }
606
607 pub fn focus_next(&mut self) -> NavResult {
609 if let Some(area) = self.areas.get_mut(self.current_area) {
610 if area.node_ids.is_empty() {
611 return NavResult::NoOp;
612 }
613 area.focus_index = (area.focus_index + 1) % area.node_ids.len();
614 NavResult::FocusChanged(area.node_ids[area.focus_index])
615 } else {
616 NavResult::NoOp
617 }
618 }
619
620 pub fn focus_prev(&mut self) -> NavResult {
622 if let Some(area) = self.areas.get_mut(self.current_area) {
623 if area.node_ids.is_empty() {
624 return NavResult::NoOp;
625 }
626 area.focus_index = if area.focus_index == 0 {
627 area.node_ids.len() - 1
628 } else {
629 area.focus_index - 1
630 };
631 NavResult::FocusChanged(area.node_ids[area.focus_index])
632 } else {
633 NavResult::NoOp
634 }
635 }
636
637 pub fn handle_key(&mut self, key: NavKey) -> NavResult {
639 match key {
640 NavKey::CycleArea => self.cycle_area(),
641 NavKey::Tab | NavKey::Down => self.focus_next(),
642 NavKey::ShiftTab | NavKey::Up => self.focus_prev(),
643 NavKey::Enter => self
644 .current_focused_node()
645 .map(NavResult::Activated)
646 .unwrap_or(NavResult::NoOp),
647 NavKey::Escape => NavResult::Cancelled,
648 }
649 }
650
651 pub fn current_focused_node(&self) -> Option<A11yNodeId> {
653 let area = self.areas.get(self.current_area)?;
654 area.node_ids.get(area.focus_index).copied()
655 }
656}
657
658#[cfg(test)]
663mod tests {
664 #[allow(unused_imports)]
665 use alloc::vec;
666
667 use super::*;
668
669 #[test]
670 fn test_a11y_node_new() {
671 let node = A11yNode::new(1, A11yRole::Button, "OK");
672 assert_eq!(node.name, "OK");
673 assert!(node.focusable);
674 assert!(!node.focused);
675 }
676
677 #[test]
678 fn test_a11y_node_label_not_focusable() {
679 let node = A11yNode::new(1, A11yRole::Label, "Status");
680 assert!(!node.focusable);
681 }
682
683 #[test]
684 fn test_a11y_tree_add_and_find() {
685 let mut tree = A11yTree::new();
686 let node = A11yNode::new(0, A11yRole::Window, "Main");
687 let id = tree.add_node(node);
688 assert!(tree.find_by_id(id).is_some());
689 assert_eq!(tree.node_count(), 1);
690 }
691
692 #[test]
693 fn test_a11y_tree_focus() {
694 let mut tree = A11yTree::new();
695 let mut btn = A11yNode::new(0, A11yRole::Button, "Click Me");
696 btn.focusable = true;
697 let id = tree.add_node(btn);
698 assert!(tree.set_focus(id));
699 assert!(tree.focused_node().is_some());
700 assert_eq!(tree.focused_node().unwrap().name, "Click Me");
701 }
702
703 #[test]
704 fn test_a11y_tree_next_focusable() {
705 let mut tree = A11yTree::new();
706 let btn1 = A11yNode::new(0, A11yRole::Button, "A");
707 let btn2 = A11yNode::new(0, A11yRole::Button, "B");
708 tree.add_node(btn1);
709 tree.add_node(btn2);
710 let next = tree.next_focusable();
711 assert!(next.is_some());
712 }
713
714 #[test]
715 fn test_a11y_tree_prev_focusable() {
716 let mut tree = A11yTree::new();
717 let btn1 = A11yNode::new(0, A11yRole::Button, "A");
718 let btn2 = A11yNode::new(0, A11yRole::Button, "B");
719 let id1 = tree.add_node(btn1);
720 let _id2 = tree.add_node(btn2);
721 tree.set_focus(id1);
722 let prev = tree.prev_focusable();
723 assert!(prev.is_some());
724 }
725
726 #[test]
727 fn test_build_from_windows() {
728 let tree = A11yTree::build_from_windows(&[("Terminal", 0, 0, 800, 600)]);
729 assert_eq!(tree.node_count(), 1);
730 }
731
732 #[test]
733 fn test_screen_reader_announce() {
734 let mut reader = ScreenReader::new();
735 reader.enabled = true;
736 reader.announce("Hello");
737 assert_eq!(reader.pending_count(), 1);
738 let msgs = reader.drain_announcements();
739 assert_eq!(msgs.len(), 1);
740 assert_eq!(msgs[0], "Hello");
741 }
742
743 #[test]
744 fn test_screen_reader_disabled() {
745 let mut reader = ScreenReader::new();
746 reader.announce("Should not be added");
747 assert_eq!(reader.pending_count(), 0);
748 }
749
750 #[test]
751 fn test_screen_reader_read_focused() {
752 let mut tree = A11yTree::new();
753 let btn = A11yNode::new(0, A11yRole::Button, "Save");
754 let id = tree.add_node(btn);
755 tree.set_focus(id);
756 let mut reader = ScreenReader::new();
757 reader.enabled = true;
758 reader.read_focused(&tree);
759 assert_eq!(reader.pending_count(), 1);
760 }
761
762 #[test]
763 fn test_high_contrast_theme() {
764 let theme = HighContrastTheme::DARK;
765 assert_eq!(theme.bg_color, 0xFF000000);
766 assert_eq!(theme.fg_color, 0xFFFFFFFF);
767 let mut buf = vec![0xFFFFFFFF_u32; 10];
768 theme.apply_background(&mut buf, 0, 5);
769 assert_eq!(buf[0], 0xFF000000);
770 assert_eq!(buf[5], 0xFFFFFFFF);
771 }
772
773 #[test]
774 fn test_accessibility_settings_default() {
775 let settings = AccessibilitySettings::default();
776 assert!(!settings.any_active());
777 }
778
779 #[test]
780 fn test_keyboard_navigator_cycle() {
781 let mut nav = KeyboardNavigator::new();
782 let mut area1 = NavigationArea::new("Menu");
783 area1.add_node(10);
784 area1.add_node(11);
785 let mut area2 = NavigationArea::new("Content");
786 area2.add_node(20);
787 nav.add_area(area1);
788 nav.add_area(area2);
789 assert_eq!(nav.current_area, 0);
790 nav.cycle_area();
791 assert_eq!(nav.current_area, 1);
792 nav.cycle_area();
793 assert_eq!(nav.current_area, 0);
794 }
795
796 #[test]
797 fn test_keyboard_navigator_focus_next_prev() {
798 let mut nav = KeyboardNavigator::new();
799 let mut area = NavigationArea::new("Toolbar");
800 area.add_node(1);
801 area.add_node(2);
802 area.add_node(3);
803 nav.add_area(area);
804 let result = nav.focus_next();
805 assert_eq!(result, NavResult::FocusChanged(2));
806 let result = nav.focus_prev();
807 assert_eq!(result, NavResult::FocusChanged(1));
808 }
809
810 #[test]
811 fn test_keyboard_navigator_handle_key() {
812 let mut nav = KeyboardNavigator::new();
813 let mut area = NavigationArea::new("Test");
814 area.add_node(5);
815 nav.add_area(area);
816 let result = nav.handle_key(NavKey::Enter);
817 assert_eq!(result, NavResult::Activated(5));
818 let result = nav.handle_key(NavKey::Escape);
819 assert_eq!(result, NavResult::Cancelled);
820 }
821
822 #[test]
823 fn test_nav_result_no_op_empty() {
824 let mut nav = KeyboardNavigator::new();
825 assert_eq!(nav.handle_key(NavKey::Tab), NavResult::NoOp);
826 }
827}