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