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// }