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.force_disabled && self.active_flag.load(Ordering::SeqCst);
214 }
215
216 pub(crate) fn is_active(&self) -> bool {
217 self.active_this_frame
218 }
219
220 pub(crate) fn set_focusable(&mut self, node_id: NodeId, focus_id: FocusId) {
221 self.focus_ids.insert(node_id, focus_id);
222 }
223
224 pub(crate) fn set_focus(&mut self, node_id: NodeId) {
229 if !self.focus_ids.contains_key(&node_id) {
231 if cfg!(debug_assertions) {
232 panic!("set_focus called for a node that was not registered with set_focusable");
233 } else {
234 log::warn!(
235 "a11y: set_focus called for a node that was not registered with \
236 set_focusable ({node_id:?})"
237 );
238 }
239 }
240 if self.nodes.has_node(node_id) {
241 self.last_focus_without_node = None;
244 let focus_id = self.focus_ids.get(&node_id).copied();
245 let existing_focus_id = self
246 .nodes
247 .focus
248 .and_then(|existing| self.focus_ids.get(&existing).copied());
249 if focus_id.is_some() && focus_id == existing_focus_id {
250 self.nodes.focus = Some(node_id);
256 } else {
257 self.nodes.set_focus(node_id);
258 }
259 } else {
260 if let Some(focus_id) = self.focus_ids.get(&node_id).copied() {
263 self.note_focus_without_node(focus_id, "it has an id but no role");
264 }
265 }
266 }
267
268 pub(crate) fn set_active_descendant(&mut self, node_id: NodeId) {
269 if self.nodes.node_is_focused(node_id) {
272 if cfg!(debug_assertions) {
273 panic!("set_active_descendant called on the focused node");
274 } else {
275 log::warn!("a11y: set_active_descendant called on the focused node ({node_id:?})");
276 }
277 return;
278 }
279 if self.nodes.has_node(node_id) && self.nodes.focus_is_ancestor_of_current() {
280 self.nodes.set_active_descendant(node_id);
281 }
282 }
283
284 pub(crate) fn begin_frame(&mut self) {
286 self.focus_ids.clear();
287 self.node_bounds.clear();
288 self.action_listeners.clear();
289 self.nodes.begin_frame(self.window_title.as_ref());
290 }
291
292 pub(crate) fn end_frame(&mut self, frame: debug::FrameDebugInfo) -> TreeUpdate {
294 let update = self.nodes.finalize();
295 self.debug.capture(
296 &update,
297 self.nodes.focus,
298 self.nodes.active_descendant,
299 self.window_title.as_ref(),
300 frame,
301 );
302 #[cfg(debug_assertions)]
303 self.debug.capture_node_info(&self.nodes.node_info);
304 update
305 }
306
307 pub(crate) fn debug_tree_json(&self) -> Option<String> {
308 self.debug.to_json()
309 }
310}
311
312pub struct A11ySubtreeBuilder<'a> {
315 parent_id: NodeId,
316 nodes: &'a mut A11yNodeBuilder,
317 #[cfg(debug_assertions)]
320 creator: debug::NodeCreator,
321}
322
323impl<'a> A11ySubtreeBuilder<'a> {
324 pub(crate) fn new(parent_id: NodeId, nodes: &'a mut A11yNodeBuilder) -> Self {
325 Self {
326 parent_id,
327 nodes,
328 #[cfg(debug_assertions)]
329 creator: debug::NodeCreator::default(),
330 }
331 }
332
333 #[cfg(debug_assertions)]
334 pub(crate) fn with_creator(mut self, creator: debug::NodeCreator) -> Self {
335 self.creator = creator;
336 self
337 }
338
339 pub fn synthetic_node_id(&self, key: impl Hash) -> NodeId {
346 let mut hasher = std::hash::DefaultHasher::default();
347 self.parent_id.0.hash(&mut hasher);
348 key.hash(&mut hasher);
349 NodeId(hasher.finish())
350 }
351
352 pub fn push_child(&mut self, id: NodeId, node: accesskit::Node) -> bool {
357 let pushed = self.nodes.push_leaf(id, node);
358 #[cfg(debug_assertions)]
359 if pushed {
360 self.nodes.record_node_info(
361 id,
362 debug::NodeDebugInfo {
363 synthetic: true,
364 view: self.creator.view,
365 element_id: self.creator.element_id.clone(),
366 source_location: self.creator.source_location,
367 },
368 );
369 }
370 pushed
371 }
372
373 pub fn parent_node(&mut self) -> &mut accesskit::Node {
375 self.nodes
376 .current_node_mut()
377 .expect("A11ySubtreeBuilder exists only while its element's node is on the stack")
378 }
379}
380
381pub(crate) struct A11yNodeBuilder {
382 ids_stack: SmallVec<[NodeId; 16]>,
383 nodes_stack: SmallVec<[accesskit::Node; 16]>,
384 all_nodes: Vec<(NodeId, accesskit::Node)>,
387 seen_ids: FxHashSet<NodeId>,
388 focus: Option<NodeId>,
391 active_descendant: Option<NodeId>,
396 #[cfg(debug_assertions)]
397 node_info: FxHashMap<NodeId, debug::NodeDebugInfo>,
398}
399
400impl A11yNodeBuilder {
401 fn new() -> Self {
402 Self {
403 ids_stack: SmallVec::new(),
404 nodes_stack: SmallVec::new(),
405 all_nodes: Vec::new(),
406 seen_ids: FxHashSet::default(),
407 focus: None,
408 active_descendant: None,
409 #[cfg(debug_assertions)]
410 node_info: FxHashMap::default(),
411 }
412 }
413
414 #[cfg(debug_assertions)]
416 pub(crate) fn record_node_info(&mut self, id: NodeId, info: debug::NodeDebugInfo) {
417 self.node_info.insert(id, info);
418 }
419
420 #[must_use]
421 fn can_push(&mut self, id: NodeId) -> bool {
422 debug_assert!(!self.ids_stack.is_empty(), "node pushed before push_root");
423
424 if !self.seen_ids.insert(id) {
425 debug_assert!(
426 false,
427 "Duplicate a11y node id: {id:?}. In a release build, this node would be silently discarded from the a11y tree."
428 );
429 return false;
430 }
431
432 true
433 }
434
435 pub(crate) fn push(&mut self, id: NodeId, node: accesskit::Node) -> bool {
440 if !self.can_push(id) {
441 return false;
442 }
443
444 if let Some(parent) = self.nodes_stack.last_mut() {
445 parent.push_child(id);
446 }
447 self.ids_stack.push(id);
448 self.nodes_stack.push(node);
449 true
450 }
451
452 pub(crate) fn push_leaf(&mut self, id: NodeId, node: accesskit::Node) -> bool {
458 if !self.can_push(id) {
459 return false;
460 }
461
462 if let Some(parent) = self.nodes_stack.last_mut() {
463 parent.push_child(id);
464 }
465 self.all_nodes.push((id, node));
466 true
467 }
468
469 pub(crate) fn current_node_mut(&mut self) -> Option<&mut accesskit::Node> {
470 self.nodes_stack.last_mut()
471 }
472
473 pub(crate) fn pop(&mut self) {
476 debug_assert!(self.ids_stack.len() > 1, "pop would remove the root node");
477
478 if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) {
479 self.all_nodes.push((id, node));
480 }
481 }
482
483 fn begin_frame(&mut self, window_title: Option<&SharedString>) {
485 self.all_nodes.clear();
486 self.ids_stack.clear();
487 self.nodes_stack.clear();
488 self.seen_ids.clear();
489 #[cfg(debug_assertions)]
490 self.node_info.clear();
491 let mut root_node = accesskit::Node::new(accesskit::Role::Window);
492 if let Some(title) = window_title {
493 root_node.set_label(title.to_string());
494 }
495
496 self.ids_stack.push(ROOT_NODE_ID);
497 self.nodes_stack.push(root_node);
498 self.focus = None;
499 self.active_descendant = None;
500 }
501
502 pub(crate) fn has_node(&self, id: NodeId) -> bool {
504 id == ROOT_NODE_ID || self.seen_ids.contains(&id)
505 }
506
507 pub(crate) fn node_is_focused(&self, id: NodeId) -> bool {
509 self.focus == Some(id)
510 }
511
512 pub(crate) fn focus_is_ancestor_of_current(&self) -> bool {
513 let Some(focus) = self.focus else {
514 return false;
515 };
516
517 let ancestor_count = self.ids_stack.len().saturating_sub(1);
520 self.ids_stack[..ancestor_count].contains(&focus)
521 }
522
523 pub(crate) fn set_active_descendant(&mut self, id: NodeId) {
524 if self
525 .active_descendant
526 .is_some_and(|existing| existing != id)
527 {
528 if cfg!(debug_assertions) {
529 panic!("active descendant claimed by multiple nodes in one frame");
530 } else {
531 log::warn!(
532 "a11y: multiple nodes claimed the active descendant this frame; \
533 using last-wins ({id:?})"
534 );
535 }
536 }
537 self.active_descendant = Some(id);
538 }
539
540 pub(crate) fn set_focus(&mut self, id: NodeId) {
541 if self.focus.is_some() {
542 if cfg!(debug_assertions) {
543 panic!("set_focus called more than once in a single frame");
544 } else {
545 log::warn!(
546 "a11y: set_focus called more than once in a single frame; \
547 using last-wins ({id:?})"
548 );
549 }
550 }
551 self.focus = Some(id);
552 }
553
554 fn finalize(&mut self) -> TreeUpdate {
555 debug_assert_eq!(self.ids_stack.len(), 1);
557 debug_assert_eq!(self.ids_stack[0], ROOT_NODE_ID);
558
559 if self.ids_stack.len() != 1 {
560 log::error!(
561 "a11y: Stack imbalance at end of frame: expected 1 (root), got {}. \
562 Some elements may have pushed without popping.",
563 self.ids_stack.len()
564 );
565 }
566
567 while !self.ids_stack.is_empty() {
569 if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) {
570 self.all_nodes.push((id, node));
571 }
572 }
573
574 let focus = match self.active_descendant {
575 Some(id) if self.has_node(id) => id,
576 Some(id) => {
577 if cfg!(debug_assertions) {
578 panic!("active_descendant set to {id:?}, which is not in the tree");
579 } else {
580 log::warn!("active_descendant set to {id:?}, which is not in the tree");
581 self.focus.unwrap_or(ROOT_NODE_ID)
582 }
583 }
584
585 _ => self.focus.unwrap_or(ROOT_NODE_ID),
586 };
587
588 let nodes = std::mem::take(&mut self.all_nodes);
589 let update = TreeUpdate {
590 nodes,
591 tree: Some(accesskit::Tree::new(ROOT_NODE_ID)),
592 tree_id: accesskit::TreeId::ROOT,
593 focus,
594 };
595
596 Self::repair_tree_update(update)
597 }
598
599 fn repair_tree_update(mut update: TreeUpdate) -> TreeUpdate {
602 let node_ids: FxHashSet<NodeId> = update.nodes.iter().map(|(id, _)| *id).collect();
603
604 if !node_ids.contains(&update.focus) {
606 log::error!(
607 "a11y: Focused node {:?} is not in the tree ({} nodes). \
608 Falling back to root. This is a bug in the a11y tree builder.",
609 update.focus,
610 update.nodes.len()
611 );
612 update.focus = ROOT_NODE_ID;
613 }
614
615 for (id, node) in &mut update.nodes {
617 let has_invalid_child = node
618 .children()
619 .iter()
620 .any(|child_id| !node_ids.contains(child_id));
621 if has_invalid_child {
622 let children = node.children();
623 let invalid_count = children
624 .iter()
625 .filter(|child_id| !node_ids.contains(child_id))
626 .count();
627 log::error!(
628 "a11y: Node {:?} references {} children not present in the tree. \
629 Stripping invalid child references.",
630 id,
631 invalid_count
632 );
633 let valid: Vec<NodeId> = children
634 .iter()
635 .copied()
636 .filter(|child_id| node_ids.contains(child_id))
637 .collect();
638 node.set_children(valid);
639 }
640 }
641
642 update
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use super::{A11y, A11yNodeBuilder, ROOT_NODE_ID};
651 use crate::FocusId;
652 use accesskit::{NodeId, Role};
653 use std::sync::{Arc, atomic::AtomicBool};
654
655 fn test_node() -> accesskit::Node {
656 accesskit::Node::new(Role::GenericContainer)
657 }
658
659 fn new_builder() -> A11yNodeBuilder {
660 let mut builder = A11yNodeBuilder::new();
661 builder.begin_frame(None);
662 builder
663 }
664
665 fn new_a11y() -> A11y {
666 let mut a11y = A11y::new(Arc::new(AtomicBool::new(true)), false, None);
667 a11y.begin_frame();
668 a11y
669 }
670
671 #[test]
672 fn active_descendant_honored_when_container_focused() {
673 let mut builder = new_builder();
674 let container = NodeId(1);
675 let item = NodeId(2);
676
677 assert!(builder.push(container, test_node()));
678 builder.set_focus(container);
679 assert!(builder.push(item, test_node()));
680
681 assert!(builder.focus_is_ancestor_of_current());
684 builder.set_active_descendant(item);
685
686 builder.pop(); builder.pop(); let update = builder.finalize();
689 assert_eq!(update.focus, item);
690 }
691
692 #[test]
693 fn active_descendant_honored_for_deep_descendant() {
694 let mut builder = new_builder();
695 let container = NodeId(1);
696 let group = NodeId(2);
697 let item = NodeId(3);
698
699 assert!(builder.push(container, test_node()));
700 builder.set_focus(container);
701 assert!(builder.push(group, test_node()));
702 assert!(builder.push(item, test_node()));
703
704 assert!(builder.focus_is_ancestor_of_current());
707 builder.set_active_descendant(item);
708
709 builder.pop(); builder.pop(); builder.pop(); let update = builder.finalize();
713 assert_eq!(update.focus, item);
714 }
715
716 #[test]
717 fn active_descendant_ignored_when_focus_in_other_subtree() {
718 let mut builder = new_builder();
719 let focused_container = NodeId(1);
720 let focused_leaf = NodeId(2);
721 let other_container = NodeId(3);
722 let other_item = NodeId(4);
723
724 assert!(builder.push(focused_container, test_node()));
726 assert!(builder.push(focused_leaf, test_node()));
727 builder.set_focus(focused_leaf);
728 builder.pop(); builder.pop(); assert!(builder.push(other_container, test_node()));
734 assert!(builder.push(other_item, test_node()));
735 assert!(!builder.focus_is_ancestor_of_current());
736 builder.pop(); builder.pop(); let update = builder.finalize();
740 assert_eq!(update.focus, focused_leaf);
741 }
742
743 #[test]
744 fn active_descendant_ignored_when_nothing_focused() {
745 let mut builder = new_builder();
746 let container = NodeId(1);
747 let item = NodeId(2);
748
749 assert!(builder.push(container, test_node()));
750 assert!(builder.push(item, test_node()));
751
752 assert!(!builder.focus_is_ancestor_of_current());
755 builder.pop();
756 builder.pop();
757
758 let update = builder.finalize();
759 assert_eq!(update.focus, ROOT_NODE_ID);
760 }
761
762 #[test]
763 fn regular_focus_used_when_no_active_descendant() {
764 let mut builder = new_builder();
765 let focused = NodeId(1);
766
767 assert!(builder.push(focused, test_node()));
768 builder.set_focus(focused);
769 builder.pop();
770
771 let update = builder.finalize();
772 assert_eq!(update.focus, focused);
773 }
774
775 #[test]
776 fn focus_is_ancestor_excludes_self_and_non_ancestors() {
777 let mut builder = new_builder();
778 let container = NodeId(1);
779 let item = NodeId(2);
780
781 assert!(builder.push(container, test_node()));
782 builder.set_focus(container);
783
784 assert!(!builder.focus_is_ancestor_of_current());
787
788 assert!(builder.push(item, test_node()));
789 assert!(builder.focus_is_ancestor_of_current());
791
792 builder.pop();
793 builder.pop();
794 }
795
796 #[test]
799 #[cfg_attr(
800 debug_assertions,
801 should_panic(expected = "active descendant claimed by multiple nodes")
802 )]
803 fn multiple_active_descendant_claims_panic_in_debug() {
804 let mut builder = new_builder();
805 builder.set_active_descendant(NodeId(1));
806 builder.set_active_descendant(NodeId(2));
807 }
808
809 #[test]
812 #[cfg_attr(
813 debug_assertions,
814 should_panic(expected = "set_focus called more than once")
815 )]
816 fn setting_focus_twice_panics_in_debug() {
817 let mut builder = new_builder();
818 builder.set_focus(NodeId(1));
819 builder.set_focus(NodeId(2));
820 }
821
822 #[test]
825 #[cfg_attr(
826 debug_assertions,
827 should_panic(expected = "was not registered with set_focusable")
828 )]
829 fn set_focus_without_set_focusable() {
830 let mut a11y = new_a11y();
831 let node = NodeId(1);
832 assert!(a11y.nodes.push(node, test_node()));
833 a11y.set_focus(node);
835 }
836
837 #[test]
840 #[cfg_attr(debug_assertions, should_panic(expected = "on the focused node"))]
841 fn set_active_descendant_on_focused_node() {
842 let mut a11y = new_a11y();
843 let node = NodeId(1);
844 assert!(a11y.nodes.push(node, test_node()));
845 a11y.set_focusable(node, FocusId::default());
846 a11y.set_focus(node);
847 a11y.set_active_descendant(node);
848 }
849
850 #[test]
854 #[cfg_attr(
855 debug_assertions,
856 should_panic(expected = "active descendant claimed by multiple nodes")
857 )]
858 fn two_siblings_claiming_active_descendant() {
859 let mut a11y = new_a11y();
860 let container = NodeId(1);
861 let first = NodeId(2);
862 let second = NodeId(3);
863
864 assert!(a11y.nodes.push(container, test_node()));
865 a11y.set_focusable(container, FocusId::default());
866 a11y.set_focus(container);
867
868 assert!(a11y.nodes.push(first, test_node()));
869 a11y.set_active_descendant(first);
870 a11y.nodes.pop(); assert!(a11y.nodes.push(second, test_node()));
873 a11y.set_active_descendant(second);
874 a11y.nodes.pop(); a11y.nodes.pop(); }
878
879 #[test]
882 fn active_descendant_in_unfocused_subtree_keeps_real_focus() {
883 let mut a11y = new_a11y();
884 let a = NodeId(1);
885 let b = NodeId(2);
886 let c = NodeId(3);
887
888 assert!(a11y.nodes.push(a, test_node()));
889 a11y.set_focusable(a, FocusId::default());
890 a11y.set_focus(a);
891 a11y.nodes.pop(); assert!(a11y.nodes.push(b, test_node()));
894 assert!(a11y.nodes.push(c, test_node()));
895 a11y.set_active_descendant(c);
896 a11y.nodes.pop(); a11y.nodes.pop(); let update = a11y.end_frame(Default::default());
900 assert_eq!(update.focus, a);
901 }
902}