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