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