Skip to main content

freya_core/accessibility/
tree.rs

1use std::any::Any;
2
3use accesskit::{
4    Action,
5    ActionRequest,
6    Affine,
7    Node,
8    Rect,
9    Role,
10    TreeId,
11    TreeUpdate,
12};
13use ragnarok::ProcessedEvents;
14use rustc_hash::FxHashMap;
15use torin::prelude::{
16    CursorPoint,
17    LayoutNode,
18};
19
20use crate::{
21    accessibility::{
22        focus_strategy::AccessibilityFocusStrategy,
23        focusable::Focusable,
24        id::AccessibilityId,
25    },
26    data::Overflow,
27    elements::{
28        label::LabelElement,
29        paragraph::ParagraphElement,
30    },
31    events::emittable::EmmitableEvent,
32    integration::{
33        EventName,
34        EventsChunk,
35    },
36    node_id::NodeId,
37    prelude::{
38        AccessibilityFocusMovement,
39        Color,
40        EventType,
41        FontSlant,
42        TextAlign,
43        TextDecoration,
44        TouchPhase,
45        WheelEventData,
46        WheelSource,
47    },
48    tree::Tree,
49};
50
51pub const ACCESSIBILITY_ROOT_ID: AccessibilityId = AccessibilityId(0);
52
53pub struct AccessibilityTree {
54    pub map: FxHashMap<AccessibilityId, NodeId>,
55    // Current focused Accessibility Node.
56    pub focused_id: AccessibilityId,
57}
58
59impl Default for AccessibilityTree {
60    fn default() -> Self {
61        Self::new(ACCESSIBILITY_ROOT_ID)
62    }
63}
64
65impl AccessibilityTree {
66    pub fn new(focused_id: AccessibilityId) -> Self {
67        Self {
68            focused_id,
69            map: FxHashMap::default(),
70        }
71    }
72
73    pub fn focused_node_id(&self) -> Option<NodeId> {
74        self.map.get(&self.focused_id).cloned()
75    }
76
77    /// Dispatch an action requested by assistive technology.
78    pub fn handle_action(
79        &self,
80        request: ActionRequest,
81        tree: &mut Tree,
82        events_sender: &futures_channel::mpsc::UnboundedSender<EventsChunk>,
83    ) -> bool {
84        if request.target_tree != TreeId::ROOT {
85            return false;
86        }
87        let Some(node_id) = self.map.get(&request.target_node).copied() else {
88            return false;
89        };
90        let Some(element) = tree.elements.get(&node_id) else {
91            return false;
92        };
93        let mut builder = element.accessibility().builder.clone();
94        element.finish_accessibility(&mut builder);
95        if builder.is_disabled() {
96            return false;
97        }
98        match request.action {
99            Action::Focus => {
100                if !element.accessibility().a11y_focusable.is_enabled() {
101                    return false;
102                }
103                tree.accessibility_diff
104                    .request_focus(AccessibilityFocusStrategy::Node(request.target_node));
105                true
106            }
107            Action::Blur => {
108                if self.focused_id == request.target_node {
109                    tree.accessibility_diff
110                        .request_focus(AccessibilityFocusStrategy::Node(ACCESSIBILITY_ROOT_ID));
111                }
112                true
113            }
114            Action::ScrollIntoView => {
115                self.scroll_to(node_id, tree, events_sender);
116                true
117            }
118            _ => events_sender
119                .unbounded_send(EventsChunk::Batch(vec![EmmitableEvent {
120                    name: EventName::AccessibilityAction,
121                    source_event: EventName::AccessibilityAction,
122                    node_id,
123                    data: EventType::AccessibilityAction(request),
124                    bubbles: true,
125                }]))
126                .is_ok(),
127        }
128    }
129
130    /// Initialize the Accessibility Tree
131    pub fn init(&mut self, tree: &mut Tree, title: &str) -> TreeUpdate {
132        let requested_focus = tree.accessibility_diff.requested_focus.take();
133        tree.accessibility_diff.clear();
134
135        let mut nodes = vec![];
136
137        tree.traverse_depth(|node_id| {
138            let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
139            let layout_node = tree.layout.get(&node_id).unwrap();
140            let accessibility_node = Self::create_node(node_id, layout_node, tree, title);
141            nodes.push((accessibility_state.a11y_id, accessibility_node));
142            self.map.insert(accessibility_state.a11y_id, node_id);
143        });
144
145        #[cfg(debug_assertions)]
146        tracing::info!(
147            "Initialized the Accessibility Tree with {} nodes.",
148            nodes.len()
149        );
150
151        if !self.can_focus(self.focused_id, tree) {
152            self.focused_id = ACCESSIBILITY_ROOT_ID;
153        }
154
155        if let Some(requested_focus) = requested_focus {
156            self.focus_node_with_strategy(requested_focus, tree);
157        }
158
159        TreeUpdate {
160            tree_id: TreeId::ROOT,
161            nodes,
162            tree: Some(accesskit::TreeInfo::new(ACCESSIBILITY_ROOT_ID)),
163            focus: self.focused_id,
164        }
165    }
166
167    /// Process any pending Accessibility Tree update
168    #[cfg_attr(feature = "hotpath", hotpath::measure)]
169    pub fn process_updates(
170        &mut self,
171        tree: &mut Tree,
172        events_sender: &futures_channel::mpsc::UnboundedSender<EventsChunk>,
173        title: &str,
174    ) -> TreeUpdate {
175        let requested_focus = tree.accessibility_diff.requested_focus.take();
176        let removed_ids = std::mem::take(&mut tree.accessibility_diff.removed);
177        let mut added_or_updated_ids =
178            std::mem::take(&mut tree.accessibility_diff.added_or_updated);
179
180        #[cfg(debug_assertions)]
181        if !removed_ids.is_empty() || !added_or_updated_ids.is_empty() {
182            tracing::info!(
183                "Updating the Accessibility Tree with {} removals and {} additions/modifications",
184                removed_ids.len(),
185                added_or_updated_ids.len()
186            );
187        }
188
189        // Remove all the removed nodes from the update list
190        for node_id in removed_ids.keys() {
191            added_or_updated_ids.remove(node_id);
192            self.map.retain(|_, id| id != node_id);
193        }
194
195        // Mark the parent of the removed nodes as updated
196        for parent_id in removed_ids.values() {
197            if !removed_ids.contains_key(parent_id) {
198                added_or_updated_ids.insert(*parent_id);
199            }
200        }
201
202        // Register the created/updated nodes
203        for node_id in added_or_updated_ids.clone() {
204            let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
205            self.map.insert(accessibility_state.a11y_id, node_id);
206
207            let node_parent_id = tree.parents.get(&node_id).unwrap_or(&NodeId::ROOT);
208            added_or_updated_ids.insert(*node_parent_id);
209        }
210
211        // Create the updated nodes
212        let mut nodes = Vec::new();
213        for node_id in added_or_updated_ids {
214            let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
215            let layout_node = tree.layout.get(&node_id).unwrap();
216            let accessibility_node = Self::create_node(node_id, layout_node, tree, title);
217            nodes.push((accessibility_state.a11y_id, accessibility_node));
218        }
219
220        let has_request_focus = requested_focus.is_some();
221
222        // Return focus to the root when the focused node is removed or disabled.
223        if !self.can_focus(self.focused_id, tree) {
224            self.focused_id = ACCESSIBILITY_ROOT_ID;
225        }
226
227        // Focus the requested node id if there is one
228        if let Some(requested_focus) = requested_focus {
229            self.focus_node_with_strategy(requested_focus, tree);
230        }
231
232        if let Some(node_id) = self.focused_node_id()
233            && has_request_focus
234        {
235            self.scroll_to(node_id, tree, events_sender);
236        }
237
238        TreeUpdate {
239            tree_id: TreeId::ROOT,
240            nodes,
241            tree: Some(accesskit::TreeInfo::new(ACCESSIBILITY_ROOT_ID)),
242            focus: self.focused_id,
243        }
244    }
245
246    fn can_focus(&self, id: AccessibilityId, tree: &Tree) -> bool {
247        self.map.get(&id).is_some_and(|node_id| {
248            id == ACCESSIBILITY_ROOT_ID
249                || tree
250                    .accessibility_state
251                    .get(node_id)
252                    .is_some_and(|state| state.a11y_focusable != Focusable::Disabled)
253        })
254    }
255
256    /// Focus a Node given the strategy.
257    pub fn focus_node_with_strategy(
258        &mut self,
259        strategy: AccessibilityFocusStrategy,
260        tree: &mut Tree,
261    ) {
262        if let AccessibilityFocusStrategy::Node(id) = strategy {
263            if self.can_focus(id, tree) {
264                self.focused_id = id;
265            }
266            return;
267        }
268
269        let (navigable_nodes, focused_id) = if strategy.mode()
270            == Some(AccessibilityFocusMovement::InsideGroup)
271        {
272            // Get all accessible nodes in the current group
273            let mut group_nodes = Vec::new();
274
275            let node_id = self.map.get(&self.focused_id).unwrap();
276            let accessibility_state = tree.accessibility_state.get(node_id).unwrap();
277            let member_accessibility_id = accessibility_state.a11y_member_of;
278            if let Some(member_accessibility_id) = member_accessibility_id {
279                group_nodes = tree
280                    .accessibility_groups
281                    .get(&member_accessibility_id)
282                    .cloned()
283                    .unwrap_or_default()
284                    .into_iter()
285                    .filter(|id| {
286                        let node_id = self.map.get(id).unwrap();
287                        let accessibility_state = tree.accessibility_state.get(node_id).unwrap();
288                        accessibility_state.a11y_focusable == Focusable::Enabled
289                    })
290                    .collect();
291            }
292            (group_nodes, self.focused_id)
293        } else {
294            let mut nodes = Vec::new();
295
296            tree.traverse_depth(|node_id| {
297                let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
298                let member_accessibility_id = accessibility_state.a11y_member_of;
299
300                // Exclude nodes that are members of groups except for the parent of the group
301                if let Some(member_accessibility_id) = member_accessibility_id
302                    && member_accessibility_id != accessibility_state.a11y_id
303                {
304                    return;
305                }
306                if accessibility_state.a11y_focusable == Focusable::Enabled {
307                    nodes.push(accessibility_state.a11y_id);
308                }
309            });
310
311            (nodes, self.focused_id)
312        };
313
314        let node_index = navigable_nodes
315            .iter()
316            .position(|accessibility_id| *accessibility_id == focused_id);
317
318        let target_node = match strategy {
319            AccessibilityFocusStrategy::Forward(_) => {
320                // Find the next Node
321                if let Some(node_index) = node_index {
322                    if node_index == navigable_nodes.len() - 1 {
323                        navigable_nodes.first().cloned()
324                    } else {
325                        navigable_nodes.get(node_index + 1).cloned()
326                    }
327                } else {
328                    navigable_nodes.first().cloned()
329                }
330            }
331            AccessibilityFocusStrategy::Backward(_) => {
332                // Find the previous Node
333                if let Some(node_index) = node_index {
334                    if node_index == 0 {
335                        navigable_nodes.last().cloned()
336                    } else {
337                        navigable_nodes.get(node_index - 1).cloned()
338                    }
339                } else {
340                    navigable_nodes.last().cloned()
341                }
342            }
343            _ => unreachable!(),
344        };
345
346        self.focused_id = target_node.unwrap_or(focused_id);
347
348        #[cfg(debug_assertions)]
349        tracing::info!("Focused {:?} node.", self.focused_id);
350    }
351
352    /// Send the necessary wheel events to scroll views so that the given focused [NodeId] is visible on screen.
353    fn scroll_to(
354        &self,
355        node_id: NodeId,
356        tree: &mut Tree,
357        events_sender: &futures_channel::mpsc::UnboundedSender<EventsChunk>,
358    ) {
359        let Some(effect_state) = tree.effect_state.get(&node_id) else {
360            return;
361        };
362        let mut target_node = node_id;
363        let mut emmitable_events = Vec::new();
364        // Iterate over the inherited scrollables from the closes to the farthest
365        for closest_scrollable in effect_state.scrollables.iter().rev() {
366            // Every scrollable has a target node, the first scrollable target is the focused node that we want to make visible,
367            // the rest scrollables will in the other hand just have the previous scrollable as target
368            let target_layout_node = tree.layout.get(&target_node).unwrap();
369            let target_area = target_layout_node.area;
370            let scrollable_layout_node = tree.layout.get(closest_scrollable).unwrap();
371            let scrollable_target_area = scrollable_layout_node.area;
372
373            // We only want to scroll if it is not visible
374            if !effect_state.is_visible(&tree.layout, &target_area) {
375                let element = tree.elements.get(closest_scrollable).unwrap();
376                let scroll_x = element
377                    .accessibility()
378                    .builder
379                    .scroll_x()
380                    .unwrap_or_default() as f32;
381                let scroll_y = element
382                    .accessibility()
383                    .builder
384                    .scroll_y()
385                    .unwrap_or_default() as f32;
386
387                // Get the relative diff from where the scrollable scroll starts
388                let diff_x = target_area.min_x() - scrollable_target_area.min_x() - scroll_x;
389                let diff_y = target_area.min_y() - scrollable_target_area.min_y() - scroll_y;
390
391                // And get the distance it needs to scroll in order to make the target visible
392                let delta_y = -(scroll_y + diff_y);
393                let delta_x = -(scroll_x + diff_x);
394                emmitable_events.push(EmmitableEvent {
395                    name: EventName::Wheel,
396                    source_event: EventName::Wheel,
397                    node_id: *closest_scrollable,
398                    data: EventType::Wheel(WheelEventData::new(
399                        delta_x as f64,
400                        delta_y as f64,
401                        WheelSource::Custom,
402                        TouchPhase::Moved,
403                        CursorPoint::default(),
404                        CursorPoint::default(),
405                    )),
406                    bubbles: false,
407                });
408                // Change the target to the current scrollable, so that the next scrollable makes sure this one is visible
409                target_node = *closest_scrollable;
410            }
411        }
412        events_sender
413            .unbounded_send(EventsChunk::Processed(ProcessedEvents {
414                emmitable_events,
415                ..Default::default()
416            }))
417            .unwrap();
418    }
419
420    /// Create an accessibility node
421    pub fn create_node(
422        node_id: NodeId,
423        layout_node: &LayoutNode,
424        tree: &Tree,
425        title: &str,
426    ) -> Node {
427        let element = tree.elements.get(&node_id).unwrap();
428        let mut accessibility_data = element.accessibility().into_owned();
429        element.finish_accessibility(&mut accessibility_data.builder);
430
431        if node_id == NodeId::ROOT {
432            accessibility_data.builder.set_role(Role::Window);
433            accessibility_data.builder.set_label(title);
434        }
435
436        // Set children
437        let children = tree
438            .children
439            .get(&node_id)
440            .cloned()
441            .unwrap_or_default()
442            .into_iter()
443            .map(|child| tree.accessibility_state.get(&child).unwrap().a11y_id)
444            .collect::<Vec<_>>();
445        accessibility_data.builder.set_children(children);
446
447        // Set the area
448        let area = layout_node.area.to_f64();
449        accessibility_data.builder.set_bounds(Rect {
450            x0: area.min_x(),
451            x1: area.max_x(),
452            y0: area.min_y(),
453            y1: area.max_y(),
454        });
455
456        // Set inner text
457        let inner_text = accessibility_data
458            .builder
459            .value()
460            .map(String::from)
461            .or_else(|| {
462                tree.children.get(&node_id).and_then(|children| {
463                    children.iter().find_map(|child| {
464                        let child_element = tree.elements.get(child).unwrap();
465                        let mut child_builder = child_element.accessibility().builder.clone();
466                        child_element.finish_accessibility(&mut child_builder);
467                        child_builder.value().map(String::from)
468                    })
469                })
470            });
471
472        if let Some(inner_text) = inner_text {
473            // Accesskit derives the name of Role::Label nodes from the value, not the label
474            if accessibility_data.builder.role() == Role::Label {
475                accessibility_data.builder.set_value(inner_text);
476            } else {
477                accessibility_data.builder.set_label(inner_text);
478            }
479        }
480
481        // Set focusable action
482        // This will cause assistive technology to offer the user an option
483        // to focus the current element if it supports it.
484        if accessibility_data.a11y_focusable.is_enabled() {
485            accessibility_data.builder.add_action(Action::Focus);
486            accessibility_data.builder.add_action(Action::Blur);
487        }
488
489        let builder = &mut accessibility_data.builder;
490
491        if let Some(effect_state) = tree.effect_state.get(&node_id) {
492            if let Some(rotation) = effect_state.rotation {
493                let rotation = (rotation as f64).to_radians();
494                let (sin, cos) = rotation.sin_cos();
495                builder.set_transform(Affine::new([cos, sin, -sin, cos, 0.0, 0.0]));
496            }
497
498            if effect_state.overflow == Overflow::Clip {
499                builder.set_clips_children();
500            }
501        }
502
503        if let Some(background) = element.style().background.as_color() {
504            builder.set_background_color(color_to_accesskit(background));
505        }
506
507        let element = element.as_ref() as &dyn Any;
508        let is_text_element = element.is::<LabelElement>() || element.is::<ParagraphElement>();
509        if !is_text_element {
510            builder.set_is_line_breaking_object();
511        }
512
513        if let Some(text_style) = tree.text_style_state.get(&node_id) {
514            if let Some(color) = text_style.color.as_color() {
515                builder.set_foreground_color(color_to_accesskit(color));
516            }
517
518            builder.set_font_size(f32::from(text_style.font_size));
519            builder.set_font_weight(f32::from(text_style.font_weight));
520            builder.set_font_family(text_style.font_families.join(", "));
521
522            if matches!(
523                text_style.font_slant,
524                FontSlant::Italic | FontSlant::Oblique
525            ) {
526                builder.set_italic();
527            }
528
529            builder.set_text_align(match text_style.text_align {
530                TextAlign::Center => accesskit::TextAlign::Center,
531                TextAlign::Justify => accesskit::TextAlign::Justify,
532                // TODO: change the representation of `Start` and `End` once writing modes are supported.
533                TextAlign::Left | TextAlign::Start => accesskit::TextAlign::Left,
534                TextAlign::Right | TextAlign::End => accesskit::TextAlign::Right,
535            });
536
537            // TODO: adjust this once text directions other than left to right are supported.
538            builder.set_text_direction(accesskit::TextDirection::LeftToRight);
539
540            let decoration = accesskit::TextDecoration {
541                style: accesskit::TextDecorationStyle::Solid,
542                color: text_style
543                    .color
544                    .as_color()
545                    .map(color_to_accesskit)
546                    .unwrap_or(color_to_accesskit(Color::BLACK)),
547            };
548            match text_style.text_decoration {
549                TextDecoration::Underline => builder.set_underline(decoration),
550                TextDecoration::Overline => builder.set_overline(decoration),
551                TextDecoration::LineThrough => builder.set_strikethrough(decoration),
552                TextDecoration::None => {}
553            }
554        }
555
556        accessibility_data.builder
557    }
558}
559
560/// Convert a Freya [Color] into its [accesskit::Color] equivalent.
561fn color_to_accesskit(color: Color) -> accesskit::Color {
562    accesskit::Color {
563        red: color.r(),
564        green: color.g(),
565        blue: color.b(),
566        alpha: color.a(),
567    }
568}