use crate::{App, Bounds, FocusId, Pixels, Window};
use accesskit::{Action, NodeId, TreeUpdate};
use open_gpui_collections::{FxHashMap, FxHashSet};
use smallvec::SmallVec;
use std::sync::{
Arc,
atomic::{AtomicBool, Ordering},
};
pub(crate) const ROOT_NODE_ID: NodeId = NodeId(0);
pub(crate) type A11yActionListener =
Box<dyn FnMut(Option<&accesskit::ActionData>, &mut Window, &mut App) + 'static>;
pub(crate) struct A11y {
force_disabled: bool,
active_flag: Arc<AtomicBool>,
active_this_frame: bool,
pub(crate) nodes: A11yNodeBuilder,
pub(crate) focus_ids: FxHashMap<NodeId, FocusId>,
pub(crate) node_bounds: FxHashMap<NodeId, Bounds<Pixels>>,
pub(crate) action_listeners: FxHashMap<NodeId, Vec<(Action, A11yActionListener)>>,
}
impl A11y {
pub(crate) fn new(active_flag: Arc<AtomicBool>, force_disabled: bool) -> Self {
Self {
force_disabled,
active_flag,
active_this_frame: false,
nodes: A11yNodeBuilder::new(),
focus_ids: FxHashMap::default(),
node_bounds: FxHashMap::default(),
action_listeners: FxHashMap::default(),
}
}
pub(crate) fn sync_active_flag(&mut self) {
self.active_this_frame = !self.force_disabled && self.active_flag.load(Ordering::SeqCst);
}
pub(crate) fn is_active(&self) -> bool {
self.active_this_frame
}
pub(crate) fn begin_frame(&mut self) {
self.focus_ids.clear();
self.node_bounds.clear();
self.action_listeners.clear();
self.nodes.begin_frame();
}
pub(crate) fn end_frame(&mut self) -> TreeUpdate {
let tree_update = self.nodes.finalize();
if tree_update.nodes.len() > 1 {
log::warn!(
"expected an empty a11y tree update (only the root node), but got {} nodes; Zed has no accessible UI elements yet",
tree_update.nodes.len()
);
}
tree_update
}
}
pub(crate) struct A11yNodeBuilder {
ids_stack: SmallVec<[NodeId; 16]>,
nodes_stack: SmallVec<[accesskit::Node; 16]>,
all_nodes: Vec<(NodeId, accesskit::Node)>,
seen_ids: FxHashSet<NodeId>,
focus: NodeId,
#[cfg(debug_assertions)]
has_set_focus: bool,
}
impl A11yNodeBuilder {
fn new() -> Self {
Self {
ids_stack: SmallVec::new(),
nodes_stack: SmallVec::new(),
all_nodes: Vec::new(),
seen_ids: FxHashSet::default(),
focus: ROOT_NODE_ID,
#[cfg(debug_assertions)]
has_set_focus: false,
}
}
pub(crate) fn push(&mut self, id: NodeId, node: accesskit::Node) -> bool {
debug_assert!(!self.ids_stack.is_empty(), "push called before push_root");
if !self.seen_ids.insert(id) {
debug_assert!(
false,
"Duplicate a11y node id: {id:?}. In a release build, this node would be silently discarded from the a11y tree."
);
return false;
}
if let Some(parent) = self.nodes_stack.last_mut() {
parent.push_child(id);
}
self.ids_stack.push(id);
self.nodes_stack.push(node);
true
}
pub(crate) fn pop(&mut self) {
debug_assert!(self.ids_stack.len() > 1, "pop would remove the root node");
if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) {
self.all_nodes.push((id, node));
}
}
fn begin_frame(&mut self) {
self.all_nodes.clear();
self.ids_stack.clear();
self.nodes_stack.clear();
self.seen_ids.clear();
#[cfg(debug_assertions)]
{
self.has_set_focus = false;
}
let root_node = accesskit::Node::new(accesskit::Role::Window);
self.ids_stack.push(ROOT_NODE_ID);
self.nodes_stack.push(root_node);
self.focus = ROOT_NODE_ID;
}
pub(crate) fn has_node(&self, id: NodeId) -> bool {
id == ROOT_NODE_ID || self.seen_ids.contains(&id)
}
pub(crate) fn set_focus(&mut self, id: NodeId) {
#[cfg(debug_assertions)]
{
debug_assert!(
!self.has_set_focus,
"set_focus called more than once in a single frame"
);
self.has_set_focus = true;
}
self.focus = id;
}
fn finalize(&mut self) -> TreeUpdate {
debug_assert_eq!(self.ids_stack.len(), 1);
debug_assert_eq!(self.ids_stack[0], ROOT_NODE_ID);
if self.ids_stack.len() != 1 {
log::error!(
"a11y: Stack imbalance at end of frame: expected 1 (root), got {}. \
Some elements may have pushed without popping.",
self.ids_stack.len()
);
}
while !self.ids_stack.is_empty() {
if let (Some(id), Some(node)) = (self.ids_stack.pop(), self.nodes_stack.pop()) {
self.all_nodes.push((id, node));
}
}
let nodes = std::mem::take(&mut self.all_nodes);
let update = TreeUpdate {
nodes,
tree: Some(accesskit::Tree::new(ROOT_NODE_ID)),
tree_id: accesskit::TreeId::ROOT,
focus: self.focus,
};
Self::repair_tree_update(update)
}
fn repair_tree_update(mut update: TreeUpdate) -> TreeUpdate {
let node_ids: FxHashSet<NodeId> = update.nodes.iter().map(|(id, _)| *id).collect();
if !node_ids.contains(&update.focus) {
log::error!(
"a11y: Focused node {:?} is not in the tree ({} nodes). \
Falling back to root. This is a bug in the a11y tree builder.",
update.focus,
update.nodes.len()
);
update.focus = ROOT_NODE_ID;
}
for (id, node) in &mut update.nodes {
let has_invalid_child = node
.children()
.iter()
.any(|child_id| !node_ids.contains(child_id));
if has_invalid_child {
let children = node.children();
let invalid_count = children
.iter()
.filter(|child_id| !node_ids.contains(child_id))
.count();
log::error!(
"a11y: Node {:?} references {} children not present in the tree. \
Stripping invalid child references.",
id,
invalid_count
);
let valid: Vec<NodeId> = children
.iter()
.copied()
.filter(|child_id| node_ids.contains(child_id))
.collect();
node.set_children(valid);
}
}
update
}
}