Skip to main content

layout/accessibility/
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/. */
4
5use std::cell::RefCell;
6use std::fmt::Debug;
7use std::iter::repeat;
8use std::sync::atomic;
9use std::sync::atomic::AtomicU64;
10
11use accesskit::{ActionRequest, NodeId, Role};
12use layout_api::{
13    AccessibilityActionRequest, AccessibilityDamage, LayoutNode, node_id_from_scroll_id,
14};
15use paint_api::display_list::SpatialTreeNodeInfo;
16use rustc_hash::{FxHashMap, FxHashSet};
17use script::layout_dom::ServoLayoutNode;
18use servo_base::Epoch;
19use servo_base::print_tree::PrintTree;
20use servo_config::opts::{self, DiagnosticsLogging, DiagnosticsLoggingOption};
21use servo_config::pref;
22use style::dom::OpaqueNode;
23use webrender_api::ExternalScrollId;
24use webrender_api::units::LayoutVector2D;
25
26use super::accessibility_node::{AccessibilityNode, DirtyState};
27use crate::ArcRefCell;
28use crate::accessibility::{AccessibilityContext, AccessibilityDamageMap};
29
30#[derive(Debug, Default)]
31pub struct UpdateCounters {
32    pub nodes_updated_from_dom: u32,
33    pub nodes_updated_from_tree: u32,
34    pub nodes_updated_bounds: u32,
35    pub nodes_in_tree_update: u32,
36}
37
38/// A retained, internal representation of the accessibility tree for a document.
39///
40/// [`accesskit`] only provides interchange types for tree updates and action requests, so we need
41/// to define our own representation for incremental tree building.
42#[derive(Debug)]
43pub struct AccessibilityTree {
44    /// All nodes currently in the tree as of the most recent update. New nodes are added and stale
45    /// nodes are pruned during [`AccessibilityTree::update_tree()`].
46    nodes: FxHashMap<NodeId, ArcRefCell<AccessibilityNode>>,
47    /// The node which has focus.
48    focused_node_id: Option<NodeId>,
49    /// A map to allow retrieving the [`AccessibilityNode`] which corresponds to a particular DOM
50    /// node, if any.
51    ///
52    /// This must be kept in sync with [`Self::id_to_opaque_node`].
53    opaque_node_to_id: FxHashMap<OpaqueNode, NodeId>,
54    /// A map to retrieve the `OpaqueNode` corresponding to a particular [`AccessibilityNode`], if
55    /// any.
56    ///
57    /// This must be kept in sync with [`Self::opaque_node_to_id`].
58    id_to_opaque_node: FxHashMap<NodeId, OpaqueNode>,
59    /// Sent with each [`accesskit::TreeUpdate`]. This allows this tree to be
60    /// [grafted](https://docs.rs/accesskit/latest/accesskit/struct.Node.html#method.tree_id) into
61    /// an application's tree.
62    tree_id: accesskit::TreeId,
63    /// This node's ID is sent with each [`accesskit::TreeUpdate`] to identify the root node.
64    /// Also used for any complete tree walk, such as in [`Self::assert_integrity()`] and
65    /// [`Self::print()`].
66    root_node: Option<ArcRefCell<AccessibilityNode>>,
67    /// If any nodes were scrolled since the last update, they are tracked here so that the next
68    /// update can update the tree accordingly.
69    pending_scroll_updates: FxHashMap<ExternalScrollId, LayoutVector2D>,
70    /// Sent to the embedder alongside each [`accesskit::TreeUpdate`], so that the embedder can
71    /// drop updates from documents which have been navigated away from.
72    embedder_epoch: Epoch,
73    /// Pending actions which have been processed from [`accesskit::ActionRequest`]s to retrieve the
74    /// [`OpaqueNode`] for the corresponding DOM node.
75    /// Any [`OpaqueNode`] in this list corresponds to an [`AccessibilityNode`] which is still in
76    /// the tree immediately after the tree has been updated, and therefore should correspond to a
77    /// live DOM node.
78    pending_actions: Vec<AccessibilityActionRequest>,
79    /// Debug options, copied from configuration to this `AccessibilityTree` in order
80    /// to avoid having to constantly access the thread-safe global options.
81    debug: DiagnosticsLogging,
82}
83
84/// Changes which have occurred during the current update, and data required to process the update.
85pub(super) struct AccessibilityUpdate<'update> {
86    /// Nodes whose internal data has changed within the current update.
87    changed_nodes: FxHashSet<NodeId>,
88    /// Sent with the initial [`accesskit::TreeUpdate`], and whenever the root node changes.
89    /// This is a property of the update, rather than the tree, because it only needs to exist for
90    /// updates where one of those two conditions is true.
91    accesskit_tree: Option<accesskit::Tree>,
92    /// Nodes that changed their relation to the tree within the current update.
93    tree_changes: FxHashMap<NodeId, TreeChange>,
94    /// Whether the tree's focused node has changed in this update.
95    focused_node_changed: bool,
96    /// Counters to track how many nodes we've checked for changes or updated in this tree update.
97    pub(super) counters: UpdateCounters,
98
99    /// Map of [`NodeId`] to the [`AccessibilityDamage`] which was passed in for that node.
100    damage_map: FxHashMap<NodeId, AccessibilityDamage>,
101    /// Map of [`NodeId`] to the corresponding [`ServoLayoutNode`]. This is populated for nodes
102    /// which have damage, including nodes which are newly added to the accessibility tree.
103    dom_node_map: RefCell<FxHashMap<NodeId, ServoLayoutNode<'update>>>,
104}
105
106/// Tracks changes to a node's relation to the tree within an update.
107///
108/// This is used to remove nodes from the accessibility tree's cache when they are no longer in the
109/// tree.
110#[derive(Debug, PartialEq, Copy, Clone)]
111pub(super) enum TreeChange {
112    /// The node was newly created in this update.
113    New,
114
115    /// The node has been re-parented in this update.
116    Moved,
117
118    /// The node has been added to its new parent, but not yet removed from its old
119    /// parent.
120    ///
121    /// When a node is moved within the tree, it must be both removed from its old parent
122    /// and added to its new parent within the same update. This may happen in either
123    /// order, depending on the relative positions of the node before and after it moves.
124    ///
125    /// - If a node's new parent is updated before its old parent, the node will be in a
126    ///   `TreeChange::PendingMove` state until its old parent is updated. We expect that it
127    ///   must later be removed from its old parent, at which point its state will be updated to
128    ///   `TreeChange::Moved`.
129    /// - If a node's old parent is updated before its new parent, the node will be first
130    ///   `TreeChange::Removed` and then `TreeChange::Moved`.
131    ///
132    /// At the end of the update, we assert that there are no pending moves remaining.
133    PendingMove,
134
135    /// The node is no longer a child of its previous parent.
136    Removed,
137}
138
139impl AccessibilityTree {
140    /// See [`Self::tree_id`] and [`Self::embedder_epoch`] for explanations of the parameters.
141    pub(crate) fn new(tree_id: accesskit::TreeId, embedder_epoch: Epoch) -> Self {
142        Self {
143            nodes: FxHashMap::default(),
144            focused_node_id: None,
145            opaque_node_to_id: FxHashMap::default(),
146            id_to_opaque_node: FxHashMap::default(),
147            tree_id,
148            root_node: None,
149            pending_scroll_updates: FxHashMap::default(),
150            embedder_epoch,
151            pending_actions: vec![],
152            debug: opts::get().debug.clone(),
153        }
154    }
155
156    /// Update this tree based on the current state of the given DOM tree, and if anything changed,
157    /// return an [`accesskit::TreeUpdate`] representing what changed.
158    pub(crate) fn update_tree<'update>(
159        &mut self,
160        root_dom_node: &ServoLayoutNode<'update>,
161        damage_from_dom: AccessibilityDamageMap<'update>,
162        action_requests: Vec<ActionRequest>,
163        context: AccessibilityContext<'update>,
164    ) -> (Option<accesskit::TreeUpdate>, UpdateCounters) {
165        let mut update = AccessibilityUpdate::new(damage_from_dom, self);
166
167        self.ensure_root_node(root_dom_node, &context, &mut update);
168
169        self.apply_changes_from_dom_tree(&context, &mut update);
170
171        self.update_focused_node(context.focused_element, &mut update);
172
173        self.handle_pending_scroll_updates(&mut update);
174
175        update.finalize(
176            self,
177            context.rooted_nodes_for_integrity_check,
178            action_requests,
179        )
180    }
181
182    /// Add all given scroll updates to [`Self::pending_scroll_updates`].
183    /// See [`Self::handle_pending_scroll_updates()`].
184    pub(crate) fn add_pending_scroll_updates(
185        &mut self,
186        scroll_states: FxHashMap<ExternalScrollId, LayoutVector2D>,
187    ) {
188        self.pending_scroll_updates.extend(scroll_states);
189    }
190
191    /// Add the given scroll update to [`Self::pending_scroll_updates`].
192    /// See [`Self::handle_pending_scroll_updates()`].
193    pub(crate) fn add_pending_scroll_update(
194        &mut self,
195        external_scroll_id: ExternalScrollId,
196        offset: LayoutVector2D,
197    ) {
198        self.pending_scroll_updates
199            .insert(external_scroll_id, offset);
200    }
201
202    /// Get the node corresponding to the root DOM node, and set it as this tree's root. If the root
203    /// node is newly created, which probably means this accessibility tree is newly created, append
204    /// an `AccessibilityDamage::Rebuild` value for it to `damage_from_dom`.
205    fn ensure_root_node<'update>(
206        &mut self,
207        root_dom_node: &ServoLayoutNode<'update>,
208        context: &AccessibilityContext<'update>,
209        update: &mut AccessibilityUpdate<'update>,
210    ) {
211        let (root_id, root_node) = self.get_or_create_node(root_dom_node, update);
212        if update.is_new(&root_id) {
213            // We're going to rebuild the whole tree, so ignore any incoming damage.
214            update.clear_damage();
215            update.insert_damage(root_id, AccessibilityDamage::Rebuild);
216            update.insert_dom_node(root_id, *root_dom_node);
217            self.populate_pending_scroll_updates_from_scroll_tree(context);
218
219            update.accesskit_tree = Some(accesskit::Tree::new(root_id));
220        }
221
222        self.root_node = Some(root_node);
223    }
224
225    /// Update all nodes with damage tracked in `update` based on their `AccessibilityDamage`. If
226    /// any [`LocalAccessibilityDamage`] results from the update, propagate
227    /// [`LocalAccessibilityDamage::SubtreeChanged`] to its ancestors.
228    fn apply_changes_from_dom_tree(
229        &mut self,
230        context: &AccessibilityContext,
231        update: &mut AccessibilityUpdate,
232    ) {
233        let Some(damage_root_id) = self.mark_nodes_and_ancestors_dirty(update) else {
234            return;
235        };
236        let damage_root = self.assert_node_for_id(&damage_root_id);
237        let hidden = false;
238        let local_damage = damage_root.borrow_mut().update_subtree(
239            damage_root.clone(),
240            AccessibilityDamage::empty(),
241            hidden,
242            context,
243            self,
244            update,
245        );
246
247        damage_root.borrow().update_ancestors(local_damage, update);
248    }
249
250    /// Read all scroll offsets directly from the scroll tree, and use them to populate
251    /// [`Self::pending_scroll_updates`].
252    /// This will clear any previous pending scroll updates, as the scroll tree contains all scroll
253    /// information.
254    fn populate_pending_scroll_updates_from_scroll_tree(&mut self, context: &AccessibilityContext) {
255        let scroll_tree = &context.stacking_context_tree.paint_info.scroll_tree;
256        let scroll_updates = scroll_tree
257            .nodes
258            .iter()
259            .filter_map(|node| match node.info {
260                SpatialTreeNodeInfo::Scroll(ref info) => {
261                    let offset = info.offset;
262                    Some((info.external_id, offset))
263                },
264                _ => None,
265            })
266            .collect();
267        self.pending_scroll_updates = scroll_updates;
268    }
269
270    /// For each entry in [`Self::pending_scroll_updates`], set the scroll offset on the
271    /// [`AccessibilityNode`] corresponding to its [`ExternalScrollId`], if any.
272    /// This sets a transformation on every direct child of the scrolled node.
273    ///
274    /// This should be called after the tree has been updated, so that we can be sure not to miss
275    /// any newly-added nodes.
276    fn handle_pending_scroll_updates(&mut self, update: &mut AccessibilityUpdate) {
277        let pending_scroll_updates = std::mem::take(&mut self.pending_scroll_updates);
278        for (opaque, offset) in
279            pending_scroll_updates
280                .into_iter()
281                .filter_map(|(scroll_id, translate)| {
282                    if scroll_id.is_root() {
283                        let root_node_opaque =
284                            self.root_node.as_ref()?.clone().borrow().opaque_node()?;
285                        return Some((root_node_opaque, translate));
286                    }
287                    let node_id = node_id_from_scroll_id(scroll_id.0 as usize);
288                    let opaque = OpaqueNode(node_id);
289                    Some((opaque, translate))
290                })
291        {
292            let Some(node) = self.node_for_opaque(opaque) else {
293                continue;
294            };
295            node.borrow_mut().set_scroll_offset(offset, update);
296        }
297    }
298
299    /// Given an iterator of `NodeId`s corresponding to nodes which have received some damage from
300    /// the DOM:
301    /// - mark each node as [`DirtyState::Dirty`];
302    /// - mark all of each node's ancestors as [`DirtyState::HasDirtyDescendants`];
303    /// - find the lowest common ancestor node of all the damaged nodes;
304    /// - remove the [`DirtyState::HasDirtyDescendants`] flag on nodes between the common ancestor
305    ///   and the root;
306    /// - return the common ancestor.
307    fn mark_nodes_and_ancestors_dirty(
308        &mut self,
309        update: &mut AccessibilityUpdate,
310    ) -> Option<NodeId> {
311        let mut dirty_node_ids = update.damage_map.keys();
312
313        // An ordered list of common ancestors for the nodes seen so far, from shallowest to
314        // deepest. At the end of the loop, the lowest common ancestor is the last node in this vec.
315        let mut common_ancestors: Vec<NodeId> = Vec::new();
316
317        {
318            // Initialize the list of potential common ancestors.
319            let node_id = dirty_node_ids.next()?;
320            update.collect_dom_node_ancestors(node_id, self);
321            let first_node = self.assert_node_for_id(node_id);
322            let mut first_node = first_node.borrow_mut();
323            first_node.add_dirty_state(DirtyState::HasDamage);
324            common_ancestors.push(first_node.id());
325            common_ancestors.extend(first_node.ancestors().map(|ancestor| {
326                let mut ancestor = ancestor.borrow_mut();
327                ancestor.add_dirty_state(DirtyState::DescendantHasDamage);
328                ancestor.id()
329            }));
330            common_ancestors.reverse();
331        }
332
333        let mut truncate_ancestors = |node: &AccessibilityNode| -> bool {
334            if node.dirty_state().descendant_has_damage() {
335                if let Some(pos) = common_ancestors.iter().position(|&id| id == node.id()) {
336                    common_ancestors.truncate(pos + 1);
337                }
338                return true;
339            }
340            false
341        };
342
343        for node_id in dirty_node_ids {
344            let node = self.assert_node_for_id(node_id);
345            let mut node = node.borrow_mut();
346            node.add_dirty_state(DirtyState::HasDamage);
347
348            if truncate_ancestors(&node) {
349                continue;
350            }
351
352            for ancestor in node.ancestors() {
353                let mut ancestor = ancestor.borrow_mut();
354
355                // If we find an ancestor we've already seen, discard any potential ancestors deeper
356                // than this one, and go on to the next dirty node.
357                if truncate_ancestors(&ancestor) {
358                    break;
359                }
360
361                ancestor.add_dirty_state(DirtyState::DescendantHasDamage);
362            }
363        }
364
365        let lowest_common_ancestor = common_ancestors.pop();
366
367        for ancestor_id in common_ancestors {
368            let ancestor = self.assert_node_for_id(&ancestor_id);
369            ancestor
370                .borrow_mut()
371                .remove_dirty_state(DirtyState::DescendantHasDamage);
372        }
373
374        lowest_common_ancestor
375    }
376
377    fn update_focused_node(
378        &mut self,
379        focused_element: Option<OpaqueNode>,
380        update: &mut AccessibilityUpdate,
381    ) {
382        let mut focused_node_id = None;
383        if let Some(focused_element) = focused_element &&
384            let Some(node_id) = self.existing_id_for_opaque(focused_element) &&
385            let Some(focused_node) = self.node_for_id(node_id)
386        {
387            let focused_node = focused_node.borrow();
388            if focused_node.role() == Role::GenericContainer {
389                // Avoid moving focus to a node which is effectively hidden from accessibility, but
390                // don't reset focus to the root node either.
391                focused_node_id = self.focused_node_id;
392            } else {
393                focused_node_id = Some(focused_node.id());
394            }
395        }
396
397        if focused_node_id == self.focused_node_id {
398            return;
399        }
400
401        update.focused_node_changed = true;
402        self.focused_node_id = focused_node_id;
403    }
404
405    pub(super) fn focused_node_id(&self) -> Option<NodeId> {
406        self.focused_node_id
407    }
408
409    pub(super) fn get_or_create_node(
410        &mut self,
411        dom_node: &ServoLayoutNode<'_>,
412        update: &mut AccessibilityUpdate,
413    ) -> (NodeId, ArcRefCell<AccessibilityNode>) {
414        let id = self.get_or_create_id_for_opaque(dom_node.opaque());
415        let node_ref = self.get_or_create_node_with_id(id, update);
416
417        if update.is_new(&id) {
418            let mut node = node_ref.borrow_mut();
419            node.initialize_from_dom_node(dom_node);
420            update.insert_damage(id, AccessibilityDamage::Rebuild);
421            node.add_dirty_state(DirtyState::HasDamage);
422        }
423
424        (id, node_ref)
425    }
426
427    pub(crate) fn accesskit_node_for_dom_node(
428        &self,
429        dom_node: &ServoLayoutNode,
430    ) -> Option<accesskit::Node> {
431        let node = self.node_for_opaque(dom_node.opaque())?;
432        Some(node.borrow().clone_accesskit_node())
433    }
434
435    fn get_or_create_node_with_id(
436        &mut self,
437        id: NodeId,
438        update: &mut AccessibilityUpdate,
439    ) -> ArcRefCell<AccessibilityNode> {
440        if let Some(node) = self.nodes.get(&id) {
441            return node.clone();
442        }
443
444        let node = ArcRefCell::new(AccessibilityNode::new(id));
445        update.set_tree_state_change(id, TreeChange::New);
446        self.nodes.insert(id, node.clone());
447
448        node
449    }
450
451    fn node_for_id(&self, id: NodeId) -> Option<ArcRefCell<AccessibilityNode>> {
452        self.nodes.get(&id).cloned()
453    }
454
455    fn assert_node_for_id(&self, id: &NodeId) -> ArcRefCell<AccessibilityNode> {
456        let Some(node) = self.nodes.get(id) else {
457            panic!("{id:?} does not exist in tree");
458        };
459        node.clone()
460    }
461
462    fn node_for_opaque(&self, opaque: OpaqueNode) -> Option<ArcRefCell<AccessibilityNode>> {
463        self.nodes
464            .get(&self.existing_id_for_opaque(opaque)?)
465            .cloned()
466    }
467
468    /// Consume the [`AccessibilityUpdate`] by deleting all nodes it detected as being removed from
469    /// the tree.
470    fn drop_removed_nodes(
471        &mut self,
472        mut update: AccessibilityUpdate,
473        mut rooted_nodes_for_integrity_check: Option<FxHashSet<OpaqueNode>>,
474    ) {
475        if let Some(rooted_nodes) = rooted_nodes_for_integrity_check.as_mut() {
476            self.assert_removed_nodes_were_rooted(&update, rooted_nodes);
477        }
478
479        let mut ids_to_remove: Vec<_> = update
480            .tree_changes
481            .iter()
482            .filter_map(|(id, change)| match change {
483                TreeChange::Removed => Some(id),
484                TreeChange::PendingMove => None,
485                TreeChange::New => None,
486                TreeChange::Moved => None,
487            })
488            .cloned()
489            .collect();
490
491        while let Some(id) = ids_to_remove.pop() {
492            if update.tree_changes.get(&id) == Some(&TreeChange::PendingMove) {
493                // Mark the move as completed by marking the node as removed from its old position.
494                update.set_tree_state_change(id, TreeChange::Removed);
495
496                // Since this node is actually moved, don't continue removing its subtree.
497                continue;
498            }
499
500            if let Some(opaque_node) = self.id_to_opaque_node.remove(&id) {
501                self.opaque_node_to_id.remove(&opaque_node);
502            }
503            let node = self.nodes.remove(&id).expect("Node {id:?} already removed");
504            ids_to_remove.extend(node.borrow().child_ids());
505        }
506
507        update
508            .tree_changes
509            .drain()
510            .for_each(|(id, change)| match change {
511                TreeChange::PendingMove => unreachable!(
512                    "Pending move found for node id {id:?} when draining tree state changes"
513                ),
514                TreeChange::Removed => (),
515                TreeChange::New => (),
516                TreeChange::Moved => (),
517            });
518
519        if let Some(rooted_nodes) = rooted_nodes_for_integrity_check {
520            self.assert_remaining_rooted_nodes_not_in_tree(rooted_nodes);
521        }
522
523        if self
524            .debug
525            .is_enabled(DiagnosticsLoggingOption::AccessibilityTree)
526        {
527            self.print();
528        }
529
530        if pref!(expensive_accessibility_test_assertions_enabled) {
531            self.assert_integrity();
532        }
533    }
534
535    /// If we got `rooted_nodes` from the document's `AccessibilityData`, assert that every node we
536    /// marked as `TreeChange::Removed` during this update was rooted.
537    fn assert_removed_nodes_were_rooted(
538        &mut self,
539        update: &AccessibilityUpdate,
540        rooted_nodes: &mut FxHashSet<OpaqueNode>,
541    ) {
542        debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
543        for (id, change) in update.tree_changes.iter() {
544            if change == &TreeChange::Removed {
545                let Some(&opaque_node) = self.id_to_opaque_node.get(id) else {
546                    panic!("No opaque node found for removed node: id {id:?}");
547                };
548                assert!(
549                    rooted_nodes.remove(&opaque_node),
550                    "Node removed from accessibility tree wasn't rooted: id {id:?}"
551                );
552            };
553        }
554    }
555
556    /// If we got `rooted_nodes` from the document's `AccessibilityData`, assert that any nodes
557    /// which were rooted but not marked as `TreeChange::Removed` are no longer in the tree after
558    /// dropping all nodes which were removed from the tree. They may have been part of a subtree
559    /// which was marked `TreeChange::Removed` on an ancestor node, or may have never made it into
560    /// the accessibility tree to begin with.
561    fn assert_remaining_rooted_nodes_not_in_tree(&self, rooted_nodes: FxHashSet<OpaqueNode>) {
562        for leftover_node in rooted_nodes {
563            assert!(
564                !self.opaque_node_to_id.contains_key(&leftover_node),
565                "Found node removed from DOM tree but not accessibility tree: {:#x}",
566                leftover_node.0
567            );
568        }
569    }
570
571    fn get_or_create_id_for_opaque(&mut self, opaque: OpaqueNode) -> NodeId {
572        let id = self.opaque_node_to_id.entry(opaque).or_insert_with(|| {
573            static LAST_ID: AtomicU64 = AtomicU64::new(0);
574            let id = LAST_ID.fetch_add(1, atomic::Ordering::SeqCst).into();
575            self.id_to_opaque_node.insert(id, opaque);
576            id
577        });
578        *id
579    }
580
581    pub(super) fn existing_id_for_opaque(&self, opaque: OpaqueNode) -> Option<NodeId> {
582        self.opaque_node_to_id.get(&opaque).cloned()
583    }
584
585    pub(crate) fn embedder_epoch(&self) -> Epoch {
586        self.embedder_epoch
587    }
588
589    pub(crate) fn take_pending_actions(&mut self) -> Vec<AccessibilityActionRequest> {
590        std::mem::take(&mut self.pending_actions)
591    }
592
593    /// Assert that the tree is a tree without any dangling references or orphaned nodes.
594    ///
595    /// For accessibility tests only, because it’s expensive.
596    fn assert_integrity(&self) {
597        debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
598        let Some(root_node) = self.root_node.clone() else {
599            return;
600        };
601
602        // Traverse the tree from the given root.
603        // `nodes` is a Vec of pairs of nodes and their expected parents.
604        let mut nodes = vec![(root_node, None)];
605        let mut seen_node_ids = FxHashSet::default();
606        while let Some((node, expected_parent)) = nodes.pop() {
607            let node = node.borrow();
608
609            // If this fails, then the tree is not a tree at all.
610            assert!(
611                seen_node_ids.insert(node.id()),
612                "Tree contains {:?} in multiple places",
613                node.id()
614            );
615
616            node.assert_integrity(expected_parent);
617
618            // assert_node_for_id() here double-checks that the node hasn't been incorrectly evicted
619            // from the map while it's still retained as a child node.
620            let weak_node = Some(self.assert_node_for_id(&node.id()).downgrade());
621            nodes.extend(node.children().cloned().zip(repeat(weak_node)));
622        }
623
624        // If this fails, then the tree has orphaned nodes (a leak).
625        // If a node has been incorrectly removed from the map, that will be caught above.
626        assert_eq!(seen_node_ids, self.nodes.keys().copied().collect());
627    }
628
629    fn print(&self) {
630        let Some(root_node) = self.root_node.clone() else {
631            return;
632        };
633
634        let mut print_tree = PrintTree::new("Accessibility Tree");
635        root_node.borrow().print(&mut print_tree, self);
636        print_tree.end_level();
637    }
638}
639
640impl<'update> AccessibilityUpdate<'update> {
641    fn new(dom_damage: AccessibilityDamageMap<'update>, tree: &AccessibilityTree) -> Self {
642        let damage_map = dom_damage
643            .iter()
644            .filter_map(|(&opaque, &(_dom_node, damage))| {
645                let id = tree.existing_id_for_opaque(opaque)?;
646                Some((id, damage))
647            })
648            .collect();
649        let dom_node_map = dom_damage
650            .into_iter()
651            .filter_map(|(opaque, (dom_node, _damage))| {
652                let id = tree.existing_id_for_opaque(opaque)?;
653                Some((id, dom_node))
654            })
655            .collect();
656        Self {
657            changed_nodes: FxHashSet::default(),
658            accesskit_tree: None,
659            tree_changes: FxHashMap::default(),
660            focused_node_changed: false,
661            counters: UpdateCounters::default(),
662            damage_map,
663            dom_node_map: RefCell::new(dom_node_map),
664        }
665    }
666
667    pub(super) fn add(&mut self, node: &mut AccessibilityNode) {
668        self.changed_nodes.insert(node.id());
669        node.remove_dirty_state(DirtyState::Updated);
670    }
671
672    pub(super) fn set_tree_state_change(&mut self, node_id: NodeId, change: TreeChange) {
673        let old_change = self.tree_changes.get(&node_id);
674
675        assert!(
676            change != TreeChange::Moved,
677            "Incoming change must never be Moved"
678        );
679
680        let resolved_change = old_change
681            .map(|old_change| match (old_change, change) {
682                (TreeChange::PendingMove, TreeChange::Removed) => TreeChange::Moved,
683                (TreeChange::Removed, TreeChange::PendingMove) => TreeChange::Moved,
684                _ => {
685                    unreachable!("Logically impossible state change: {old_change:?} → {change:?}")
686                },
687            })
688            .unwrap_or(change);
689
690        self.tree_changes.insert(node_id, resolved_change);
691    }
692
693    pub(super) fn is_new(&mut self, node_id: &NodeId) -> bool {
694        self.tree_changes.get(node_id) == Some(&TreeChange::New)
695    }
696
697    /// Consume this `AccessibilityUpdate`, producing an [`accesskit::TreeUpdate`] if there have
698    /// been any changes to `tree`.
699    /// This will pass `self` into [`AccessibilityTree::remove_stale_nodes()`] to consume
700    /// [`Self::tree_changes`].
701    fn finalize(
702        mut self,
703        tree: &mut AccessibilityTree,
704        rooted_nodes_for_integrity_check: Option<FxHashSet<OpaqueNode>>,
705        action_requests: Vec<ActionRequest>,
706    ) -> (Option<accesskit::TreeUpdate>, UpdateCounters) {
707        let mut tree_update = None;
708        let mut counters = std::mem::take(&mut self.counters);
709        if !self.changed_nodes.is_empty() ||
710            self.accesskit_tree.is_some() ||
711            self.focused_node_changed
712        {
713            let changed_nodes = std::mem::take(&mut self.changed_nodes);
714            let accesskit_tree = std::mem::take(&mut self.accesskit_tree);
715
716            tree.drop_removed_nodes(self, rooted_nodes_for_integrity_check);
717
718            let changed_nodes: Vec<_> = changed_nodes
719                .into_iter()
720                .filter_map(|id| Some((id, tree.node_for_id(id)?.borrow().clone_accesskit_node())))
721                .collect();
722
723            counters.nodes_in_tree_update = changed_nodes.len().try_into().unwrap_or_default();
724
725            let focus = tree.focused_node_id.unwrap_or(
726                tree.root_node
727                    .as_ref()
728                    .expect("Root node must be set")
729                    .borrow()
730                    .id(),
731            );
732            tree_update = Some(accesskit::TreeUpdate {
733                // Filter out any nodes which were both changed and removed.
734                nodes: changed_nodes,
735                tree: accesskit_tree,
736                focus,
737                tree_id: tree.tree_id,
738            });
739        } else {
740            assert!(self.tree_changes.is_empty());
741        }
742
743        for action in action_requests {
744            assert_eq!(
745                action.target_tree, tree.tree_id,
746                "Got action with wrong tree ID: {action:?}"
747            );
748            let Some(&opaque) = tree.id_to_opaque_node.get(&action.target_node) else {
749                // If the action is on a node which has been dropped, silently drop the action.
750                continue;
751            };
752            let dom_action_request = AccessibilityActionRequest {
753                action: action.action,
754                target: opaque,
755                data: action.data,
756            };
757            tree.pending_actions.push(dom_action_request);
758        }
759
760        (tree_update, counters)
761    }
762
763    fn clear_damage(&mut self) {
764        self.damage_map.clear();
765    }
766
767    fn insert_damage(&mut self, node_id: NodeId, damage: AccessibilityDamage) {
768        self.damage_map.insert(node_id, damage);
769    }
770
771    pub(super) fn insert_dom_node(&self, node_id: NodeId, dom_node: ServoLayoutNode<'update>) {
772        self.dom_node_map.borrow_mut().insert(node_id, dom_node);
773    }
774
775    pub(super) fn take_damage(&mut self, node_id: &NodeId) -> AccessibilityDamage {
776        self.damage_map
777            .remove(node_id)
778            .unwrap_or(AccessibilityDamage::empty())
779    }
780
781    pub(super) fn take_dom_node(&mut self, node_id: &NodeId) -> Option<ServoLayoutNode<'update>> {
782        self.dom_node_map.borrow_mut().remove(node_id)
783    }
784
785    #[expect(unsafe_code)]
786    fn collect_dom_node_ancestors(&self, node_id: &NodeId, tree: &AccessibilityTree) {
787        let mut dom_node_map = self.dom_node_map.borrow_mut();
788        let dom_node = dom_node_map
789            .get(node_id)
790            .expect("collect_dom_node_ancestors should be called for a known DOM node");
791        let mut parent = unsafe { dom_node.dangerous_flat_tree_parent() };
792        while let Some(node) = parent {
793            if let Some(node_id) = tree.existing_id_for_opaque(node.opaque()) {
794                dom_node_map.insert(node_id, node);
795            }
796            parent = unsafe { node.dangerous_flat_tree_parent() };
797        }
798    }
799}
800
801#[cfg(test)]
802#[test]
803fn test_accessibility_update_add_some_nodes_twice() {
804    let mut tree = AccessibilityTree::new(accesskit::TreeId::ROOT, Epoch::default());
805    let mut root_update = AccessibilityUpdate::new(AccessibilityDamageMap::default(), &tree);
806
807    let root_node = tree.get_or_create_node_with_id(NodeId(2), &mut root_update);
808    tree.root_node = Some(root_node.clone());
809
810    let nodes: Vec<_> = [
811        (3, Role::GenericContainer),
812        (4, Role::Heading),
813        (5, Role::Paragraph),
814    ]
815    .into_iter()
816    .map(|(id, role)| {
817        let id = NodeId(id);
818        let node = tree.get_or_create_node_with_id(id, &mut root_update);
819        node.borrow_mut().set_role(role);
820        (id, node)
821    })
822    .collect();
823
824    {
825        let (child_node_ids, child_nodes): (Vec<_>, Vec<_>) = nodes.iter().cloned().unzip();
826        let mut root_node = root_node.borrow_mut();
827        root_node.set_children_for_testing(child_node_ids, child_nodes);
828    }
829
830    let mut update = AccessibilityUpdate::new(AccessibilityDamageMap::default(), &tree);
831
832    {
833        let node_3 = tree.assert_node_for_id(&NodeId(3));
834        let mut node_3 = node_3.borrow_mut();
835        let node_4 = tree.assert_node_for_id(&NodeId(4));
836        let mut node_4 = node_4.borrow_mut();
837        let node_5 = tree.assert_node_for_id(&NodeId(5));
838        let mut node_5 = node_5.borrow_mut();
839
840        update.add(&mut node_5);
841        update.add(&mut node_3);
842        update.add(&mut node_4);
843        update.add(&mut node_4);
844
845        node_3.set_role(Role::ScrollView);
846        update.add(&mut node_3);
847    }
848
849    let (tree_update, _) = update.finalize(&mut tree, None, vec![]);
850    let mut tree_update = tree_update.expect("finalize should produce a tree update");
851    tree_update.nodes.sort_by_key(|(node_id, _node)| *node_id);
852    assert_eq!(
853        tree_update,
854        accesskit::TreeUpdate {
855            nodes: vec![
856                (NodeId(3), accesskit::Node::new(Role::ScrollView)),
857                (NodeId(4), accesskit::Node::new(Role::Heading)),
858                (NodeId(5), accesskit::Node::new(Role::Paragraph)),
859            ],
860            tree: None,
861            tree_id: accesskit::TreeId::ROOT,
862            focus: NodeId(2),
863        }
864    );
865}