Skip to main content

layout/
accessibility_tree.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4use std::collections::VecDeque;
5use std::fmt::Debug;
6use std::sync::atomic::AtomicU64;
7use std::sync::{LazyLock, atomic};
8
9use accesskit::{NodeId, Role};
10use layout_api::{LayoutElement, LayoutNode, LayoutNodeType};
11use log::trace;
12use rustc_hash::{FxHashMap, FxHashSet};
13use script::layout_dom::ServoLayoutNode;
14use servo_base::Epoch;
15use servo_base::print_tree::PrintTree;
16use servo_config::opts::{self, DiagnosticsLogging, DiagnosticsLoggingOption};
17use servo_config::pref;
18use style::dom::{NodeInfo, OpaqueNode};
19use web_atoms::{LocalName, local_name};
20
21use crate::ArcRefCell;
22
23/// Changes which have occurred during the current update.
24struct AccessibilityUpdate {
25    /// Nodes whose internal data has changed within the current update.
26    changed_nodes: FxHashSet<NodeId>,
27    /// Nodes that changed their relation to the tree within the current update.
28    tree_changes: FxHashMap<NodeId, TreeChange>,
29}
30
31struct AccessibilityNode {
32    /// The unique ID for the node. This is used both as a key in [`AccessibilityTree`]'s cache of
33    /// nodes, and as an identifier in [`accesskit`] datastructures: [`accesskit::Node`]s,
34    /// [`accesskit::TreeUpdate`]s and [`accesskit::ActionRequest`]s.
35    id: NodeId,
36    /// The computed [`accesskit::Node`] data. This will be copied and serialized into a
37    /// [`accesskit::TreeUpdate`] whenever it is changed during an update.
38    accesskit_node: accesskit::Node,
39    /// The [`OpaqueNode`] for the DOM node which corresponds to this accessibility node, if any.
40    /// An accessibility node may not correspond to a DOM node if it corresponds to a
41    /// pseudo-element, or in a test.
42    opaque_node: Option<OpaqueNode>,
43    /// Whether this node has been updated in the current tree update. This is reset to `false`
44    /// when the node is added to the [`AccessibilityUpdate`] - see [`AccessibilityUpdate::add()`].
45    updated: bool,
46}
47
48/// A retained, internal representation of the accessibility tree for a document.
49///
50/// [`accesskit`] only provides interchange types for tree updates and action requests, so we need
51/// to define our own representation for incremental tree building.
52#[derive(Debug)]
53pub struct AccessibilityTree {
54    /// All nodes currently in the tree as of the most recent update. New nodes are added and stale
55    /// nodes are pruned during [`AccessibilityTree::update_tree()`].
56    nodes: FxHashMap<NodeId, ArcRefCell<AccessibilityNode>>,
57    /// A map to allow retrieving the [`AccessibilityNode`] which corresponds to a particular DOM
58    /// node, if any.
59    opaque_node_to_id: FxHashMap<OpaqueNode, NodeId>,
60    /// Sent with each [`accesskit::TreeUpdate`]. This allows this tree to be
61    /// [grafted](https://docs.rs/accesskit/latest/accesskit/struct.Node.html#method.tree_id) into
62    /// an application's tree.
63    tree_id: accesskit::TreeId,
64    /// Sent with each [`accesskit::TreeUpdate`] to identify the root node, and also used in
65    /// [`Self::assert_integrity()`].
66    root_node_id: Option<accesskit::NodeId>,
67    /// Sent to the embedder alongside each [`accesskit::TreeUpdate`], so that the embedder can
68    /// drop updates from documents which have been navigated away from.
69    embedder_epoch: Epoch,
70    /// Debug options, copied from configuration to this `AccessibilityTree` in order
71    /// to avoid having to constantly access the thread-safe global options.
72    debug: DiagnosticsLogging,
73}
74
75/// Tracks changes to a node's relation to the tree within an update.
76///
77/// This is used to remove nodes from the accessibility tree's cache when they are no longer in the
78/// tree.
79#[derive(Debug, PartialEq, Copy, Clone)]
80enum TreeChange {
81    /// The node was newly created in this update.
82    New,
83
84    /// The node has been re-parented in this update.
85    Moved,
86
87    /// The node has been added to its new parent, but not yet removed from its old
88    /// parent.
89    ///
90    /// When a node is moved within the tree, it must be both removed from its old parent
91    /// and added to its new parent within the same update. This may happen in either
92    /// order, depending on the relative positions of the node before and after it moves.
93    ///
94    /// - If a node's new parent is updated before its old parent, the node will be in a
95    ///   `TreeChange::PendingMove` state until its old parent is updated. We expect that it
96    ///   must later be removed from its old parent, at which point its state will be updated to
97    ///   `TreeChange::Moved`.
98    /// - If a node's old parent is updated before its new parent, the node will be first
99    ///   `TreeChange::Removed` and then `TreeChange::Moved`.
100    ///
101    /// At the end of the update, we assert that there are no pending moves remaining.
102    PendingMove,
103
104    /// The node is no longer a child of its previous parent.
105    Removed,
106}
107
108impl AccessibilityTree {
109    /// See [`Self::tree_id`] and [`Self::embedder_epoch`] for explanations of the parameters.
110    pub(super) fn new(tree_id: accesskit::TreeId, embedder_epoch: Epoch) -> Self {
111        Self {
112            nodes: FxHashMap::default(),
113            opaque_node_to_id: FxHashMap::default(),
114            tree_id,
115            root_node_id: None,
116            embedder_epoch,
117            debug: opts::get().debug.clone(),
118        }
119    }
120
121    /// Update this tree based on the current state of the given DOM tree, and if anything changed,
122    /// return an [`accesskit::TreeUpdate`] representing what changed.
123    pub(super) fn update_tree(
124        &mut self,
125        root_dom_node: &ServoLayoutNode<'_>,
126    ) -> Option<accesskit::TreeUpdate> {
127        let mut update = AccessibilityUpdate::new();
128        let root_node = self.get_or_create_node(root_dom_node, &mut update);
129        let root_node_id = root_node.borrow().id;
130        self.root_node_id = Some(root_node_id);
131
132        let any_node_updated = self.update_node_and_descendants(root_dom_node, &mut update);
133
134        if !any_node_updated {
135            assert!(update.tree_changes.is_empty());
136            return None;
137        }
138
139        let accesskit_update = update.finalize(self)?;
140
141        if self
142            .debug
143            .is_enabled(DiagnosticsLoggingOption::AccessibilityTree)
144        {
145            self.print();
146        }
147
148        if pref!(expensive_accessibility_test_assertions_enabled) {
149            self.assert_integrity();
150        }
151
152        Some(accesskit_update)
153    }
154
155    /// Update this tree starting at the given DOM node, adding any changed nodes to the given
156    /// [`AccessibilityUpdate`].
157    fn update_node_and_descendants(
158        &mut self,
159        dom_node: &ServoLayoutNode<'_>,
160        update: &mut AccessibilityUpdate,
161    ) -> bool {
162        let node = self.assert_node_for_dom_node(dom_node);
163        let mut node = node.borrow_mut();
164
165        // TODO: read accessibility damage (right now, assume damage is complete)
166        let any_descendant_updated = node.update_descendants(dom_node, self, update);
167
168        node.update_node(dom_node, self, any_descendant_updated);
169
170        if node.updated {
171            update.add(&mut node);
172            return true;
173        }
174
175        any_descendant_updated
176    }
177
178    fn get_or_create_node(
179        &mut self,
180        dom_node: &ServoLayoutNode<'_>,
181        update: &mut AccessibilityUpdate,
182    ) -> ArcRefCell<AccessibilityNode> {
183        let id = self.id_for_opaque(dom_node.opaque());
184
185        let node = self.nodes.entry(id).or_insert_with(|| {
186            update.tree_changes.insert(id, TreeChange::New);
187            ArcRefCell::new(AccessibilityNode::new(id))
188        });
189
190        let mut new_node = node.borrow_mut();
191
192        new_node.opaque_node = Some(dom_node.opaque());
193        if let Some(dom_element) = dom_node.as_element() {
194            let local_name = dom_element.local_name().to_ascii_lowercase();
195            new_node.set_html_tag(&local_name);
196        }
197
198        node.clone()
199    }
200
201    fn node_for_dom_node(
202        &mut self,
203        dom_node: &ServoLayoutNode<'_>,
204    ) -> Option<ArcRefCell<AccessibilityNode>> {
205        let id = self.id_for_opaque(dom_node.opaque());
206        self.nodes.get(&id).cloned()
207    }
208
209    fn assert_node_for_dom_node(
210        &mut self,
211        dom_node: &ServoLayoutNode<'_>,
212    ) -> ArcRefCell<AccessibilityNode> {
213        let id = self.id_for_opaque(dom_node.opaque());
214        let node = self.assert_node_for_id(id);
215        assert!(node.borrow().opaque_node == Some(dom_node.opaque()));
216        node
217    }
218
219    fn assert_node_for_id(&self, id: NodeId) -> ArcRefCell<AccessibilityNode> {
220        let Some(node) = self.nodes.get(&id) else {
221            panic!("{id:?} does not exist in tree");
222        };
223        node.clone()
224    }
225
226    /// Remove the node with the given ID from the cache, if it exists.
227    fn remove_node(&mut self, id: &NodeId) {
228        let Some(node) = self.nodes.remove(id) else {
229            return;
230        };
231        if let Some(opaque_node) = node.borrow().opaque_node {
232            self.opaque_node_to_id.remove(&opaque_node);
233        }
234    }
235
236    fn id_for_opaque(&mut self, opaque: OpaqueNode) -> NodeId {
237        let id = self.opaque_node_to_id.entry(opaque).or_insert_with(|| {
238            static LAST_ID: AtomicU64 = AtomicU64::new(0);
239            LAST_ID.fetch_add(1, atomic::Ordering::SeqCst).into()
240        });
241        *id
242    }
243
244    pub(crate) fn embedder_epoch(&self) -> Epoch {
245        self.embedder_epoch
246    }
247
248    /// Assert that the tree is a tree without any dangling references or orphaned nodes.
249    ///
250    /// For accessibility tests only, because it’s expensive.
251    fn assert_integrity(&self) {
252        let Some(root_node_id) = self.root_node_id else {
253            return;
254        };
255
256        assert!(pref!(expensive_accessibility_test_assertions_enabled));
257        // Traverse the tree from the given root.
258        let mut node_ids = vec![root_node_id];
259        let mut seen_node_ids = FxHashSet::default();
260        while let Some(node_id) = node_ids.pop() {
261            // If this fails, then the tree is not a tree at all.
262            assert!(
263                seen_node_ids.insert(node_id),
264                "Tree contains {node_id:?} in multiple places"
265            );
266            // If this fails, then the tree has dangling references.
267            let node = self.assert_node_for_id(node_id);
268            let node = node.borrow();
269            node_ids.extend(node.children().iter().rev());
270        }
271        // If this fails, then the tree has orphaned nodes (a leak).
272        // Dangling references are already caught in the loop above.
273        assert_eq!(seen_node_ids, self.nodes.keys().copied().collect());
274    }
275
276    fn print(&self) {
277        let Some(root_node_id) = self.root_node_id else {
278            return;
279        };
280
281        let mut print_tree = PrintTree::new("Accessibility Tree");
282        let node = self.assert_node_for_id(root_node_id);
283        node.borrow().print(self, &mut print_tree);
284        print_tree.end_level();
285    }
286}
287
288fn role_from_dom_node(dom_node: &ServoLayoutNode<'_>) -> Role {
289    if let Some(dom_element) = dom_node.as_element() {
290        let local_name = dom_element.local_name().to_ascii_lowercase();
291        *HTML_ELEMENT_ROLE_MAPPINGS
292            .get(&local_name)
293            .unwrap_or(&Role::GenericContainer)
294    } else if dom_node.type_id() == Some(LayoutNodeType::Text) {
295        Role::TextRun
296    } else {
297        Role::GenericContainer
298    }
299}
300
301impl AccessibilityNode {
302    fn new(id: NodeId) -> Self {
303        Self::new_with_role(id, Role::Unknown)
304    }
305
306    fn new_with_role(id: NodeId, role: Role) -> Self {
307        Self {
308            id,
309            accesskit_node: accesskit::Node::new(role),
310            opaque_node: None,
311            updated: true,
312        }
313    }
314
315    fn update_descendants<'dom>(
316        &mut self,
317        dom_node: &ServoLayoutNode<'dom>,
318        tree: &mut AccessibilityTree,
319        update: &mut AccessibilityUpdate,
320    ) -> bool {
321        let mut any_descendant_updated = false;
322        let mut newly_created = FxHashSet::default();
323        let new_children: Vec<_> = dom_node
324            .flat_tree_children()
325            .map(|dom_child| {
326                let child_node_id = match tree.node_for_dom_node(&dom_child) {
327                    Some(child_node) => child_node.borrow().id,
328                    None => {
329                        let new_child = tree.get_or_create_node(&dom_child, update);
330                        let child_node_id = new_child.borrow().id;
331                        newly_created.insert(child_node_id);
332                        child_node_id
333                    },
334                };
335
336                // TODO: We actually need to propagate damage within the accessibility tree, rather than
337                // assuming it matches the DOM tree, but this will do for now.
338                any_descendant_updated |= tree.update_node_and_descendants(&dom_child, update);
339
340                child_node_id
341            })
342            .collect();
343
344        if new_children != self.children() {
345            let old_children = self.children();
346            for old_child_id in old_children {
347                if !new_children.contains(old_child_id) {
348                    let removed_child = tree.assert_node_for_id(*old_child_id);
349                    removed_child.borrow().set_subtree_state_change(
350                        TreeChange::Removed,
351                        tree,
352                        update,
353                    );
354                }
355            }
356            for new_child_id in new_children.iter() {
357                if !newly_created.contains(new_child_id) && !old_children.contains(new_child_id) {
358                    let moved_child = tree.assert_node_for_id(*new_child_id);
359                    moved_child.borrow().set_subtree_state_change(
360                        TreeChange::PendingMove,
361                        tree,
362                        update,
363                    );
364                }
365            }
366            self.set_children(new_children);
367        }
368
369        any_descendant_updated
370    }
371
372    /// Recursively mark this subtree as having the given `TreeChange `.
373    ///
374    /// This is used when a node is `Moved` or `Removed`, since its entire subtree will also need to
375    /// be marked accordingly. When a node is `New`, it's marked as such when it is created. We
376    /// shouldn't call this method in that case, since it may have descendants which are not being
377    /// created in this update and shouldn't have a `New` state. Any descendants which are new will
378    /// already have their `New` state set when they are created.
379    ///
380    /// Note: if a node is moved, the requested `change` must always be `Moved(Pending)`: the logic
381    /// in this method will determine whether the move is `Complete` and set the stored value
382    /// accordingly.
383    fn set_subtree_state_change(
384        &self,
385        change: TreeChange,
386        tree: &mut AccessibilityTree,
387        update: &mut AccessibilityUpdate,
388    ) {
389        let old_change = update.tree_changes.get(&self.id);
390
391        assert!(
392            change != TreeChange::New,
393            "New shouldn't be set recursively"
394        );
395        assert!(
396            change != TreeChange::Moved,
397            "Incoming change must never be Moved"
398        );
399
400        let new_change = old_change
401            .map(|old_change| match (old_change, change) {
402                (TreeChange::PendingMove, TreeChange::Removed) => TreeChange::Moved,
403                (TreeChange::Removed, TreeChange::PendingMove) => TreeChange::Moved,
404                _ => {
405                    unreachable!("Logically impossible state change: {old_change:?} → {change:?}")
406                },
407            })
408            .unwrap_or(change);
409
410        update.tree_changes.insert(self.id, new_change);
411
412        for child_id in self.children().to_vec() {
413            let child = tree.assert_node_for_id(child_id);
414            // `new_change` might be different per node, if only some nodes were moved elsewhere.
415            child
416                .borrow()
417                .set_subtree_state_change(change, tree, update);
418        }
419    }
420
421    fn update_node(
422        &mut self,
423        dom_node: &ServoLayoutNode<'_>,
424        tree: &mut AccessibilityTree,
425        any_descendant_updated: bool,
426    ) {
427        self.set_role(role_from_dom_node(dom_node));
428        if dom_node.is_element() {
429            if any_descendant_updated && let Some(text) = self.label_from_descendants(tree) {
430                self.set_label(text.as_str());
431            }
432        } else if dom_node.type_id() == Some(LayoutNodeType::Text) {
433            let text_content = dom_node.text_content();
434            trace!("node text content = {text_content:?}");
435            // FIXME: this should take into account editing selection units (grapheme clusters?)
436            self.set_value(&text_content);
437        }
438    }
439
440    fn label_from_descendants(&self, tree: &AccessibilityTree) -> Option<String> {
441        if !NAME_FROM_CONTENTS_ROLES.contains(&self.role()) {
442            return None;
443        }
444        let mut children = VecDeque::from_iter(self.children().iter().copied());
445        let mut text = String::new();
446        while let Some(child_id) = children.pop_front() {
447            let child = tree.assert_node_for_id(child_id);
448            let child = child.borrow();
449            match child.role() {
450                Role::TextRun => {
451                    if let Some(child_text) = child.value() {
452                        text.push_str(child_text);
453                    }
454                },
455                _ => {
456                    for id in child.children().iter().rev() {
457                        children.push_front(*id);
458                    }
459                },
460            }
461        }
462        Some(text.trim().to_owned())
463    }
464
465    fn print(&self, tree: &AccessibilityTree, print_tree: &mut PrintTree) {
466        if self.children().is_empty() {
467            print_tree.add_item(format!("{self:?}"));
468            return;
469        }
470
471        print_tree.new_level(format!("{self:?}"));
472
473        for child_id in self.children() {
474            let child = tree.assert_node_for_id(*child_id);
475            child.borrow().print(tree, print_tree);
476        }
477        print_tree.end_level();
478    }
479
480    // TODO: use macros to generate getter/setter methods.
481
482    fn children(&self) -> &[NodeId] {
483        self.accesskit_node.children()
484    }
485
486    fn set_children(&mut self, children: Vec<NodeId>) {
487        self.accesskit_node.set_children(children);
488        self.updated = true;
489    }
490
491    fn role(&self) -> Role {
492        self.accesskit_node.role()
493    }
494
495    fn set_role(&mut self, role: Role) {
496        if role == self.accesskit_node.role() {
497            return;
498        }
499        self.accesskit_node.set_role(role);
500        self.updated = true;
501    }
502
503    fn label(&self) -> Option<&str> {
504        self.accesskit_node.label()
505    }
506
507    fn set_label(&mut self, label: &str) {
508        if Some(label) == self.accesskit_node.label() {
509            return;
510        }
511        self.accesskit_node.set_label(label);
512        self.updated = true;
513    }
514
515    fn html_tag(&self) -> Option<&str> {
516        self.accesskit_node.html_tag()
517    }
518
519    fn set_html_tag(&mut self, html_tag: &str) {
520        if Some(html_tag) == self.accesskit_node.html_tag() {
521            return;
522        }
523        self.accesskit_node.set_html_tag(html_tag);
524        self.updated = true;
525    }
526
527    fn value(&self) -> Option<&str> {
528        self.accesskit_node.value()
529    }
530
531    fn set_value(&mut self, value: &str) {
532        if Some(value) == self.accesskit_node.value() {
533            return;
534        }
535        self.accesskit_node.set_value(value);
536        self.updated = true;
537    }
538}
539
540impl Debug for AccessibilityNode {
541    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
542        write!(f, "{:?}: {:?}", self.id, self.role())?;
543        if let Some(html_tag) = self.html_tag() {
544            write!(f, " (html_tag: {html_tag:?})")?;
545        }
546        if let Some(label) = self.label() {
547            write!(f, "\nlabel: {label:?}")?;
548        }
549        if !self.children().is_empty() {
550            write!(f, "\nchildren: {:?}", self.children())?;
551        }
552        Ok(())
553    }
554}
555
556impl AccessibilityUpdate {
557    fn new() -> Self {
558        Self {
559            changed_nodes: FxHashSet::default(),
560            tree_changes: FxHashMap::default(),
561        }
562    }
563
564    fn add(&mut self, node: &mut AccessibilityNode) {
565        self.changed_nodes.insert(node.id);
566
567        node.updated = false;
568    }
569
570    fn finalize(mut self, tree: &mut AccessibilityTree) -> Option<accesskit::TreeUpdate> {
571        for id in self
572            .tree_changes
573            .drain()
574            .filter_map(|(id, change)| match change {
575                TreeChange::PendingMove => {
576                    unreachable!(
577                        "Pending move found for node id {id:?} when draining tree state changes"
578                    );
579                },
580                TreeChange::Removed => Some(id),
581                _ => None,
582            })
583        {
584            tree.remove_node(&id);
585        }
586
587        let accesskit_tree = accesskit::Tree::new(tree.root_node_id?);
588        Some(accesskit::TreeUpdate {
589            nodes: self
590                .changed_nodes
591                .into_iter()
592                .map(|id| {
593                    (
594                        id,
595                        tree.assert_node_for_id(id).borrow().accesskit_node.clone(),
596                    )
597                })
598                .collect(),
599            tree: Some(accesskit_tree),
600            focus: accesskit::NodeId(1),
601            tree_id: tree.tree_id,
602        })
603    }
604}
605
606#[cfg(test)]
607#[test]
608fn test_accessibility_update_add_some_nodes_twice() {
609    let mut tree = AccessibilityTree::new(accesskit::TreeId::ROOT, Epoch::default());
610    tree.root_node_id = Some(accesskit::NodeId(2));
611
612    for (id, role) in [
613        (3, Role::GenericContainer),
614        (4, Role::Heading),
615        (5, Role::Paragraph),
616    ] {
617        let id = accesskit::NodeId(id);
618        tree.nodes.insert(
619            id,
620            ArcRefCell::new(AccessibilityNode::new_with_role(id, role)),
621        );
622    }
623
624    let mut update = AccessibilityUpdate::new();
625
626    {
627        let node_3 = tree.assert_node_for_id(accesskit::NodeId(3));
628        let mut node_3 = node_3.borrow_mut();
629        let node_4 = tree.assert_node_for_id(accesskit::NodeId(4));
630        let mut node_4 = node_4.borrow_mut();
631        let node_5 = tree.assert_node_for_id(accesskit::NodeId(5));
632        let mut node_5 = node_5.borrow_mut();
633
634        update.add(&mut node_5);
635        update.add(&mut node_3);
636        update.add(&mut node_4);
637        update.add(&mut node_4);
638
639        node_3.set_role(Role::ScrollView);
640        update.add(&mut node_3);
641    }
642
643    let mut tree_update = update
644        .finalize(&mut tree)
645        .expect("finalize should produce a tree update");
646    tree_update.nodes.sort_by_key(|(node_id, _node)| *node_id);
647    assert_eq!(
648        tree_update,
649        accesskit::TreeUpdate {
650            nodes: vec![
651                (NodeId(3), accesskit::Node::new(Role::ScrollView)),
652                (NodeId(4), accesskit::Node::new(Role::Heading)),
653                (NodeId(5), accesskit::Node::new(Role::Paragraph)),
654            ],
655            tree: Some(accesskit::Tree {
656                root: NodeId(2),
657                toolkit_name: None,
658                toolkit_version: None
659            }),
660            tree_id: accesskit::TreeId::ROOT,
661            focus: NodeId(1),
662        }
663    );
664}
665
666static HTML_ELEMENT_ROLE_MAPPINGS: LazyLock<FxHashMap<LocalName, Role>> = LazyLock::new(|| {
667    [
668        (local_name!("article"), Role::Article),
669        (local_name!("aside"), Role::Complementary),
670        (local_name!("body"), Role::RootWebArea),
671        (local_name!("footer"), Role::ContentInfo),
672        (local_name!("h1"), Role::Heading),
673        (local_name!("h2"), Role::Heading),
674        (local_name!("h3"), Role::Heading),
675        (local_name!("h4"), Role::Heading),
676        (local_name!("h5"), Role::Heading),
677        (local_name!("h6"), Role::Heading),
678        (local_name!("header"), Role::Banner),
679        (local_name!("hr"), Role::Splitter),
680        (local_name!("main"), Role::Main),
681        (local_name!("nav"), Role::Navigation),
682        (local_name!("p"), Role::Paragraph),
683    ]
684    .into_iter()
685    .collect()
686});
687
688/// <https://w3c.github.io/aria/#namefromcontent>
689static NAME_FROM_CONTENTS_ROLES: LazyLock<FxHashSet<Role>> =
690    LazyLock::new(|| [(Role::Heading)].into_iter().collect());