script/dom/document/accessibility_data.rs
1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use js::context::NoGC;
6use layout_api::{AccessibilityDamage, TrustedNodeAddress};
7use rustc_hash::{FxHashMap, FxHashSet};
8use script_bindings::cell::DomRefCell;
9use script_bindings::root::Dom;
10use servo_config::pref;
11use style::dom::OpaqueNode;
12
13use crate::dom::Node;
14use crate::dom::bindings::trace::NoTrace;
15
16#[derive(Clone, Default, JSTraceable, MallocSizeOf)]
17#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
18pub(crate) struct AccessibilityData {
19 /// Nodes which have been removed from the DOM but may not yet have been removed from the
20 /// accessibility tree. This is cleared after each reflow.
21 rooted_nodes: FxHashSet<Dom<Node>>,
22
23 /// Damage to the accessibility tree as a result of DOM mutations. This is drained and sent to
24 /// the accessibility tree during reflow.
25 pending_damage: DomRefCell<FxHashMap<Dom<Node>, NoTrace<AccessibilityDamage>>>,
26}
27
28impl AccessibilityData {
29 /// Root a node which has been removed from the DOM but which may still have an associated
30 /// accessibility tree node. It will be unrooted after the next reflow, since the accessibility
31 /// tree is updated as part of the reflow process.
32 ///
33 /// Longer explanation:
34 /// - The accessibility tree doesn't hold strong references to DOM nodes, but uses
35 /// [`OpaqueNode`]s as a way of mapping from an incoming DOM node to an existing accessibility
36 /// tree node. This allows us to cache previously computed accessibility data, and update it
37 /// based on the current DOM node state, which is passed in to the update function.
38 /// - If a DOM node is garbage collected before its corresponding node is removed from the
39 /// accessibility tree, there is a risk that another new DOM node may be created at the same
40 /// memory address, causing it to have an identical `OpaqueNode`. If this `OpaqueNode` was
41 /// used to look up a node in the accessibility tree, we would get the stale accessibility
42 /// node corresponding to the node which was removed.
43 /// - A DOM node is prevented from being garbage collected while it's connected to the document;
44 /// it's kept alive by strong references in its parent, child and/or sibling [`Node`]s (and in
45 /// the case of the document itself, by a strong reference in the [`Window`]). See
46 /// [`Node::first_child`], [`Node::next_sibling`], etc.
47 /// - Note that this means we only need to root nodes which are removed from the document,
48 /// and not their descendants, as descendant nodes will still be rooted via these
49 /// properties as long as the subtree root is stored here.
50 /// - After a node is removed from the tree, those strong references are removed, and it _may_
51 /// become a candidate for GC if its DOM object isn't held (directly or indirectly) in script
52 /// and it isn't immediately inserted elsewhere in the DOM.
53 /// - To make sure the node isn't GCed before the next accessibility update occurs, we
54 /// temporarily root it here in between its removal from the tree and the subsequent reflow.
55 /// - During reflow, the accessibility tree is updated, and all stale accessibility nodes are
56 /// removed.
57 /// - Once reflow has begun, no further DOM mutations can occur, and we can safely un-root these
58 /// nodes by dropping all the strong references being held here. This will allow them to be
59 /// potential candidates for GC after reflow has finished.
60 /// See [`Self::unroot_all_removed_nodes()`] and
61 /// [`Self::unroot_and_drain_all_removed_nodes()`].
62 pub(crate) fn root_removed_node(&mut self, _no_gc: &NoGC, node_to_root: &Node) {
63 debug_assert!(pref!(accessibility_enabled));
64
65 self.rooted_nodes.insert(Dom::from_ref(node_to_root));
66 }
67
68 /// Clear all nodes which were rooted using [`Self::root_removed_node()`], and return the nodes
69 /// which are still disconnected from the tree.
70 /// This should be called instead of [`Self::unroot_all_removed_nodes()`] during reflow
71 /// if [`pref::expensive_accessibility_test_assertions_enabled`] set.
72 pub(crate) fn unroot_and_drain_all_removed_nodes(&mut self) -> FxHashSet<OpaqueNode> {
73 self.rooted_nodes
74 .drain()
75 .filter_map(|node| {
76 if node.is_connected() {
77 return None;
78 }
79 Some(node.to_opaque())
80 })
81 .collect()
82 }
83
84 /// Clear all nodes which were rooted using [`Self::root_removed_node()`].
85 /// This should only be called during reflow.
86 pub(crate) fn unroot_all_removed_nodes(&mut self) {
87 self.rooted_nodes.clear();
88 }
89
90 /// Track accessibility damage to the given node caused by mutations in the DOM tree.
91 pub(crate) fn add_pending_accessibility_damage_for_node(
92 &self,
93 node: &Node,
94 damage: AccessibilityDamage,
95 ) {
96 assert!(pref!(accessibility_enabled));
97
98 let map = &mut self.pending_damage.borrow_mut();
99 let pending_damage = map.entry(Dom::from_ref(node)).or_default();
100 pending_damage.0 |= damage;
101 }
102
103 /// Drain all pending accessibility damage so that it can be passed to the accessibility tree.
104 pub(crate) fn drain_pending_accessibility_damage(
105 &mut self,
106 ) -> Vec<(TrustedNodeAddress, AccessibilityDamage)> {
107 let pending_damage = &mut self.pending_damage.borrow_mut();
108 pending_damage
109 .drain()
110 .map(|(node, damage)| (node.to_trusted_node_address(), damage.0))
111 .collect()
112 }
113}