freya_core/accessibility/
tree.rs1use 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 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 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 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 #[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 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 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 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 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 if !self.can_focus(self.focused_id, tree) {
224 self.focused_id = ACCESSIBILITY_ROOT_ID;
225 }
226
227 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 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 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 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 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 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 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 for closest_scrollable in effect_state.scrollables.iter().rev() {
366 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 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 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 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 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 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 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 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 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 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 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 TextAlign::Left | TextAlign::Start => accesskit::TextAlign::Left,
534 TextAlign::Right | TextAlign::End => accesskit::TextAlign::Right,
535 });
536
537 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
560fn 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}