freya_core/accessibility/tree.rs
1use accesskit::{
2 Action,
3 Node,
4 Rect,
5 Role,
6 TreeUpdate,
7};
8use ragnarok::ProcessedEvents;
9use rustc_hash::{
10 FxHashMap,
11 FxHashSet,
12};
13use torin::prelude::LayoutNode;
14
15use crate::{
16 accessibility::{
17 focus_strategy::AccessibilityFocusStrategy,
18 focusable::Focusable,
19 id::AccessibilityId,
20 },
21 elements::label::Label,
22 events::emittable::EmmitableEvent,
23 integration::{
24 EventName,
25 EventsChunk,
26 },
27 node_id::NodeId,
28 prelude::{
29 AccessibilityFocusMovement,
30 EventType,
31 WheelEventData,
32 WheelSource,
33 },
34 tree::Tree,
35};
36
37pub const ACCESSIBILITY_ROOT_ID: AccessibilityId = AccessibilityId(0);
38
39pub struct AccessibilityTree {
40 pub map: FxHashMap<AccessibilityId, NodeId>,
41 // Current focused Accessibility Node.
42 pub focused_id: AccessibilityId,
43}
44
45impl Default for AccessibilityTree {
46 fn default() -> Self {
47 Self::new(ACCESSIBILITY_ROOT_ID)
48 }
49}
50
51impl AccessibilityTree {
52 pub fn new(focused_id: AccessibilityId) -> Self {
53 Self {
54 focused_id,
55 map: FxHashMap::default(),
56 }
57 }
58
59 pub fn focused_node_id(&self) -> Option<NodeId> {
60 self.map.get(&self.focused_id).cloned()
61 }
62
63 /// Initialize the Accessibility Tree
64 pub fn init(&mut self, tree: &mut Tree) -> TreeUpdate {
65 tree.accessibility_diff.clear();
66
67 let mut nodes = vec![];
68
69 tree.traverse_depth(|node_id| {
70 let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
71 let layout_node = tree.layout.get(&node_id).unwrap();
72 let accessibility_node = Self::create_node(node_id, layout_node, tree);
73 nodes.push((accessibility_state.a11y_id, accessibility_node));
74 self.map.insert(accessibility_state.a11y_id, node_id);
75 });
76
77 #[cfg(debug_assertions)]
78 tracing::info!(
79 "Initialized the Accessibility Tree with {} nodes.",
80 nodes.len()
81 );
82
83 if !self.map.contains_key(&self.focused_id) {
84 self.focused_id = ACCESSIBILITY_ROOT_ID;
85 }
86
87 TreeUpdate {
88 nodes,
89 tree: Some(accesskit::Tree::new(ACCESSIBILITY_ROOT_ID)),
90 focus: self.focused_id,
91 }
92 }
93
94 /// Process any pending Accessibility Tree update
95 #[cfg_attr(feature = "hotpath", hotpath::measure)]
96 pub fn process_updates(
97 &mut self,
98 tree: &mut Tree,
99 events_sender: &futures_channel::mpsc::UnboundedSender<EventsChunk>,
100 ) -> TreeUpdate {
101 let requested_focus = tree.accessibility_diff.requested_focus.take();
102 let removed_ids = tree
103 .accessibility_diff
104 .removed
105 .drain()
106 .collect::<FxHashMap<_, _>>();
107 let mut added_or_updated_ids = tree
108 .accessibility_diff
109 .added_or_updated
110 .drain()
111 .collect::<FxHashSet<_>>();
112
113 #[cfg(debug_assertions)]
114 if !removed_ids.is_empty() || !added_or_updated_ids.is_empty() {
115 tracing::info!(
116 "Updating the Accessibility Tree with {} removals and {} additions/modifications",
117 removed_ids.len(),
118 added_or_updated_ids.len()
119 );
120 }
121
122 // Remove all the removed nodes from the update list
123 for (node_id, _) in removed_ids.iter() {
124 added_or_updated_ids.remove(node_id);
125 self.map.retain(|_, id| id != node_id);
126 }
127
128 // Mark the parent of the removed nodes as updated
129 for (_, parent_id) in removed_ids.iter() {
130 if !removed_ids.contains_key(parent_id) {
131 added_or_updated_ids.insert(*parent_id);
132 }
133 }
134
135 // Register the created/updated nodes
136 for node_id in added_or_updated_ids.clone() {
137 let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
138 self.map.insert(accessibility_state.a11y_id, node_id);
139
140 let node_parent_id = tree.parents.get(&node_id).unwrap_or(&NodeId::ROOT);
141 added_or_updated_ids.insert(*node_parent_id);
142 }
143
144 // Create the updated nodes
145 let mut nodes = Vec::new();
146 for node_id in added_or_updated_ids {
147 let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
148 let layout_node = tree.layout.get(&node_id).unwrap();
149 let accessibility_node = Self::create_node(node_id, layout_node, tree);
150 nodes.push((accessibility_state.a11y_id, accessibility_node));
151 }
152
153 let has_request_focus = requested_focus.is_some();
154
155 // Fallback the focused id to the root if the focused node no longer exists
156 if !self.map.contains_key(&self.focused_id) {
157 self.focused_id = ACCESSIBILITY_ROOT_ID;
158 }
159
160 // Focus the requested node id if there is one
161 if let Some(requested_focus) = requested_focus {
162 self.focus_node_with_strategy(requested_focus, tree);
163 }
164
165 if let Some(node_id) = self.focused_node_id()
166 && has_request_focus
167 {
168 self.scroll_to(node_id, tree, events_sender);
169 }
170
171 TreeUpdate {
172 nodes,
173 tree: Some(accesskit::Tree::new(ACCESSIBILITY_ROOT_ID)),
174 focus: self.focused_id,
175 }
176 }
177
178 /// Focus a Node given the strategy.
179 pub fn focus_node_with_strategy(
180 &mut self,
181 strategy: AccessibilityFocusStrategy,
182 tree: &mut Tree,
183 ) {
184 if let AccessibilityFocusStrategy::Node(id) = strategy {
185 if self.map.contains_key(&id) {
186 self.focused_id = id;
187 }
188 return;
189 }
190
191 let (navigable_nodes, focused_id) = if strategy.mode()
192 == Some(AccessibilityFocusMovement::InsideGroup)
193 {
194 // Get all accessible nodes in the current group
195 let mut group_nodes = Vec::new();
196
197 let node_id = self.map.get(&self.focused_id).unwrap();
198 let accessibility_state = tree.accessibility_state.get(node_id).unwrap();
199 let member_accessibility_id = accessibility_state.a11y_member_of;
200 if let Some(member_accessibility_id) = member_accessibility_id {
201 group_nodes = tree
202 .accessibility_groups
203 .get(&member_accessibility_id)
204 .cloned()
205 .unwrap_or_default()
206 .into_iter()
207 .filter(|id| {
208 let node_id = self.map.get(id).unwrap();
209 let accessibility_state = tree.accessibility_state.get(node_id).unwrap();
210 accessibility_state.a11y_focusable == Focusable::Enabled
211 })
212 .collect();
213 }
214 (group_nodes, self.focused_id)
215 } else {
216 let mut nodes = Vec::new();
217
218 tree.traverse_depth(|node_id| {
219 let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
220 let member_accessibility_id = accessibility_state.a11y_member_of;
221
222 // Exclude nodes that are members of groups except for the parent of the group
223 if let Some(member_accessibility_id) = member_accessibility_id
224 && member_accessibility_id != accessibility_state.a11y_id
225 {
226 return;
227 }
228 if accessibility_state.a11y_focusable == Focusable::Enabled {
229 nodes.push(accessibility_state.a11y_id);
230 }
231 });
232
233 (nodes, self.focused_id)
234 };
235
236 let node_index = navigable_nodes
237 .iter()
238 .position(|accessibility_id| *accessibility_id == focused_id);
239
240 let target_node = match strategy {
241 AccessibilityFocusStrategy::Forward(_) => {
242 // Find the next Node
243 if let Some(node_index) = node_index {
244 if node_index == navigable_nodes.len() - 1 {
245 navigable_nodes.first().cloned()
246 } else {
247 navigable_nodes.get(node_index + 1).cloned()
248 }
249 } else {
250 navigable_nodes.first().cloned()
251 }
252 }
253 AccessibilityFocusStrategy::Backward(_) => {
254 // Find the previous Node
255 if let Some(node_index) = node_index {
256 if node_index == 0 {
257 navigable_nodes.last().cloned()
258 } else {
259 navigable_nodes.get(node_index - 1).cloned()
260 }
261 } else {
262 navigable_nodes.last().cloned()
263 }
264 }
265 _ => unreachable!(),
266 };
267
268 self.focused_id = target_node.unwrap_or(focused_id);
269
270 #[cfg(debug_assertions)]
271 tracing::info!("Focused {:?} node.", self.focused_id);
272 }
273
274 /// Send the necessary wheel events to scroll views so that the given focused [NodeId] is visible on screen.
275 fn scroll_to(
276 &self,
277 node_id: NodeId,
278 tree: &mut Tree,
279 events_sender: &futures_channel::mpsc::UnboundedSender<EventsChunk>,
280 ) {
281 let Some(effect_state) = tree.effect_state.get(&node_id) else {
282 return;
283 };
284 let mut target_node = node_id;
285 let mut emmitable_events = Vec::new();
286 // Iterate over the inherited scrollables from the closes to the farest
287 for closest_scrollable in effect_state.scrollables.iter().rev() {
288 // Every scrollable has a target node, the first scrollable target is the focused node that we want to make visible,
289 // the rest scrollables will in the other hand just have the previous scrollable as target
290 let target_layout_node = tree.layout.get(&target_node).unwrap();
291 let target_area = target_layout_node.area;
292 let scrollable_layout_node = tree.layout.get(closest_scrollable).unwrap();
293 let scrollable_target_area = scrollable_layout_node.area;
294
295 // We only want to scroll if it is not visible
296 if !effect_state.is_visible(&tree.layout, &target_area) {
297 let element = tree.elements.get(closest_scrollable).unwrap();
298 let scroll_x = element
299 .accessibility()
300 .builder
301 .scroll_x()
302 .unwrap_or_default() as f32;
303 let scroll_y = element
304 .accessibility()
305 .builder
306 .scroll_y()
307 .unwrap_or_default() as f32;
308
309 // Get the relative diff from where the scrollable scroll starts
310 let diff_x = target_area.min_x() - scrollable_target_area.min_x() - scroll_x;
311 let diff_y = target_area.min_y() - scrollable_target_area.min_y() - scroll_y;
312
313 // And get the distance it needs to scroll in order to make the target visible
314 let delta_y = -(scroll_y + diff_y);
315 let delta_x = -(scroll_x + diff_x);
316 emmitable_events.push(EmmitableEvent {
317 name: EventName::Wheel,
318 source_event: EventName::Wheel,
319 node_id: *closest_scrollable,
320 data: EventType::Wheel(WheelEventData::new(
321 delta_x as f64,
322 delta_y as f64,
323 WheelSource::Custom,
324 )),
325 bubbles: false,
326 });
327 // Change the target to the current scrollable, so that the next scrollable makes sure this one is visible
328 target_node = *closest_scrollable;
329 }
330 }
331 events_sender
332 .unbounded_send(EventsChunk::Processed(ProcessedEvents {
333 emmitable_events,
334 ..Default::default()
335 }))
336 .unwrap();
337 }
338
339 /// Create an accessibility node
340 pub fn create_node(node_id: NodeId, layout_node: &LayoutNode, tree: &Tree) -> Node {
341 let element = tree.elements.get(&node_id).unwrap();
342 let mut accessibility_data = element.accessibility().into_owned();
343
344 if node_id == NodeId::ROOT {
345 accessibility_data.builder.set_role(Role::Window);
346 }
347
348 // Set children
349 let children = tree
350 .children
351 .get(&node_id)
352 .cloned()
353 .unwrap_or_default()
354 .into_iter()
355 .map(|child| tree.accessibility_state.get(&child).unwrap().a11y_id)
356 .collect::<Vec<_>>();
357 accessibility_data.builder.set_children(children);
358
359 // Set the area
360 let area = layout_node.area.to_f64();
361 accessibility_data.builder.set_bounds(Rect {
362 x0: area.min_x(),
363 x1: area.max_x(),
364 y0: area.min_y(),
365 y1: area.max_y(),
366 });
367
368 // Set inner text
369 if let Some(children) = tree.children.get(&node_id) {
370 for child in children {
371 let children_element = tree.elements.get(child).unwrap();
372 // TODO: Maybe support paragraphs too, or use a new trait
373 let Some(label) = Label::try_downcast(children_element.as_ref()) else {
374 continue;
375 };
376 accessibility_data.builder.set_label(label.text);
377 }
378 }
379
380 // Set focusable action
381 // This will cause assistive technology to offer the user an option
382 // to focus the current element if it supports it.
383 if accessibility_data.a11y_focusable.is_enabled() {
384 accessibility_data.builder.add_action(Action::Focus);
385 // accessibility_data.builder.add_action(Action::Click);
386 }
387
388 // // Rotation transform
389 // if let Some((_, rotation)) = transform_state
390 // .rotations
391 // .iter()
392 // .find(|(id, _)| id == &node_ref.id())
393 // {
394 // let rotation = rotation.to_radians() as f64;
395 // let (s, c) = rotation.sin_cos();
396 // builder.set_transform(Affine::new([c, s, -s, c, 0.0, 0.0]));
397 // }
398
399 // // Clipping overflow
400 // if style_state.overflow == OverflowMode::Clip {
401 // builder.set_clips_children();
402 // }
403
404 // Foreground/Background color
405 // builder.set_foreground_color(font_style_state.color.into());
406 // if let Fill::Color(color) = style_state.background {
407 // builder.set_background_color(color.into());
408 // }
409
410 // // If the node is a block-level element in the layout, indicate that it will cause a linebreak.
411 // if !node_type.is_text() {
412 // if let NodeType::Element(node) = &*node_type {
413 // // This should be impossible currently but i'm checking for it just in case.
414 // // In the future, inline text spans should have their own own accessibility node,
415 // // but that's not a concern yet.
416 // if node.tag != TagName::Text {
417 // builder.set_is_line_breaking_object();
418 // }
419 // }
420 // }
421
422 // Font size
423 // builder.set_font_size(font_style_state.font_size as _);
424
425 // // If the font family has changed since the parent node, then we inform accesskit of this change.
426 // if let Some(parent_node) = node_ref.parent() {
427 // if parent_node.get::<FontStyleState>().unwrap().font_family
428 // != font_style_state.font_family
429 // {
430 // builder.set_font_family(font_style_state.font_family.join(", "));
431 // }
432 // } else {
433 // // Element has no parent elements, so we set the initial font style.
434 // builder.set_font_family(font_style_state.font_family.join(", "));
435 // }
436
437 // // Set bold flag for weights above 700
438 // if font_style_state.font_weight > 700.into() {
439 // builder.set_bold();
440 // }
441
442 // // Text alignment
443 // builder.set_text_align(match font_style_state.text_align {
444 // TextAlign::Center => accesskit::TextAlign::Center,
445 // TextAlign::Justify => accesskit::TextAlign::Justify,
446 // // TODO: change representation of `Start` and `End` once RTL text/writing modes are supported.
447 // TextAlign::Left | TextAlign::Start => accesskit::TextAlign::Left,
448 // TextAlign::Right | TextAlign::End => accesskit::TextAlign::Right,
449 // });
450
451 // // TODO: Adjust this once text direction support other than RTL is properly added
452 // builder.set_text_direction(TextDirection::LeftToRight);
453
454 // // Set italic property for italic/oblique font slants
455 // match font_style_state.font_slant {
456 // FontSlant::Italic | FontSlant::Oblique => builder.set_italic(),
457 // _ => {}
458 // }
459
460 // // Text decoration
461 // if font_style_state
462 // .text_decoration
463 // .contains(TextDecoration::LINE_THROUGH)
464 // {
465 // builder.set_strikethrough(skia_decoration_style_to_accesskit(
466 // font_style_state.text_decoration_style,
467 // ));
468 // }
469 // if font_style_state
470 // .text_decoration
471 // .contains(TextDecoration::UNDERLINE)
472 // {
473 // builder.set_underline(skia_decoration_style_to_accesskit(
474 // font_style_state.text_decoration_style,
475 // ));
476 // }
477 // if font_style_state
478 // .text_decoration
479 // .contains(TextDecoration::OVERLINE)
480 // {
481 // builder.set_overline(skia_decoration_style_to_accesskit(
482 // font_style_state.text_decoration_style,
483 // ));
484 // }
485
486 accessibility_data.builder
487 }
488}
489
490// fn skia_decoration_style_to_accesskit(style: TextDecorationStyle) -> accesskit::TextDecoration {
491// match style {
492// TextDecorationStyle::Solid => accesskit::TextDecoration::Solid,
493// TextDecorationStyle::Dotted => accesskit::TextDecoration::Dotted,
494// TextDecorationStyle::Dashed => accesskit::TextDecoration::Dashed,
495// TextDecorationStyle::Double => accesskit::TextDecoration::Double,
496// TextDecorationStyle::Wavy => accesskit::TextDecoration::Wavy,
497// }
498// }