1//! The interface as assistive technology sees it: an AccessKit tree of the latest frame's
2//! boxes that have a role, and the keyboard's way among its controls, as `arc/ui.md`
3//! describes them.
4
5use crate::{Axis, Event, Flags, Id, Ui};
6use accesskit::{
7 Action, ActionData, ActionRequest, Node, NodeId, Orientation, Rect, Role, TreeId, TreeInfo,
8 TreeUpdate,
9};
10use std::collections::{HashMap, HashSet};
11use winit::{event::Ime, keyboard::NamedKey};
12
13/// A control the keyboard can focus.
14pub(crate) struct Stop {
15 id: Id,
16 rect: [f32; 4],
17 /// The toolbar or tab list it moves within by arrows, with its role and their axis.
18 group: Option<(Id, Role, Axis)>,
19 popup: Option<Id>,
20 selected: bool,
21 /// Takes keys of its own: a text field, or a box the host handles.
22 typing: bool,
23 custom: bool,
24}
25
26impl Stop {
27 /// What Tab steps over as one: its group, or itself.
28 fn step(&self) -> Id {
29 self.group.map_or(self.id, |(group, ..)| group)
30 }
31}
32
33/// Roles that hold controls rather than being one, and whose text is their own.
34pub(crate) fn holds(role: Role) -> bool {
35 matches!(
36 role,
37 Role::GenericContainer
38 | Role::Window
39 | Role::Group
40 | Role::Toolbar
41 | Role::Dialog
42 | Role::AlertDialog
43 | Role::Menu
44 | Role::MenuBar
45 | Role::ListBox
46 | Role::List
47 | Role::TabList
48 | Role::Tree
49 | Role::Grid
50 | Role::Pane
51 | Role::ScrollView
52 | Role::RadioGroup
53 | Role::Form
54 | Role::Region
55 )
56}
57
58/// Roles whose text is their value rather than their name.
59fn valued(role: Role) -> bool {
60 matches!(
61 role,
62 Role::TextInput
63 | Role::SearchInput
64 | Role::MultilineTextInput
65 | Role::ComboBox
66 | Role::EditableComboBox
67 | Role::SpinButton
68 )
69}
70
71/// The axis a group's arrows move along, where it is one Tab steps over whole.
72fn composite(node: &Node) -> Option<Axis> {
73 let along = match node.role() {
74 Role::Toolbar | Role::TabList => Axis::X,
75 Role::Tree | Role::RadioGroup => Axis::Y,
76 _ => return None,
77 };
78 Some(match node.orientation() {
79 Some(Orientation::Horizontal) => Axis::X,
80 Some(Orientation::Vertical) => Axis::Y,
81 None => along,
82 })
83}
84
85/// Lists and menus move their own selection by the keys of the box that owns them.
86fn navigates_itself(role: Role) -> bool {
87 matches!(role, Role::Menu | Role::ListBox)
88}
89
90/// Whether a box shows: not folded away, nor a clip shut to nothing, as a closed panel is.
91fn shown(built: &crate::Built) -> bool {
92 let [left, top, right, bottom] = built.rect;
93 !built.hidden && !(built.flags.contains(Flags::CLIP) && (left >= right || top >= bottom))
94}
95
96fn bounds(rect: [f32; 4], scale: f64) -> Rect {
97 let [x0, y0, x1, y1] = rect.map(|side| f64::from(side) * scale);
98 Rect::new(x0, y0, x1, y1)
99}
100
101impl Ui {
102 /// Box `id` as built this frame, as assistive technology sees it, a generic container
103 /// where it has no `Spec::role`. Bounds, children and the actions its flags allow are
104 /// filled in; where a control has no label, its text and its children's name it.
105 pub fn access(&mut self, id: Id) -> Option<&mut Node> {
106 let index = self.nodes.iter().rposition(|node| node.id == id)?;
107 Some(
108 self.nodes[index]
109 .access
110 .get_or_insert_with(|| Node::new(Role::GenericContainer)),
111 )
112 }
113
114 /// The changes to the interface's tree since the last update, with the window as its root
115 /// named `label`; `scale` is the window's pixels per logical pixel. Call after `end`.
116 pub fn accessibility(&mut self, label: &str, scale: f64) -> TreeUpdate {
117 let (nodes, focus) = self.tree_nodes(label, scale);
118 let fresh = self.sent.is_empty();
119 let mut changed = Vec::new();
120 let mut sent = HashMap::with_capacity(nodes.len());
121 for (id, node) in nodes {
122 if self.sent.get(&id) != Some(&node) {
123 changed.push((id.node(), node.clone()));
124 }
125 sent.insert(id, node);
126 }
127 self.sent = sent;
128 TreeUpdate {
129 nodes: changed,
130 tree: fresh.then(|| TreeInfo::new(Id::ROOT.node())),
131 tree_id: TreeId::ROOT,
132 focus: focus.node(),
133 }
134 }
135
136 /// The interface's whole tree, as `accessibility` first sends it, leaving what it last
137 /// sent alone.
138 pub fn accessibility_tree(&self, label: &str, scale: f64) -> TreeUpdate {
139 let (nodes, focus) = self.tree_nodes(label, scale);
140 TreeUpdate {
141 nodes: nodes
142 .into_iter()
143 .map(|(id, node)| (id.node(), node))
144 .collect(),
145 tree: Some(TreeInfo::new(Id::ROOT.node())),
146 tree_id: TreeId::ROOT,
147 focus: focus.node(),
148 }
149 }
150
151 /// Every node of the tree, and the focused one.
152 fn tree_nodes(&self, label: &str, scale: f64) -> (Vec<(Id, Node)>, Id) {
153 let mut nodes = Vec::new();
154 let mut children = Vec::new();
155 let mut seen = HashSet::from([Id::ROOT]);
156 let mut root = Node::new(Role::Window);
157 root.set_label(label);
158 // Before the first frame is built, the window is all there is.
159 if let Some(built) = self.nodes.first() {
160 self.describe(0, scale, None, &mut children, &mut nodes, &mut seen);
161 root.set_bounds(bounds(built.rect, scale));
162 }
163 root.set_children(children.into_iter().map(|(id, _)| id).collect::<Vec<_>>());
164 nodes.push((Id::ROOT, root));
165 let focus = self.focus.filter(|focus| seen.contains(focus));
166 (nodes, focus.unwrap_or(Id::ROOT))
167 }
168
169 /// Forgets what was sent, so the next update sends the whole tree.
170 pub fn deactivate_accessibility(&mut self) {
171 self.sent.clear();
172 }
173
174 /// Adds the nodes of box `index`'s children to `out` and their ids and rectangles to
175 /// `children`. Inside a control, text without a role goes to `texts` to name it.
176 fn describe(
177 &self,
178 index: usize,
179 scale: f64,
180 mut texts: Option<&mut Vec<String>>,
181 children: &mut Vec<(NodeId, [f32; 4])>,
182 out: &mut Vec<(Id, Node)>,
183 seen: &mut HashSet<Id>,
184 ) {
185 let tooltip = self.tip.map(|tip| tip.id.child("tooltip"));
186 for &child in &self.nodes[index].children {
187 let built = &self.nodes[child];
188 // A box built twice in a frame would give its node two parents.
189 if !shown(built) || Some(built.id) == tooltip || !seen.insert(built.id) {
190 continue;
191 }
192 let text = built
193 .label
194 .as_ref()
195 .map(|label| label.key.0.as_str())
196 .filter(|text| !text.is_empty());
197 let Some(access) = &built.access else {
198 match (text, texts.as_deref_mut()) {
199 (Some(text), Some(texts)) => texts.push(text.to_owned()),
200 (Some(text), None) => {
201 let mut node = Node::new(Role::Label);
202 node.set_value(text);
203 node.set_bounds(bounds(built.rect, scale));
204 children.push((built.id.node(), built.rect));
205 out.push((built.id, node));
206 }
207 (None, _) => {}
208 }
209 self.describe(child, scale, texts.as_deref_mut(), children, out, seen);
210 continue;
211 };
212 let mut node = access.clone();
213 let role = node.role();
214 node.set_bounds(bounds(built.rect, scale));
215 children.push((built.id.node(), built.rect));
216 if node.tree_id().is_some() {
217 // A graft holds only the tree it names, so what is built inside it shows
218 // beside it.
219 out.push((built.id, node));
220 self.describe(child, scale, texts.as_deref_mut(), children, out, seen);
221 continue;
222 }
223 let control = !holds(role);
224 let mut own = Vec::new();
225 let mut inner = Vec::new();
226 self.describe(
227 child,
228 scale,
229 control.then_some(&mut own),
230 &mut inner,
231 out,
232 seen,
233 );
234 if node.label().is_none() && !valued(role) {
235 let name: Vec<_> = text
236 .map(str::to_owned)
237 .into_iter()
238 .chain(own)
239 .collect::<Vec<_>>();
240 if !name.is_empty() {
241 node.set_label(name.join(" "));
242 }
243 }
244 if let Some(axis) = composite(&node) {
245 // Built in paint order, as the open tab over the rest, but read in place.
246 let along = usize::from(axis == Axis::Y);
247 inner.sort_by(|a, b| a.1[along].total_cmp(&b.1[along]));
248 }
249 node.set_children(inner.into_iter().map(|(id, _)| id).collect::<Vec<_>>());
250 if control && !node.is_disabled() {
251 // A text field takes the focus where the pointer would click it.
252 if built.flags.contains(Flags::FOCUSABLE) {
253 node.add_action(Action::Focus);
254 if valued(role) {
255 node.add_action(Action::SetValue);
256 }
257 } else if built.flags.contains(Flags::CLICKABLE) {
258 node.add_action(Action::Click);
259 node.add_action(Action::Focus);
260 }
261 }
262 out.push((built.id, node));
263 }
264 }
265
266 /// The controls the keyboard can focus, in the order built.
267 pub(crate) fn stops(&self) -> Vec<Stop> {
268 let mut stops = Vec::new();
269 self.collect_stops(0, None, None, &mut stops);
270 stops
271 }
272
273 fn collect_stops(
274 &self,
275 index: usize,
276 group: Option<(Id, Role, Axis)>,
277 popup: Option<Id>,
278 stops: &mut Vec<Stop>,
279 ) {
280 for &child in &self.nodes[index].children {
281 let built = &self.nodes[child];
282 if !shown(built) {
283 continue;
284 }
285 let popup = if built.anchor.is_some() {
286 Some(built.id)
287 } else {
288 popup
289 };
290 let mut group = group;
291 if let Some(access) = &built.access {
292 let role = access.role();
293 if navigates_itself(role) {
294 continue;
295 }
296 if let Some(axis) = composite(access) {
297 group = Some((built.id, role, axis));
298 } else if built.flags.intersects(Flags::CLICKABLE | Flags::FOCUSABLE)
299 && !access.is_disabled()
300 && (!holds(role) || built.flags.contains(Flags::CUSTOM))
301 {
302 stops.push(Stop {
303 id: built.id,
304 rect: built.rect,
305 group,
306 popup,
307 selected: access.is_selected() == Some(true)
308 || role == Role::RadioButton
309 && access.toggled() == Some(accesskit::Toggled::True),
310 typing: built.flags.intersects(Flags::FOCUSABLE | Flags::CUSTOM),
311 custom: built.flags.contains(Flags::CUSTOM),
312 });
313 }
314 }
315 self.collect_stops(child, group, popup, stops);
316 }
317 }
318
319 /// Moves the focus for a key among the controls, returning whether it took the key.
320 pub(crate) fn traverse(&mut self, key: NamedKey) -> bool {
321 let back = self.modifiers.shift_key();
322 let scope = self.popups.last().map(|popup| popup.id);
323 let at = self
324 .focus
325 .and_then(|focus| self.stops.iter().position(|stop| stop.id == focus));
326 // The focused control, where it takes no keys of its own.
327 let control = at.filter(|at| !self.stops[*at].typing);
328 let target = match key {
329 NamedKey::Tab => {
330 let from = match (self.focus, at) {
331 (None, _) => None,
332 (focus, _) if focus == scope => None,
333 (_, Some(at)) if !self.stops[at].custom => Some(at),
334 _ => return false,
335 };
336 self.step(scope, from, back, |_| true)
337 }
338 NamedKey::F6 if scope.is_none() => {
339 self.step(None, at, back, |stop| stop.group.is_some() || stop.custom)
340 }
341 NamedKey::F5
342 if self.modifiers.control_key()
343 && draw::edit::Platform::CURRENT == draw::edit::Platform::MacOs
344 && scope.is_none() =>
345 {
346 self.entry(|stop| matches!(stop.group, Some((_, Role::Toolbar, _))))
347 }
348 NamedKey::ArrowLeft
349 | NamedKey::ArrowRight
350 | NamedKey::ArrowUp
351 | NamedKey::ArrowDown
352 | NamedKey::Home
353 | NamedKey::End => {
354 let Some(at) = control else {
355 return false;
356 };
357 let Some((group, _, axis)) = self.stops[at].group else {
358 return false;
359 };
360 let along = usize::from(axis == Axis::Y);
361 let mut members: Vec<_> = self
362 .stops
363 .iter()
364 .filter(|stop| stop.group.is_some_and(|(id, ..)| id == group))
365 .collect();
366 members.sort_by(|a, b| a.rect[along].total_cmp(&b.rect[along]));
367 let place = members.iter().position(|stop| stop.id == self.stops[at].id);
368 let (Some(place), count) = (place, members.len()) else {
369 return false;
370 };
371 let [before, after] = match axis {
372 Axis::X => [NamedKey::ArrowLeft, NamedKey::ArrowRight],
373 Axis::Y => [NamedKey::ArrowUp, NamedKey::ArrowDown],
374 };
375 let to = match key {
376 NamedKey::Home => 0,
377 NamedKey::End => count - 1,
378 key if key == before => (place + count - 1) % count,
379 key if key == after => (place + 1) % count,
380 _ => return false,
381 };
382 Some(members[to].id)
383 }
384 NamedKey::Space | NamedKey::Enter => {
385 let Some(at) = control else {
386 return false;
387 };
388 self.press(self.stops[at].id);
389 return true;
390 }
391 NamedKey::Escape if scope.is_none() && control.is_some() => {
392 self.focus = self.resume.take();
393 self.focus_ring = false;
394 return true;
395 }
396 _ => return false,
397 };
398 let Some(target) = target else {
399 return false;
400 };
401 if at.is_none_or(|at| self.stops[at].custom) {
402 self.resume = self.focus;
403 }
404 self.focus = Some(target);
405 self.focus_ring = true;
406 true
407 }
408
409 /// The control Tab or F6 steps to in popup `scope` from stop `from`, over the steps
410 /// `counts` keeps: a group's is its selected control, or its first.
411 fn step(
412 &self,
413 scope: Option<Id>,
414 from: Option<usize>,
415 back: bool,
416 counts: impl Fn(&Stop) -> bool,
417 ) -> Option<Id> {
418 let mut entries: Vec<usize> = Vec::new();
419 for (index, stop) in self.stops.iter().enumerate() {
420 if stop.popup != scope {
421 continue;
422 }
423 match entries.last_mut() {
424 Some(last) if self.stops[*last].step() == stop.step() => {
425 if stop.selected {
426 *last = index;
427 }
428 }
429 _ => entries.push(index),
430 }
431 }
432 entries.retain(|entry| counts(&self.stops[*entry]));
433 let count = entries.len();
434 if count == 0 {
435 return None;
436 }
437 let here = from.and_then(|from| {
438 let step = self.stops[from].step();
439 entries
440 .iter()
441 .position(|entry| self.stops[*entry].step() == step)
442 });
443 let to = match (here, from) {
444 (Some(here), _) if back => (here + count - 1) % count,
445 (Some(here), _) => (here + 1) % count,
446 // From a control not counted, the step either side of it.
447 (None, Some(from)) => {
448 let after = entries
449 .iter()
450 .position(|entry| *entry > from)
451 .unwrap_or(count);
452 (if back { after + count - 1 } else { after }) % count
453 }
454 (None, None) if back => count - 1,
455 (None, None) => 0,
456 };
457 Some(self.stops[entries[to]].id)
458 }
459
460 /// The control a group `matching` is entered at.
461 fn entry(&self, matching: impl Fn(&Stop) -> bool) -> Option<Id> {
462 let first = self.stops.iter().position(&matching)?;
463 let group = self.stops[first].step();
464 let selected = self
465 .stops
466 .iter()
467 .find(|stop| stop.step() == group && stop.selected);
468 Some(selected.unwrap_or(&self.stops[first]).id)
469 }
470
471 /// Presses and clicks box `id` in one, as a key or assistive technology does.
472 fn press(&mut self, id: Id) {
473 let signal = self.signals.entry(id).or_default();
474 signal.pressed = true;
475 signal.clicked = true;
476 }
477
478 /// Carries out assistive technology's `request`.
479 pub(crate) fn act(&mut self, request: ActionRequest) {
480 let id = Id(request.target_node.0);
481 match (request.action, request.data) {
482 (Action::Click, _) => {
483 // As a press there would, one on a box beneath the popups closes them.
484 if self.hits[..self.modal].iter().any(|hit| hit.id == id) {
485 self.close_from(0);
486 }
487 self.press(id);
488 }
489 (Action::Focus, _) => self.focus = Some(id),
490 (Action::SetValue, Some(ActionData::Value(value))) => {
491 self.focus_all(id);
492 self.signals
493 .entry(id)
494 .or_default()
495 .events
496 .push(Event::Ime(Ime::Commit(value.into())));
497 }
498 _ => {}
499 }
500 }
501}
502
503#[cfg(test)]
504mod tests;