Skip to main content

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}