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