Skip to main content

layout/
accessibility_tree.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4use 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    /// Damage which was caused by changes to the accessibility tree. These changes can cause other
26    /// properties to need to be re-computed based on the updated values, either on the same node or
27    /// on other nodes.
28    #[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
29    struct LocalAccessibilityDamage: u16 {
30        /// This node's children changed, and/or any node in its subtree changed.
31        const SUBTREE_CHANGED = 0b0001;
32        /// This node's computed role changed.
33        const ROLE_CHANGED = 0b0010;
34        /// This node's computed label or text value (for a text node) changed.
35        const TEXT_CHANGED = 0b0100;
36    }
37}
38
39/// Changes which have occurred during the current update.
40struct AccessibilityUpdate {
41    /// Nodes whose internal data has changed within the current update.
42    changed_nodes: FxHashSet<NodeId>,
43    /// Nodes that changed their relation to the tree within the current update.
44    tree_changes: FxHashMap<NodeId, TreeChange>,
45    /// Nodes which were removed from the DOM tree since the last reflow, which were rooted in
46    /// [`AccessibilityData`]. Only set if [`pref::expensive_accessibility_test_assertions_enabled`]
47    /// is set.
48    rooted_nodes: Option<FxHashSet<OpaqueNode>>,
49}
50
51struct AccessibilityNode {
52    /// The unique ID for the node. This is used both as a key in [`AccessibilityTree`]'s cache of
53    /// nodes, and as an identifier in [`accesskit`] datastructures: [`accesskit::Node`]s,
54    /// [`accesskit::TreeUpdate`]s and [`accesskit::ActionRequest`]s.
55    id: NodeId,
56    /// The computed [`accesskit::Node`] data. This will be copied and serialized into a
57    /// [`accesskit::TreeUpdate`] whenever it is changed during an update.
58    accesskit_node: accesskit::Node,
59    /// The [`OpaqueNode`] for the DOM node which corresponds to this accessibility node, if any.
60    /// An accessibility node may not correspond to a DOM node if it corresponds to a
61    /// pseudo-element, or in a test.
62    opaque_node: Option<OpaqueNode>,
63    /// Whether this node has been updated in the current tree update. This is reset to `false`
64    /// when the node is added to the [`AccessibilityUpdate`] - see [`AccessibilityUpdate::add()`].
65    updated: bool,
66}
67
68/// A retained, internal representation of the accessibility tree for a document.
69///
70/// [`accesskit`] only provides interchange types for tree updates and action requests, so we need
71/// to define our own representation for incremental tree building.
72#[derive(Debug)]
73pub struct AccessibilityTree {
74    /// All nodes currently in the tree as of the most recent update. New nodes are added and stale
75    /// nodes are pruned during [`AccessibilityTree::update_tree()`].
76    nodes: FxHashMap<NodeId, ArcRefCell<AccessibilityNode>>,
77    /// A map to allow retrieving the [`AccessibilityNode`] which corresponds to a particular DOM
78    /// node, if any.
79    opaque_node_to_id: FxHashMap<OpaqueNode, NodeId>,
80    /// Sent with each [`accesskit::TreeUpdate`]. This allows this tree to be
81    /// [grafted](https://docs.rs/accesskit/latest/accesskit/struct.Node.html#method.tree_id) into
82    /// an application's tree.
83    tree_id: accesskit::TreeId,
84    /// Sent with each [`accesskit::TreeUpdate`] to identify the root node, and also used in
85    /// [`Self::assert_integrity()`].
86    root_node_id: Option<NodeId>,
87    /// Sent to the embedder alongside each [`accesskit::TreeUpdate`], so that the embedder can
88    /// drop updates from documents which have been navigated away from.
89    embedder_epoch: Epoch,
90    /// Debug options, copied from configuration to this `AccessibilityTree` in order
91    /// to avoid having to constantly access the thread-safe global options.
92    debug: DiagnosticsLogging,
93}
94
95/// Tracks changes to a node's relation to the tree within an update.
96///
97/// This is used to remove nodes from the accessibility tree's cache when they are no longer in the
98/// tree.
99#[derive(Debug, PartialEq, Copy, Clone)]
100enum TreeChange {
101    /// The node was newly created in this update.
102    New,
103
104    /// The node has been re-parented in this update.
105    Moved,
106
107    /// The node has been added to its new parent, but not yet removed from its old
108    /// parent.
109    ///
110    /// When a node is moved within the tree, it must be both removed from its old parent
111    /// and added to its new parent within the same update. This may happen in either
112    /// order, depending on the relative positions of the node before and after it moves.
113    ///
114    /// - If a node's new parent is updated before its old parent, the node will be in a
115    ///   `TreeChange::PendingMove` state until its old parent is updated. We expect that it
116    ///   must later be removed from its old parent, at which point its state will be updated to
117    ///   `TreeChange::Moved`.
118    /// - If a node's old parent is updated before its new parent, the node will be first
119    ///   `TreeChange::Removed` and then `TreeChange::Moved`.
120    ///
121    /// At the end of the update, we assert that there are no pending moves remaining.
122    PendingMove,
123
124    /// The node is no longer a child of its previous parent.
125    Removed,
126}
127
128impl AccessibilityTree {
129    /// See [`Self::tree_id`] and [`Self::embedder_epoch`] for explanations of the parameters.
130    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    /// Update this tree based on the current state of the given DOM tree, and if anything changed,
142    /// return an [`accesskit::TreeUpdate`] representing what changed.
143    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    /// Update the given AccessibilityNode from its corresponding DOM node.
158    /// If it has new children, those will be recursively populated here.
159    // Any changed nodes will be added to the given [`AccessibilityUpdate`].
160    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        // TODO: read accessibility damage from DOM (right now, assume damage is complete)
170        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    /// Consume the [`AccessibilityUpdate`] by deleting all nodes it detected as being removed from
217    /// the tree.
218    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    /// If we got `rooted_nodes` from the document's `AccessibilityData`, assert that every node we
256    /// removed during this update was rooted, and any leftover rooted nodes were never known to the
257    /// accessibility tree.
258    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    /// Assert that the tree is a tree without any dangling references or orphaned nodes.
297    ///
298    /// For accessibility tests only, because it’s expensive.
299    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        // Traverse the tree from the given root.
305        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            // If this fails, then the tree is not a tree at all.
309            assert!(
310                seen_node_ids.insert(node_id),
311                "Tree contains {node_id:?} in multiple places"
312            );
313            // If this fails, then the tree has dangling references.
314            let node = self.assert_node_for_id(&node_id);
315            let node = node.borrow();
316            node_ids.extend(node.children().iter().rev());
317        }
318        // If this fails, then the tree has orphaned nodes (a leak).
319        // Dangling references are already caught in the loop above.
320        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    /// Update this node's [`Self::children`] from its corresponding DOM node. If any children are
363    /// newly added to the tree, populate them and recursively populate their children.
364    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    /// Recursively mark this subtree as having the given `TreeChange`.
395    ///
396    /// This is used when a node is `Moved` or `Removed`, since its entire subtree will also need to
397    /// be marked accordingly. When a node is `New`, it's marked as such when it is created. We
398    /// shouldn't call this method in that case, since it may have descendants which are not being
399    /// created in this update and shouldn't have a `New` state. Any descendants which are new will
400    /// already have their `New` state set when they are created.
401    ///
402    /// Note: if a node is moved, the requested `change` must always be `Moved(Pending)`: the logic
403    /// in this method will determine whether the move is `Complete` and set the stored value
404    /// accordingly.
405    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            // `new_change` might be different per node, if only some nodes were moved elsewhere.
421            child
422                .borrow()
423                .set_subtree_state_change(change, tree, update);
424        }
425    }
426
427    /// Update this node's properties from its corresponding DOM node.
428    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            // FIXME: this should take into account editing selection units (grapheme clusters?)
438            damage.insert(self.set_value(&text_content));
439        }
440
441        damage
442    }
443
444    /// Update this node's properties based on changes already made to the accessibility tree.
445    /// For example, if there were nodes added or removed in its subtree, its computed text may have
446    /// changed, so that will be recomputed here.
447    /// If any changes are made, add this node to the given [`AccessibilityUpdate`].
448    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    // TODO: use macros to generate getter/setter methods.
508
509    fn children(&self) -> &[NodeId] {
510        self.accesskit_node.children()
511    }
512
513    /// Set the children for this node, and set the subtree state change for any moved or removed
514    /// children.
515    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    /// Consume this `AccessibilityUpdate`, producing an [`accesskit::TreeUpdate`] if there have
665    /// been any changes to `tree`.
666    /// This will pass `self` into [`AccessibilityTree::remove_stale_nodes()`] to consume
667    /// [`Self::tree_changes`].
668    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
782/// <https://w3c.github.io/aria/#namefromcontent>
783static NAME_FROM_CONTENTS_ROLES: LazyLock<FxHashSet<Role>> =
784    LazyLock::new(|| [(Role::Heading)].into_iter().collect());