freya_core/accessibility/
tree.rs1use 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::{
15 CursorPoint,
16 LayoutNode,
17};
18
19use crate::{
20 accessibility::{
21 focus_strategy::AccessibilityFocusStrategy,
22 focusable::Focusable,
23 id::AccessibilityId,
24 },
25 elements::label::Label,
26 events::emittable::EmmitableEvent,
27 integration::{
28 EventName,
29 EventsChunk,
30 },
31 node_id::NodeId,
32 prelude::{
33 AccessibilityFocusMovement,
34 EventType,
35 Paragraph,
36 WheelEventData,
37 WheelSource,
38 },
39 tree::Tree,
40};
41
42pub const ACCESSIBILITY_ROOT_ID: AccessibilityId = AccessibilityId(0);
43
44pub struct AccessibilityTree {
45 pub map: FxHashMap<AccessibilityId, NodeId>,
46 pub focused_id: AccessibilityId,
48}
49
50impl Default for AccessibilityTree {
51 fn default() -> Self {
52 Self::new(ACCESSIBILITY_ROOT_ID)
53 }
54}
55
56impl AccessibilityTree {
57 pub fn new(focused_id: AccessibilityId) -> Self {
58 Self {
59 focused_id,
60 map: FxHashMap::default(),
61 }
62 }
63
64 pub fn focused_node_id(&self) -> Option<NodeId> {
65 self.map.get(&self.focused_id).cloned()
66 }
67
68 pub fn init(&mut self, tree: &mut Tree) -> TreeUpdate {
70 tree.accessibility_diff.clear();
71
72 let mut nodes = vec![];
73
74 tree.traverse_depth(|node_id| {
75 let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
76 let layout_node = tree.layout.get(&node_id).unwrap();
77 let accessibility_node = Self::create_node(node_id, layout_node, tree);
78 nodes.push((accessibility_state.a11y_id, accessibility_node));
79 self.map.insert(accessibility_state.a11y_id, node_id);
80 });
81
82 #[cfg(debug_assertions)]
83 tracing::info!(
84 "Initialized the Accessibility Tree with {} nodes.",
85 nodes.len()
86 );
87
88 if !self.map.contains_key(&self.focused_id) {
89 self.focused_id = ACCESSIBILITY_ROOT_ID;
90 }
91
92 TreeUpdate {
93 tree_id: TreeId::ROOT,
94 nodes,
95 tree: Some(accesskit::Tree::new(ACCESSIBILITY_ROOT_ID)),
96 focus: self.focused_id,
97 }
98 }
99
100 #[cfg_attr(feature = "hotpath", hotpath::measure)]
102 pub fn process_updates(
103 &mut self,
104 tree: &mut Tree,
105 events_sender: &futures_channel::mpsc::UnboundedSender<EventsChunk>,
106 ) -> TreeUpdate {
107 let requested_focus = tree.accessibility_diff.requested_focus.take();
108 let removed_ids = tree
109 .accessibility_diff
110 .removed
111 .drain()
112 .collect::<FxHashMap<_, _>>();
113 let mut added_or_updated_ids = tree
114 .accessibility_diff
115 .added_or_updated
116 .drain()
117 .collect::<FxHashSet<_>>();
118
119 #[cfg(debug_assertions)]
120 if !removed_ids.is_empty() || !added_or_updated_ids.is_empty() {
121 tracing::info!(
122 "Updating the Accessibility Tree with {} removals and {} additions/modifications",
123 removed_ids.len(),
124 added_or_updated_ids.len()
125 );
126 }
127
128 for (node_id, _) in removed_ids.iter() {
130 added_or_updated_ids.remove(node_id);
131 self.map.retain(|_, id| id != node_id);
132 }
133
134 for (_, parent_id) in removed_ids.iter() {
136 if !removed_ids.contains_key(parent_id) {
137 added_or_updated_ids.insert(*parent_id);
138 }
139 }
140
141 for node_id in added_or_updated_ids.clone() {
143 let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
144 self.map.insert(accessibility_state.a11y_id, node_id);
145
146 let node_parent_id = tree.parents.get(&node_id).unwrap_or(&NodeId::ROOT);
147 added_or_updated_ids.insert(*node_parent_id);
148 }
149
150 let mut nodes = Vec::new();
152 for node_id in added_or_updated_ids {
153 let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
154 let layout_node = tree.layout.get(&node_id).unwrap();
155 let accessibility_node = Self::create_node(node_id, layout_node, tree);
156 nodes.push((accessibility_state.a11y_id, accessibility_node));
157 }
158
159 let has_request_focus = requested_focus.is_some();
160
161 if !self.map.contains_key(&self.focused_id) {
163 self.focused_id = ACCESSIBILITY_ROOT_ID;
164 }
165
166 if let Some(requested_focus) = requested_focus {
168 self.focus_node_with_strategy(requested_focus, tree);
169 }
170
171 if let Some(node_id) = self.focused_node_id()
172 && has_request_focus
173 {
174 self.scroll_to(node_id, tree, events_sender);
175 }
176
177 TreeUpdate {
178 tree_id: TreeId::ROOT,
179 nodes,
180 tree: Some(accesskit::Tree::new(ACCESSIBILITY_ROOT_ID)),
181 focus: self.focused_id,
182 }
183 }
184
185 pub fn focus_node_with_strategy(
187 &mut self,
188 strategy: AccessibilityFocusStrategy,
189 tree: &mut Tree,
190 ) {
191 if let AccessibilityFocusStrategy::Node(id) = strategy {
192 if self.map.contains_key(&id) {
193 self.focused_id = id;
194 }
195 return;
196 }
197
198 let (navigable_nodes, focused_id) = if strategy.mode()
199 == Some(AccessibilityFocusMovement::InsideGroup)
200 {
201 let mut group_nodes = Vec::new();
203
204 let node_id = self.map.get(&self.focused_id).unwrap();
205 let accessibility_state = tree.accessibility_state.get(node_id).unwrap();
206 let member_accessibility_id = accessibility_state.a11y_member_of;
207 if let Some(member_accessibility_id) = member_accessibility_id {
208 group_nodes = tree
209 .accessibility_groups
210 .get(&member_accessibility_id)
211 .cloned()
212 .unwrap_or_default()
213 .into_iter()
214 .filter(|id| {
215 let node_id = self.map.get(id).unwrap();
216 let accessibility_state = tree.accessibility_state.get(node_id).unwrap();
217 accessibility_state.a11y_focusable == Focusable::Enabled
218 })
219 .collect();
220 }
221 (group_nodes, self.focused_id)
222 } else {
223 let mut nodes = Vec::new();
224
225 tree.traverse_depth(|node_id| {
226 let accessibility_state = tree.accessibility_state.get(&node_id).unwrap();
227 let member_accessibility_id = accessibility_state.a11y_member_of;
228
229 if let Some(member_accessibility_id) = member_accessibility_id
231 && member_accessibility_id != accessibility_state.a11y_id
232 {
233 return;
234 }
235 if accessibility_state.a11y_focusable == Focusable::Enabled {
236 nodes.push(accessibility_state.a11y_id);
237 }
238 });
239
240 (nodes, self.focused_id)
241 };
242
243 let node_index = navigable_nodes
244 .iter()
245 .position(|accessibility_id| *accessibility_id == focused_id);
246
247 let target_node = match strategy {
248 AccessibilityFocusStrategy::Forward(_) => {
249 if let Some(node_index) = node_index {
251 if node_index == navigable_nodes.len() - 1 {
252 navigable_nodes.first().cloned()
253 } else {
254 navigable_nodes.get(node_index + 1).cloned()
255 }
256 } else {
257 navigable_nodes.first().cloned()
258 }
259 }
260 AccessibilityFocusStrategy::Backward(_) => {
261 if let Some(node_index) = node_index {
263 if node_index == 0 {
264 navigable_nodes.last().cloned()
265 } else {
266 navigable_nodes.get(node_index - 1).cloned()
267 }
268 } else {
269 navigable_nodes.last().cloned()
270 }
271 }
272 _ => unreachable!(),
273 };
274
275 self.focused_id = target_node.unwrap_or(focused_id);
276
277 #[cfg(debug_assertions)]
278 tracing::info!("Focused {:?} node.", self.focused_id);
279 }
280
281 fn scroll_to(
283 &self,
284 node_id: NodeId,
285 tree: &mut Tree,
286 events_sender: &futures_channel::mpsc::UnboundedSender<EventsChunk>,
287 ) {
288 let Some(effect_state) = tree.effect_state.get(&node_id) else {
289 return;
290 };
291 let mut target_node = node_id;
292 let mut emmitable_events = Vec::new();
293 for closest_scrollable in effect_state.scrollables.iter().rev() {
295 let target_layout_node = tree.layout.get(&target_node).unwrap();
298 let target_area = target_layout_node.area;
299 let scrollable_layout_node = tree.layout.get(closest_scrollable).unwrap();
300 let scrollable_target_area = scrollable_layout_node.area;
301
302 if !effect_state.is_visible(&tree.layout, &target_area) {
304 let element = tree.elements.get(closest_scrollable).unwrap();
305 let scroll_x = element
306 .accessibility()
307 .builder
308 .scroll_x()
309 .unwrap_or_default() as f32;
310 let scroll_y = element
311 .accessibility()
312 .builder
313 .scroll_y()
314 .unwrap_or_default() as f32;
315
316 let diff_x = target_area.min_x() - scrollable_target_area.min_x() - scroll_x;
318 let diff_y = target_area.min_y() - scrollable_target_area.min_y() - scroll_y;
319
320 let delta_y = -(scroll_y + diff_y);
322 let delta_x = -(scroll_x + diff_x);
323 emmitable_events.push(EmmitableEvent {
324 name: EventName::Wheel,
325 source_event: EventName::Wheel,
326 node_id: *closest_scrollable,
327 data: EventType::Wheel(WheelEventData::new(
328 delta_x as f64,
329 delta_y as f64,
330 WheelSource::Custom,
331 CursorPoint::default(),
332 CursorPoint::default(),
333 )),
334 bubbles: false,
335 });
336 target_node = *closest_scrollable;
338 }
339 }
340 events_sender
341 .unbounded_send(EventsChunk::Processed(ProcessedEvents {
342 emmitable_events,
343 ..Default::default()
344 }))
345 .unwrap();
346 }
347
348 pub fn create_node(node_id: NodeId, layout_node: &LayoutNode, tree: &Tree) -> Node {
350 let element = tree.elements.get(&node_id).unwrap();
351 let mut accessibility_data = element.accessibility().into_owned();
352
353 if node_id == NodeId::ROOT {
354 accessibility_data.builder.set_role(Role::Window);
355 }
356
357 let children = tree
359 .children
360 .get(&node_id)
361 .cloned()
362 .unwrap_or_default()
363 .into_iter()
364 .map(|child| tree.accessibility_state.get(&child).unwrap().a11y_id)
365 .collect::<Vec<_>>();
366 accessibility_data.builder.set_children(children);
367
368 let area = layout_node.area.to_f64();
370 accessibility_data.builder.set_bounds(Rect {
371 x0: area.min_x(),
372 x1: area.max_x(),
373 y0: area.min_y(),
374 y1: area.max_y(),
375 });
376
377 if let Some(children) = tree.children.get(&node_id) {
379 for child in children {
380 let children_element = tree.elements.get(child).unwrap();
381 if let Some(label) = Label::try_downcast(children_element.as_ref()) {
383 accessibility_data.builder.set_label(label.text);
384 } else if let Some(paragraph) = Paragraph::try_downcast(children_element.as_ref()) {
385 accessibility_data.builder.set_label(
386 paragraph
387 .spans
388 .iter()
389 .map(|span| span.text.to_string())
390 .collect::<String>(),
391 );
392 };
393 }
394 }
395
396 if accessibility_data.a11y_focusable.is_enabled() {
400 accessibility_data.builder.add_action(Action::Focus);
401 }
403
404 accessibility_data.builder
503 }
504}
505
506