Skip to main content

gpui/window/
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.is_enabled() && self.active_flag.load(Ordering::SeqCst);
214    }
215
216    pub(crate) fn is_enabled(&self) -> bool {
217        !self.force_disabled
218    }
219
220    pub(crate) fn is_active(&self) -> bool {
221        self.active_this_frame
222    }
223
224    pub(crate) fn set_focusable(&mut self, node_id: NodeId, focus_id: FocusId) {
225        self.focus_ids.insert(node_id, focus_id);
226    }
227
228    /// Report `node_id` as the currently-focused node, if it is present in the
229    /// tree.
230    ///
231    /// Must only be called once per frame.
232    pub(crate) fn set_focus(&mut self, node_id: NodeId) {
233        // A focused node must have been registered as focusable this frame.
234        if !self.focus_ids.contains_key(&node_id) {
235            if cfg!(debug_assertions) {
236                panic!("set_focus called for a node that was not registered with set_focusable");
237            } else {
238                log::warn!(
239                    "a11y: set_focus called for a node that was not registered with \
240                     set_focusable ({node_id:?})"
241                );
242            }
243        }
244        if self.nodes.has_node(node_id) {
245            // The focused element is properly exposed; reset the dedup so a
246            // later focus on a node-less element logs again.
247            self.last_focus_without_node = None;
248            self.nodes.set_focus(node_id);
249        } else {
250            // The element registered a focus handle and an id, but never got a
251            // node because it has no role.
252            if let Some(focus_id) = self.focus_ids.get(&node_id).copied() {
253                self.note_focus_without_node(focus_id, "it has an id but no role");
254            }
255        }
256    }
257
258    pub(crate) fn set_active_descendant(&mut self, node_id: NodeId) {
259        // The active descendant must be a descendant of the focused container,
260        // not the focused node itself.
261        if self.nodes.node_is_focused(node_id) {
262            if cfg!(debug_assertions) {
263                panic!("set_active_descendant called on the focused node");
264            } else {
265                log::warn!("a11y: set_active_descendant called on the focused node ({node_id:?})");
266            }
267            return;
268        }
269        if self.nodes.has_node(node_id) && self.nodes.focus_is_ancestor_of_current() {
270            self.nodes.set_active_descendant(node_id);
271        }
272    }
273
274    /// Clear per-frame state and push the root node to start a new frame.
275    pub(crate) fn begin_frame(&mut self) {
276        self.focus_ids.clear();
277        self.node_bounds.clear();
278        self.action_listeners.clear();
279        self.nodes.begin_frame(self.window_title.as_ref());
280    }
281
282    /// Finalize the tree and produce a [`TreeUpdate`] for the platform adapter.
283    pub(crate) fn end_frame(&mut self, frame: debug::FrameDebugInfo) -> TreeUpdate {
284        let update = self.nodes.finalize();
285        self.debug.capture(
286            &update,
287            self.nodes.focus,
288            self.nodes.active_descendant,
289            self.window_title.as_ref(),
290            frame,
291        );
292        #[cfg(debug_assertions)]
293        self.debug.capture_node_info(&self.nodes.node_info);
294        update
295    }
296
297    pub(crate) fn debug_tree_json(&self) -> Option<String> {
298        self.debug.to_json()
299    }
300}
301
302/// Builder API for synthetic children. See the docs for
303/// [`Element::a11y_synthetic_children`].
304pub struct A11ySubtreeBuilder<'a> {
305    parent_id: NodeId,
306    nodes: &'a mut A11yNodeBuilder,
307    /// Provenance of the real element whose `a11y_synthetic_children` is
308    /// running.
309    #[cfg(debug_assertions)]
310    creator: debug::NodeCreator,
311}
312
313impl<'a> A11ySubtreeBuilder<'a> {
314    pub(crate) fn new(parent_id: NodeId, nodes: &'a mut A11yNodeBuilder) -> Self {
315        Self {
316            parent_id,
317            nodes,
318            #[cfg(debug_assertions)]
319            creator: debug::NodeCreator::default(),
320        }
321    }
322
323    #[cfg(debug_assertions)]
324    pub(crate) fn with_creator(mut self, creator: debug::NodeCreator) -> Self {
325        self.creator = creator;
326        self
327    }
328
329    /// Derive a [`NodeId`] for a synthetic child.
330    ///
331    /// The generated ID is based on the hash of `key`, as well as the parent's
332    /// ID. This means that `key`s must be unique within the same
333    /// [`Element::a11y_synthetic_children`] call, but may be duplicated across
334    /// different calls.
335    pub fn synthetic_node_id(&self, key: impl Hash) -> NodeId {
336        let mut hasher = std::hash::DefaultHasher::default();
337        self.parent_id.0.hash(&mut hasher);
338        key.hash(&mut hasher);
339        NodeId(hasher.finish())
340    }
341
342    /// Append a synthetic leaf node as a child of this element's node.
343    ///
344    /// Returns `false` if a node with this id is already present in the tree,
345    /// in which case the node is discarded.
346    pub fn push_child(&mut self, id: NodeId, node: accesskit::Node) -> bool {
347        let pushed = self.nodes.push_leaf(id, node);
348        #[cfg(debug_assertions)]
349        if pushed {
350            self.nodes.record_node_info(
351                id,
352                debug::NodeDebugInfo {
353                    synthetic: true,
354                    view: self.creator.view,
355                    element_id: self.creator.element_id.clone(),
356                    source_location: self.creator.source_location,
357                },
358            );
359        }
360        pushed
361    }
362
363    /// A mutable reference to the parent node.
364    pub fn parent_node(&mut self) -> &mut accesskit::Node {
365        self.nodes
366            .current_node_mut()
367            .expect("A11ySubtreeBuilder exists only while its element's node is on the stack")
368    }
369}
370
371pub(crate) struct A11yNodeBuilder {
372    ids_stack: SmallVec<[NodeId; 16]>,
373    nodes_stack: SmallVec<[accesskit::Node; 16]>,
374    /// This is the exact type required by accesskit, so we can't just make it a
375    /// `HashMap<NodeId, Node>` to remove the need for `seen_ids`
376    all_nodes: Vec<(NodeId, accesskit::Node)>,
377    seen_ids: FxHashSet<NodeId>,
378    /// The node that GPUI considers focused. Note that this may be different to
379    /// what is reported to accesskit - see [`Self::active_descendant`]
380    focus: Option<NodeId>,
381    /// If a node calls `.aria_active_descendant()`, AND an ancestor is focused,
382    /// override it as the focused node. This supports the "active descendant"
383    /// pattern, which allows a focused container to act as if a descendant is
384    /// focused.
385    active_descendant: Option<NodeId>,
386    #[cfg(debug_assertions)]
387    node_info: FxHashMap<NodeId, debug::NodeDebugInfo>,
388}
389
390impl A11yNodeBuilder {
391    fn new() -> Self {
392        Self {
393            ids_stack: SmallVec::new(),
394            nodes_stack: SmallVec::new(),
395            all_nodes: Vec::new(),
396            seen_ids: FxHashSet::default(),
397            focus: None,
398            active_descendant: None,
399            #[cfg(debug_assertions)]
400            node_info: FxHashMap::default(),
401        }
402    }
403
404    /// Records provenance for a node already pushed this frame. Debug builds only.
405    #[cfg(debug_assertions)]
406    pub(crate) fn record_node_info(&mut self, id: NodeId, info: debug::NodeDebugInfo) {
407        self.node_info.insert(id, info);
408    }
409
410    #[must_use]
411    fn can_push(&mut self, id: NodeId) -> bool {
412        debug_assert!(!self.ids_stack.is_empty(), "node pushed before push_root");
413
414        if !self.seen_ids.insert(id) {
415            debug_assert!(
416                false,
417                "Duplicate a11y node id: {id:?}. In a release build, this node would be silently discarded from the a11y tree."
418            );
419            return false;
420        }
421
422        true
423    }
424
425    /// Push a new node onto the stack. It becomes a child of the current
426    /// top-of-stack node.
427    ///
428    /// Returns `true` if the node was successfully pushed.
429    pub(crate) fn push(&mut self, id: NodeId, node: accesskit::Node) -> bool {
430        if !self.can_push(id) {
431            return false;
432        }
433
434        if let Some(parent) = self.nodes_stack.last_mut() {
435            parent.push_child(id);
436        }
437        self.ids_stack.push(id);
438        self.nodes_stack.push(node);
439        true
440    }
441
442    /// Add a leaf node as a child of the current top-of-stack node, without
443    /// pushing it onto the stack. Semantically equivalent to a [`Self::push`]
444    /// followed by a [`Self::pop`].
445    ///
446    /// Returns `true` if the node was successfully pushed.
447    pub(crate) fn push_leaf(&mut self, id: NodeId, node: accesskit::Node) -> bool {
448        if !self.can_push(id) {
449            return false;
450        }
451
452        if let Some(parent) = self.nodes_stack.last_mut() {
453            parent.push_child(id);
454        }
455        self.all_nodes.push((id, node));
456        true
457    }
458
459    pub(crate) fn current_node_mut(&mut self) -> Option<&mut accesskit::Node> {
460        self.nodes_stack.last_mut()
461    }
462
463    /// Pop the current node off the stack and finalize it into the all_nodes
464    /// list.
465    pub(crate) fn pop(&mut self) {
466        debug_assert!(self.ids_stack.len() > 1, "pop would remove the root node");
467
468        if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) {
469            self.all_nodes.push((id, node));
470        }
471    }
472
473    /// Push the root node to start a new frame.
474    fn begin_frame(&mut self, window_title: Option<&SharedString>) {
475        self.all_nodes.clear();
476        self.ids_stack.clear();
477        self.nodes_stack.clear();
478        self.seen_ids.clear();
479        #[cfg(debug_assertions)]
480        self.node_info.clear();
481        let mut root_node = accesskit::Node::new(accesskit::Role::Window);
482        if let Some(title) = window_title {
483            root_node.set_label(title.to_string());
484        }
485
486        self.ids_stack.push(ROOT_NODE_ID);
487        self.nodes_stack.push(root_node);
488        self.focus = None;
489        self.active_descendant = None;
490    }
491
492    /// Returns whether a node with the given ID has been pushed in this frame.
493    pub(crate) fn has_node(&self, id: NodeId) -> bool {
494        id == ROOT_NODE_ID || self.seen_ids.contains(&id)
495    }
496
497    /// Returns whether `id` is the node currently reported as focused.
498    pub(crate) fn node_is_focused(&self, id: NodeId) -> bool {
499        self.focus == Some(id)
500    }
501
502    pub(crate) fn focus_is_ancestor_of_current(&self) -> bool {
503        let Some(focus) = self.focus else {
504            return false;
505        };
506
507        // The current node is on top of the stack; everything below it is an
508        // ancestor.
509        let ancestor_count = self.ids_stack.len().saturating_sub(1);
510        self.ids_stack[..ancestor_count].contains(&focus)
511    }
512
513    pub(crate) fn set_active_descendant(&mut self, id: NodeId) {
514        if self
515            .active_descendant
516            .is_some_and(|existing| existing != id)
517        {
518            if cfg!(debug_assertions) {
519                panic!("active descendant claimed by multiple nodes in one frame");
520            } else {
521                log::warn!(
522                    "a11y: multiple nodes claimed the active descendant this frame; \
523                     using last-wins ({id:?})"
524                );
525            }
526        }
527        self.active_descendant = Some(id);
528    }
529
530    pub(crate) fn set_focus(&mut self, id: NodeId) {
531        if self.focus.is_some() {
532            if cfg!(debug_assertions) {
533                panic!("set_focus called more than once in a single frame");
534            } else {
535                log::warn!(
536                    "a11y: set_focus called more than once in a single frame; \
537                     using last-wins ({id:?})"
538                );
539            }
540        }
541        self.focus = Some(id);
542    }
543
544    fn finalize(&mut self) -> TreeUpdate {
545        // Stack should contain only the root node
546        debug_assert_eq!(self.ids_stack.len(), 1);
547        debug_assert_eq!(self.ids_stack[0], ROOT_NODE_ID);
548
549        if self.ids_stack.len() != 1 {
550            log::error!(
551                "a11y: Stack imbalance at end of frame: expected 1 (root), got {}. \
552                 Some elements may have pushed without popping.",
553                self.ids_stack.len()
554            );
555        }
556
557        // Pop remaining nodes (should just be the root).
558        while !self.ids_stack.is_empty() {
559            if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) {
560                self.all_nodes.push((id, node));
561            }
562        }
563
564        let focus = match self.active_descendant {
565            Some(id) if self.has_node(id) => id,
566            Some(id) => {
567                if cfg!(debug_assertions) {
568                    panic!("active_descendant set to {id:?}, which is not in the tree");
569                } else {
570                    log::warn!("active_descendant set to {id:?}, which is not in the tree");
571                    self.focus.unwrap_or(ROOT_NODE_ID)
572                }
573            }
574
575            _ => self.focus.unwrap_or(ROOT_NODE_ID),
576        };
577
578        let nodes = std::mem::take(&mut self.all_nodes);
579        let update = TreeUpdate {
580            nodes,
581            tree: Some(accesskit::Tree::new(ROOT_NODE_ID)),
582            tree_id: accesskit::TreeId::ROOT,
583            focus,
584        };
585
586        Self::repair_tree_update(update)
587    }
588
589    /// Accesskit panics on invalid [`TreeUpdate`]s. This function defensively
590    /// checks invariants that accesskit panics on, and tries to fix them.
591    fn repair_tree_update(mut update: TreeUpdate) -> TreeUpdate {
592        let node_ids: FxHashSet<NodeId> = update.nodes.iter().map(|(id, _)| *id).collect();
593
594        // Focus must point to a node in the tree.
595        if !node_ids.contains(&update.focus) {
596            log::error!(
597                "a11y: Focused node {:?} is not in the tree ({} nodes). \
598                 Falling back to root. This is a bug in the a11y tree builder.",
599                update.focus,
600                update.nodes.len()
601            );
602            update.focus = ROOT_NODE_ID;
603        }
604
605        // Every child reference must point to a node in the update.
606        for (id, node) in &mut update.nodes {
607            let has_invalid_child = node
608                .children()
609                .iter()
610                .any(|child_id| !node_ids.contains(child_id));
611            if has_invalid_child {
612                let children = node.children();
613                let invalid_count = children
614                    .iter()
615                    .filter(|child_id| !node_ids.contains(child_id))
616                    .count();
617                log::error!(
618                    "a11y: Node {:?} references {} children not present in the tree. \
619                     Stripping invalid child references.",
620                    id,
621                    invalid_count
622                );
623                let valid: Vec<NodeId> = children
624                    .iter()
625                    .copied()
626                    .filter(|child_id| node_ids.contains(child_id))
627                    .collect();
628                node.set_children(valid);
629            }
630        }
631
632        update
633    }
634}
635
636#[cfg(test)]
637mod tests {
638    // Import specific items rather than glob-importing `super`, which would pull
639    // in gpui's own `test` attribute macro and shadow the standard one.
640    use super::{A11y, A11yNodeBuilder, ROOT_NODE_ID};
641    use crate::FocusId;
642    use accesskit::{NodeId, Role};
643    use std::sync::{Arc, atomic::AtomicBool};
644
645    fn test_node() -> accesskit::Node {
646        accesskit::Node::new(Role::GenericContainer)
647    }
648
649    fn new_builder() -> A11yNodeBuilder {
650        let mut builder = A11yNodeBuilder::new();
651        builder.begin_frame(None);
652        builder
653    }
654
655    fn new_a11y() -> A11y {
656        let mut a11y = A11y::new(Arc::new(AtomicBool::new(true)), false, None);
657        a11y.begin_frame();
658        a11y
659    }
660
661    #[test]
662    fn accessibility_enabled_is_independent_of_activation() {
663        for force_disabled in [false, true] {
664            for active in [false, true] {
665                let mut a11y = A11y::new(Arc::new(AtomicBool::new(active)), force_disabled, None);
666                a11y.sync_active_flag();
667
668                assert_eq!(a11y.is_enabled(), !force_disabled);
669                assert_eq!(a11y.is_active(), !force_disabled && active);
670            }
671        }
672    }
673
674    #[test]
675    fn active_descendant_honored_when_container_focused() {
676        let mut builder = new_builder();
677        let container = NodeId(1);
678        let item = NodeId(2);
679
680        assert!(builder.push(container, test_node()));
681        builder.set_focus(container);
682        assert!(builder.push(item, test_node()));
683
684        // The item is on top of the stack; the focused container is its
685        // ancestor, so the claim is honored.
686        assert!(builder.focus_is_ancestor_of_current());
687        builder.set_active_descendant(item);
688
689        builder.pop(); // item
690        builder.pop(); // container
691        let update = builder.finalize();
692        assert_eq!(update.focus, item);
693    }
694
695    #[test]
696    fn active_descendant_honored_for_deep_descendant() {
697        let mut builder = new_builder();
698        let container = NodeId(1);
699        let group = NodeId(2);
700        let item = NodeId(3);
701
702        assert!(builder.push(container, test_node()));
703        builder.set_focus(container);
704        assert!(builder.push(group, test_node()));
705        assert!(builder.push(item, test_node()));
706
707        // The item is a grandchild of the focused container; depth doesn't
708        // matter, the focused ancestor is still on the stack.
709        assert!(builder.focus_is_ancestor_of_current());
710        builder.set_active_descendant(item);
711
712        builder.pop(); // item
713        builder.pop(); // group
714        builder.pop(); // container
715        let update = builder.finalize();
716        assert_eq!(update.focus, item);
717    }
718
719    #[test]
720    fn active_descendant_ignored_when_focus_in_other_subtree() {
721        let mut builder = new_builder();
722        let focused_container = NodeId(1);
723        let focused_leaf = NodeId(2);
724        let other_container = NodeId(3);
725        let other_item = NodeId(4);
726
727        // First subtree holds real focus.
728        assert!(builder.push(focused_container, test_node()));
729        assert!(builder.push(focused_leaf, test_node()));
730        builder.set_focus(focused_leaf);
731        builder.pop(); // focused_leaf
732        builder.pop(); // focused_container
733
734        // Second subtree: its item would claim the active descendant, but the
735        // focus is not on any of its ancestors, so the gate rejects it.
736        assert!(builder.push(other_container, test_node()));
737        assert!(builder.push(other_item, test_node()));
738        assert!(!builder.focus_is_ancestor_of_current());
739        builder.pop(); // other_item
740        builder.pop(); // other_container
741
742        let update = builder.finalize();
743        assert_eq!(update.focus, focused_leaf);
744    }
745
746    #[test]
747    fn active_descendant_ignored_when_nothing_focused() {
748        let mut builder = new_builder();
749        let container = NodeId(1);
750        let item = NodeId(2);
751
752        assert!(builder.push(container, test_node()));
753        assert!(builder.push(item, test_node()));
754
755        // Nothing is focused (focus defaults to the root window node), so the
756        // gate rejects the claim.
757        assert!(!builder.focus_is_ancestor_of_current());
758        builder.pop();
759        builder.pop();
760
761        let update = builder.finalize();
762        assert_eq!(update.focus, ROOT_NODE_ID);
763    }
764
765    #[test]
766    fn regular_focus_used_when_no_active_descendant() {
767        let mut builder = new_builder();
768        let focused = NodeId(1);
769
770        assert!(builder.push(focused, test_node()));
771        builder.set_focus(focused);
772        builder.pop();
773
774        let update = builder.finalize();
775        assert_eq!(update.focus, focused);
776    }
777
778    #[test]
779    fn focus_is_ancestor_excludes_self_and_non_ancestors() {
780        let mut builder = new_builder();
781        let container = NodeId(1);
782        let item = NodeId(2);
783
784        assert!(builder.push(container, test_node()));
785        builder.set_focus(container);
786
787        // With the focused container itself on top, it is not its own (strict)
788        // ancestor, so the gate is false.
789        assert!(!builder.focus_is_ancestor_of_current());
790
791        assert!(builder.push(item, test_node()));
792        // Now the focused container is a strict ancestor of the item on top.
793        assert!(builder.focus_is_ancestor_of_current());
794
795        builder.pop();
796        builder.pop();
797    }
798
799    // The double-claim guard panics only in debug builds; in release it falls
800    // back to last-wins with a warning.
801    #[test]
802    #[cfg_attr(
803        debug_assertions,
804        should_panic(expected = "active descendant claimed by multiple nodes")
805    )]
806    fn multiple_active_descendant_claims_panic_in_debug() {
807        let mut builder = new_builder();
808        builder.set_active_descendant(NodeId(1));
809        builder.set_active_descendant(NodeId(2));
810    }
811
812    // Setting focus twice in one frame means two elements both claimed window
813    // focus; that panics in debug and falls back to last-wins in release.
814    #[test]
815    #[cfg_attr(
816        debug_assertions,
817        should_panic(expected = "set_focus called more than once")
818    )]
819    fn setting_focus_twice_panics_in_debug() {
820        let mut builder = new_builder();
821        builder.set_focus(NodeId(1));
822        builder.set_focus(NodeId(2));
823    }
824
825    // Focusing a node that was never registered as focusable is a bug: panic in
826    // debug, warn in release.
827    #[test]
828    #[cfg_attr(
829        debug_assertions,
830        should_panic(expected = "was not registered with set_focusable")
831    )]
832    fn set_focus_without_set_focusable() {
833        let mut a11y = new_a11y();
834        let node = NodeId(1);
835        assert!(a11y.nodes.push(node, test_node()));
836        // set_focusable was never called for `node`.
837        a11y.set_focus(node);
838    }
839
840    // The focused node cannot also be its own active descendant: panic in
841    // debug, warn in release.
842    #[test]
843    #[cfg_attr(debug_assertions, should_panic(expected = "on the focused node"))]
844    fn set_active_descendant_on_focused_node() {
845        let mut a11y = new_a11y();
846        let node = NodeId(1);
847        assert!(a11y.nodes.push(node, test_node()));
848        a11y.set_focusable(node, FocusId::default());
849        a11y.set_focus(node);
850        a11y.set_active_descendant(node);
851    }
852
853    // Two sibling children of a focused container both claim the active
854    // descendant (both pass the focus gate). The second claim is a bug: panic
855    // in debug, last-wins + warn in release.
856    #[test]
857    #[cfg_attr(
858        debug_assertions,
859        should_panic(expected = "active descendant claimed by multiple nodes")
860    )]
861    fn two_siblings_claiming_active_descendant() {
862        let mut a11y = new_a11y();
863        let container = NodeId(1);
864        let first = NodeId(2);
865        let second = NodeId(3);
866
867        assert!(a11y.nodes.push(container, test_node()));
868        a11y.set_focusable(container, FocusId::default());
869        a11y.set_focus(container);
870
871        assert!(a11y.nodes.push(first, test_node()));
872        a11y.set_active_descendant(first);
873        a11y.nodes.pop(); // first
874
875        assert!(a11y.nodes.push(second, test_node()));
876        a11y.set_active_descendant(second);
877        a11y.nodes.pop(); // second
878
879        a11y.nodes.pop(); // container
880    }
881
882    // Node A is focused; node C (a child of the unfocused node B) claims the
883    // active descendant. The final tree must still report A as focused.
884    #[test]
885    fn active_descendant_in_unfocused_subtree_keeps_real_focus() {
886        let mut a11y = new_a11y();
887        let a = NodeId(1);
888        let b = NodeId(2);
889        let c = NodeId(3);
890
891        assert!(a11y.nodes.push(a, test_node()));
892        a11y.set_focusable(a, FocusId::default());
893        a11y.set_focus(a);
894        a11y.nodes.pop(); // a
895
896        assert!(a11y.nodes.push(b, test_node()));
897        assert!(a11y.nodes.push(c, test_node()));
898        a11y.set_active_descendant(c);
899        a11y.nodes.pop(); // c
900        a11y.nodes.pop(); // b
901
902        let update = a11y.end_frame(Default::default());
903        assert_eq!(update.focus, a);
904    }
905}