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::iter::repeat;
7use std::sync::atomic::AtomicU64;
8use std::sync::{LazyLock, atomic};
9
10use accesskit::{NodeId, Role};
11use bitflags::bitflags;
12use layout_api::{AccessibilityDamage, LayoutElement, LayoutNode, LayoutNodeType};
13use log::trace;
14use rustc_hash::{FxHashMap, FxHashSet};
15use script::layout_dom::{ServoLayoutElement, ServoLayoutNode};
16use servo_base::Epoch;
17use servo_base::print_tree::PrintTree;
18use servo_config::opts::{self, DiagnosticsLogging, DiagnosticsLoggingOption};
19use servo_config::pref;
20use style::Atom;
21use style::dom::OpaqueNode;
22use web_atoms::{LocalName, local_name, ns};
23
24use crate::ArcRefCell;
25use crate::cell::WeakRefCell;
26
27bitflags! {
28    /// Damage which was caused by changes to the accessibility tree. These changes can cause other
29    /// properties to need to be re-computed based on the updated values, either on the same node or
30    /// on other nodes.
31    #[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
32    struct LocalAccessibilityDamage: u16 {
33        /// This node's children changed, and/or any node in its subtree changed.
34        const SubtreeChanged = 0b0001;
35        /// This node's computed role changed.
36        const RoleChanged = 0b0010;
37        /// This node's computed label or text value (for a text node) changed.
38        const TextChanged = 0b0100;
39    }
40}
41
42/// Changes which have occurred during the current update.
43struct AccessibilityUpdate {
44    /// Nodes whose internal data has changed within the current update.
45    changed_nodes: FxHashSet<NodeId>,
46    /// Nodes that changed their relation to the tree within the current update.
47    tree_changes: FxHashMap<NodeId, TreeChange>,
48    /// Counters to track how many nodes we've checked for changes or updated in this tree update.
49    counters: UpdateCounters,
50    /// Nodes which were removed from the DOM tree since the last reflow, which were rooted in
51    /// `AccessibilityData`. Only set if `pref::expensive_accessibility_test_assertions_enabled`
52    /// is set.
53    rooted_nodes: Option<FxHashSet<OpaqueNode>>,
54}
55
56#[derive(Debug, Default)]
57pub struct UpdateCounters {
58    pub nodes_updated_from_dom: u32,
59    pub nodes_updated_from_tree: u32,
60    pub nodes_in_tree_update: u32,
61}
62
63bitflags! {
64    /// Flags tracking an [`AccessibilityNode`]'s dirty state during an update. All flags which are
65    /// set during the update should be unset by the end of the update.
66    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
67    struct DirtyState : u16 {
68        /// At least one descendant of this node has unresolved damage from the DOM tree.
69        const DescendantHasDamage = 0b0001;
70        /// This node has unresolved damage from the DOM tree.
71        const HasDamage = 0b0010;
72        /// This node's data changed, but it hasn't yet been added to the [`AccessibilityUpdate`].
73        const Updated = 0b0100;
74    }
75}
76
77struct AccessibilityNode {
78    /// The unique ID for the node. This is used both as a key in [`AccessibilityTree`]'s cache of
79    /// nodes, and as an identifier in [`accesskit`] datastructures: [`accesskit::Node`]s,
80    /// [`accesskit::TreeUpdate`]s and [`accesskit::ActionRequest`]s.
81    id: NodeId,
82    /// The computed [`accesskit::Node`] data. This will be copied and serialized into a
83    /// [`accesskit::TreeUpdate`] whenever it is changed during an update.
84    accesskit_node: accesskit::Node,
85    /// This node's parent, if any.
86    parent_node: Option<WeakRefCell<AccessibilityNode>>,
87    /// All this node's children.
88    child_nodes: Vec<ArcRefCell<AccessibilityNode>>,
89    /// The [`OpaqueNode`] for the DOM node which corresponds to this accessibility node, if any.
90    /// An accessibility node may not correspond to a DOM node if it corresponds to a
91    /// pseudo-element, or in a test.
92    opaque_node: Option<OpaqueNode>,
93    /// Any dirty state for the current update.
94    dirty_state: DirtyState,
95}
96
97/// A retained, internal representation of the accessibility tree for a document.
98///
99/// [`accesskit`] only provides interchange types for tree updates and action requests, so we need
100/// to define our own representation for incremental tree building.
101#[derive(Debug)]
102pub struct AccessibilityTree {
103    /// All nodes currently in the tree as of the most recent update. New nodes are added and stale
104    /// nodes are pruned during [`AccessibilityTree::update_tree()`].
105    nodes: FxHashMap<NodeId, ArcRefCell<AccessibilityNode>>,
106    /// A map to allow retrieving the [`AccessibilityNode`] which corresponds to a particular DOM
107    /// node, if any.
108    ///
109    /// This must be kept in sync with [`Self::id_to_opaque_node`].
110    opaque_node_to_id: FxHashMap<OpaqueNode, NodeId>,
111    /// A map to retrieve the `OpaqueNode` corresponding to a particular [`AccessibilityNode`], if
112    /// any.
113    ///
114    /// This must be kept in sync with [`Self::opaque_node_to_id`].
115    id_to_opaque_node: FxHashMap<NodeId, OpaqueNode>,
116    /// Sent with each [`accesskit::TreeUpdate`]. This allows this tree to be
117    /// [grafted](https://docs.rs/accesskit/latest/accesskit/struct.Node.html#method.tree_id) into
118    /// an application's tree.
119    tree_id: accesskit::TreeId,
120    /// This node's ID is sent with each [`accesskit::TreeUpdate`] to identify the root node.
121    /// Also used for any complete tree walk, such as in [`Self::assert_integrity()`] and
122    /// [`Self::print()`].
123    root_node: Option<ArcRefCell<AccessibilityNode>>,
124    /// Sent to the embedder alongside each [`accesskit::TreeUpdate`], so that the embedder can
125    /// drop updates from documents which have been navigated away from.
126    embedder_epoch: Epoch,
127    /// Debug options, copied from configuration to this `AccessibilityTree` in order
128    /// to avoid having to constantly access the thread-safe global options.
129    debug: DiagnosticsLogging,
130}
131
132/// Tracks changes to a node's relation to the tree within an update.
133///
134/// This is used to remove nodes from the accessibility tree's cache when they are no longer in the
135/// tree.
136#[derive(Debug, PartialEq, Copy, Clone)]
137enum TreeChange {
138    /// The node was newly created in this update.
139    New,
140
141    /// The node has been re-parented in this update.
142    Moved,
143
144    /// The node has been added to its new parent, but not yet removed from its old
145    /// parent.
146    ///
147    /// When a node is moved within the tree, it must be both removed from its old parent
148    /// and added to its new parent within the same update. This may happen in either
149    /// order, depending on the relative positions of the node before and after it moves.
150    ///
151    /// - If a node's new parent is updated before its old parent, the node will be in a
152    ///   `TreeChange::PendingMove` state until its old parent is updated. We expect that it
153    ///   must later be removed from its old parent, at which point its state will be updated to
154    ///   `TreeChange::Moved`.
155    /// - If a node's old parent is updated before its new parent, the node will be first
156    ///   `TreeChange::Removed` and then `TreeChange::Moved`.
157    ///
158    /// At the end of the update, we assert that there are no pending moves remaining.
159    PendingMove,
160
161    /// The node is no longer a child of its previous parent.
162    Removed,
163}
164
165impl AccessibilityTree {
166    /// See [`Self::tree_id`] and [`Self::embedder_epoch`] for explanations of the parameters.
167    pub(super) fn new(tree_id: accesskit::TreeId, embedder_epoch: Epoch) -> Self {
168        Self {
169            nodes: FxHashMap::default(),
170            opaque_node_to_id: FxHashMap::default(),
171            id_to_opaque_node: FxHashMap::default(),
172            tree_id,
173            root_node: None,
174            embedder_epoch,
175            debug: opts::get().debug.clone(),
176        }
177    }
178
179    /// Update this tree based on the current state of the given DOM tree, and if anything changed,
180    /// return an [`accesskit::TreeUpdate`] representing what changed.
181    pub(super) fn update_tree<'dom>(
182        &mut self,
183        root_dom_node: &ServoLayoutNode<'dom>,
184        mut damage_from_dom: VecDeque<(ServoLayoutNode<'dom>, AccessibilityDamage)>,
185        rooted_nodes: Option<FxHashSet<OpaqueNode>>,
186    ) -> (Option<accesskit::TreeUpdate>, UpdateCounters) {
187        let mut update = AccessibilityUpdate::new(rooted_nodes);
188
189        self.ensure_root_node(root_dom_node, &mut damage_from_dom, &mut update);
190
191        self.apply_changes_from_dom_tree(damage_from_dom, &mut update);
192
193        update.finalize(self)
194    }
195
196    /// Get the node corresponding to the root DOM node, and set it as this tree's root. If the root
197    /// node is newly created, which probably means this accessibility tree is newly created, append
198    /// an `AccessibilityDamage::REBUILD` value for it to `damage_from_dom`.
199    fn ensure_root_node<'dom>(
200        &mut self,
201        root_dom_node: &ServoLayoutNode<'dom>,
202        damage_from_dom: &mut VecDeque<(ServoLayoutNode<'dom>, AccessibilityDamage)>,
203        update: &mut AccessibilityUpdate,
204    ) {
205        let (root_id, root_node) = self.get_or_create_node(root_dom_node, update);
206        if update.is_new(&root_id) {
207            damage_from_dom.push_front((*root_dom_node, AccessibilityDamage::Rebuild));
208        }
209        self.root_node = Some(root_node);
210    }
211
212    /// For each DOM node in `damage_from_dom`, update the corresponding accessibility node based on
213    /// its `AccessibilityDamage`. If any [`LocalAccessibilityDamage`] results from the update,
214    /// propagate [`LocalAccessibilityDamage::SubtreeChanged`] to its ancestors.
215    fn apply_changes_from_dom_tree<'dom>(
216        &mut self,
217        damage_from_dom: VecDeque<(ServoLayoutNode<'dom>, AccessibilityDamage)>,
218        update: &mut AccessibilityUpdate,
219    ) {
220        let mut dom_damage_map = FxHashMap::from(
221            damage_from_dom
222                .into_iter()
223                .filter_map(|(dom_node, dom_node_damage)| {
224                    let id = self.existing_id_for_opaque(dom_node.opaque())?;
225                    Some((id, (dom_node, dom_node_damage)))
226                })
227                .collect(),
228        );
229        let damage_root = self.mark_nodes_and_ancestors_dirty(dom_damage_map.keys().cloned());
230        let Some(damage_root) = damage_root else {
231            return;
232        };
233        let local_damage = damage_root.borrow_mut().update_subtree(
234            damage_root.clone(),
235            &mut dom_damage_map,
236            self,
237            update,
238        );
239
240        damage_root.borrow().update_ancestors(local_damage, update);
241    }
242
243    /// Given an iterator of `NodeId`s corresponding to nodes which have received some damage from
244    /// the DOM:
245    /// - mark each node as `dirty`;
246    /// - mark all of each node's ancestors as `has_dirty_descendants`;
247    /// - return the lowest common ancestor node of all the damaged nodes.
248    fn mark_nodes_and_ancestors_dirty(
249        &mut self,
250        mut dirty_node_ids: impl Iterator<Item = NodeId>,
251    ) -> Option<ArcRefCell<AccessibilityNode>> {
252        // An ordered list of common ancestors for the nodes seen so far, from shallowest to
253        // deepest. At the end of the loop, the lowest common ancestor is the last node in this vec.
254        let mut common_ancestors: Vec<NodeId> = Vec::new();
255
256        {
257            // Initialize the list of potential common ancestors.
258            let first_node = self.assert_node_for_id(&dirty_node_ids.next()?);
259            let mut first_node = first_node.borrow_mut();
260            first_node.dirty_state |= DirtyState::HasDamage;
261            common_ancestors.push(first_node.id);
262            common_ancestors.extend(first_node.ancestors().map(|ancestor| {
263                let mut ancestor = ancestor.borrow_mut();
264                ancestor.dirty_state |= DirtyState::DescendantHasDamage;
265                ancestor.id
266            }));
267            common_ancestors.reverse();
268        }
269
270        let mut truncate_ancestors = |node: &AccessibilityNode| -> bool {
271            if node.dirty_state.descendant_has_damage() {
272                if let Some(pos) = common_ancestors.iter().position(|&id| id == node.id) {
273                    common_ancestors.truncate(pos + 1);
274                }
275                return true;
276            }
277            false
278        };
279
280        for node_id in dirty_node_ids {
281            let node = self.assert_node_for_id(&node_id);
282            let mut node = node.borrow_mut();
283            node.dirty_state |= DirtyState::HasDamage;
284
285            if truncate_ancestors(&node) {
286                continue;
287            }
288
289            for ancestor in node.ancestors() {
290                let mut ancestor = ancestor.borrow_mut();
291
292                // If we find an ancestor we've already seen, discard any potential ancestors deeper
293                // than this one, and go on to the next dirty node.
294                if truncate_ancestors(&ancestor) {
295                    break;
296                }
297
298                ancestor.dirty_state |= DirtyState::DescendantHasDamage;
299            }
300        }
301
302        self.nodes.get(common_ancestors.last()?).cloned()
303    }
304
305    fn get_or_create_node(
306        &mut self,
307        dom_node: &ServoLayoutNode<'_>,
308        update: &mut AccessibilityUpdate,
309    ) -> (NodeId, ArcRefCell<AccessibilityNode>) {
310        let id = self.get_or_create_id_for_opaque(dom_node.opaque());
311        let node_ref = self.get_or_create_node_with_id(id, update);
312
313        if update.is_new(&id) {
314            let mut node = node_ref.borrow_mut();
315            node.opaque_node = Some(dom_node.opaque());
316            if let Some(dom_element) = dom_node.as_element() {
317                let local_name = dom_element.local_name().to_ascii_lowercase();
318                node.set_html_tag(&local_name);
319            }
320        }
321
322        (id, node_ref)
323    }
324
325    fn get_or_create_node_with_id(
326        &mut self,
327        id: NodeId,
328        update: &mut AccessibilityUpdate,
329    ) -> ArcRefCell<AccessibilityNode> {
330        if let Some(node) = self.nodes.get(&id) {
331            return node.clone();
332        }
333
334        let node = ArcRefCell::new(AccessibilityNode::new(id));
335        update.set_tree_state_change(id, TreeChange::New);
336        self.nodes.insert(id, node.clone());
337
338        node
339    }
340
341    fn node_for_id(&self, id: NodeId) -> Option<ArcRefCell<AccessibilityNode>> {
342        self.nodes.get(&id).cloned()
343    }
344
345    fn assert_node_for_id(&self, id: &NodeId) -> ArcRefCell<AccessibilityNode> {
346        let Some(node) = self.nodes.get(id) else {
347            panic!("{id:?} does not exist in tree");
348        };
349        node.clone()
350    }
351
352    /// Consume the [`AccessibilityUpdate`] by deleting all nodes it detected as being removed from
353    /// the tree.
354    fn drop_removed_nodes(&mut self, mut update: AccessibilityUpdate) {
355        let mut rooted_nodes = std::mem::take(&mut update.rooted_nodes);
356        if let Some(rooted_nodes) = rooted_nodes.as_mut() {
357            self.assert_removed_nodes_were_rooted(&update, rooted_nodes);
358        }
359
360        let mut ids_to_remove: Vec<_> = update
361            .tree_changes
362            .iter()
363            .filter_map(|(id, change)| match change {
364                TreeChange::Removed => Some(id),
365                TreeChange::PendingMove => None,
366                TreeChange::New => None,
367                TreeChange::Moved => None,
368            })
369            .cloned()
370            .collect();
371
372        while let Some(id) = ids_to_remove.pop() {
373            if update.tree_changes.get(&id) == Some(&TreeChange::PendingMove) {
374                // Mark the move as completed by marking the node as removed from its old position.
375                update.set_tree_state_change(id, TreeChange::Removed);
376
377                // Since this node is actually moved, don't continue removing its subtree.
378                continue;
379            }
380
381            if let Some(opaque_node) = self.id_to_opaque_node.remove(&id) {
382                self.opaque_node_to_id.remove(&opaque_node);
383            }
384            let node = self.nodes.remove(&id).expect("Node {id:?} already removed");
385            ids_to_remove.extend(node.borrow().child_ids());
386        }
387
388        update
389            .tree_changes
390            .drain()
391            .for_each(|(id, change)| match change {
392                TreeChange::PendingMove => unreachable!(
393                    "Pending move found for node id {id:?} when draining tree state changes"
394                ),
395                TreeChange::Removed => (),
396                TreeChange::New => (),
397                TreeChange::Moved => (),
398            });
399
400        if let Some(rooted_nodes) = rooted_nodes {
401            self.assert_remaining_rooted_nodes_not_in_tree(rooted_nodes);
402        }
403
404        if self
405            .debug
406            .is_enabled(DiagnosticsLoggingOption::AccessibilityTree)
407        {
408            self.print();
409        }
410
411        if pref!(expensive_accessibility_test_assertions_enabled) {
412            self.assert_integrity();
413        }
414    }
415
416    /// If we got `rooted_nodes` from the document's `AccessibilityData`, assert that every node we
417    /// marked as `TreeChange::Removed` during this update was rooted.
418    fn assert_removed_nodes_were_rooted(
419        &mut self,
420        update: &AccessibilityUpdate,
421        rooted_nodes: &mut FxHashSet<OpaqueNode>,
422    ) {
423        debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
424        for (id, change) in update.tree_changes.iter() {
425            if change == &TreeChange::Removed {
426                let Some(&opaque_node) = self.id_to_opaque_node.get(id) else {
427                    panic!("No opaque node found for removed node: id {id:?}");
428                };
429                assert!(
430                    rooted_nodes.remove(&opaque_node),
431                    "Node removed from accessibility tree wasn't rooted: id {id:?}"
432                );
433            };
434        }
435    }
436
437    /// If we got `rooted_nodes` from the document's `AccessibilityData`, assert that any nodes
438    /// which were rooted but not marked as `TreeChange::Removed` are no longer in the tree after
439    /// dropping all nodes which were removed from the tree. They may have been part of a subtree
440    /// which was marked `TreeChange::Removed` on an ancestor node, or may have never made it into
441    /// the accessibility tree to begin with.
442    fn assert_remaining_rooted_nodes_not_in_tree(&self, rooted_nodes: FxHashSet<OpaqueNode>) {
443        for leftover_node in rooted_nodes {
444            assert!(
445                !self.opaque_node_to_id.contains_key(&leftover_node),
446                "Found node removed from DOM tree but not accessibility tree: {:#x}",
447                leftover_node.0
448            );
449        }
450    }
451
452    fn get_or_create_id_for_opaque(&mut self, opaque: OpaqueNode) -> NodeId {
453        let id = self.opaque_node_to_id.entry(opaque).or_insert_with(|| {
454            static LAST_ID: AtomicU64 = AtomicU64::new(0);
455            let id = LAST_ID.fetch_add(1, atomic::Ordering::SeqCst).into();
456            self.id_to_opaque_node.insert(id, opaque);
457            id
458        });
459        *id
460    }
461
462    fn existing_id_for_opaque(&self, opaque: OpaqueNode) -> Option<NodeId> {
463        self.opaque_node_to_id.get(&opaque).cloned()
464    }
465
466    pub(crate) fn embedder_epoch(&self) -> Epoch {
467        self.embedder_epoch
468    }
469
470    /// Assert that the tree is a tree without any dangling references or orphaned nodes.
471    ///
472    /// For accessibility tests only, because it’s expensive.
473    fn assert_integrity(&self) {
474        debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
475        let Some(root_node) = self.root_node.clone() else {
476            return;
477        };
478
479        // Traverse the tree from the given root.
480        // `nodes` is a Vec of pairs of nodes and their expected parents.
481        let mut nodes = vec![(root_node, None)];
482        let mut seen_node_ids = FxHashSet::default();
483        while let Some((node, expected_parent)) = nodes.pop() {
484            let node = node.borrow();
485
486            // If this fails, then the tree is not a tree at all.
487            assert!(
488                seen_node_ids.insert(node.id),
489                "Tree contains {:?} in multiple places",
490                node.id
491            );
492
493            node.assert_integrity(expected_parent);
494
495            // assert_node_for_id() here double-checks that the node hasn't been incorrectly evicted
496            // from the map while it's still retained as a child node.
497            let weak_node = Some(self.assert_node_for_id(&node.id).downgrade());
498            nodes.extend(node.children().cloned().zip(repeat(weak_node)));
499        }
500
501        // If this fails, then the tree has orphaned nodes (a leak).
502        // If a node has been incorrectly removed from the map, that will be caught above.
503        assert_eq!(seen_node_ids, self.nodes.keys().copied().collect());
504    }
505
506    fn print(&self) {
507        let Some(root_node) = self.root_node.clone() else {
508            return;
509        };
510
511        let mut print_tree = PrintTree::new("Accessibility Tree");
512        root_node.borrow().print(&mut print_tree);
513        print_tree.end_level();
514    }
515}
516
517/// <https://w3c.github.io/aria/#host_general_role>
518fn role_from_role_attribute(dom_element: &ServoLayoutElement<'_>) -> Option<Role> {
519    let role_attribute = dom_element.attribute(&ns!(), &local_name!("role"))?;
520    role_attribute
521        .as_tokens()
522        .iter()
523        .filter_map(|role_name_in_attribute| SUPPORTED_ARIA_ROLES.get(role_name_in_attribute))
524        .next()
525        .cloned()
526}
527
528fn role_from_dom_node(dom_node: &ServoLayoutNode<'_>) -> Role {
529    if let Some(dom_element) = dom_node.as_element() {
530        role_from_role_attribute(&dom_element).unwrap_or_else(|| {
531            let local_name = dom_element.local_name().to_ascii_lowercase();
532            *HTML_ELEMENT_ROLE_MAPPINGS
533                .get(&local_name)
534                .unwrap_or(&Role::GenericContainer)
535        })
536    } else if dom_node.type_id() == Some(LayoutNodeType::Text) {
537        Role::TextRun
538    } else {
539        Role::GenericContainer
540    }
541}
542
543struct AccessibilityNodeIterator<I>
544where
545    I: Fn(&AccessibilityNode) -> Option<ArcRefCell<AccessibilityNode>>,
546{
547    next_value: Option<ArcRefCell<AccessibilityNode>>,
548    next_fn: I,
549}
550
551impl<I> AccessibilityNodeIterator<I>
552where
553    I: Fn(&AccessibilityNode) -> Option<ArcRefCell<AccessibilityNode>>,
554{
555    fn new(next_value: Option<ArcRefCell<AccessibilityNode>>, next_fn: I) -> Self {
556        AccessibilityNodeIterator {
557            next_value,
558            next_fn,
559        }
560    }
561}
562
563impl<I> Iterator for AccessibilityNodeIterator<I>
564where
565    I: Fn(&AccessibilityNode) -> Option<ArcRefCell<AccessibilityNode>>,
566{
567    type Item = ArcRefCell<AccessibilityNode>;
568
569    fn next(&mut self) -> Option<Self::Item> {
570        let next_value = self.next_value.take();
571        self.next_value = next_value
572            .as_ref()
573            .and_then(|node| (self.next_fn)(&node.borrow()));
574        next_value
575    }
576}
577
578impl AccessibilityNode {
579    fn new(id: NodeId) -> Self {
580        Self::new_with_role(id, Role::Unknown)
581    }
582
583    fn new_with_role(id: NodeId, role: Role) -> Self {
584        Self {
585            id,
586            accesskit_node: accesskit::Node::new(role),
587            parent_node: None,
588            child_nodes: vec![],
589            opaque_node: None,
590            dirty_state: DirtyState::empty(),
591        }
592    }
593
594    /// Update this node and its subtree based on damage from the DOM.
595    ///
596    /// - First, if this node has damage from the DOM to be resolved, update the node from the DOM
597    ///   tree, recursively populating any new children.
598    /// - Next, recursively call this method for any children which are dirty, or have dirty
599    ///   descendants.
600    /// - Finally, update any properties on this node which are may have changed due to other
601    ///   changes in the tree.
602    ///
603    /// At the end of this method, both `has_dirty_descendants` and `is_dirty` should be false for
604    /// this node and all its descendants.
605    fn update_subtree<'dom>(
606        &mut self,
607        ref_self: ArcRefCell<Self>,
608        dom_damage_map: &mut FxHashMap<NodeId, (ServoLayoutNode<'dom>, AccessibilityDamage)>,
609        tree: &mut AccessibilityTree,
610        update: &mut AccessibilityUpdate,
611    ) -> LocalAccessibilityDamage {
612        let mut local_damage = LocalAccessibilityDamage::empty();
613
614        if let Some((dom_node, dom_damage)) = dom_damage_map.get(&self.id) {
615            local_damage.insert(self.update_node_and_populate_new_descendants_from_dom_node(
616                ref_self,
617                dom_node,
618                *dom_damage,
619                tree,
620                update,
621            ));
622
623            self.dirty_state -= DirtyState::HasDamage;
624        }
625
626        if self.dirty_state.descendant_has_damage() {
627            for child_node in self.children() {
628                let strong_child_node = child_node.clone();
629                let mut child_node = child_node.borrow_mut();
630                if !child_node.dirty_state.self_or_descendant_has_damage() {
631                    continue;
632                }
633                let child_damage =
634                    child_node.update_subtree(strong_child_node, dom_damage_map, tree, update);
635                if !child_damage.is_empty() {
636                    local_damage.insert(LocalAccessibilityDamage::SubtreeChanged);
637                }
638            }
639
640            self.dirty_state -= DirtyState::DescendantHasDamage;
641        }
642
643        local_damage.insert(self.update_node_local(local_damage, update));
644
645        if self.dirty_state.updated() {
646            update.add(self);
647        }
648
649        local_damage
650    }
651
652    /// Update each of this node's ancestors based on changes which have already been applied in the
653    /// tree.
654    fn update_ancestors(
655        &self,
656        local_damage: LocalAccessibilityDamage,
657        update: &mut AccessibilityUpdate,
658    ) {
659        if local_damage.is_empty() {
660            return;
661        }
662        for node in self.ancestors() {
663            let mut node = node.borrow_mut();
664            node.update_node_local(LocalAccessibilityDamage::SubtreeChanged, update);
665            node.dirty_state -= DirtyState::DescendantHasDamage;
666            if node.dirty_state.updated() {
667                update.add(&mut node);
668            }
669        }
670    }
671
672    /// Update the given [`AccessibilityNode`] from its corresponding DOM node and
673    /// [`AccessibilityDamage`].
674    /// If it has new children, those will be recursively populated here.
675    // Any changed nodes will be added to the given [`AccessibilityUpdate`].
676    fn update_node_and_populate_new_descendants_from_dom_node<'dom>(
677        &mut self,
678        ref_self: ArcRefCell<Self>,
679        dom_node: &ServoLayoutNode<'dom>,
680        dom_damage: AccessibilityDamage,
681        tree: &mut AccessibilityTree,
682        update: &mut AccessibilityUpdate,
683    ) -> LocalAccessibilityDamage {
684        update.counters.nodes_updated_from_dom += 1;
685
686        let mut local_damage = LocalAccessibilityDamage::empty();
687
688        local_damage.insert(self.update_properties_from_dom_node(dom_node, dom_damage));
689        local_damage.insert(
690            self.update_children_and_populate_new_descendants_from_dom_node(
691                ref_self, dom_node, dom_damage, tree, update,
692            ),
693        );
694
695        local_damage
696    }
697
698    /// Update this node's [`Self::children`] from its corresponding DOM node. If any children are
699    /// newly added to the tree, populate them and recursively populate their children.
700    fn update_children_and_populate_new_descendants_from_dom_node<'dom>(
701        &mut self,
702        ref_self: ArcRefCell<AccessibilityNode>,
703        dom_node: &ServoLayoutNode<'dom>,
704        dom_damage: AccessibilityDamage,
705        tree: &mut AccessibilityTree,
706        update: &mut AccessibilityUpdate,
707    ) -> LocalAccessibilityDamage {
708        if !dom_damage.contains(AccessibilityDamage::Children) {
709            return LocalAccessibilityDamage::empty();
710        }
711
712        let mut remaining_dom_children = dom_node.flat_tree_children().peekable();
713        let mut old_child_ids = self.child_ids().iter().peekable();
714        let mut unchanged_count = 0usize;
715
716        // Iterate over existing children and DOM children while they match. No action is necessary
717        // for these nodes.
718        while let Some(&old_id) = old_child_ids.peek() &&
719            let Some(dom_child) = remaining_dom_children.peek()
720        {
721            if tree.existing_id_for_opaque(dom_child.opaque()) == Some(*old_id) {
722                unchanged_count += 1;
723                old_child_ids.next();
724                remaining_dom_children.next();
725            } else {
726                break;
727            }
728        }
729
730        // If we iterated over all the DOM children without finding any changes, we're done.
731        if old_child_ids.peek().is_none() && remaining_dom_children.peek().is_none() {
732            return LocalAccessibilityDamage::empty();
733        }
734
735        // Remove all child nodes after the first `unchanged_count`.
736        self.child_nodes.truncate(unchanged_count);
737        let mut new_child_ids = Vec::from(self.child_ids());
738        for removed_child_id in new_child_ids.split_off(unchanged_count) {
739            update.set_tree_state_change(removed_child_id, TreeChange::Removed);
740        }
741
742        // Then, (re-)add all the remaining DOM children. Note that this means that some children
743        // may end up being "Moved" even though they haven't changed parents, and may even be in the
744        // same position as previously.
745        let weak_self = ref_self.downgrade();
746        for dom_child in remaining_dom_children {
747            let (child_id, child_ref) = tree.get_or_create_node(&dom_child, update);
748
749            // Update self.child_nodes in place.
750            self.child_nodes.push(child_ref.clone());
751            new_child_ids.push(child_id);
752
753            let mut child = child_ref.borrow_mut();
754            child.parent_node = Some(weak_self.clone());
755
756            if update.is_new(&child_id) {
757                let child_damage = child.update_node_and_populate_new_descendants_from_dom_node(
758                    child_ref.clone(),
759                    &dom_child,
760                    AccessibilityDamage::Rebuild,
761                    tree,
762                    update,
763                );
764                child.update_node_local(child_damage, update);
765                update.add(&mut child);
766            } else {
767                update.set_tree_state_change(child_id, TreeChange::PendingMove);
768            }
769
770            self.dirty_state
771                .propagate_descendant_has_damage(child.dirty_state);
772        }
773
774        // We can't update the AccessKit node's `children` in place, so we build up the full list
775        // and then set it here.
776        self.accesskit_node.set_children(new_child_ids);
777        self.dirty_state |= DirtyState::Updated;
778
779        LocalAccessibilityDamage::SubtreeChanged
780    }
781
782    /// Update this node's properties from its corresponding DOM node.
783    fn update_properties_from_dom_node(
784        &mut self,
785        dom_node: &ServoLayoutNode<'_>,
786        dom_damage: AccessibilityDamage,
787    ) -> LocalAccessibilityDamage {
788        let mut local_damage = LocalAccessibilityDamage::empty();
789        if !dom_damage.contains(AccessibilityDamage::Node) {
790            return local_damage;
791        }
792        local_damage.insert(self.set_role(role_from_dom_node(dom_node)));
793        if dom_node.type_id() == Some(LayoutNodeType::Text) {
794            let text_content = dom_node.text_content();
795            trace!("node text content = {text_content:?}");
796            // FIXME: this should take into account editing selection units (grapheme clusters?)
797            local_damage.insert(self.set_value(&text_content));
798        }
799
800        local_damage
801    }
802
803    /// Update this node's properties based on changes already made to the accessibility tree.
804    /// For example, if there were nodes added or removed in its subtree, its computed text may have
805    /// changed, so that will be recomputed here.
806    /// If any changes are made, add this node to the given [`AccessibilityUpdate`].
807    fn update_node_local(
808        &mut self,
809        local_damage: LocalAccessibilityDamage,
810        update: &mut AccessibilityUpdate,
811    ) -> LocalAccessibilityDamage {
812        update.counters.nodes_updated_from_tree += 1;
813
814        let mut new_damage = LocalAccessibilityDamage::empty();
815        if local_damage.contains(LocalAccessibilityDamage::SubtreeChanged) ||
816            local_damage.contains(LocalAccessibilityDamage::RoleChanged)
817        {
818            if let Some(text) = self.label_from_descendants() {
819                new_damage.insert(self.set_label(text.as_str()));
820            } else {
821                new_damage.insert(self.clear_label());
822            }
823        }
824
825        new_damage
826    }
827
828    fn label_from_descendants(&self) -> Option<String> {
829        if !NAME_FROM_CONTENTS_ROLES.contains(&self.role()) {
830            return None;
831        }
832        let mut children = VecDeque::from_iter(self.children().cloned());
833        let mut text = String::new();
834        while let Some(child) = children.pop_front() {
835            let child = child.borrow();
836            match child.role() {
837                Role::TextRun => {
838                    if let Some(child_text) = child.value() {
839                        text.push_str(child_text);
840                    }
841                },
842                _ => {
843                    for node in child.children().rev() {
844                        children.push_front(node.clone());
845                    }
846                },
847            }
848        }
849        Some(text.trim().to_owned())
850    }
851
852    fn print(&self, print_tree: &mut PrintTree) {
853        if self.child_nodes.is_empty() {
854            print_tree.add_item(format!("{self:?}"));
855            return;
856        }
857
858        print_tree.new_level(format!("{self:?}"));
859
860        for child in self.children() {
861            child.borrow().print(print_tree);
862        }
863        print_tree.end_level();
864    }
865
866    fn parent(&self) -> Option<ArcRefCell<AccessibilityNode>> {
867        self.parent_node.as_ref().and_then(|weak| weak.upgrade())
868    }
869
870    // TODO: use macros to generate getter/setter methods.
871
872    fn children(&self) -> impl DoubleEndedIterator<Item = &ArcRefCell<AccessibilityNode>> {
873        self.child_nodes.iter()
874    }
875
876    fn ancestors(&self) -> impl Iterator<Item = ArcRefCell<AccessibilityNode>> {
877        AccessibilityNodeIterator::new(self.parent(), |node| node.parent_node.clone()?.upgrade())
878    }
879
880    fn child_ids(&self) -> &[NodeId] {
881        self.accesskit_node.children()
882    }
883
884    fn role(&self) -> Role {
885        self.accesskit_node.role()
886    }
887
888    fn set_role(&mut self, role: Role) -> LocalAccessibilityDamage {
889        if role == self.accesskit_node.role() {
890            return LocalAccessibilityDamage::empty();
891        }
892        self.accesskit_node.set_role(role);
893        self.dirty_state |= DirtyState::Updated;
894        LocalAccessibilityDamage::RoleChanged
895    }
896
897    fn label(&self) -> Option<&str> {
898        self.accesskit_node.label()
899    }
900
901    fn set_label(&mut self, label: &str) -> LocalAccessibilityDamage {
902        if Some(label) == self.accesskit_node.label() {
903            return LocalAccessibilityDamage::empty();
904        }
905        self.accesskit_node.set_label(label);
906        self.dirty_state |= DirtyState::Updated;
907        LocalAccessibilityDamage::TextChanged
908    }
909
910    fn clear_label(&mut self) -> LocalAccessibilityDamage {
911        if self.accesskit_node.label().is_none() {
912            return LocalAccessibilityDamage::empty();
913        }
914        self.accesskit_node.clear_label();
915        self.dirty_state |= DirtyState::Updated;
916        LocalAccessibilityDamage::TextChanged
917    }
918
919    fn html_tag(&self) -> Option<&str> {
920        self.accesskit_node.html_tag()
921    }
922
923    fn set_html_tag(&mut self, html_tag: &str) {
924        if Some(html_tag) == self.accesskit_node.html_tag() {
925            return;
926        }
927        self.accesskit_node.set_html_tag(html_tag);
928        self.dirty_state |= DirtyState::Updated;
929    }
930
931    fn value(&self) -> Option<&str> {
932        self.accesskit_node.value()
933    }
934
935    fn set_value(&mut self, value: &str) -> LocalAccessibilityDamage {
936        if Some(value) == self.accesskit_node.value() {
937            return LocalAccessibilityDamage::empty();
938        }
939        self.accesskit_node.set_value(value);
940        self.dirty_state |= DirtyState::Updated;
941        LocalAccessibilityDamage::TextChanged
942    }
943
944    fn assert_integrity(&self, expected_parent: Option<WeakRefCell<AccessibilityNode>>) {
945        debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
946
947        if let Some(actual_parent) = &self.parent_node {
948            let expected = expected_parent.expect("Actual parent but no expected parent");
949            let expected = expected.upgrade().expect("Expected parent was dropped");
950            let actual = actual_parent.upgrade().expect("Actual parent was dropped");
951            assert!(actual.ptr_eq(&expected));
952        } else {
953            assert!(
954                expected_parent.is_none(),
955                "Expected parent but no actual parent"
956            );
957        }
958
959        assert!(
960            self.dirty_state.is_empty(),
961            "{self:?} has dirty state {:?}",
962            self.dirty_state
963        );
964
965        let children_ids: Vec<_> = self.children().map(|child| child.borrow().id).collect();
966        assert_eq!(
967            children_ids,
968            self.child_ids(),
969            "children() IDs didn't match child_ids() for {self:?}"
970        );
971    }
972}
973
974impl Debug for AccessibilityNode {
975    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
976        write!(f, "{:?}: {:?}", self.id, self.role())?;
977        if let Some(html_tag) = self.html_tag() {
978            write!(f, " (html_tag: {html_tag:?})")?;
979        }
980        if let Some(label) = self.label() {
981            write!(f, "\nlabel: {label:?}")?;
982        }
983        if !self.child_ids().is_empty() {
984            write!(f, "\nchildren: {:?}", self.child_ids())?;
985        }
986        Ok(())
987    }
988}
989
990impl AccessibilityUpdate {
991    fn new(rooted_nodes: Option<FxHashSet<OpaqueNode>>) -> Self {
992        Self {
993            changed_nodes: FxHashSet::default(),
994            tree_changes: FxHashMap::default(),
995            counters: UpdateCounters::default(),
996            rooted_nodes,
997        }
998    }
999
1000    fn add(&mut self, node: &mut AccessibilityNode) {
1001        self.changed_nodes.insert(node.id);
1002        node.dirty_state -= DirtyState::Updated;
1003    }
1004
1005    fn set_tree_state_change(&mut self, node_id: NodeId, change: TreeChange) {
1006        let old_change = self.tree_changes.get(&node_id);
1007
1008        assert!(
1009            change != TreeChange::Moved,
1010            "Incoming change must never be Moved"
1011        );
1012
1013        let resolved_change = old_change
1014            .map(|old_change| match (old_change, change) {
1015                (TreeChange::PendingMove, TreeChange::Removed) => TreeChange::Moved,
1016                (TreeChange::Removed, TreeChange::PendingMove) => TreeChange::Moved,
1017                _ => {
1018                    unreachable!("Logically impossible state change: {old_change:?} → {change:?}")
1019                },
1020            })
1021            .unwrap_or(change);
1022
1023        self.tree_changes.insert(node_id, resolved_change);
1024    }
1025
1026    fn is_new(&mut self, node_id: &NodeId) -> bool {
1027        self.tree_changes.get(node_id) == Some(&TreeChange::New)
1028    }
1029
1030    /// Consume this `AccessibilityUpdate`, producing an [`accesskit::TreeUpdate`] if there have
1031    /// been any changes to `tree`.
1032    /// This will pass `self` into [`AccessibilityTree::remove_stale_nodes()`] to consume
1033    /// [`Self::tree_changes`].
1034    fn finalize(
1035        mut self,
1036        tree: &mut AccessibilityTree,
1037    ) -> (Option<accesskit::TreeUpdate>, UpdateCounters) {
1038        let root_node_id = tree
1039            .root_node
1040            .clone()
1041            .expect("AccessibilityUpdate::finalize() called but no root_node set in tree")
1042            .borrow()
1043            .id;
1044
1045        if self.changed_nodes.is_empty() {
1046            assert!(self.tree_changes.is_empty());
1047            return (None, self.counters);
1048        }
1049
1050        let changed_nodes = std::mem::take(&mut self.changed_nodes);
1051        let mut counters = std::mem::take(&mut self.counters);
1052
1053        tree.drop_removed_nodes(self);
1054
1055        let changed_nodes: Vec<_> = changed_nodes
1056            .into_iter()
1057            .filter_map(|id| Some((id, tree.node_for_id(id)?.borrow().accesskit_node.clone())))
1058            .collect();
1059
1060        counters.nodes_in_tree_update = changed_nodes.len().try_into().unwrap_or_default();
1061
1062        let accesskit_tree = accesskit::Tree::new(root_node_id);
1063        let tree_update = accesskit::TreeUpdate {
1064            // Filter out any nodes which were both changed and removed.
1065            nodes: changed_nodes,
1066            tree: Some(accesskit_tree),
1067            focus: NodeId(1),
1068            tree_id: tree.tree_id,
1069        };
1070
1071        (Some(tree_update), counters)
1072    }
1073}
1074
1075impl DirtyState {
1076    fn updated(&self) -> bool {
1077        self.contains(DirtyState::Updated)
1078    }
1079
1080    fn descendant_has_damage(&self) -> bool {
1081        self.contains(DirtyState::DescendantHasDamage)
1082    }
1083
1084    fn propagate_descendant_has_damage(&mut self, child_dirty_state: DirtyState) {
1085        if child_dirty_state.self_or_descendant_has_damage() {
1086            self.insert(DirtyState::DescendantHasDamage)
1087        }
1088    }
1089
1090    fn self_or_descendant_has_damage(&self) -> bool {
1091        self.intersects(DirtyState::HasDamage | DirtyState::DescendantHasDamage)
1092    }
1093}
1094
1095#[cfg(test)]
1096#[test]
1097fn test_accessibility_update_add_some_nodes_twice() {
1098    let mut tree = AccessibilityTree::new(accesskit::TreeId::ROOT, Epoch::default());
1099    let mut root_update = AccessibilityUpdate::new(None);
1100
1101    let root_node = tree.get_or_create_node_with_id(NodeId(2), &mut root_update);
1102    tree.root_node = Some(root_node.clone());
1103
1104    let nodes: Vec<_> = [
1105        (3, Role::GenericContainer),
1106        (4, Role::Heading),
1107        (5, Role::Paragraph),
1108    ]
1109    .into_iter()
1110    .map(|(id, role)| {
1111        let id = NodeId(id);
1112        let node = tree.get_or_create_node_with_id(id, &mut root_update);
1113        node.borrow_mut().set_role(role);
1114        (id, node)
1115    })
1116    .collect();
1117
1118    {
1119        let (child_node_ids, child_nodes): (Vec<_>, Vec<_>) = nodes.iter().cloned().unzip();
1120        let mut root_node = root_node.borrow_mut();
1121        root_node.accesskit_node.set_children(child_node_ids);
1122        root_node.child_nodes = child_nodes;
1123    }
1124
1125    let mut update = AccessibilityUpdate::new(None);
1126
1127    {
1128        let node_3 = tree.assert_node_for_id(&NodeId(3));
1129        let mut node_3 = node_3.borrow_mut();
1130        let node_4 = tree.assert_node_for_id(&NodeId(4));
1131        let mut node_4 = node_4.borrow_mut();
1132        let node_5 = tree.assert_node_for_id(&NodeId(5));
1133        let mut node_5 = node_5.borrow_mut();
1134
1135        update.add(&mut node_5);
1136        update.add(&mut node_3);
1137        update.add(&mut node_4);
1138        update.add(&mut node_4);
1139
1140        node_3.set_role(Role::ScrollView);
1141        update.add(&mut node_3);
1142    }
1143
1144    let (tree_update, _) = update.finalize(&mut tree);
1145    let mut tree_update = tree_update.expect("finalize should produce a tree update");
1146    tree_update.nodes.sort_by_key(|(node_id, _node)| *node_id);
1147    assert_eq!(
1148        tree_update,
1149        accesskit::TreeUpdate {
1150            nodes: vec![
1151                (NodeId(3), accesskit::Node::new(Role::ScrollView)),
1152                (NodeId(4), accesskit::Node::new(Role::Heading)),
1153                (NodeId(5), accesskit::Node::new(Role::Paragraph)),
1154            ],
1155            tree: Some(accesskit::Tree {
1156                root: NodeId(2),
1157                toolkit_name: None,
1158                toolkit_version: None
1159            }),
1160            tree_id: accesskit::TreeId::ROOT,
1161            focus: NodeId(1),
1162        }
1163    );
1164}
1165
1166static HTML_ELEMENT_ROLE_MAPPINGS: LazyLock<FxHashMap<LocalName, Role>> = LazyLock::new(|| {
1167    [
1168        (local_name!("article"), Role::Article),
1169        (local_name!("aside"), Role::Complementary),
1170        (local_name!("body"), Role::RootWebArea),
1171        (local_name!("footer"), Role::ContentInfo),
1172        (local_name!("h1"), Role::Heading),
1173        (local_name!("h2"), Role::Heading),
1174        (local_name!("h3"), Role::Heading),
1175        (local_name!("h4"), Role::Heading),
1176        (local_name!("h5"), Role::Heading),
1177        (local_name!("h6"), Role::Heading),
1178        (local_name!("header"), Role::Banner),
1179        (local_name!("hr"), Role::Splitter),
1180        (local_name!("main"), Role::Main),
1181        (local_name!("nav"), Role::Navigation),
1182        (local_name!("p"), Role::Paragraph),
1183    ]
1184    .into_iter()
1185    .collect()
1186});
1187
1188/// A map from role names allowed in the 'role' attribute of an HTML element to the corresponding
1189/// [`Role`] in AccessKit.
1190///
1191/// This is currently just the roles that don't have any [supported][1] or [required][2] properties
1192/// and also don't require an [accessible name][3].
1193/// [1]: https://w3c.github.io/aria/#supportedState
1194/// [2]: https://w3c.github.io/aria/#requiredState
1195/// [3]: https://w3c.github.io/aria/#namefromauthor
1196static SUPPORTED_ARIA_ROLES: LazyLock<FxHashMap<Atom, Role>> = LazyLock::new(|| {
1197    [
1198        (Atom::from("alert"), Role::Alert),
1199        (Atom::from("banner"), Role::Banner),
1200        (Atom::from("blockquote"), Role::Blockquote),
1201        (Atom::from("caption"), Role::Caption),
1202        (Atom::from("code"), Role::Code),
1203        (Atom::from("complementary"), Role::Complementary),
1204        (Atom::from("contentinfo"), Role::ContentInfo),
1205        (Atom::from("definition"), Role::Definition),
1206        (Atom::from("deletion"), Role::ContentDeletion),
1207        (Atom::from("directory"), Role::Unknown),
1208        (Atom::from("document"), Role::Document),
1209        (Atom::from("emphasis"), Role::Emphasis),
1210        (Atom::from("feed"), Role::Feed),
1211        (Atom::from("figure"), Role::Figure),
1212        (Atom::from("generic"), Role::GenericContainer),
1213        (Atom::from("insertion"), Role::ContentInsertion),
1214        (Atom::from("list"), Role::List),
1215        (Atom::from("log"), Role::Log),
1216        (Atom::from("main"), Role::Main),
1217        (Atom::from("math"), Role::Math),
1218        (Atom::from("navigation"), Role::Navigation),
1219        (Atom::from("none"), Role::GenericContainer),
1220        (Atom::from("note"), Role::Note),
1221        (Atom::from("paragraph"), Role::Paragraph),
1222        (Atom::from("presentation"), Role::GenericContainer),
1223        (Atom::from("rowgroup"), Role::RowGroup),
1224        (Atom::from("search"), Role::Search),
1225        (Atom::from("status"), Role::Status),
1226        (Atom::from("strong"), Role::Strong),
1227        // (Atom::from("subscript"), Role::Subscript), // no corresponding accesskit role.
1228        // (Atom::from("superscript"), Role::Superscript), // no corresponding accesskit role.
1229        (Atom::from("term"), Role::Term),
1230        (Atom::from("time"), Role::Time),
1231        (Atom::from("timer"), Role::Timer),
1232    ]
1233    .into_iter()
1234    .collect()
1235});
1236
1237/// <https://w3c.github.io/aria/#namefromcontent>
1238static NAME_FROM_CONTENTS_ROLES: LazyLock<FxHashSet<Role>> =
1239    LazyLock::new(|| [(Role::Heading)].into_iter().collect());