1use crate::*;
102
103pub(crate) mod debug;
104
105use crate::{App, Bounds, FocusId, Pixels, SharedString, Window};
106use accesskit::{Action, NodeId, TreeUpdate};
107use collections::{FxHashMap, FxHashSet};
108use smallvec::SmallVec;
109use std::hash::{Hash, Hasher};
110use std::sync::{
111 Arc,
112 atomic::{AtomicBool, Ordering},
113};
114
115pub(crate) const ROOT_NODE_ID: NodeId = NodeId(0);
117
118pub(crate) type A11yActionListener =
120 Box<dyn FnMut(Option<&accesskit::ActionData>, &mut Window, &mut App) + 'static>;
121
122pub(crate) struct A11y {
127 force_disabled: bool,
131 active_flag: Arc<AtomicBool>,
136 active_this_frame: bool,
148 pub(crate) nodes: A11yNodeBuilder,
149 pub(crate) focus_ids: FxHashMap<NodeId, FocusId>,
150 pub(crate) node_bounds: FxHashMap<NodeId, Bounds<Pixels>>,
151 pub(crate) action_listeners: FxHashMap<NodeId, Vec<(Action, A11yActionListener)>>,
152 window_title: Option<SharedString>,
155 last_focus_without_node: Option<FocusId>,
158 debug: debug::A11yDebug,
161 #[cfg(debug_assertions)]
163 pub(crate) view_type_names: FxHashMap<EntityId, &'static str>,
164}
165
166impl A11y {
167 pub(crate) fn new(
168 active_flag: Arc<AtomicBool>,
169 force_disabled: bool,
170 window_title: Option<SharedString>,
171 ) -> Self {
172 Self {
173 force_disabled,
174 active_flag,
175 active_this_frame: false,
176 nodes: A11yNodeBuilder::new(),
177 focus_ids: FxHashMap::default(),
178 node_bounds: FxHashMap::default(),
179 action_listeners: FxHashMap::default(),
180 window_title,
181 last_focus_without_node: None,
182 debug: debug::A11yDebug::default(),
183 #[cfg(debug_assertions)]
184 view_type_names: FxHashMap::default(),
185 }
186 }
187
188 pub(crate) fn note_focus_without_node(&mut self, focus_id: FocusId, reason: &str) {
194 if self.last_focus_without_node != Some(focus_id) {
195 self.last_focus_without_node = Some(focus_id);
196 log::info!(
197 "a11y: focused element ({focus_id:?}) has no accessibility node \
198 ({reason}); assistive technology will announce the whole window \
199 instead. Give it both an `.id(...)` and a `.role(...)` to expose it."
200 );
201 }
202 }
203
204 pub(crate) fn set_window_title(&mut self, title: impl Into<SharedString>) {
205 self.window_title = Some(title.into());
206 }
207
208 pub(crate) fn sync_active_flag(&mut self) {
213 self.active_this_frame = self.is_enabled() && self.active_flag.load(Ordering::SeqCst);
214 }
215
216 pub(crate) fn is_enabled(&self) -> bool {
217 !self.force_disabled
218 }
219
220 pub(crate) fn is_active(&self) -> bool {
221 self.active_this_frame
222 }
223
224 pub(crate) fn set_focusable(&mut self, node_id: NodeId, focus_id: FocusId) {
225 self.focus_ids.insert(node_id, focus_id);
226 }
227
228 pub(crate) fn set_focus(&mut self, node_id: NodeId) {
233 if !self.focus_ids.contains_key(&node_id) {
235 if cfg!(debug_assertions) {
236 panic!("set_focus called for a node that was not registered with set_focusable");
237 } else {
238 log::warn!(
239 "a11y: set_focus called for a node that was not registered with \
240 set_focusable ({node_id:?})"
241 );
242 }
243 }
244 if self.nodes.has_node(node_id) {
245 self.last_focus_without_node = None;
248 self.nodes.set_focus(node_id);
249 } else {
250 if let Some(focus_id) = self.focus_ids.get(&node_id).copied() {
253 self.note_focus_without_node(focus_id, "it has an id but no role");
254 }
255 }
256 }
257
258 pub(crate) fn set_active_descendant(&mut self, node_id: NodeId) {
259 if self.nodes.node_is_focused(node_id) {
262 if cfg!(debug_assertions) {
263 panic!("set_active_descendant called on the focused node");
264 } else {
265 log::warn!("a11y: set_active_descendant called on the focused node ({node_id:?})");
266 }
267 return;
268 }
269 if self.nodes.has_node(node_id) && self.nodes.focus_is_ancestor_of_current() {
270 self.nodes.set_active_descendant(node_id);
271 }
272 }
273
274 pub(crate) fn begin_frame(&mut self) {
276 self.focus_ids.clear();
277 self.node_bounds.clear();
278 self.action_listeners.clear();
279 self.nodes.begin_frame(self.window_title.as_ref());
280 }
281
282 pub(crate) fn end_frame(&mut self, frame: debug::FrameDebugInfo) -> TreeUpdate {
284 let update = self.nodes.finalize();
285 self.debug.capture(
286 &update,
287 self.nodes.focus,
288 self.nodes.active_descendant,
289 self.window_title.as_ref(),
290 frame,
291 );
292 #[cfg(debug_assertions)]
293 self.debug.capture_node_info(&self.nodes.node_info);
294 update
295 }
296
297 pub(crate) fn debug_tree_json(&self) -> Option<String> {
298 self.debug.to_json()
299 }
300}
301
302pub struct A11ySubtreeBuilder<'a> {
305 parent_id: NodeId,
306 nodes: &'a mut A11yNodeBuilder,
307 #[cfg(debug_assertions)]
310 creator: debug::NodeCreator,
311}
312
313impl<'a> A11ySubtreeBuilder<'a> {
314 pub(crate) fn new(parent_id: NodeId, nodes: &'a mut A11yNodeBuilder) -> Self {
315 Self {
316 parent_id,
317 nodes,
318 #[cfg(debug_assertions)]
319 creator: debug::NodeCreator::default(),
320 }
321 }
322
323 #[cfg(debug_assertions)]
324 pub(crate) fn with_creator(mut self, creator: debug::NodeCreator) -> Self {
325 self.creator = creator;
326 self
327 }
328
329 pub fn synthetic_node_id(&self, key: impl Hash) -> NodeId {
336 let mut hasher = std::hash::DefaultHasher::default();
337 self.parent_id.0.hash(&mut hasher);
338 key.hash(&mut hasher);
339 NodeId(hasher.finish())
340 }
341
342 pub fn push_child(&mut self, id: NodeId, node: accesskit::Node) -> bool {
347 let pushed = self.nodes.push_leaf(id, node);
348 #[cfg(debug_assertions)]
349 if pushed {
350 self.nodes.record_node_info(
351 id,
352 debug::NodeDebugInfo {
353 synthetic: true,
354 view: self.creator.view,
355 element_id: self.creator.element_id.clone(),
356 source_location: self.creator.source_location,
357 },
358 );
359 }
360 pushed
361 }
362
363 pub fn parent_node(&mut self) -> &mut accesskit::Node {
365 self.nodes
366 .current_node_mut()
367 .expect("A11ySubtreeBuilder exists only while its element's node is on the stack")
368 }
369}
370
371pub(crate) struct A11yNodeBuilder {
372 ids_stack: SmallVec<[NodeId; 16]>,
373 nodes_stack: SmallVec<[accesskit::Node; 16]>,
374 all_nodes: Vec<(NodeId, accesskit::Node)>,
377 seen_ids: FxHashSet<NodeId>,
378 focus: Option<NodeId>,
381 active_descendant: Option<NodeId>,
386 #[cfg(debug_assertions)]
387 node_info: FxHashMap<NodeId, debug::NodeDebugInfo>,
388}
389
390impl A11yNodeBuilder {
391 fn new() -> Self {
392 Self {
393 ids_stack: SmallVec::new(),
394 nodes_stack: SmallVec::new(),
395 all_nodes: Vec::new(),
396 seen_ids: FxHashSet::default(),
397 focus: None,
398 active_descendant: None,
399 #[cfg(debug_assertions)]
400 node_info: FxHashMap::default(),
401 }
402 }
403
404 #[cfg(debug_assertions)]
406 pub(crate) fn record_node_info(&mut self, id: NodeId, info: debug::NodeDebugInfo) {
407 self.node_info.insert(id, info);
408 }
409
410 #[must_use]
411 fn can_push(&mut self, id: NodeId) -> bool {
412 debug_assert!(!self.ids_stack.is_empty(), "node pushed before push_root");
413
414 if !self.seen_ids.insert(id) {
415 debug_assert!(
416 false,
417 "Duplicate a11y node id: {id:?}. In a release build, this node would be silently discarded from the a11y tree."
418 );
419 return false;
420 }
421
422 true
423 }
424
425 pub(crate) fn push(&mut self, id: NodeId, node: accesskit::Node) -> bool {
430 if !self.can_push(id) {
431 return false;
432 }
433
434 if let Some(parent) = self.nodes_stack.last_mut() {
435 parent.push_child(id);
436 }
437 self.ids_stack.push(id);
438 self.nodes_stack.push(node);
439 true
440 }
441
442 pub(crate) fn push_leaf(&mut self, id: NodeId, node: accesskit::Node) -> bool {
448 if !self.can_push(id) {
449 return false;
450 }
451
452 if let Some(parent) = self.nodes_stack.last_mut() {
453 parent.push_child(id);
454 }
455 self.all_nodes.push((id, node));
456 true
457 }
458
459 pub(crate) fn current_node_mut(&mut self) -> Option<&mut accesskit::Node> {
460 self.nodes_stack.last_mut()
461 }
462
463 pub(crate) fn pop(&mut self) {
466 debug_assert!(self.ids_stack.len() > 1, "pop would remove the root node");
467
468 if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) {
469 self.all_nodes.push((id, node));
470 }
471 }
472
473 fn begin_frame(&mut self, window_title: Option<&SharedString>) {
475 self.all_nodes.clear();
476 self.ids_stack.clear();
477 self.nodes_stack.clear();
478 self.seen_ids.clear();
479 #[cfg(debug_assertions)]
480 self.node_info.clear();
481 let mut root_node = accesskit::Node::new(accesskit::Role::Window);
482 if let Some(title) = window_title {
483 root_node.set_label(title.to_string());
484 }
485
486 self.ids_stack.push(ROOT_NODE_ID);
487 self.nodes_stack.push(root_node);
488 self.focus = None;
489 self.active_descendant = None;
490 }
491
492 pub(crate) fn has_node(&self, id: NodeId) -> bool {
494 id == ROOT_NODE_ID || self.seen_ids.contains(&id)
495 }
496
497 pub(crate) fn node_is_focused(&self, id: NodeId) -> bool {
499 self.focus == Some(id)
500 }
501
502 pub(crate) fn focus_is_ancestor_of_current(&self) -> bool {
503 let Some(focus) = self.focus else {
504 return false;
505 };
506
507 let ancestor_count = self.ids_stack.len().saturating_sub(1);
510 self.ids_stack[..ancestor_count].contains(&focus)
511 }
512
513 pub(crate) fn set_active_descendant(&mut self, id: NodeId) {
514 if self
515 .active_descendant
516 .is_some_and(|existing| existing != id)
517 {
518 if cfg!(debug_assertions) {
519 panic!("active descendant claimed by multiple nodes in one frame");
520 } else {
521 log::warn!(
522 "a11y: multiple nodes claimed the active descendant this frame; \
523 using last-wins ({id:?})"
524 );
525 }
526 }
527 self.active_descendant = Some(id);
528 }
529
530 pub(crate) fn set_focus(&mut self, id: NodeId) {
531 if self.focus.is_some() {
532 if cfg!(debug_assertions) {
533 panic!("set_focus called more than once in a single frame");
534 } else {
535 log::warn!(
536 "a11y: set_focus called more than once in a single frame; \
537 using last-wins ({id:?})"
538 );
539 }
540 }
541 self.focus = Some(id);
542 }
543
544 fn finalize(&mut self) -> TreeUpdate {
545 debug_assert_eq!(self.ids_stack.len(), 1);
547 debug_assert_eq!(self.ids_stack[0], ROOT_NODE_ID);
548
549 if self.ids_stack.len() != 1 {
550 log::error!(
551 "a11y: Stack imbalance at end of frame: expected 1 (root), got {}. \
552 Some elements may have pushed without popping.",
553 self.ids_stack.len()
554 );
555 }
556
557 while !self.ids_stack.is_empty() {
559 if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) {
560 self.all_nodes.push((id, node));
561 }
562 }
563
564 let focus = match self.active_descendant {
565 Some(id) if self.has_node(id) => id,
566 Some(id) => {
567 if cfg!(debug_assertions) {
568 panic!("active_descendant set to {id:?}, which is not in the tree");
569 } else {
570 log::warn!("active_descendant set to {id:?}, which is not in the tree");
571 self.focus.unwrap_or(ROOT_NODE_ID)
572 }
573 }
574
575 _ => self.focus.unwrap_or(ROOT_NODE_ID),
576 };
577
578 let nodes = std::mem::take(&mut self.all_nodes);
579 let update = TreeUpdate {
580 nodes,
581 tree: Some(accesskit::Tree::new(ROOT_NODE_ID)),
582 tree_id: accesskit::TreeId::ROOT,
583 focus,
584 };
585
586 Self::repair_tree_update(update)
587 }
588
589 fn repair_tree_update(mut update: TreeUpdate) -> TreeUpdate {
592 let node_ids: FxHashSet<NodeId> = update.nodes.iter().map(|(id, _)| *id).collect();
593
594 if !node_ids.contains(&update.focus) {
596 log::error!(
597 "a11y: Focused node {:?} is not in the tree ({} nodes). \
598 Falling back to root. This is a bug in the a11y tree builder.",
599 update.focus,
600 update.nodes.len()
601 );
602 update.focus = ROOT_NODE_ID;
603 }
604
605 for (id, node) in &mut update.nodes {
607 let has_invalid_child = node
608 .children()
609 .iter()
610 .any(|child_id| !node_ids.contains(child_id));
611 if has_invalid_child {
612 let children = node.children();
613 let invalid_count = children
614 .iter()
615 .filter(|child_id| !node_ids.contains(child_id))
616 .count();
617 log::error!(
618 "a11y: Node {:?} references {} children not present in the tree. \
619 Stripping invalid child references.",
620 id,
621 invalid_count
622 );
623 let valid: Vec<NodeId> = children
624 .iter()
625 .copied()
626 .filter(|child_id| node_ids.contains(child_id))
627 .collect();
628 node.set_children(valid);
629 }
630 }
631
632 update
633 }
634}
635
636#[cfg(test)]
637mod tests {
638 use super::{A11y, A11yNodeBuilder, ROOT_NODE_ID};
641 use crate::FocusId;
642 use accesskit::{NodeId, Role};
643 use std::sync::{Arc, atomic::AtomicBool};
644
645 fn test_node() -> accesskit::Node {
646 accesskit::Node::new(Role::GenericContainer)
647 }
648
649 fn new_builder() -> A11yNodeBuilder {
650 let mut builder = A11yNodeBuilder::new();
651 builder.begin_frame(None);
652 builder
653 }
654
655 fn new_a11y() -> A11y {
656 let mut a11y = A11y::new(Arc::new(AtomicBool::new(true)), false, None);
657 a11y.begin_frame();
658 a11y
659 }
660
661 #[test]
662 fn accessibility_enabled_is_independent_of_activation() {
663 for force_disabled in [false, true] {
664 for active in [false, true] {
665 let mut a11y = A11y::new(Arc::new(AtomicBool::new(active)), force_disabled, None);
666 a11y.sync_active_flag();
667
668 assert_eq!(a11y.is_enabled(), !force_disabled);
669 assert_eq!(a11y.is_active(), !force_disabled && active);
670 }
671 }
672 }
673
674 #[test]
675 fn active_descendant_honored_when_container_focused() {
676 let mut builder = new_builder();
677 let container = NodeId(1);
678 let item = NodeId(2);
679
680 assert!(builder.push(container, test_node()));
681 builder.set_focus(container);
682 assert!(builder.push(item, test_node()));
683
684 assert!(builder.focus_is_ancestor_of_current());
687 builder.set_active_descendant(item);
688
689 builder.pop(); builder.pop(); let update = builder.finalize();
692 assert_eq!(update.focus, item);
693 }
694
695 #[test]
696 fn active_descendant_honored_for_deep_descendant() {
697 let mut builder = new_builder();
698 let container = NodeId(1);
699 let group = NodeId(2);
700 let item = NodeId(3);
701
702 assert!(builder.push(container, test_node()));
703 builder.set_focus(container);
704 assert!(builder.push(group, test_node()));
705 assert!(builder.push(item, test_node()));
706
707 assert!(builder.focus_is_ancestor_of_current());
710 builder.set_active_descendant(item);
711
712 builder.pop(); builder.pop(); builder.pop(); let update = builder.finalize();
716 assert_eq!(update.focus, item);
717 }
718
719 #[test]
720 fn active_descendant_ignored_when_focus_in_other_subtree() {
721 let mut builder = new_builder();
722 let focused_container = NodeId(1);
723 let focused_leaf = NodeId(2);
724 let other_container = NodeId(3);
725 let other_item = NodeId(4);
726
727 assert!(builder.push(focused_container, test_node()));
729 assert!(builder.push(focused_leaf, test_node()));
730 builder.set_focus(focused_leaf);
731 builder.pop(); builder.pop(); assert!(builder.push(other_container, test_node()));
737 assert!(builder.push(other_item, test_node()));
738 assert!(!builder.focus_is_ancestor_of_current());
739 builder.pop(); builder.pop(); let update = builder.finalize();
743 assert_eq!(update.focus, focused_leaf);
744 }
745
746 #[test]
747 fn active_descendant_ignored_when_nothing_focused() {
748 let mut builder = new_builder();
749 let container = NodeId(1);
750 let item = NodeId(2);
751
752 assert!(builder.push(container, test_node()));
753 assert!(builder.push(item, test_node()));
754
755 assert!(!builder.focus_is_ancestor_of_current());
758 builder.pop();
759 builder.pop();
760
761 let update = builder.finalize();
762 assert_eq!(update.focus, ROOT_NODE_ID);
763 }
764
765 #[test]
766 fn regular_focus_used_when_no_active_descendant() {
767 let mut builder = new_builder();
768 let focused = NodeId(1);
769
770 assert!(builder.push(focused, test_node()));
771 builder.set_focus(focused);
772 builder.pop();
773
774 let update = builder.finalize();
775 assert_eq!(update.focus, focused);
776 }
777
778 #[test]
779 fn focus_is_ancestor_excludes_self_and_non_ancestors() {
780 let mut builder = new_builder();
781 let container = NodeId(1);
782 let item = NodeId(2);
783
784 assert!(builder.push(container, test_node()));
785 builder.set_focus(container);
786
787 assert!(!builder.focus_is_ancestor_of_current());
790
791 assert!(builder.push(item, test_node()));
792 assert!(builder.focus_is_ancestor_of_current());
794
795 builder.pop();
796 builder.pop();
797 }
798
799 #[test]
802 #[cfg_attr(
803 debug_assertions,
804 should_panic(expected = "active descendant claimed by multiple nodes")
805 )]
806 fn multiple_active_descendant_claims_panic_in_debug() {
807 let mut builder = new_builder();
808 builder.set_active_descendant(NodeId(1));
809 builder.set_active_descendant(NodeId(2));
810 }
811
812 #[test]
815 #[cfg_attr(
816 debug_assertions,
817 should_panic(expected = "set_focus called more than once")
818 )]
819 fn setting_focus_twice_panics_in_debug() {
820 let mut builder = new_builder();
821 builder.set_focus(NodeId(1));
822 builder.set_focus(NodeId(2));
823 }
824
825 #[test]
828 #[cfg_attr(
829 debug_assertions,
830 should_panic(expected = "was not registered with set_focusable")
831 )]
832 fn set_focus_without_set_focusable() {
833 let mut a11y = new_a11y();
834 let node = NodeId(1);
835 assert!(a11y.nodes.push(node, test_node()));
836 a11y.set_focus(node);
838 }
839
840 #[test]
843 #[cfg_attr(debug_assertions, should_panic(expected = "on the focused node"))]
844 fn set_active_descendant_on_focused_node() {
845 let mut a11y = new_a11y();
846 let node = NodeId(1);
847 assert!(a11y.nodes.push(node, test_node()));
848 a11y.set_focusable(node, FocusId::default());
849 a11y.set_focus(node);
850 a11y.set_active_descendant(node);
851 }
852
853 #[test]
857 #[cfg_attr(
858 debug_assertions,
859 should_panic(expected = "active descendant claimed by multiple nodes")
860 )]
861 fn two_siblings_claiming_active_descendant() {
862 let mut a11y = new_a11y();
863 let container = NodeId(1);
864 let first = NodeId(2);
865 let second = NodeId(3);
866
867 assert!(a11y.nodes.push(container, test_node()));
868 a11y.set_focusable(container, FocusId::default());
869 a11y.set_focus(container);
870
871 assert!(a11y.nodes.push(first, test_node()));
872 a11y.set_active_descendant(first);
873 a11y.nodes.pop(); assert!(a11y.nodes.push(second, test_node()));
876 a11y.set_active_descendant(second);
877 a11y.nodes.pop(); a11y.nodes.pop(); }
881
882 #[test]
885 fn active_descendant_in_unfocused_subtree_keeps_real_focus() {
886 let mut a11y = new_a11y();
887 let a = NodeId(1);
888 let b = NodeId(2);
889 let c = NodeId(3);
890
891 assert!(a11y.nodes.push(a, test_node()));
892 a11y.set_focusable(a, FocusId::default());
893 a11y.set_focus(a);
894 a11y.nodes.pop(); assert!(a11y.nodes.push(b, test_node()));
897 assert!(a11y.nodes.push(c, test_node()));
898 a11y.set_active_descendant(c);
899 a11y.nodes.pop(); a11y.nodes.pop(); let update = a11y.end_frame(Default::default());
903 assert_eq!(update.focus, a);
904 }
905}