Skip to repository content889 lines · 34.4 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T06:50:33.340Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
a11y.rs
1//! Accessibility support, provided by [AccessKit][accesskit].
2//!
3//! There are user-facing guide-level docs [here](crate::_accessibility).
4//!
5//! ## Architecture
6//!
7//! ```text
8//! ┌────────────────────────────────┐ ┌─────────────────────┐
9//! ┌─▶│ AccessKit Adapter (MacOS) │◀─▶│ MacOS System APIs │
10//! │ └────────────────────────────────┘ └─────────────────────┘
11//! │
12//! ┌──────┐ ┌───────────┐ │ ┌────────────────────────────────┐ ┌─────────────────────┐
13//! │ GPUI │◀─▶│ AccessKit │◀─┼─▶│ AccessKit Adapter (Windows) │◀─▶│ Windows System APIs │
14//! └──────┘ └───────────┘ │ └────────────────────────────────┘ └─────────────────────┘
15//! │
16//! │ ┌────────────────────────────────┐ ┌─────────────────────┐
17//! └─▶│ AccessKit Adapter (Linux) │◀─▶│ dbus │
18//! └────────────────────────────────┘ └─────────────────────┘
19//! ```
20//!
21//! In order for GPUI apps to be usable for people using assistive technology,
22//! we must do a few things:
23//! - Inform the system when the UI changes meaningfully. This includes:
24//! - Reporting new/removed/changed UI elements
25//! - *Not* reporting irrelevant UI changes, e.g. an invisible `div()` being
26//! added.
27//! - Reporting the appearance and capabilities of each UI element. For example:
28//! - What does this piece of text say?
29//! - How far along is this progress bar?
30//! - Can this node be focused?
31//! - Can this node have a value directly assigned? (e.g. a slider)
32//! - Allowing the system to interact with the UI by dispatching actions to
33//! nodes. Note that AccessKit has its own [`Action`] type, which is not the
34//! [`crate::Action`] trait.
35//! - Activate and deactivate accessibility features when requested by the
36//! system.
37//!
38//! Activating and deactivating at the right time is trivial, so I won't go into
39//! detail here. The other two are almost orthogonal in implementation.
40//!
41//! The state for both lives in the [`A11y`] struct in this module.
42//!
43//! ### Reporting UI changes
44//!
45//! Every frame, we build a [`TreeUpdate`] and send it to the platform-specific
46//! adapter. A [`TreeUpdate`] is a representation of a subset of the UI tree.
47//! When the adapter receives the update, it diffs it against the previous
48//! update, and calls platform-specific APIs to inform screen readers about the
49//! changes. Nodes may have been created, destroyed, or updated.
50//!
51//! Each node has an ID, and this ID *should* be stable across frames. If a
52//! node's ID changes, then, from AccessKit's point of view, it is a different
53//! node.
54//!
55//! We derive the node ID from the [`GlobalElementId`] in
56//! [`GlobalElementId::accesskit_node_id`]. Nodes without [`GlobalElementId`]s
57//! cannot produce an AccessKit [`NodeId`], and so are not included in the
58//! accessibility tree. We try to warn when using accessibility APIs on
59//! [`div()`] without setting an ID.
60//!
61//! This all happens in [`Drawable::prepaint`]. The [`A11y`] struct maintains a
62//! stack of nodes during prepainting, which we can use to calculate the
63//! [`NodeId`]s, and record parent-child relationships. Once all [`Element`]s in
64//! a frame have been prepainted, we send the resulting [`TreeUpdate`] object to
65//! the adapter and the screen reader can announce the changes.
66//!
67//! #### Synthetic children
68//!
69//! Additionally, some nodes can register "synthetic children" using
70//! [`Element::a11y_synthetic_children`]. Normally, one accesskit node is pushed
71//! for every [`Element`] with a role and id. However, sometimes a single
72//! element may want to produce many accesskit nodes. These extra nodes are
73//! referred to as "synthetic children" of the element providing a non-default
74//! [`Element::a11y_synthetic_children`] implementation.
75//!
76//! The user is provided a builder-style API using [`A11ySubtreeBuilder`], which
77//! allows them to create push nodes that are children of the current node, as
78//! well as modify the current node itself.
79//!
80//! GPUI calls this callback *after* prepainting (and just before popping the
81//! corresponding element), since this step may need prepaint information to be
82//! available. In the future, we may want to add prepaint information more
83//! generally to [`Element::write_a11y_info`], but for now that's not necessary.
84//!
85//! ### Responding to actions
86//!
87//! On adapter creation, we provide a callback to the adapter, which can be used
88//! to dispatch actions. This callback forwards to [`A11y::action_listeners`], a
89//! mapping from [`NodeId`]s to action handlers (basically just `Box<dyn
90//! Fn()>`).
91//!
92//! This is populated in:
93//! - [`Window::on_a11y_action`], which is called by:
94//! - [`Interactivity::paint`], which is called by:
95//! - [`StatefulInteractiveElement::on_a11y_action`], which is a public-facing API
96//!
97//! These are cleared at the start of a frame, and re-populated during painting.
98//!
99//! [`NodeId`]: accesskit::NodeId
100
101use crate::*;
102
103pub(crate) mod debug;
104
105use crate::{App, Bounds, FocusId, Pixels, SharedString, Window};
106use accesskit::{Action, NodeId, TreeUpdate};
107use collections::{FxHashMap, FxHashSet};
108use smallvec::SmallVec;
109use std::hash::{Hash, Hasher};
110use std::sync::{
111 Arc,
112 atomic::{AtomicBool, Ordering},
113};
114
115/// The fixed AccessKit node ID used for the root of every window's a11y tree.
116pub(crate) const ROOT_NODE_ID: NodeId = NodeId(0);
117
118/// A listener for an accessibility action on a specific node.
119pub(crate) type A11yActionListener =
120 Box<dyn FnMut(Option<&accesskit::ActionData>, &mut Window, &mut App) + 'static>;
121
122/// Per-window accessibility state.
123///
124/// Manages the AccessKit tree that is built each frame and the mappings
125/// needed to dispatch incoming action requests back to the right elements.
126pub(crate) struct A11y {
127 /// Whether accessibility has been [forcibly disabled] for this window.
128 ///
129 /// [forcibly disabled]: crate::Application::new_inaccessible
130 force_disabled: bool,
131 /// Whether a11y features have been requested by the system.
132 ///
133 /// Updated by AccessKit using callbacks provided to the adapter. Can change
134 /// halfway through a frame.
135 active_flag: Arc<AtomicBool>,
136 /// Whether a11y features are active for *this specific frame*.
137 ///
138 /// At the start of each frame, we load [`Self::active_flag`] (using
139 /// [`Self::sync_active_flag`]) and use this to determine whether we
140 /// should construct a [`TreeUpdate`] for this frame. It's important that
141 /// this value is stable within a frame, because the builder API exposed by
142 /// this type maintains a stack of nodes and each must be pushed and popped
143 /// exactly once.
144 ///
145 /// At the end of the frame, we re-call [`Self::sync_active_flag`] to
146 /// determine whether we should actually send the finished [`TreeUpdate`].
147 active_this_frame: bool,
148 pub(crate) nodes: A11yNodeBuilder,
149 pub(crate) focus_ids: FxHashMap<NodeId, FocusId>,
150 pub(crate) node_bounds: FxHashMap<NodeId, Bounds<Pixels>>,
151 pub(crate) action_listeners: FxHashMap<NodeId, Vec<(Action, A11yActionListener)>>,
152 /// The window's title, used to label the root node so assistive
153 /// technology can tell windows apart.
154 window_title: Option<SharedString>,
155 /// The focus id we most recently reported as having no accessibility node,
156 /// used to log at most once per focus change rather than every frame.
157 last_focus_without_node: Option<FocusId>,
158 /// Retains the last tree update (and, in debug builds, per-node provenance)
159 /// so it can be dumped via [`crate::Window::debug_a11y_tree_json`].
160 debug: debug::A11yDebug,
161 /// Maps a view's [`EntityId`] to its `Render` type name
162 #[cfg(debug_assertions)]
163 pub(crate) view_type_names: FxHashMap<EntityId, &'static str>,
164}
165
166impl A11y {
167 pub(crate) fn new(
168 active_flag: Arc<AtomicBool>,
169 force_disabled: bool,
170 window_title: Option<SharedString>,
171 ) -> Self {
172 Self {
173 force_disabled,
174 active_flag,
175 active_this_frame: false,
176 nodes: A11yNodeBuilder::new(),
177 focus_ids: FxHashMap::default(),
178 node_bounds: FxHashMap::default(),
179 action_listeners: FxHashMap::default(),
180 window_title,
181 last_focus_without_node: None,
182 debug: debug::A11yDebug::default(),
183 #[cfg(debug_assertions)]
184 view_type_names: FxHashMap::default(),
185 }
186 }
187
188 /// Logs (once per focus change) that the focused element is not exposed to
189 /// assistive technology because it has no accessibility node. When this
190 /// happens, screen readers fall back to announcing the whole window instead
191 /// of the focused element. The fix is to give the element both an
192 /// `.id(...)` and a `.role(...)`.
193 pub(crate) fn note_focus_without_node(&mut self, focus_id: FocusId, reason: &str) {
194 if self.last_focus_without_node != Some(focus_id) {
195 self.last_focus_without_node = Some(focus_id);
196 log::info!(
197 "a11y: focused element ({focus_id:?}) has no accessibility node \
198 ({reason}); assistive technology will announce the whole window \
199 instead. Give it both an `.id(...)` and a `.role(...)` to expose it."
200 );
201 }
202 }
203
204 pub(crate) fn set_window_title(&mut self, title: impl Into<SharedString>) {
205 self.window_title = Some(title.into());
206 }
207
208 /// Ensures that [`Self::is_active`] returns up to date information.
209 ///
210 /// See the docs for [`Self::active_flag`] and [`Self::active_this_frame`]
211 /// for more commentary.
212 pub(crate) fn sync_active_flag(&mut self) {
213 self.active_this_frame = !self.force_disabled && self.active_flag.load(Ordering::SeqCst);
214 }
215
216 pub(crate) fn is_active(&self) -> bool {
217 self.active_this_frame
218 }
219
220 pub(crate) fn set_focusable(&mut self, node_id: NodeId, focus_id: FocusId) {
221 self.focus_ids.insert(node_id, focus_id);
222 }
223
224 /// Report `node_id` as the currently-focused node, if it is present in the
225 /// tree.
226 ///
227 /// Must only be called once per frame.
228 pub(crate) fn set_focus(&mut self, node_id: NodeId) {
229 // A focused node must have been registered as focusable this frame.
230 if !self.focus_ids.contains_key(&node_id) {
231 if cfg!(debug_assertions) {
232 panic!("set_focus called for a node that was not registered with set_focusable");
233 } else {
234 log::warn!(
235 "a11y: set_focus called for a node that was not registered with \
236 set_focusable ({node_id:?})"
237 );
238 }
239 }
240 if self.nodes.has_node(node_id) {
241 // The focused element is properly exposed; reset the dedup so a
242 // later focus on a node-less element logs again.
243 self.last_focus_without_node = None;
244 self.nodes.set_focus(node_id);
245 } else {
246 // The element registered a focus handle and an id, but never got a
247 // node because it has no role.
248 if let Some(focus_id) = self.focus_ids.get(&node_id).copied() {
249 self.note_focus_without_node(focus_id, "it has an id but no role");
250 }
251 }
252 }
253
254 pub(crate) fn set_active_descendant(&mut self, node_id: NodeId) {
255 // The active descendant must be a descendant of the focused container,
256 // not the focused node itself.
257 if self.nodes.node_is_focused(node_id) {
258 if cfg!(debug_assertions) {
259 panic!("set_active_descendant called on the focused node");
260 } else {
261 log::warn!("a11y: set_active_descendant called on the focused node ({node_id:?})");
262 }
263 return;
264 }
265 if self.nodes.has_node(node_id) && self.nodes.focus_is_ancestor_of_current() {
266 self.nodes.set_active_descendant(node_id);
267 }
268 }
269
270 /// Clear per-frame state and push the root node to start a new frame.
271 pub(crate) fn begin_frame(&mut self) {
272 self.focus_ids.clear();
273 self.node_bounds.clear();
274 self.action_listeners.clear();
275 self.nodes.begin_frame(self.window_title.as_ref());
276 }
277
278 /// Finalize the tree and produce a [`TreeUpdate`] for the platform adapter.
279 pub(crate) fn end_frame(&mut self, frame: debug::FrameDebugInfo) -> TreeUpdate {
280 let update = self.nodes.finalize();
281 self.debug.capture(
282 &update,
283 self.nodes.focus,
284 self.nodes.active_descendant,
285 self.window_title.as_ref(),
286 frame,
287 );
288 #[cfg(debug_assertions)]
289 self.debug.capture_node_info(&self.nodes.node_info);
290 update
291 }
292
293 pub(crate) fn debug_tree_json(&self) -> Option<String> {
294 self.debug.to_json()
295 }
296}
297
298/// Builder API for synthetic children. See the docs for
299/// [`Element::a11y_synthetic_children`].
300pub struct A11ySubtreeBuilder<'a> {
301 parent_id: NodeId,
302 nodes: &'a mut A11yNodeBuilder,
303 /// Provenance of the real element whose `a11y_synthetic_children` is
304 /// running.
305 #[cfg(debug_assertions)]
306 creator: debug::NodeCreator,
307}
308
309impl<'a> A11ySubtreeBuilder<'a> {
310 pub(crate) fn new(parent_id: NodeId, nodes: &'a mut A11yNodeBuilder) -> Self {
311 Self {
312 parent_id,
313 nodes,
314 #[cfg(debug_assertions)]
315 creator: debug::NodeCreator::default(),
316 }
317 }
318
319 #[cfg(debug_assertions)]
320 pub(crate) fn with_creator(mut self, creator: debug::NodeCreator) -> Self {
321 self.creator = creator;
322 self
323 }
324
325 /// Derive a [`NodeId`] for a synthetic child.
326 ///
327 /// The generated ID is based on the hash of `key`, as well as the parent's
328 /// ID. This means that `key`s must be unique within the same
329 /// [`Element::a11y_synthetic_children`] call, but may be duplicated across
330 /// different calls.
331 pub fn synthetic_node_id(&self, key: impl Hash) -> NodeId {
332 let mut hasher = std::hash::DefaultHasher::default();
333 self.parent_id.0.hash(&mut hasher);
334 key.hash(&mut hasher);
335 NodeId(hasher.finish())
336 }
337
338 /// Append a synthetic leaf node as a child of this element's node.
339 ///
340 /// Returns `false` if a node with this id is already present in the tree,
341 /// in which case the node is discarded.
342 pub fn push_child(&mut self, id: NodeId, node: accesskit::Node) -> bool {
343 let pushed = self.nodes.push_leaf(id, node);
344 #[cfg(debug_assertions)]
345 if pushed {
346 self.nodes.record_node_info(
347 id,
348 debug::NodeDebugInfo {
349 synthetic: true,
350 view: self.creator.view,
351 element_id: self.creator.element_id.clone(),
352 source_location: self.creator.source_location,
353 },
354 );
355 }
356 pushed
357 }
358
359 /// A mutable reference to the parent node.
360 pub fn parent_node(&mut self) -> &mut accesskit::Node {
361 self.nodes
362 .current_node_mut()
363 .expect("A11ySubtreeBuilder exists only while its element's node is on the stack")
364 }
365}
366
367pub(crate) struct A11yNodeBuilder {
368 ids_stack: SmallVec<[NodeId; 16]>,
369 nodes_stack: SmallVec<[accesskit::Node; 16]>,
370 /// This is the exact type required by accesskit, so we can't just make it a
371 /// `HashMap<NodeId, Node>` to remove the need for `seen_ids`
372 all_nodes: Vec<(NodeId, accesskit::Node)>,
373 seen_ids: FxHashSet<NodeId>,
374 /// The node that GPUI considers focused. Note that this may be different to
375 /// what is reported to accesskit - see [`Self::active_descendant`]
376 focus: Option<NodeId>,
377 /// If a node calls `.aria_active_descendant()`, AND an ancestor is focused,
378 /// override it as the focused node. This supports the "active descendant"
379 /// pattern, which allows a focused container to act as if a descendant is
380 /// focused.
381 active_descendant: Option<NodeId>,
382 #[cfg(debug_assertions)]
383 node_info: FxHashMap<NodeId, debug::NodeDebugInfo>,
384}
385
386impl A11yNodeBuilder {
387 fn new() -> Self {
388 Self {
389 ids_stack: SmallVec::new(),
390 nodes_stack: SmallVec::new(),
391 all_nodes: Vec::new(),
392 seen_ids: FxHashSet::default(),
393 focus: None,
394 active_descendant: None,
395 #[cfg(debug_assertions)]
396 node_info: FxHashMap::default(),
397 }
398 }
399
400 /// Records provenance for a node already pushed this frame. Debug builds only.
401 #[cfg(debug_assertions)]
402 pub(crate) fn record_node_info(&mut self, id: NodeId, info: debug::NodeDebugInfo) {
403 self.node_info.insert(id, info);
404 }
405
406 #[must_use]
407 fn can_push(&mut self, id: NodeId) -> bool {
408 debug_assert!(!self.ids_stack.is_empty(), "node pushed before push_root");
409
410 if !self.seen_ids.insert(id) {
411 debug_assert!(
412 false,
413 "Duplicate a11y node id: {id:?}. In a release build, this node would be silently discarded from the a11y tree."
414 );
415 return false;
416 }
417
418 true
419 }
420
421 /// Push a new node onto the stack. It becomes a child of the current
422 /// top-of-stack node.
423 ///
424 /// Returns `true` if the node was successfully pushed.
425 pub(crate) fn push(&mut self, id: NodeId, node: accesskit::Node) -> bool {
426 if !self.can_push(id) {
427 return false;
428 }
429
430 if let Some(parent) = self.nodes_stack.last_mut() {
431 parent.push_child(id);
432 }
433 self.ids_stack.push(id);
434 self.nodes_stack.push(node);
435 true
436 }
437
438 /// Add a leaf node as a child of the current top-of-stack node, without
439 /// pushing it onto the stack. Semantically equivalent to a [`Self::push`]
440 /// followed by a [`Self::pop`].
441 ///
442 /// Returns `true` if the node was successfully pushed.
443 pub(crate) fn push_leaf(&mut self, id: NodeId, node: accesskit::Node) -> bool {
444 if !self.can_push(id) {
445 return false;
446 }
447
448 if let Some(parent) = self.nodes_stack.last_mut() {
449 parent.push_child(id);
450 }
451 self.all_nodes.push((id, node));
452 true
453 }
454
455 pub(crate) fn current_node_mut(&mut self) -> Option<&mut accesskit::Node> {
456 self.nodes_stack.last_mut()
457 }
458
459 /// Pop the current node off the stack and finalize it into the all_nodes
460 /// list.
461 pub(crate) fn pop(&mut self) {
462 debug_assert!(self.ids_stack.len() > 1, "pop would remove the root node");
463
464 if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) {
465 self.all_nodes.push((id, node));
466 }
467 }
468
469 /// Push the root node to start a new frame.
470 fn begin_frame(&mut self, window_title: Option<&SharedString>) {
471 self.all_nodes.clear();
472 self.ids_stack.clear();
473 self.nodes_stack.clear();
474 self.seen_ids.clear();
475 #[cfg(debug_assertions)]
476 self.node_info.clear();
477 let mut root_node = accesskit::Node::new(accesskit::Role::Window);
478 if let Some(title) = window_title {
479 root_node.set_label(title.to_string());
480 }
481
482 self.ids_stack.push(ROOT_NODE_ID);
483 self.nodes_stack.push(root_node);
484 self.focus = None;
485 self.active_descendant = None;
486 }
487
488 /// Returns whether a node with the given ID has been pushed in this frame.
489 pub(crate) fn has_node(&self, id: NodeId) -> bool {
490 id == ROOT_NODE_ID || self.seen_ids.contains(&id)
491 }
492
493 /// Returns whether `id` is the node currently reported as focused.
494 pub(crate) fn node_is_focused(&self, id: NodeId) -> bool {
495 self.focus == Some(id)
496 }
497
498 pub(crate) fn focus_is_ancestor_of_current(&self) -> bool {
499 let Some(focus) = self.focus else {
500 return false;
501 };
502
503 // The current node is on top of the stack; everything below it is an
504 // ancestor.
505 let ancestor_count = self.ids_stack.len().saturating_sub(1);
506 self.ids_stack[..ancestor_count].contains(&focus)
507 }
508
509 pub(crate) fn set_active_descendant(&mut self, id: NodeId) {
510 if self
511 .active_descendant
512 .is_some_and(|existing| existing != id)
513 {
514 if cfg!(debug_assertions) {
515 panic!("active descendant claimed by multiple nodes in one frame");
516 } else {
517 log::warn!(
518 "a11y: multiple nodes claimed the active descendant this frame; \
519 using last-wins ({id:?})"
520 );
521 }
522 }
523 self.active_descendant = Some(id);
524 }
525
526 pub(crate) fn set_focus(&mut self, id: NodeId) {
527 if self.focus.is_some() {
528 if cfg!(debug_assertions) {
529 panic!("set_focus called more than once in a single frame");
530 } else {
531 log::warn!(
532 "a11y: set_focus called more than once in a single frame; \
533 using last-wins ({id:?})"
534 );
535 }
536 }
537 self.focus = Some(id);
538 }
539
540 fn finalize(&mut self) -> TreeUpdate {
541 // Stack should contain only the root node
542 debug_assert_eq!(self.ids_stack.len(), 1);
543 debug_assert_eq!(self.ids_stack[0], ROOT_NODE_ID);
544
545 if self.ids_stack.len() != 1 {
546 log::error!(
547 "a11y: Stack imbalance at end of frame: expected 1 (root), got {}. \
548 Some elements may have pushed without popping.",
549 self.ids_stack.len()
550 );
551 }
552
553 // Pop remaining nodes (should just be the root).
554 while !self.ids_stack.is_empty() {
555 if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) {
556 self.all_nodes.push((id, node));
557 }
558 }
559
560 let focus = match self.active_descendant {
561 Some(id) if self.has_node(id) => id,
562 Some(id) => {
563 if cfg!(debug_assertions) {
564 panic!("active_descendant set to {id:?}, which is not in the tree");
565 } else {
566 log::warn!("active_descendant set to {id:?}, which is not in the tree");
567 self.focus.unwrap_or(ROOT_NODE_ID)
568 }
569 }
570
571 _ => self.focus.unwrap_or(ROOT_NODE_ID),
572 };
573
574 let nodes = std::mem::take(&mut self.all_nodes);
575 let update = TreeUpdate {
576 nodes,
577 tree: Some(accesskit::Tree::new(ROOT_NODE_ID)),
578 tree_id: accesskit::TreeId::ROOT,
579 focus,
580 };
581
582 Self::repair_tree_update(update)
583 }
584
585 /// Accesskit panics on invalid [`TreeUpdate`]s. This function defensively
586 /// checks invariants that accesskit panics on, and tries to fix them.
587 fn repair_tree_update(mut update: TreeUpdate) -> TreeUpdate {
588 let node_ids: FxHashSet<NodeId> = update.nodes.iter().map(|(id, _)| *id).collect();
589
590 // Focus must point to a node in the tree.
591 if !node_ids.contains(&update.focus) {
592 log::error!(
593 "a11y: Focused node {:?} is not in the tree ({} nodes). \
594 Falling back to root. This is a bug in the a11y tree builder.",
595 update.focus,
596 update.nodes.len()
597 );
598 update.focus = ROOT_NODE_ID;
599 }
600
601 // Every child reference must point to a node in the update.
602 for (id, node) in &mut update.nodes {
603 let has_invalid_child = node
604 .children()
605 .iter()
606 .any(|child_id| !node_ids.contains(child_id));
607 if has_invalid_child {
608 let children = node.children();
609 let invalid_count = children
610 .iter()
611 .filter(|child_id| !node_ids.contains(child_id))
612 .count();
613 log::error!(
614 "a11y: Node {:?} references {} children not present in the tree. \
615 Stripping invalid child references.",
616 id,
617 invalid_count
618 );
619 let valid: Vec<NodeId> = children
620 .iter()
621 .copied()
622 .filter(|child_id| node_ids.contains(child_id))
623 .collect();
624 node.set_children(valid);
625 }
626 }
627
628 update
629 }
630}
631
632#[cfg(test)]
633mod tests {
634 // Import specific items rather than glob-importing `super`, which would pull
635 // in gpui's own `test` attribute macro and shadow the standard one.
636 use super::{A11y, A11yNodeBuilder, ROOT_NODE_ID};
637 use crate::FocusId;
638 use accesskit::{NodeId, Role};
639 use std::sync::{Arc, atomic::AtomicBool};
640
641 fn test_node() -> accesskit::Node {
642 accesskit::Node::new(Role::GenericContainer)
643 }
644
645 fn new_builder() -> A11yNodeBuilder {
646 let mut builder = A11yNodeBuilder::new();
647 builder.begin_frame(None);
648 builder
649 }
650
651 fn new_a11y() -> A11y {
652 let mut a11y = A11y::new(Arc::new(AtomicBool::new(true)), false, None);
653 a11y.begin_frame();
654 a11y
655 }
656
657 #[test]
658 fn active_descendant_honored_when_container_focused() {
659 let mut builder = new_builder();
660 let container = NodeId(1);
661 let item = NodeId(2);
662
663 assert!(builder.push(container, test_node()));
664 builder.set_focus(container);
665 assert!(builder.push(item, test_node()));
666
667 // The item is on top of the stack; the focused container is its
668 // ancestor, so the claim is honored.
669 assert!(builder.focus_is_ancestor_of_current());
670 builder.set_active_descendant(item);
671
672 builder.pop(); // item
673 builder.pop(); // container
674 let update = builder.finalize();
675 assert_eq!(update.focus, item);
676 }
677
678 #[test]
679 fn active_descendant_honored_for_deep_descendant() {
680 let mut builder = new_builder();
681 let container = NodeId(1);
682 let group = NodeId(2);
683 let item = NodeId(3);
684
685 assert!(builder.push(container, test_node()));
686 builder.set_focus(container);
687 assert!(builder.push(group, test_node()));
688 assert!(builder.push(item, test_node()));
689
690 // The item is a grandchild of the focused container; depth doesn't
691 // matter, the focused ancestor is still on the stack.
692 assert!(builder.focus_is_ancestor_of_current());
693 builder.set_active_descendant(item);
694
695 builder.pop(); // item
696 builder.pop(); // group
697 builder.pop(); // container
698 let update = builder.finalize();
699 assert_eq!(update.focus, item);
700 }
701
702 #[test]
703 fn active_descendant_ignored_when_focus_in_other_subtree() {
704 let mut builder = new_builder();
705 let focused_container = NodeId(1);
706 let focused_leaf = NodeId(2);
707 let other_container = NodeId(3);
708 let other_item = NodeId(4);
709
710 // First subtree holds real focus.
711 assert!(builder.push(focused_container, test_node()));
712 assert!(builder.push(focused_leaf, test_node()));
713 builder.set_focus(focused_leaf);
714 builder.pop(); // focused_leaf
715 builder.pop(); // focused_container
716
717 // Second subtree: its item would claim the active descendant, but the
718 // focus is not on any of its ancestors, so the gate rejects it.
719 assert!(builder.push(other_container, test_node()));
720 assert!(builder.push(other_item, test_node()));
721 assert!(!builder.focus_is_ancestor_of_current());
722 builder.pop(); // other_item
723 builder.pop(); // other_container
724
725 let update = builder.finalize();
726 assert_eq!(update.focus, focused_leaf);
727 }
728
729 #[test]
730 fn active_descendant_ignored_when_nothing_focused() {
731 let mut builder = new_builder();
732 let container = NodeId(1);
733 let item = NodeId(2);
734
735 assert!(builder.push(container, test_node()));
736 assert!(builder.push(item, test_node()));
737
738 // Nothing is focused (focus defaults to the root window node), so the
739 // gate rejects the claim.
740 assert!(!builder.focus_is_ancestor_of_current());
741 builder.pop();
742 builder.pop();
743
744 let update = builder.finalize();
745 assert_eq!(update.focus, ROOT_NODE_ID);
746 }
747
748 #[test]
749 fn regular_focus_used_when_no_active_descendant() {
750 let mut builder = new_builder();
751 let focused = NodeId(1);
752
753 assert!(builder.push(focused, test_node()));
754 builder.set_focus(focused);
755 builder.pop();
756
757 let update = builder.finalize();
758 assert_eq!(update.focus, focused);
759 }
760
761 #[test]
762 fn focus_is_ancestor_excludes_self_and_non_ancestors() {
763 let mut builder = new_builder();
764 let container = NodeId(1);
765 let item = NodeId(2);
766
767 assert!(builder.push(container, test_node()));
768 builder.set_focus(container);
769
770 // With the focused container itself on top, it is not its own (strict)
771 // ancestor, so the gate is false.
772 assert!(!builder.focus_is_ancestor_of_current());
773
774 assert!(builder.push(item, test_node()));
775 // Now the focused container is a strict ancestor of the item on top.
776 assert!(builder.focus_is_ancestor_of_current());
777
778 builder.pop();
779 builder.pop();
780 }
781
782 // The double-claim guard panics only in debug builds; in release it falls
783 // back to last-wins with a warning.
784 #[test]
785 #[cfg_attr(
786 debug_assertions,
787 should_panic(expected = "active descendant claimed by multiple nodes")
788 )]
789 fn multiple_active_descendant_claims_panic_in_debug() {
790 let mut builder = new_builder();
791 builder.set_active_descendant(NodeId(1));
792 builder.set_active_descendant(NodeId(2));
793 }
794
795 // Setting focus twice in one frame means two elements both claimed window
796 // focus; that panics in debug and falls back to last-wins in release.
797 #[test]
798 #[cfg_attr(
799 debug_assertions,
800 should_panic(expected = "set_focus called more than once")
801 )]
802 fn setting_focus_twice_panics_in_debug() {
803 let mut builder = new_builder();
804 builder.set_focus(NodeId(1));
805 builder.set_focus(NodeId(2));
806 }
807
808 // Focusing a node that was never registered as focusable is a bug: panic in
809 // debug, warn in release.
810 #[test]
811 #[cfg_attr(
812 debug_assertions,
813 should_panic(expected = "was not registered with set_focusable")
814 )]
815 fn set_focus_without_set_focusable() {
816 let mut a11y = new_a11y();
817 let node = NodeId(1);
818 assert!(a11y.nodes.push(node, test_node()));
819 // set_focusable was never called for `node`.
820 a11y.set_focus(node);
821 }
822
823 // The focused node cannot also be its own active descendant: panic in
824 // debug, warn in release.
825 #[test]
826 #[cfg_attr(debug_assertions, should_panic(expected = "on the focused node"))]
827 fn set_active_descendant_on_focused_node() {
828 let mut a11y = new_a11y();
829 let node = NodeId(1);
830 assert!(a11y.nodes.push(node, test_node()));
831 a11y.set_focusable(node, FocusId::default());
832 a11y.set_focus(node);
833 a11y.set_active_descendant(node);
834 }
835
836 // Two sibling children of a focused container both claim the active
837 // descendant (both pass the focus gate). The second claim is a bug: panic
838 // in debug, last-wins + warn in release.
839 #[test]
840 #[cfg_attr(
841 debug_assertions,
842 should_panic(expected = "active descendant claimed by multiple nodes")
843 )]
844 fn two_siblings_claiming_active_descendant() {
845 let mut a11y = new_a11y();
846 let container = NodeId(1);
847 let first = NodeId(2);
848 let second = NodeId(3);
849
850 assert!(a11y.nodes.push(container, test_node()));
851 a11y.set_focusable(container, FocusId::default());
852 a11y.set_focus(container);
853
854 assert!(a11y.nodes.push(first, test_node()));
855 a11y.set_active_descendant(first);
856 a11y.nodes.pop(); // first
857
858 assert!(a11y.nodes.push(second, test_node()));
859 a11y.set_active_descendant(second);
860 a11y.nodes.pop(); // second
861
862 a11y.nodes.pop(); // container
863 }
864
865 // Node A is focused; node C (a child of the unfocused node B) claims the
866 // active descendant. The final tree must still report A as focused.
867 #[test]
868 fn active_descendant_in_unfocused_subtree_keeps_real_focus() {
869 let mut a11y = new_a11y();
870 let a = NodeId(1);
871 let b = NodeId(2);
872 let c = NodeId(3);
873
874 assert!(a11y.nodes.push(a, test_node()));
875 a11y.set_focusable(a, FocusId::default());
876 a11y.set_focus(a);
877 a11y.nodes.pop(); // a
878
879 assert!(a11y.nodes.push(b, test_node()));
880 assert!(a11y.nodes.push(c, test_node()));
881 a11y.set_active_descendant(c);
882 a11y.nodes.pop(); // c
883 a11y.nodes.pop(); // b
884
885 let update = a11y.end_frame(Default::default());
886 assert_eq!(update.focus, a);
887 }
888}
889