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