1use std::collections::VecDeque;
5use std::fmt::Debug;
6use std::sync::atomic::AtomicU64;
7use std::sync::{LazyLock, atomic};
8
9use accesskit::{NodeId, Role};
10use bitflags::bitflags;
11use layout_api::{LayoutElement, LayoutNode, LayoutNodeType};
12use log::trace;
13use rustc_hash::{FxHashMap, FxHashSet};
14use script::layout_dom::ServoLayoutNode;
15use servo_base::Epoch;
16use servo_base::print_tree::PrintTree;
17use servo_config::opts::{self, DiagnosticsLogging, DiagnosticsLoggingOption};
18use servo_config::pref;
19use style::dom::OpaqueNode;
20use web_atoms::{LocalName, local_name};
21
22use crate::ArcRefCell;
23
24bitflags! {
25 #[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
29 struct LocalAccessibilityDamage: u16 {
30 const SUBTREE_CHANGED = 0b0001;
32 const ROLE_CHANGED = 0b0010;
34 const TEXT_CHANGED = 0b0100;
36 }
37}
38
39struct AccessibilityUpdate {
41 changed_nodes: FxHashSet<NodeId>,
43 tree_changes: FxHashMap<NodeId, TreeChange>,
45 rooted_nodes: Option<FxHashSet<OpaqueNode>>,
49}
50
51struct AccessibilityNode {
52 id: NodeId,
56 accesskit_node: accesskit::Node,
59 opaque_node: Option<OpaqueNode>,
63 updated: bool,
66}
67
68#[derive(Debug)]
73pub struct AccessibilityTree {
74 nodes: FxHashMap<NodeId, ArcRefCell<AccessibilityNode>>,
77 opaque_node_to_id: FxHashMap<OpaqueNode, NodeId>,
80 tree_id: accesskit::TreeId,
84 root_node_id: Option<NodeId>,
87 embedder_epoch: Epoch,
90 debug: DiagnosticsLogging,
93}
94
95#[derive(Debug, PartialEq, Copy, Clone)]
100enum TreeChange {
101 New,
103
104 Moved,
106
107 PendingMove,
123
124 Removed,
126}
127
128impl AccessibilityTree {
129 pub(super) fn new(tree_id: accesskit::TreeId, embedder_epoch: Epoch) -> Self {
131 Self {
132 nodes: FxHashMap::default(),
133 opaque_node_to_id: FxHashMap::default(),
134 tree_id,
135 root_node_id: None,
136 embedder_epoch,
137 debug: opts::get().debug.clone(),
138 }
139 }
140
141 pub(super) fn update_tree(
144 &mut self,
145 root_dom_node: &ServoLayoutNode<'_>,
146 rooted_nodes: Option<FxHashSet<OpaqueNode>>,
147 ) -> Option<accesskit::TreeUpdate> {
148 let mut update = AccessibilityUpdate::new(rooted_nodes);
149 let (root_node_id, root_node) = self.get_or_create_node(root_dom_node, &mut update);
150 self.root_node_id = Some(root_node_id);
151
152 self.update_node_and_descendants_from_dom_node(&root_node, root_dom_node, &mut update);
153
154 update.finalize(self)
155 }
156
157 fn update_node_and_descendants_from_dom_node(
161 &mut self,
162 node: &ArcRefCell<AccessibilityNode>,
163 dom_node: &ServoLayoutNode<'_>,
164 update: &mut AccessibilityUpdate,
165 ) -> LocalAccessibilityDamage {
166 let mut node = node.borrow_mut();
167 let mut damage = LocalAccessibilityDamage::empty();
168
169 damage.insert(node.update_node_from_dom_node(dom_node));
171 damage.insert(node.update_descendants_from_dom_node(dom_node, self, update));
172
173 damage.insert(node.update_node_local(damage, self));
174
175 if node.updated {
176 update.add(&mut node);
177 }
178
179 damage
180 }
181
182 fn get_or_create_node(
183 &mut self,
184 dom_node: &ServoLayoutNode<'_>,
185 update: &mut AccessibilityUpdate,
186 ) -> (NodeId, ArcRefCell<AccessibilityNode>) {
187 let id = self.id_for_opaque(dom_node.opaque());
188
189 let node = self.nodes.entry(id).or_insert_with(|| {
190 update.set_tree_state_change(id, TreeChange::New);
191 ArcRefCell::new(AccessibilityNode::new(id))
192 });
193
194 let mut new_node = node.borrow_mut();
195
196 new_node.opaque_node = Some(dom_node.opaque());
197 if let Some(dom_element) = dom_node.as_element() {
198 let local_name = dom_element.local_name().to_ascii_lowercase();
199 new_node.set_html_tag(&local_name);
200 }
201
202 (id, node.clone())
203 }
204
205 fn node_for_id(&self, id: &NodeId) -> Option<ArcRefCell<AccessibilityNode>> {
206 self.nodes.get(id).cloned()
207 }
208
209 fn assert_node_for_id(&self, id: &NodeId) -> ArcRefCell<AccessibilityNode> {
210 let Some(node) = self.nodes.get(id) else {
211 panic!("{id:?} does not exist in tree");
212 };
213 node.clone()
214 }
215
216 fn remove_stale_nodes(&mut self, mut update: AccessibilityUpdate) {
219 if let Some(rooted_nodes) = std::mem::take(&mut update.rooted_nodes) {
220 self.assert_removed_nodes_were_rooted(&update, rooted_nodes);
221 }
222
223 for id in update
224 .tree_changes
225 .drain()
226 .filter_map(|(id, change)| match change {
227 TreeChange::PendingMove => {
228 unreachable!(
229 "Pending move found for node id {id:?} when draining tree state changes"
230 );
231 },
232 TreeChange::Removed => Some(id),
233 _ => None,
234 })
235 {
236 if let Some(node) = self.nodes.remove(&id) &&
237 let Some(opaque_node) = node.borrow().opaque_node
238 {
239 self.opaque_node_to_id.remove(&opaque_node);
240 }
241 }
242
243 if self
244 .debug
245 .is_enabled(DiagnosticsLoggingOption::AccessibilityTree)
246 {
247 self.print();
248 }
249
250 if pref!(expensive_accessibility_test_assertions_enabled) {
251 self.assert_integrity();
252 }
253 }
254
255 fn assert_removed_nodes_were_rooted(
259 &mut self,
260 update: &AccessibilityUpdate,
261 mut rooted_nodes: FxHashSet<OpaqueNode>,
262 ) {
263 debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
264 for (id, change) in update.tree_changes.iter() {
265 if change == &TreeChange::Removed {
266 let node = self.assert_node_for_id(id);
267 if let Some(opaque_node) = node.borrow().opaque_node {
268 assert!(
269 rooted_nodes.remove(&opaque_node),
270 "Node removed from accessibility tree wasn't rooted: {node:?}"
271 );
272 }
273 };
274 }
275
276 for leftover_node in rooted_nodes {
277 assert!(
278 !self.opaque_node_to_id.contains_key(&leftover_node),
279 "Found node removed from DOM tree but not accessibility tree"
280 );
281 }
282 }
283
284 fn id_for_opaque(&mut self, opaque: OpaqueNode) -> NodeId {
285 let id = self.opaque_node_to_id.entry(opaque).or_insert_with(|| {
286 static LAST_ID: AtomicU64 = AtomicU64::new(0);
287 LAST_ID.fetch_add(1, atomic::Ordering::SeqCst).into()
288 });
289 *id
290 }
291
292 pub(crate) fn embedder_epoch(&self) -> Epoch {
293 self.embedder_epoch
294 }
295
296 fn assert_integrity(&self) {
300 debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
301 let Some(root_node_id) = self.root_node_id else {
302 return;
303 };
304 let mut node_ids = vec![root_node_id];
306 let mut seen_node_ids = FxHashSet::default();
307 while let Some(node_id) = node_ids.pop() {
308 assert!(
310 seen_node_ids.insert(node_id),
311 "Tree contains {node_id:?} in multiple places"
312 );
313 let node = self.assert_node_for_id(&node_id);
315 let node = node.borrow();
316 node_ids.extend(node.children().iter().rev());
317 }
318 assert_eq!(seen_node_ids, self.nodes.keys().copied().collect());
321 }
322
323 fn print(&self) {
324 let Some(root_node_id) = self.root_node_id else {
325 return;
326 };
327
328 let mut print_tree = PrintTree::new("Accessibility Tree");
329 let node = self.assert_node_for_id(&root_node_id);
330 node.borrow().print(self, &mut print_tree);
331 print_tree.end_level();
332 }
333}
334
335fn role_from_dom_node(dom_node: &ServoLayoutNode<'_>) -> Role {
336 if let Some(dom_element) = dom_node.as_element() {
337 let local_name = dom_element.local_name().to_ascii_lowercase();
338 *HTML_ELEMENT_ROLE_MAPPINGS
339 .get(&local_name)
340 .unwrap_or(&Role::GenericContainer)
341 } else if dom_node.type_id() == Some(LayoutNodeType::Text) {
342 Role::TextRun
343 } else {
344 Role::GenericContainer
345 }
346}
347
348impl AccessibilityNode {
349 fn new(id: NodeId) -> Self {
350 Self::new_with_role(id, Role::Unknown)
351 }
352
353 fn new_with_role(id: NodeId, role: Role) -> Self {
354 Self {
355 id,
356 accesskit_node: accesskit::Node::new(role),
357 opaque_node: None,
358 updated: true,
359 }
360 }
361
362 fn update_descendants_from_dom_node<'dom>(
365 &mut self,
366 dom_node: &ServoLayoutNode<'dom>,
367 tree: &mut AccessibilityTree,
368 update: &mut AccessibilityUpdate,
369 ) -> LocalAccessibilityDamage {
370 let mut damage = LocalAccessibilityDamage::empty();
371
372 let dom_children: Vec<ServoLayoutNode> = dom_node.flat_tree_children().collect();
373 let new_children: Vec<NodeId> = dom_children
374 .iter()
375 .map(|dom_child| tree.id_for_opaque(dom_child.opaque()))
376 .collect();
377
378 damage.insert(self.set_children(new_children, tree, update));
379
380 let mut damage_from_children = LocalAccessibilityDamage::empty();
381 for dom_child in dom_children {
382 let (_, child_node) = tree.get_or_create_node(&dom_child, update);
383 let child_damage =
384 tree.update_node_and_descendants_from_dom_node(&child_node, &dom_child, update);
385 damage_from_children.insert(child_damage);
386 }
387 if !damage_from_children.is_empty() {
388 damage.insert(LocalAccessibilityDamage::SUBTREE_CHANGED);
389 }
390
391 damage
392 }
393
394 fn set_subtree_state_change(
406 &self,
407 change: TreeChange,
408 tree: &mut AccessibilityTree,
409 update: &mut AccessibilityUpdate,
410 ) {
411 assert!(
412 change != TreeChange::New,
413 "New shouldn't be set recursively"
414 );
415
416 update.set_tree_state_change(self.id, change);
417
418 for child_id in self.children().iter() {
419 let child = tree.assert_node_for_id(child_id);
420 child
422 .borrow()
423 .set_subtree_state_change(change, tree, update);
424 }
425 }
426
427 fn update_node_from_dom_node(
429 &mut self,
430 dom_node: &ServoLayoutNode<'_>,
431 ) -> LocalAccessibilityDamage {
432 let mut damage = LocalAccessibilityDamage::empty();
433 damage.insert(self.set_role(role_from_dom_node(dom_node)));
434 if dom_node.type_id() == Some(LayoutNodeType::Text) {
435 let text_content = dom_node.text_content();
436 trace!("node text content = {text_content:?}");
437 damage.insert(self.set_value(&text_content));
439 }
440
441 damage
442 }
443
444 fn update_node_local(
449 &mut self,
450 damage: LocalAccessibilityDamage,
451 tree: &mut AccessibilityTree,
452 ) -> LocalAccessibilityDamage {
453 let mut new_damage = LocalAccessibilityDamage::empty();
454 if damage.contains(LocalAccessibilityDamage::SUBTREE_CHANGED) ||
455 damage.contains(LocalAccessibilityDamage::ROLE_CHANGED)
456 {
457 if let Some(text) = self.label_from_descendants(tree) {
458 new_damage.insert(self.set_label(text.as_str()));
459 } else {
460 new_damage.insert(self.clear_label());
461 }
462 }
463
464 new_damage
465 }
466
467 fn label_from_descendants(&self, tree: &AccessibilityTree) -> Option<String> {
468 if !NAME_FROM_CONTENTS_ROLES.contains(&self.role()) {
469 return None;
470 }
471 let mut children = VecDeque::from_iter(self.children().iter().copied());
472 let mut text = String::new();
473 while let Some(child_id) = children.pop_front() {
474 let child = tree.assert_node_for_id(&child_id);
475 let child = child.borrow();
476 match child.role() {
477 Role::TextRun => {
478 if let Some(child_text) = child.value() {
479 text.push_str(child_text);
480 }
481 },
482 _ => {
483 for id in child.children().iter().rev() {
484 children.push_front(*id);
485 }
486 },
487 }
488 }
489 Some(text.trim().to_owned())
490 }
491
492 fn print(&self, tree: &AccessibilityTree, print_tree: &mut PrintTree) {
493 if self.children().is_empty() {
494 print_tree.add_item(format!("{self:?}"));
495 return;
496 }
497
498 print_tree.new_level(format!("{self:?}"));
499
500 for child_id in self.children() {
501 let child = tree.assert_node_for_id(child_id);
502 child.borrow().print(tree, print_tree);
503 }
504 print_tree.end_level();
505 }
506
507 fn children(&self) -> &[NodeId] {
510 self.accesskit_node.children()
511 }
512
513 fn set_children(
516 &mut self,
517 children: Vec<NodeId>,
518 tree: &mut AccessibilityTree,
519 update: &mut AccessibilityUpdate,
520 ) -> LocalAccessibilityDamage {
521 if children == self.children() {
522 return LocalAccessibilityDamage::empty();
523 }
524 let old_children = self.children();
525 for old_child_id in old_children {
526 if !children.contains(old_child_id) {
527 let removed_child = tree.assert_node_for_id(old_child_id);
528 removed_child
529 .borrow()
530 .set_subtree_state_change(TreeChange::Removed, tree, update);
531 }
532 }
533 for new_child_id in children.iter() {
534 if !old_children.contains(new_child_id) &&
535 let Some(moved_child) = tree.node_for_id(new_child_id)
536 {
537 moved_child.borrow().set_subtree_state_change(
538 TreeChange::PendingMove,
539 tree,
540 update,
541 );
542 }
543 }
544
545 self.accesskit_node.set_children(children);
546 self.updated = true;
547
548 LocalAccessibilityDamage::SUBTREE_CHANGED
549 }
550
551 fn role(&self) -> Role {
552 self.accesskit_node.role()
553 }
554
555 fn set_role(&mut self, role: Role) -> LocalAccessibilityDamage {
556 if role == self.accesskit_node.role() {
557 return LocalAccessibilityDamage::empty();
558 }
559 self.accesskit_node.set_role(role);
560 self.updated = true;
561 LocalAccessibilityDamage::ROLE_CHANGED
562 }
563
564 fn label(&self) -> Option<&str> {
565 self.accesskit_node.label()
566 }
567
568 fn set_label(&mut self, label: &str) -> LocalAccessibilityDamage {
569 if Some(label) == self.accesskit_node.label() {
570 return LocalAccessibilityDamage::empty();
571 }
572 self.accesskit_node.set_label(label);
573 self.updated = true;
574 LocalAccessibilityDamage::TEXT_CHANGED
575 }
576
577 fn clear_label(&mut self) -> LocalAccessibilityDamage {
578 if self.accesskit_node.label().is_none() {
579 return LocalAccessibilityDamage::empty();
580 }
581 self.accesskit_node.clear_label();
582 self.updated = true;
583 LocalAccessibilityDamage::TEXT_CHANGED
584 }
585
586 fn html_tag(&self) -> Option<&str> {
587 self.accesskit_node.html_tag()
588 }
589
590 fn set_html_tag(&mut self, html_tag: &str) {
591 if Some(html_tag) == self.accesskit_node.html_tag() {
592 return;
593 }
594 self.accesskit_node.set_html_tag(html_tag);
595 self.updated = true;
596 }
597
598 fn value(&self) -> Option<&str> {
599 self.accesskit_node.value()
600 }
601
602 fn set_value(&mut self, value: &str) -> LocalAccessibilityDamage {
603 if Some(value) == self.accesskit_node.value() {
604 return LocalAccessibilityDamage::empty();
605 }
606 self.accesskit_node.set_value(value);
607 self.updated = true;
608 LocalAccessibilityDamage::TEXT_CHANGED
609 }
610}
611
612impl Debug for AccessibilityNode {
613 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
614 write!(f, "{:?}: {:?}", self.id, self.role())?;
615 if let Some(html_tag) = self.html_tag() {
616 write!(f, " (html_tag: {html_tag:?})")?;
617 }
618 if let Some(label) = self.label() {
619 write!(f, "\nlabel: {label:?}")?;
620 }
621 if !self.children().is_empty() {
622 write!(f, "\nchildren: {:?}", self.children())?;
623 }
624 Ok(())
625 }
626}
627
628impl AccessibilityUpdate {
629 fn new(rooted_nodes: Option<FxHashSet<OpaqueNode>>) -> Self {
630 Self {
631 changed_nodes: FxHashSet::default(),
632 tree_changes: FxHashMap::default(),
633 rooted_nodes,
634 }
635 }
636
637 fn add(&mut self, node: &mut AccessibilityNode) {
638 self.changed_nodes.insert(node.id);
639
640 node.updated = false;
641 }
642
643 fn set_tree_state_change(&mut self, node_id: NodeId, change: TreeChange) {
644 let old_change = self.tree_changes.get(&node_id);
645
646 assert!(
647 change != TreeChange::Moved,
648 "Incoming change must never be Moved"
649 );
650
651 let new_change = old_change
652 .map(|old_change| match (old_change, change) {
653 (TreeChange::PendingMove, TreeChange::Removed) => TreeChange::Moved,
654 (TreeChange::Removed, TreeChange::PendingMove) => TreeChange::Moved,
655 _ => {
656 unreachable!("Logically impossible state change: {old_change:?} → {change:?}")
657 },
658 })
659 .unwrap_or(change);
660
661 self.tree_changes.insert(node_id, new_change);
662 }
663
664 fn finalize(mut self, tree: &mut AccessibilityTree) -> Option<accesskit::TreeUpdate> {
669 let root_node_id = tree
670 .root_node_id
671 .expect("AccessibilityUpdate::finalize() called but no root_node_id set in tree");
672
673 if self.changed_nodes.is_empty() {
674 assert!(self.tree_changes.is_empty());
675 return None;
676 }
677
678 let accesskit_tree = accesskit::Tree::new(root_node_id);
679 let tree_update = accesskit::TreeUpdate {
680 nodes: std::mem::take(&mut self.changed_nodes)
681 .into_iter()
682 .map(|id| {
683 (
684 id,
685 tree.assert_node_for_id(&id).borrow().accesskit_node.clone(),
686 )
687 })
688 .collect(),
689 tree: Some(accesskit_tree),
690 focus: NodeId(1),
691 tree_id: tree.tree_id,
692 };
693
694 tree.remove_stale_nodes(self);
695
696 Some(tree_update)
697 }
698}
699
700#[cfg(test)]
701#[test]
702fn test_accessibility_update_add_some_nodes_twice() {
703 let mut tree = AccessibilityTree::new(accesskit::TreeId::ROOT, Epoch::default());
704 tree.root_node_id = Some(NodeId(2));
705
706 for (id, role) in [
707 (3, Role::GenericContainer),
708 (4, Role::Heading),
709 (5, Role::Paragraph),
710 ] {
711 let id = NodeId(id);
712 tree.nodes.insert(
713 id,
714 ArcRefCell::new(AccessibilityNode::new_with_role(id, role)),
715 );
716 }
717
718 let mut update = AccessibilityUpdate::new(None);
719
720 {
721 let node_3 = tree.assert_node_for_id(&NodeId(3));
722 let mut node_3 = node_3.borrow_mut();
723 let node_4 = tree.assert_node_for_id(&NodeId(4));
724 let mut node_4 = node_4.borrow_mut();
725 let node_5 = tree.assert_node_for_id(&NodeId(5));
726 let mut node_5 = node_5.borrow_mut();
727
728 update.add(&mut node_5);
729 update.add(&mut node_3);
730 update.add(&mut node_4);
731 update.add(&mut node_4);
732
733 node_3.set_role(Role::ScrollView);
734 update.add(&mut node_3);
735 }
736
737 let mut tree_update = update
738 .finalize(&mut tree)
739 .expect("finalize should produce a tree update");
740 tree_update.nodes.sort_by_key(|(node_id, _node)| *node_id);
741 assert_eq!(
742 tree_update,
743 accesskit::TreeUpdate {
744 nodes: vec![
745 (NodeId(3), accesskit::Node::new(Role::ScrollView)),
746 (NodeId(4), accesskit::Node::new(Role::Heading)),
747 (NodeId(5), accesskit::Node::new(Role::Paragraph)),
748 ],
749 tree: Some(accesskit::Tree {
750 root: NodeId(2),
751 toolkit_name: None,
752 toolkit_version: None
753 }),
754 tree_id: accesskit::TreeId::ROOT,
755 focus: NodeId(1),
756 }
757 );
758}
759
760static HTML_ELEMENT_ROLE_MAPPINGS: LazyLock<FxHashMap<LocalName, Role>> = LazyLock::new(|| {
761 [
762 (local_name!("article"), Role::Article),
763 (local_name!("aside"), Role::Complementary),
764 (local_name!("body"), Role::RootWebArea),
765 (local_name!("footer"), Role::ContentInfo),
766 (local_name!("h1"), Role::Heading),
767 (local_name!("h2"), Role::Heading),
768 (local_name!("h3"), Role::Heading),
769 (local_name!("h4"), Role::Heading),
770 (local_name!("h5"), Role::Heading),
771 (local_name!("h6"), Role::Heading),
772 (local_name!("header"), Role::Banner),
773 (local_name!("hr"), Role::Splitter),
774 (local_name!("main"), Role::Main),
775 (local_name!("nav"), Role::Navigation),
776 (local_name!("p"), Role::Paragraph),
777 ]
778 .into_iter()
779 .collect()
780});
781
782static NAME_FROM_CONTENTS_ROLES: LazyLock<FxHashSet<Role>> =
784 LazyLock::new(|| [(Role::Heading)].into_iter().collect());