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::ServoLayoutNode;
16use servo_base::Epoch;
17use servo_base::print_tree::PrintTree;
18use servo_config::opts::{self, DiagnosticsLogging, DiagnosticsLoggingOption};
19use servo_config::pref;
20use style::dom::OpaqueNode;
21use web_atoms::{LocalName, local_name};
22
23use crate::ArcRefCell;
24use crate::cell::WeakRefCell;
25
26bitflags! {
27    /// Damage which was caused by changes to the accessibility tree. These changes can cause other
28    /// properties to need to be re-computed based on the updated values, either on the same node or
29    /// on other nodes.
30    #[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
31    struct LocalAccessibilityDamage: u16 {
32        /// This node's children changed, and/or any node in its subtree changed.
33        const SubtreeChanged = 0b0001;
34        /// This node's computed role changed.
35        const RoleChanged = 0b0010;
36        /// This node's computed label or text value (for a text node) changed.
37        const TextChanged = 0b0100;
38    }
39}
40
41/// Changes which have occurred during the current update.
42struct AccessibilityUpdate {
43    /// Nodes whose internal data has changed within the current update.
44    changed_nodes: FxHashSet<NodeId>,
45    /// Nodes that changed their relation to the tree within the current update.
46    tree_changes: FxHashMap<NodeId, TreeChange>,
47    /// Damage to nodes caused by changes in the accessibility tree.
48    unresolved_local_damage: FxHashMap<NodeId, LocalAccessibilityDamage>,
49    /// Nodes which were removed from the DOM tree since the last reflow, which were rooted in
50    /// `AccessibilityData`. Only set if `pref::expensive_accessibility_test_assertions_enabled`
51    /// is set.
52    rooted_nodes: Option<FxHashSet<OpaqueNode>>,
53}
54
55struct AccessibilityNode {
56    /// The unique ID for the node. This is used both as a key in [`AccessibilityTree`]'s cache of
57    /// nodes, and as an identifier in [`accesskit`] datastructures: [`accesskit::Node`]s,
58    /// [`accesskit::TreeUpdate`]s and [`accesskit::ActionRequest`]s.
59    id: NodeId,
60    /// The computed [`accesskit::Node`] data. This will be copied and serialized into a
61    /// [`accesskit::TreeUpdate`] whenever it is changed during an update.
62    accesskit_node: accesskit::Node,
63    /// This node's parent, if any.
64    parent_node: Option<WeakRefCell<AccessibilityNode>>,
65    /// All this node's children.
66    child_nodes: Vec<ArcRefCell<AccessibilityNode>>,
67    /// The [`OpaqueNode`] for the DOM node which corresponds to this accessibility node, if any.
68    /// An accessibility node may not correspond to a DOM node if it corresponds to a
69    /// pseudo-element, or in a test.
70    opaque_node: Option<OpaqueNode>,
71    /// Whether this node has been updated in the current tree update. This is reset to `false`
72    /// when the node is added to the [`AccessibilityUpdate`] - see [`AccessibilityUpdate::add()`].
73    updated: bool,
74}
75
76/// A retained, internal representation of the accessibility tree for a document.
77///
78/// [`accesskit`] only provides interchange types for tree updates and action requests, so we need
79/// to define our own representation for incremental tree building.
80#[derive(Debug)]
81pub struct AccessibilityTree {
82    /// All nodes currently in the tree as of the most recent update. New nodes are added and stale
83    /// nodes are pruned during [`AccessibilityTree::update_tree()`].
84    nodes: FxHashMap<NodeId, ArcRefCell<AccessibilityNode>>,
85    /// A map to allow retrieving the [`AccessibilityNode`] which corresponds to a particular DOM
86    /// node, if any.
87    ///
88    /// This must be kept in sync with [`Self::id_to_opaque_node`].
89    opaque_node_to_id: FxHashMap<OpaqueNode, NodeId>,
90    /// A map to retrieve the `OpaqueNode` corresponding to a particular [`AccessibilityNode`], if
91    /// any.
92    ///
93    /// This must be kept in sync with [`Self::opaque_node_to_id`].
94    id_to_opaque_node: FxHashMap<NodeId, OpaqueNode>,
95    /// Sent with each [`accesskit::TreeUpdate`]. This allows this tree to be
96    /// [grafted](https://docs.rs/accesskit/latest/accesskit/struct.Node.html#method.tree_id) into
97    /// an application's tree.
98    tree_id: accesskit::TreeId,
99    /// This node's ID is sent with each [`accesskit::TreeUpdate`] to identify the root node.
100    /// Also used for any complete tree walk, such as in [`Self::assert_integrity()`] and
101    /// [`Self::print()`].
102    root_node: Option<ArcRefCell<AccessibilityNode>>,
103    /// Sent to the embedder alongside each [`accesskit::TreeUpdate`], so that the embedder can
104    /// drop updates from documents which have been navigated away from.
105    embedder_epoch: Epoch,
106    /// Debug options, copied from configuration to this `AccessibilityTree` in order
107    /// to avoid having to constantly access the thread-safe global options.
108    debug: DiagnosticsLogging,
109}
110
111/// Tracks changes to a node's relation to the tree within an update.
112///
113/// This is used to remove nodes from the accessibility tree's cache when they are no longer in the
114/// tree.
115#[derive(Debug, PartialEq, Copy, Clone)]
116enum TreeChange {
117    /// The node was newly created in this update.
118    New,
119
120    /// The node has been re-parented in this update.
121    Moved,
122
123    /// The node has been added to its new parent, but not yet removed from its old
124    /// parent.
125    ///
126    /// When a node is moved within the tree, it must be both removed from its old parent
127    /// and added to its new parent within the same update. This may happen in either
128    /// order, depending on the relative positions of the node before and after it moves.
129    ///
130    /// - If a node's new parent is updated before its old parent, the node will be in a
131    ///   `TreeChange::PendingMove` state until its old parent is updated. We expect that it
132    ///   must later be removed from its old parent, at which point its state will be updated to
133    ///   `TreeChange::Moved`.
134    /// - If a node's old parent is updated before its new parent, the node will be first
135    ///   `TreeChange::Removed` and then `TreeChange::Moved`.
136    ///
137    /// At the end of the update, we assert that there are no pending moves remaining.
138    PendingMove,
139
140    /// The node is no longer a child of its previous parent.
141    Removed,
142}
143
144impl AccessibilityTree {
145    /// See [`Self::tree_id`] and [`Self::embedder_epoch`] for explanations of the parameters.
146    pub(super) fn new(tree_id: accesskit::TreeId, embedder_epoch: Epoch) -> Self {
147        Self {
148            nodes: FxHashMap::default(),
149            opaque_node_to_id: FxHashMap::default(),
150            id_to_opaque_node: FxHashMap::default(),
151            tree_id,
152            root_node: None,
153            embedder_epoch,
154            debug: opts::get().debug.clone(),
155        }
156    }
157
158    /// Update this tree based on the current state of the given DOM tree, and if anything changed,
159    /// return an [`accesskit::TreeUpdate`] representing what changed.
160    pub(super) fn update_tree<'dom>(
161        &mut self,
162        root_dom_node: &ServoLayoutNode<'dom>,
163        mut damage_from_dom: VecDeque<(ServoLayoutNode<'dom>, AccessibilityDamage)>,
164        rooted_nodes: Option<FxHashSet<OpaqueNode>>,
165    ) -> Option<accesskit::TreeUpdate> {
166        let mut update = AccessibilityUpdate::new(rooted_nodes);
167
168        self.ensure_root_node(root_dom_node, &mut damage_from_dom, &mut update);
169
170        self.apply_changes_from_dom_tree(damage_from_dom, &mut update);
171
172        // FIXME: This assumes any local subtree damage always propagates up to the root. This is
173        // currently true, but we might be able to improve at stopping propagation.
174        self.resolve_local_damage_for_node_and_subtree(self.assert_root_node(), &mut update);
175
176        update.finalize(self)
177    }
178
179    /// Get the node corresponding to the root DOM node, and set it as this tree's root. If the root
180    /// node is newly created, which probably means this accessibility tree is newly created, append
181    /// an `AccessibilityDamage::REBUILD` value for it to `damage_from_dom`.
182    fn ensure_root_node<'dom>(
183        &mut self,
184        root_dom_node: &ServoLayoutNode<'dom>,
185        damage_from_dom: &mut VecDeque<(ServoLayoutNode<'dom>, AccessibilityDamage)>,
186        update: &mut AccessibilityUpdate,
187    ) {
188        let (root_id, root_node) = self.get_or_create_node(root_dom_node, update);
189        if update.is_new(&root_id) {
190            damage_from_dom.push_front((*root_dom_node, AccessibilityDamage::Rebuild));
191        }
192        self.root_node = Some(root_node);
193    }
194
195    /// Get the root node for this tree, asserting that it has been set.
196    fn assert_root_node(&self) -> ArcRefCell<AccessibilityNode> {
197        self.root_node.clone().expect("Root node was asserted")
198    }
199
200    /// For each DOM node in `damage_from_dom`, update the corresponding accessibility node based on
201    /// its `AccessibilityDamage`. If any [`LocalDamage`] results from the update, propagate
202    /// [`LocalDamage::SUBTREE_CHANGED`] to its ancestors.
203    fn apply_changes_from_dom_tree<'dom>(
204        &mut self,
205        mut damage_from_dom: VecDeque<(ServoLayoutNode<'dom>, AccessibilityDamage)>,
206        update: &mut AccessibilityUpdate,
207    ) {
208        while let Some((dom_node, dom_node_damage)) = damage_from_dom.pop_front() {
209            let Some((_, node)) = self.node_for_dom_node(&dom_node) else {
210                // If we don't have a node for this DOM node yet, it will be created and populated
211                // when it's added to its parent node.
212                continue;
213            };
214            self.update_node_and_descendants_from_dom_node(
215                node.clone(),
216                &dom_node,
217                dom_node_damage,
218                update,
219            );
220        }
221
222        self.propagate_subtree_damage_to_ancestors(update);
223    }
224
225    /// After applying changes from the DOM tree, mark the ancestors of any changed nodes with
226    /// [`LocalDamage::SubtreeChanged`].
227    fn propagate_subtree_damage_to_ancestors(&mut self, update: &mut AccessibilityUpdate) {
228        for (node_id, damage) in update.unresolved_local_damage.clone() {
229            if damage.is_empty() {
230                continue;
231            }
232            let node = self.assert_node_for_id(&node_id);
233
234            let mut parent_node = node.borrow().parent();
235            while let Some(node) = parent_node {
236                let node = node.borrow();
237                let existing_damage = update.unresolved_local_damage.entry(node.id).or_default();
238
239                // If we encounter a node which already has `SubtreeChanged` damage, we know that
240                // all of its ancestors have it too, so we can bail out early from our ancestor walk
241                if existing_damage.contains(LocalAccessibilityDamage::SubtreeChanged) {
242                    break;
243                }
244
245                existing_damage.insert(LocalAccessibilityDamage::SubtreeChanged);
246                parent_node = node.parent();
247            }
248        }
249    }
250
251    /// Update the given AccessibilityNode from its corresponding DOM node and
252    /// ['AccessibilityDamage'].
253    /// If it has new children, those will be recursively populated here.
254    // Any changed nodes will be added to the given [`AccessibilityUpdate`].
255    fn update_node_and_descendants_from_dom_node(
256        &mut self,
257        node: ArcRefCell<AccessibilityNode>,
258        dom_node: &ServoLayoutNode<'_>,
259        dom_damage: AccessibilityDamage,
260        update: &mut AccessibilityUpdate,
261    ) -> LocalAccessibilityDamage {
262        let weak_node = node.downgrade();
263        let mut node = node.borrow_mut();
264        let mut local_damage = LocalAccessibilityDamage::empty();
265
266        local_damage.insert(node.update_node_from_dom_node(dom_node, dom_damage));
267        local_damage.insert(
268            node.update_descendants_from_dom_node(weak_node, dom_node, dom_damage, self, update),
269        );
270
271        if node.updated {
272            update.add(&mut node);
273        }
274
275        if !local_damage.is_empty() {
276            update
277                .unresolved_local_damage
278                .entry(node.id)
279                .or_default()
280                .insert(local_damage);
281        }
282
283        local_damage
284    }
285
286    /// Update the given node and, where necessary, its descendants, based on damage propagated
287    /// within the accessibility as a result of changes made based on DOM tree changes.
288    /// For example, if a node's descendants changed as a result of the DOM tree changing, its
289    /// computed text may also have changed, so it would have had
290    /// [`LocalAccessibilityDamage::SubtreeChanged`] set when changes from the DOM tree were
291    /// applied. That damage is resolved here.
292    fn resolve_local_damage_for_node_and_subtree(
293        &mut self,
294        node: ArcRefCell<AccessibilityNode>,
295        update: &mut AccessibilityUpdate,
296    ) {
297        let mut node = node.borrow_mut();
298        let Some(&local_damage) = update.unresolved_local_damage.get(&node.id) else {
299            return;
300        };
301
302        node.update_node_local(local_damage);
303
304        if local_damage.contains(LocalAccessibilityDamage::SubtreeChanged) {
305            for child in node.children() {
306                self.resolve_local_damage_for_node_and_subtree(child.clone(), update);
307            }
308        }
309
310        if node.updated {
311            update.add(&mut node);
312        }
313
314        update.unresolved_local_damage.remove(&node.id);
315    }
316
317    fn get_or_create_node(
318        &mut self,
319        dom_node: &ServoLayoutNode<'_>,
320        update: &mut AccessibilityUpdate,
321    ) -> (NodeId, ArcRefCell<AccessibilityNode>) {
322        let id = self.get_or_create_id_for_opaque(dom_node.opaque());
323        let node = self.get_or_create_node_with_id(id, update);
324
325        {
326            let mut node = node.borrow_mut();
327            node.opaque_node = Some(dom_node.opaque());
328            if let Some(dom_element) = dom_node.as_element() {
329                let local_name = dom_element.local_name().to_ascii_lowercase();
330                node.set_html_tag(&local_name);
331            }
332        }
333
334        (id, node)
335    }
336
337    fn get_or_create_node_with_id(
338        &mut self,
339        id: NodeId,
340        update: &mut AccessibilityUpdate,
341    ) -> ArcRefCell<AccessibilityNode> {
342        if let Some(node) = self.nodes.get(&id) {
343            return node.clone();
344        }
345
346        let node = ArcRefCell::new(AccessibilityNode::new(id));
347        update.set_tree_state_change(id, TreeChange::New);
348        self.nodes.insert(id, node.clone());
349
350        node
351    }
352
353    fn node_for_id(&self, id: NodeId) -> Option<ArcRefCell<AccessibilityNode>> {
354        self.nodes.get(&id).cloned()
355    }
356
357    fn node_for_dom_node(
358        &self,
359        dom_node: &ServoLayoutNode<'_>,
360    ) -> Option<(NodeId, ArcRefCell<AccessibilityNode>)> {
361        let id = self.existing_id_for_opaque(dom_node.opaque())?;
362        Some((id, self.node_for_id(id)?))
363    }
364
365    fn assert_node_for_id(&self, id: &NodeId) -> ArcRefCell<AccessibilityNode> {
366        let Some(node) = self.nodes.get(id) else {
367            panic!("{id:?} does not exist in tree");
368        };
369        node.clone()
370    }
371
372    /// Consume the [`AccessibilityUpdate`] by deleting all nodes it detected as being removed from
373    /// the tree.
374    fn remove_stale_nodes(&mut self, mut update: AccessibilityUpdate) {
375        if let Some(rooted_nodes) = std::mem::take(&mut update.rooted_nodes) {
376            self.assert_removed_nodes_were_rooted(&update, rooted_nodes);
377        }
378
379        for id in update
380            .tree_changes
381            .drain()
382            .filter_map(|(id, change)| match change {
383                TreeChange::PendingMove => {
384                    unreachable!(
385                        "Pending move found for node id {id:?} when draining tree state changes"
386                    );
387                },
388                TreeChange::Removed => Some(id),
389                _ => None,
390            })
391        {
392            let _node = self.nodes.remove(&id);
393            {
394                #[cfg(debug_assertions)]
395                assert!(_node.is_some(), "Node for id {id:?} was already removed");
396            }
397            if let Some(opaque_node) = self.id_to_opaque_node.remove(&id) {
398                self.opaque_node_to_id.remove(&opaque_node);
399            }
400        }
401
402        if self
403            .debug
404            .is_enabled(DiagnosticsLoggingOption::AccessibilityTree)
405        {
406            self.print();
407        }
408
409        if pref!(expensive_accessibility_test_assertions_enabled) {
410            self.assert_integrity();
411        }
412    }
413
414    /// If we got `rooted_nodes` from the document's `AccessibilityData`, assert that every node we
415    /// removed during this update was rooted, and any leftover rooted nodes were never known to the
416    /// accessibility tree.
417    fn assert_removed_nodes_were_rooted(
418        &mut self,
419        update: &AccessibilityUpdate,
420        mut rooted_nodes: FxHashSet<OpaqueNode>,
421    ) {
422        debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
423        for (id, change) in update.tree_changes.iter() {
424            if change == &TreeChange::Removed {
425                let Some(&opaque_node) = self.id_to_opaque_node.get(id) else {
426                    panic!("No opaque node found for removed node: id {id:?}");
427                };
428                assert!(
429                    rooted_nodes.remove(&opaque_node),
430                    "Node removed from accessibility tree wasn't rooted: id {id:?}"
431                );
432            };
433        }
434
435        for leftover_node in rooted_nodes {
436            assert!(
437                !self.opaque_node_to_id.contains_key(&leftover_node),
438                "Found node removed from DOM tree but not accessibility tree"
439            );
440        }
441    }
442
443    fn get_or_create_id_for_opaque(&mut self, opaque: OpaqueNode) -> NodeId {
444        let id = self.opaque_node_to_id.entry(opaque).or_insert_with(|| {
445            static LAST_ID: AtomicU64 = AtomicU64::new(0);
446            let id = LAST_ID.fetch_add(1, atomic::Ordering::SeqCst).into();
447            self.id_to_opaque_node.insert(id, opaque);
448            id
449        });
450        *id
451    }
452
453    fn existing_id_for_opaque(&self, opaque: OpaqueNode) -> Option<NodeId> {
454        self.opaque_node_to_id.get(&opaque).cloned()
455    }
456
457    pub(crate) fn embedder_epoch(&self) -> Epoch {
458        self.embedder_epoch
459    }
460
461    /// Assert that the tree is a tree without any dangling references or orphaned nodes.
462    ///
463    /// For accessibility tests only, because it’s expensive.
464    fn assert_integrity(&self) {
465        debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
466        let Some(root_node) = self.root_node.clone() else {
467            return;
468        };
469
470        // Traverse the tree from the given root.
471        // `nodes` is a Vec of pairs of nodes and their expected parents.
472        let mut nodes = vec![(root_node, None)];
473        let mut seen_node_ids = FxHashSet::default();
474        while let Some((node, expected_parent)) = nodes.pop() {
475            let node = node.borrow();
476
477            // If this fails, then the tree is not a tree at all.
478            assert!(
479                seen_node_ids.insert(node.id),
480                "Tree contains {:?} in multiple places",
481                node.id
482            );
483
484            node.assert_integrity(expected_parent);
485
486            // assert_node_for_id() here double-checks that the node hasn't been incorrectly evicted
487            // from the map while it's still retained as a child node.
488            let weak_node = Some(self.assert_node_for_id(&node.id).downgrade());
489            nodes.extend(node.children().iter().cloned().zip(repeat(weak_node)));
490        }
491
492        // If this fails, then the tree has orphaned nodes (a leak).
493        // If a node has been incorrectly removed from the map, that will be caught above.
494        assert_eq!(seen_node_ids, self.nodes.keys().copied().collect());
495    }
496
497    fn print(&self) {
498        let Some(root_node) = self.root_node.clone() else {
499            return;
500        };
501
502        let mut print_tree = PrintTree::new("Accessibility Tree");
503        root_node.borrow().print(&mut print_tree);
504        print_tree.end_level();
505    }
506}
507
508fn role_from_dom_node(dom_node: &ServoLayoutNode<'_>) -> Role {
509    if let Some(dom_element) = dom_node.as_element() {
510        let local_name = dom_element.local_name().to_ascii_lowercase();
511        *HTML_ELEMENT_ROLE_MAPPINGS
512            .get(&local_name)
513            .unwrap_or(&Role::GenericContainer)
514    } else if dom_node.type_id() == Some(LayoutNodeType::Text) {
515        Role::TextRun
516    } else {
517        Role::GenericContainer
518    }
519}
520
521impl AccessibilityNode {
522    fn new(id: NodeId) -> Self {
523        Self::new_with_role(id, Role::Unknown)
524    }
525
526    fn new_with_role(id: NodeId, role: Role) -> Self {
527        Self {
528            id,
529            accesskit_node: accesskit::Node::new(role),
530            parent_node: None,
531            child_nodes: vec![],
532            opaque_node: None,
533            updated: true,
534        }
535    }
536
537    /// Update this node's [`Self::children`] from its corresponding DOM node. If any children are
538    /// newly added to the tree, populate them and recursively populate their children.
539    fn update_descendants_from_dom_node<'dom>(
540        &mut self,
541        weak_self: WeakRefCell<Self>,
542        dom_node: &ServoLayoutNode<'dom>,
543        dom_damage: AccessibilityDamage,
544        tree: &mut AccessibilityTree,
545        update: &mut AccessibilityUpdate,
546    ) -> LocalAccessibilityDamage {
547        let mut local_damage = LocalAccessibilityDamage::empty();
548        if !dom_damage.contains(AccessibilityDamage::Children) {
549            return local_damage;
550        }
551
552        let dom_children: Vec<ServoLayoutNode> = dom_node.flat_tree_children().collect();
553
554        let mut damage_from_children = LocalAccessibilityDamage::empty();
555        let mut new_child_ids = vec![];
556        let mut new_child_nodes = vec![];
557        for dom_child in dom_children {
558            let (child_id, child_node) = tree.get_or_create_node(&dom_child, update);
559            if update.is_new(&child_id) {
560                let child_damage = tree.update_node_and_descendants_from_dom_node(
561                    child_node.clone(),
562                    &dom_child,
563                    AccessibilityDamage::Rebuild,
564                    update,
565                );
566                damage_from_children.insert(child_damage);
567            }
568            new_child_ids.push(child_id);
569            new_child_nodes.push(child_node);
570        }
571        if !damage_from_children.is_empty() {
572            local_damage.insert(LocalAccessibilityDamage::SubtreeChanged);
573        }
574
575        local_damage.insert(self.set_children(weak_self, new_child_ids, new_child_nodes, update));
576        local_damage
577    }
578
579    /// Recursively mark this subtree as having the given `TreeChange`.
580    ///
581    /// This is used when a node is `Moved` or `Removed`, since its entire subtree will also need to
582    /// be marked accordingly. When a node is `New`, it's marked as such when it is created. We
583    /// shouldn't call this method in that case, since it may have descendants which are not being
584    /// created in this update and shouldn't have a `New` state. Any descendants which are new will
585    /// already have their `New` state set when they are created.
586    ///
587    /// Note: if a node is moved, the requested `change` must always be `Moved(Pending)`: the logic
588    /// in this method will determine whether the move is `Complete` and set the stored value
589    /// accordingly.
590    fn set_subtree_state_change(&self, change: TreeChange, update: &mut AccessibilityUpdate) {
591        assert!(
592            change != TreeChange::New,
593            "New shouldn't be set recursively"
594        );
595
596        update.set_tree_state_change(self.id, change);
597
598        for child in self.children().iter() {
599            // `new_change` might be different per node, if only some nodes were moved elsewhere.
600            child.borrow().set_subtree_state_change(change, update);
601        }
602    }
603
604    /// Update this node's properties from its corresponding DOM node.
605    fn update_node_from_dom_node(
606        &mut self,
607        dom_node: &ServoLayoutNode<'_>,
608        dom_damage: AccessibilityDamage,
609    ) -> LocalAccessibilityDamage {
610        let mut local_damage = LocalAccessibilityDamage::empty();
611        if !dom_damage.contains(AccessibilityDamage::Text) {
612            return local_damage;
613        }
614        local_damage.insert(self.set_role(role_from_dom_node(dom_node)));
615        if dom_node.type_id() == Some(LayoutNodeType::Text) {
616            let text_content = dom_node.text_content();
617            trace!("node text content = {text_content:?}");
618            // FIXME: this should take into account editing selection units (grapheme clusters?)
619            local_damage.insert(self.set_value(&text_content));
620        }
621
622        local_damage
623    }
624
625    /// Update this node's properties based on changes already made to the accessibility tree.
626    /// For example, if there were nodes added or removed in its subtree, its computed text may have
627    /// changed, so that will be recomputed here.
628    /// If any changes are made, add this node to the given [`AccessibilityUpdate`].
629    fn update_node_local(
630        &mut self,
631        local_damage: LocalAccessibilityDamage,
632    ) -> LocalAccessibilityDamage {
633        let mut new_damage = LocalAccessibilityDamage::empty();
634        if local_damage.contains(LocalAccessibilityDamage::SubtreeChanged) ||
635            local_damage.contains(LocalAccessibilityDamage::RoleChanged)
636        {
637            if let Some(text) = self.label_from_descendants() {
638                new_damage.insert(self.set_label(text.as_str()));
639            } else {
640                new_damage.insert(self.clear_label());
641            }
642        }
643
644        new_damage
645    }
646
647    fn label_from_descendants(&self) -> Option<String> {
648        if !NAME_FROM_CONTENTS_ROLES.contains(&self.role()) {
649            return None;
650        }
651        let mut children = VecDeque::from_iter(self.children().iter().cloned());
652        let mut text = String::new();
653        while let Some(child) = children.pop_front() {
654            let child = child.borrow();
655            match child.role() {
656                Role::TextRun => {
657                    if let Some(child_text) = child.value() {
658                        text.push_str(child_text);
659                    }
660                },
661                _ => {
662                    for node in child.children().iter().rev() {
663                        children.push_front(node.clone());
664                    }
665                },
666            }
667        }
668        Some(text.trim().to_owned())
669    }
670
671    fn print(&self, print_tree: &mut PrintTree) {
672        if self.children().is_empty() {
673            print_tree.add_item(format!("{self:?}"));
674            return;
675        }
676
677        print_tree.new_level(format!("{self:?}"));
678
679        for child in self.children() {
680            child.borrow().print(print_tree);
681        }
682        print_tree.end_level();
683    }
684
685    fn parent(&self) -> Option<ArcRefCell<AccessibilityNode>> {
686        self.parent_node.as_ref().and_then(|weak| weak.upgrade())
687    }
688
689    // TODO: use macros to generate getter/setter methods.
690
691    fn children(&self) -> &Vec<ArcRefCell<AccessibilityNode>> {
692        &self.child_nodes
693    }
694
695    fn child_ids(&self) -> &[NodeId] {
696        self.accesskit_node.children()
697    }
698
699    /// Set the children for this node, and set the subtree state change for any moved or removed
700    /// children.
701    fn set_children(
702        &mut self,
703        weak_self: WeakRefCell<Self>,
704        new_child_ids: Vec<NodeId>,
705        new_child_nodes: Vec<ArcRefCell<AccessibilityNode>>,
706        update: &mut AccessibilityUpdate,
707    ) -> LocalAccessibilityDamage {
708        if new_child_ids == self.child_ids() {
709            return LocalAccessibilityDamage::empty();
710        }
711
712        let old_child_ids = self.child_ids();
713
714        for (old_id, old_child) in old_child_ids.iter().zip(self.children().iter()) {
715            if !new_child_ids.contains(old_id) {
716                let mut removed_child = old_child.borrow_mut();
717                removed_child.set_subtree_state_change(TreeChange::Removed, update);
718                if let Some(parent_node) = removed_child.parent_node.clone() &&
719                    parent_node.ptr_eq(&weak_self)
720                {
721                    removed_child.parent_node = None;
722                }
723            }
724        }
725
726        for (new_id, new_child) in new_child_ids.iter().zip(new_child_nodes.iter()) {
727            if !old_child_ids.contains(new_id) {
728                let mut new_child = new_child.borrow_mut();
729                new_child.parent_node = Some(weak_self.clone());
730                if !update.is_new(new_id) {
731                    new_child.set_subtree_state_change(TreeChange::PendingMove, update);
732                }
733            }
734        }
735
736        self.child_nodes = new_child_nodes;
737        self.accesskit_node.set_children(new_child_ids);
738        self.updated = true;
739
740        LocalAccessibilityDamage::SubtreeChanged
741    }
742
743    fn role(&self) -> Role {
744        self.accesskit_node.role()
745    }
746
747    fn set_role(&mut self, role: Role) -> LocalAccessibilityDamage {
748        if role == self.accesskit_node.role() {
749            return LocalAccessibilityDamage::empty();
750        }
751        self.accesskit_node.set_role(role);
752        self.updated = true;
753        LocalAccessibilityDamage::RoleChanged
754    }
755
756    fn label(&self) -> Option<&str> {
757        self.accesskit_node.label()
758    }
759
760    fn set_label(&mut self, label: &str) -> LocalAccessibilityDamage {
761        if Some(label) == self.accesskit_node.label() {
762            return LocalAccessibilityDamage::empty();
763        }
764        self.accesskit_node.set_label(label);
765        self.updated = true;
766        LocalAccessibilityDamage::TextChanged
767    }
768
769    fn clear_label(&mut self) -> LocalAccessibilityDamage {
770        if self.accesskit_node.label().is_none() {
771            return LocalAccessibilityDamage::empty();
772        }
773        self.accesskit_node.clear_label();
774        self.updated = true;
775        LocalAccessibilityDamage::TextChanged
776    }
777
778    fn html_tag(&self) -> Option<&str> {
779        self.accesskit_node.html_tag()
780    }
781
782    fn set_html_tag(&mut self, html_tag: &str) {
783        if Some(html_tag) == self.accesskit_node.html_tag() {
784            return;
785        }
786        self.accesskit_node.set_html_tag(html_tag);
787        self.updated = true;
788    }
789
790    fn value(&self) -> Option<&str> {
791        self.accesskit_node.value()
792    }
793
794    fn set_value(&mut self, value: &str) -> LocalAccessibilityDamage {
795        if Some(value) == self.accesskit_node.value() {
796            return LocalAccessibilityDamage::empty();
797        }
798        self.accesskit_node.set_value(value);
799        self.updated = true;
800        LocalAccessibilityDamage::TextChanged
801    }
802
803    fn assert_integrity(&self, expected_parent: Option<WeakRefCell<AccessibilityNode>>) {
804        debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
805
806        if let Some(actual_parent) = &self.parent_node {
807            let expected = expected_parent.expect("Actual parent but no expected parent");
808            let expected = expected.upgrade().expect("Expected parent was dropped");
809            let actual = actual_parent.upgrade().expect("Actual parent was dropped");
810            assert!(actual.ptr_eq(&expected));
811        } else {
812            assert!(
813                expected_parent.is_none(),
814                "Expected parent but no actual parent"
815            );
816        }
817
818        let children_ids: Vec<_> = self
819            .children()
820            .iter()
821            .map(|child| child.borrow().id)
822            .collect();
823        assert_eq!(
824            children_ids,
825            self.child_ids(),
826            "children() IDs didn't match child_ids() for {self:?}"
827        );
828    }
829}
830
831impl Debug for AccessibilityNode {
832    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
833        write!(f, "{:?}: {:?}", self.id, self.role())?;
834        if let Some(html_tag) = self.html_tag() {
835            write!(f, " (html_tag: {html_tag:?})")?;
836        }
837        if let Some(label) = self.label() {
838            write!(f, "\nlabel: {label:?}")?;
839        }
840        if !self.child_ids().is_empty() {
841            write!(f, "\nchildren: {:?}", self.child_ids())?;
842        }
843        Ok(())
844    }
845}
846
847impl AccessibilityUpdate {
848    fn new(rooted_nodes: Option<FxHashSet<OpaqueNode>>) -> Self {
849        Self {
850            changed_nodes: FxHashSet::default(),
851            tree_changes: FxHashMap::default(),
852            unresolved_local_damage: FxHashMap::default(),
853            rooted_nodes,
854        }
855    }
856
857    fn add(&mut self, node: &mut AccessibilityNode) {
858        self.changed_nodes.insert(node.id);
859
860        node.updated = false;
861    }
862
863    fn set_tree_state_change(&mut self, node_id: NodeId, change: TreeChange) {
864        let old_change = self.tree_changes.get(&node_id);
865
866        assert!(
867            change != TreeChange::Moved,
868            "Incoming change must never be Moved"
869        );
870
871        let resolved_change = old_change
872            .map(|old_change| match (old_change, change) {
873                (TreeChange::PendingMove, TreeChange::Removed) => TreeChange::Moved,
874                (TreeChange::Removed, TreeChange::PendingMove) => TreeChange::Moved,
875                _ => {
876                    unreachable!("Logically impossible state change: {old_change:?} → {change:?}")
877                },
878            })
879            .unwrap_or(change);
880
881        self.tree_changes.insert(node_id, resolved_change);
882    }
883
884    fn is_new(&mut self, node_id: &NodeId) -> bool {
885        self.tree_changes.get(node_id) == Some(&TreeChange::New)
886    }
887
888    /// Consume this `AccessibilityUpdate`, producing an [`accesskit::TreeUpdate`] if there have
889    /// been any changes to `tree`.
890    /// This will pass `self` into [`AccessibilityTree::remove_stale_nodes()`] to consume
891    /// [`Self::tree_changes`].
892    fn finalize(mut self, tree: &mut AccessibilityTree) -> Option<accesskit::TreeUpdate> {
893        let root_node_id = tree
894            .root_node
895            .clone()
896            .expect("AccessibilityUpdate::finalize() called but no root_node set in tree")
897            .borrow()
898            .id;
899
900        debug_assert!(self.unresolved_local_damage.is_empty());
901
902        if self.changed_nodes.is_empty() {
903            assert!(self.tree_changes.is_empty());
904            return None;
905        }
906
907        let accesskit_tree = accesskit::Tree::new(root_node_id);
908        let tree_update = accesskit::TreeUpdate {
909            nodes: std::mem::take(&mut self.changed_nodes)
910                .into_iter()
911                .map(|id| {
912                    (
913                        id,
914                        tree.assert_node_for_id(&id).borrow().accesskit_node.clone(),
915                    )
916                })
917                .collect(),
918            tree: Some(accesskit_tree),
919            focus: NodeId(1),
920            tree_id: tree.tree_id,
921        };
922
923        tree.remove_stale_nodes(self);
924
925        Some(tree_update)
926    }
927}
928
929#[cfg(test)]
930#[test]
931fn test_accessibility_update_add_some_nodes_twice() {
932    let mut tree = AccessibilityTree::new(accesskit::TreeId::ROOT, Epoch::default());
933    let mut root_update = AccessibilityUpdate::new(None);
934
935    let root_node = tree.get_or_create_node_with_id(NodeId(2), &mut root_update);
936    tree.root_node = Some(root_node.clone());
937
938    let nodes: Vec<_> = [
939        (3, Role::GenericContainer),
940        (4, Role::Heading),
941        (5, Role::Paragraph),
942    ]
943    .into_iter()
944    .map(|(id, role)| {
945        let id = NodeId(id);
946        let node = tree.get_or_create_node_with_id(id, &mut root_update);
947        node.borrow_mut().set_role(role);
948        (id, node)
949    })
950    .collect();
951    let (child_node_ids, child_nodes) = nodes.iter().cloned().unzip();
952    root_node.borrow_mut().set_children(
953        root_node.downgrade(),
954        child_node_ids,
955        child_nodes,
956        &mut root_update,
957    );
958
959    let mut update = AccessibilityUpdate::new(None);
960
961    {
962        let node_3 = tree.assert_node_for_id(&NodeId(3));
963        let mut node_3 = node_3.borrow_mut();
964        let node_4 = tree.assert_node_for_id(&NodeId(4));
965        let mut node_4 = node_4.borrow_mut();
966        let node_5 = tree.assert_node_for_id(&NodeId(5));
967        let mut node_5 = node_5.borrow_mut();
968
969        update.add(&mut node_5);
970        update.add(&mut node_3);
971        update.add(&mut node_4);
972        update.add(&mut node_4);
973
974        node_3.set_role(Role::ScrollView);
975        update.add(&mut node_3);
976    }
977
978    let mut tree_update = update
979        .finalize(&mut tree)
980        .expect("finalize should produce a tree update");
981    tree_update.nodes.sort_by_key(|(node_id, _node)| *node_id);
982    assert_eq!(
983        tree_update,
984        accesskit::TreeUpdate {
985            nodes: vec![
986                (NodeId(3), accesskit::Node::new(Role::ScrollView)),
987                (NodeId(4), accesskit::Node::new(Role::Heading)),
988                (NodeId(5), accesskit::Node::new(Role::Paragraph)),
989            ],
990            tree: Some(accesskit::Tree {
991                root: NodeId(2),
992                toolkit_name: None,
993                toolkit_version: None
994            }),
995            tree_id: accesskit::TreeId::ROOT,
996            focus: NodeId(1),
997        }
998    );
999}
1000
1001static HTML_ELEMENT_ROLE_MAPPINGS: LazyLock<FxHashMap<LocalName, Role>> = LazyLock::new(|| {
1002    [
1003        (local_name!("article"), Role::Article),
1004        (local_name!("aside"), Role::Complementary),
1005        (local_name!("body"), Role::RootWebArea),
1006        (local_name!("footer"), Role::ContentInfo),
1007        (local_name!("h1"), Role::Heading),
1008        (local_name!("h2"), Role::Heading),
1009        (local_name!("h3"), Role::Heading),
1010        (local_name!("h4"), Role::Heading),
1011        (local_name!("h5"), Role::Heading),
1012        (local_name!("h6"), Role::Heading),
1013        (local_name!("header"), Role::Banner),
1014        (local_name!("hr"), Role::Splitter),
1015        (local_name!("main"), Role::Main),
1016        (local_name!("nav"), Role::Navigation),
1017        (local_name!("p"), Role::Paragraph),
1018    ]
1019    .into_iter()
1020    .collect()
1021});
1022
1023/// <https://w3c.github.io/aria/#namefromcontent>
1024static NAME_FROM_CONTENTS_ROLES: LazyLock<FxHashSet<Role>> =
1025    LazyLock::new(|| [(Role::Heading)].into_iter().collect());