1use std::collections::VecDeque;
5use std::sync::atomic::AtomicU64;
6use std::sync::{LazyLock, atomic};
7
8use accesskit::Role;
9use layout_api::{LayoutElement, LayoutNode, LayoutNodeType};
10use log::trace;
11use rustc_hash::{FxHashMap, FxHashSet};
12use script::layout_dom::ServoLayoutNode;
13use servo_base::Epoch;
14use style::dom::{NodeInfo, OpaqueNode};
15use web_atoms::{LocalName, local_name};
16
17use crate::ArcRefCell;
18
19struct AccessibilityUpdate {
21 accesskit_update: accesskit::TreeUpdate,
22 nodes: FxHashMap<accesskit::NodeId, accesskit::Node>,
23}
24
25#[derive(Debug)]
26struct AccessibilityNode {
27 id: accesskit::NodeId,
28 accesskit_node: accesskit::Node,
29 opaque_node: Option<OpaqueNode>,
30 updated: bool,
31}
32
33#[derive(Debug)]
38pub struct AccessibilityTree {
39 nodes: FxHashMap<accesskit::NodeId, ArcRefCell<AccessibilityNode>>,
40 opaque_node_to_id: FxHashMap<OpaqueNode, accesskit::NodeId>,
41 tree_id: accesskit::TreeId,
42 epoch: Epoch,
43}
44
45impl AccessibilityTree {
46 pub(super) fn new(tree_id: accesskit::TreeId, epoch: Epoch) -> Self {
47 Self {
48 nodes: FxHashMap::default(),
49 opaque_node_to_id: FxHashMap::default(),
50 tree_id,
51 epoch,
52 }
53 }
54
55 pub(super) fn update_tree(
58 &mut self,
59 root_dom_node: &ServoLayoutNode<'_>,
60 ) -> Option<accesskit::TreeUpdate> {
61 let root_node = self.get_or_create_node(root_dom_node);
62 let mut tree_update =
63 AccessibilityUpdate::new(accesskit::Tree::new(root_node.borrow().id), self.tree_id);
64 let any_node_updated = self.update_node_and_descendants(root_dom_node, &mut tree_update);
65
66 if !any_node_updated {
67 return None;
68 }
69
70 Some(tree_update.finalize())
71 }
72
73 fn update_node_and_descendants(
76 &mut self,
77 dom_node: &ServoLayoutNode<'_>,
78 tree_update: &mut AccessibilityUpdate,
79 ) -> bool {
80 let node = self.assert_node_by_dom_node(dom_node);
81 let mut node = node.borrow_mut();
82
83 let any_descendant_updated = node.update_descendants(dom_node, self, tree_update);
85
86 node.update_node(dom_node, self, any_descendant_updated);
87
88 if node.updated {
89 tree_update.add(&mut node);
90 return true;
91 }
92
93 any_descendant_updated
94 }
95
96 fn get_or_create_node(
97 &mut self,
98 dom_node: &ServoLayoutNode<'_>,
99 ) -> ArcRefCell<AccessibilityNode> {
100 let id = self.id_for_opaque(dom_node.opaque());
101
102 let node = self
103 .nodes
104 .entry(id)
105 .or_insert_with(|| ArcRefCell::new(AccessibilityNode::new(id)));
106 {
107 let mut new_node = node.borrow_mut();
108
109 new_node.opaque_node = Some(dom_node.opaque());
110 if let Some(dom_element) = dom_node.as_element() {
111 let local_name = dom_element.local_name().to_ascii_lowercase();
112 new_node.set_html_tag(&local_name);
113 }
114 }
115
116 node.clone()
117 }
118
119 fn assert_node_by_dom_node(
120 &mut self,
121 dom_node: &ServoLayoutNode<'_>,
122 ) -> ArcRefCell<AccessibilityNode> {
123 let id = self.id_for_opaque(dom_node.opaque());
124 let node = self.assert_node_by_id(id);
125 assert!(node.borrow().opaque_node == Some(dom_node.opaque()));
126 node
127 }
128
129 fn assert_node_by_id(&self, id: accesskit::NodeId) -> ArcRefCell<AccessibilityNode> {
130 let Some(node) = self.nodes.get(&id) else {
131 panic!("Stale node ID found: {id:?}");
132 };
133 node.clone()
134 }
135
136 fn id_for_opaque(&mut self, opaque: OpaqueNode) -> accesskit::NodeId {
137 let id = self.opaque_node_to_id.entry(opaque).or_insert_with(|| {
138 static LAST_ID: AtomicU64 = AtomicU64::new(0);
139 LAST_ID.fetch_add(1, atomic::Ordering::SeqCst).into()
140 });
141 *id
142 }
143
144 pub(crate) fn epoch(&self) -> Epoch {
145 self.epoch
146 }
147}
148
149fn role_from_dom_node(dom_node: &ServoLayoutNode<'_>) -> Role {
150 if let Some(dom_element) = dom_node.as_element() {
151 let local_name = dom_element.local_name().to_ascii_lowercase();
152 *HTML_ELEMENT_ROLE_MAPPINGS
153 .get(&local_name)
154 .unwrap_or(&Role::GenericContainer)
155 } else if dom_node.type_id() == Some(LayoutNodeType::Text) {
156 Role::TextRun
157 } else {
158 Role::GenericContainer
159 }
160}
161
162impl AccessibilityNode {
163 fn new(id: accesskit::NodeId) -> Self {
164 Self::new_with_role(id, Role::Unknown)
165 }
166
167 fn new_with_role(id: accesskit::NodeId, role: accesskit::Role) -> Self {
168 Self {
169 id,
170 accesskit_node: accesskit::Node::new(role),
171 opaque_node: None,
172 updated: true,
173 }
174 }
175
176 fn update_descendants<'dom>(
177 &mut self,
178 dom_node: &ServoLayoutNode<'dom>,
179 tree: &mut AccessibilityTree,
180 tree_update: &mut AccessibilityUpdate,
181 ) -> bool {
182 let mut any_descendant_updated = false;
183 let new_children = dom_node
184 .flat_tree_children()
185 .map(|dom_child| {
186 let child_node = tree.get_or_create_node(&dom_child);
187 let child_node_id = child_node.borrow().id;
188
189 any_descendant_updated |= tree.update_node_and_descendants(&dom_child, tree_update);
192
193 child_node_id
194 })
195 .collect();
196 if new_children != self.children() {
197 self.set_children(new_children);
198 }
199 any_descendant_updated
200 }
201
202 fn update_node(
203 &mut self,
204 dom_node: &ServoLayoutNode<'_>,
205 tree: &mut AccessibilityTree,
206 any_descendant_updated: bool,
207 ) {
208 self.set_role(role_from_dom_node(dom_node));
209 if dom_node.is_element() {
210 if any_descendant_updated {
211 if let Some(text) = self.label_from_descendants(tree) {
212 self.set_label(text.as_str());
213 }
214 }
215 } else if dom_node.type_id() == Some(LayoutNodeType::Text) {
216 let text_content = dom_node.text_content();
217 trace!("node text content = {text_content:?}");
218 self.set_value(&text_content);
220 }
221 }
222
223 fn label_from_descendants(&self, tree: &AccessibilityTree) -> Option<String> {
224 if !NAME_FROM_CONTENTS_ROLES.contains(&self.role()) {
225 return None;
226 }
227 let mut children = VecDeque::from_iter(self.children().iter().copied());
228 let mut text = String::new();
229 while let Some(child_id) = children.pop_front() {
230 let child = tree.assert_node_by_id(child_id);
231 let child = child.borrow();
232 match child.role() {
233 Role::TextRun => {
234 if let Some(child_text) = child.value() {
235 text.push_str(child_text);
236 }
237 },
238 _ => {
239 for id in child.children().iter().rev() {
240 children.push_front(*id);
241 }
242 },
243 }
244 }
245 Some(text.trim().to_owned())
246 }
247
248 fn children(&self) -> &[accesskit::NodeId] {
251 self.accesskit_node.children()
252 }
253
254 fn set_children(&mut self, children: Vec<accesskit::NodeId>) {
255 self.accesskit_node.set_children(children);
256 self.updated = true;
257 }
258
259 fn role(&self) -> accesskit::Role {
260 self.accesskit_node.role()
261 }
262
263 fn set_role(&mut self, role: accesskit::Role) {
264 if role == self.accesskit_node.role() {
265 return;
266 }
267 self.accesskit_node.set_role(role);
268 self.updated = true;
269 }
270
271 #[expect(dead_code)]
272 fn label(&self) -> Option<&str> {
273 self.accesskit_node.label()
274 }
275
276 fn set_label(&mut self, label: &str) {
277 if Some(label) == self.accesskit_node.label() {
278 return;
279 }
280 self.accesskit_node.set_label(label);
281 self.updated = true;
282 }
283
284 #[expect(dead_code)]
285 fn html_tag(&self) -> Option<&str> {
286 self.accesskit_node.html_tag()
287 }
288
289 fn set_html_tag(&mut self, html_tag: &str) {
290 if Some(html_tag) == self.accesskit_node.html_tag() {
291 return;
292 }
293 self.accesskit_node.set_html_tag(html_tag);
294 self.updated = true;
295 }
296
297 fn value(&self) -> Option<&str> {
298 self.accesskit_node.value()
299 }
300
301 fn set_value(&mut self, value: &str) {
302 if Some(value) == self.accesskit_node.value() {
303 return;
304 }
305 self.accesskit_node.set_value(value);
306 self.updated = true;
307 }
308}
309
310impl AccessibilityUpdate {
311 fn new(tree: accesskit::Tree, tree_id: accesskit::TreeId) -> Self {
312 Self {
313 accesskit_update: accesskit::TreeUpdate {
314 nodes: vec![],
315 tree: Some(tree),
316 focus: accesskit::NodeId(1),
317 tree_id,
318 },
319 nodes: FxHashMap::default(),
320 }
321 }
322
323 fn add(&mut self, node: &mut AccessibilityNode) {
324 self.nodes.insert(node.id, node.accesskit_node.clone());
325 node.updated = false;
326 }
327
328 fn finalize(mut self) -> accesskit::TreeUpdate {
329 self.accesskit_update.nodes.extend(self.nodes.drain());
330 self.accesskit_update
331 }
332}
333
334#[cfg(test)]
335#[test]
336fn test_accessibility_update_add_some_nodes_twice() {
337 let mut update = AccessibilityUpdate::new(
338 accesskit::Tree {
339 root: accesskit::NodeId(2),
340 toolkit_name: None,
341 toolkit_version: None,
342 },
343 accesskit::TreeId::ROOT,
344 );
345 update.add(&mut AccessibilityNode::new_with_role(
346 accesskit::NodeId(5),
347 Role::Paragraph,
348 ));
349 update.add(&mut AccessibilityNode::new_with_role(
350 accesskit::NodeId(3),
351 Role::GenericContainer,
352 ));
353 update.add(&mut AccessibilityNode::new_with_role(
354 accesskit::NodeId(4),
355 Role::Heading,
356 ));
357 update.add(&mut AccessibilityNode::new_with_role(
358 accesskit::NodeId(4),
359 Role::Heading,
360 ));
361 update.add(&mut AccessibilityNode::new_with_role(
362 accesskit::NodeId(3),
363 Role::ScrollView,
364 ));
365 let mut tree_update = update.finalize();
366 tree_update.nodes.sort_by_key(|(node_id, _node)| *node_id);
367 assert_eq!(
368 tree_update,
369 accesskit::TreeUpdate {
370 nodes: vec![
371 (accesskit::NodeId(3), accesskit::Node::new(Role::ScrollView)),
372 (accesskit::NodeId(4), accesskit::Node::new(Role::Heading)),
373 (accesskit::NodeId(5), accesskit::Node::new(Role::Paragraph)),
374 ],
375 tree: Some(accesskit::Tree {
376 root: accesskit::NodeId(2),
377 toolkit_name: None,
378 toolkit_version: None
379 }),
380 tree_id: accesskit::TreeId::ROOT,
381 focus: accesskit::NodeId(1),
382 }
383 );
384}
385
386static HTML_ELEMENT_ROLE_MAPPINGS: LazyLock<FxHashMap<LocalName, Role>> = LazyLock::new(|| {
387 [
388 (local_name!("article"), Role::Article),
389 (local_name!("aside"), Role::Complementary),
390 (local_name!("body"), Role::RootWebArea),
391 (local_name!("footer"), Role::ContentInfo),
392 (local_name!("h1"), Role::Heading),
393 (local_name!("h2"), Role::Heading),
394 (local_name!("h3"), Role::Heading),
395 (local_name!("h4"), Role::Heading),
396 (local_name!("h5"), Role::Heading),
397 (local_name!("h6"), Role::Heading),
398 (local_name!("header"), Role::Banner),
399 (local_name!("hr"), Role::Splitter),
400 (local_name!("main"), Role::Main),
401 (local_name!("nav"), Role::Navigation),
402 (local_name!("p"), Role::Paragraph),
403 ]
404 .into_iter()
405 .collect()
406});
407
408static NAME_FROM_CONTENTS_ROLES: LazyLock<FxHashSet<Role>> =
410 LazyLock::new(|| [(Role::Heading)].into_iter().collect());