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; Open GPUI 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;
}
macro_rules! repair_node_id_slice {
($node:ident, $id:ident, $getter:ident, $setter:ident) => {
if let Some(valid) =
filter_node_id_slice($id, stringify!($getter), $node.$getter(), &node_ids)
{
$node.$setter(valid);
}
};
}
macro_rules! repair_node_id {
($node:ident, $id:ident, $getter:ident, $clearer:ident) => {
if let Some(reference) = $node.$getter()
&& !node_ids.contains(&reference)
{
log_invalid_node_id_reference($id, stringify!($getter), reference);
$node.$clearer();
}
};
}
for (id, node) in &mut update.nodes {
repair_node_id_slice!(node, id, children, set_children);
repair_node_id_slice!(node, id, controls, set_controls);
repair_node_id_slice!(node, id, details, set_details);
repair_node_id_slice!(node, id, described_by, set_described_by);
repair_node_id_slice!(node, id, flow_to, set_flow_to);
repair_node_id_slice!(node, id, labelled_by, set_labelled_by);
repair_node_id_slice!(node, id, owns, set_owns);
repair_node_id_slice!(node, id, radio_group, set_radio_group);
repair_node_id!(node, id, active_descendant, clear_active_descendant);
repair_node_id!(node, id, error_message, clear_error_message);
repair_node_id!(node, id, in_page_link_target, clear_in_page_link_target);
repair_node_id!(node, id, member_of, clear_member_of);
repair_node_id!(node, id, next_on_line, clear_next_on_line);
repair_node_id!(node, id, previous_on_line, clear_previous_on_line);
repair_node_id!(node, id, popup_for, clear_popup_for);
}
update
}
}
fn log_invalid_node_id_reference(node_id: &NodeId, property: &'static str, reference: NodeId) {
log::error!(
"a11y: Node {:?} references {} node {:?} not present in the tree. \
Stripping invalid reference.",
node_id,
property,
reference
);
}
fn filter_node_id_slice(
node_id: &NodeId,
property: &'static str,
references: &[NodeId],
node_ids: &FxHashSet<NodeId>,
) -> Option<Vec<NodeId>> {
if references
.iter()
.all(|reference| node_ids.contains(reference))
{
return None;
}
let invalid_count = references
.iter()
.filter(|reference| !node_ids.contains(reference))
.count();
log::error!(
"a11y: Node {:?} references {} {} nodes not present in the tree. \
Stripping invalid references.",
node_id,
invalid_count,
property
);
Some(
references
.iter()
.copied()
.filter(|reference| node_ids.contains(reference))
.collect(),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repair_tree_update_strips_invalid_node_references() {
let valid_label = NodeId(2);
let missing = NodeId(99);
let mut root = accesskit::Node::new(accesskit::Role::Window);
let mut button = accesskit::Node::new(accesskit::Role::Button);
let label = accesskit::Node::new(accesskit::Role::Label);
root.set_children([NodeId(1), missing]);
button.set_controls([valid_label, missing]);
button.set_labelled_by([valid_label, missing]);
button.set_active_descendant(missing);
let update = accesskit::TreeUpdate {
nodes: vec![
(ROOT_NODE_ID, root),
(NodeId(1), button),
(valid_label, label),
],
tree: Some(accesskit::Tree::new(ROOT_NODE_ID)),
tree_id: accesskit::TreeId::ROOT,
focus: missing,
};
let repaired = A11yNodeBuilder::repair_tree_update(update);
let root = repaired
.nodes
.iter()
.find(|(id, _)| *id == ROOT_NODE_ID)
.map(|(_, node)| node)
.unwrap();
let button = repaired
.nodes
.iter()
.find(|(id, _)| *id == NodeId(1))
.map(|(_, node)| node)
.unwrap();
assert_eq!(repaired.focus, ROOT_NODE_ID);
assert_eq!(root.children(), &[NodeId(1)]);
assert_eq!(button.controls(), &[valid_label]);
assert_eq!(button.labelled_by(), &[valid_label]);
assert_eq!(button.active_descendant(), None);
}
#[test]
fn repair_tree_update_preserves_valid_node_references() {
let button_id = NodeId(1);
let label_id = NodeId(2);
let controlled_id = NodeId(3);
let mut root = accesskit::Node::new(accesskit::Role::Window);
let mut button = accesskit::Node::new(accesskit::Role::Button);
let label = accesskit::Node::new(accesskit::Role::Label);
let controlled = accesskit::Node::new(accesskit::Role::List);
root.set_children([button_id, label_id, controlled_id]);
button.set_controls([controlled_id]);
button.set_labelled_by([label_id]);
button.set_active_descendant(controlled_id);
let update = accesskit::TreeUpdate {
nodes: vec![
(ROOT_NODE_ID, root),
(button_id, button),
(label_id, label),
(controlled_id, controlled),
],
tree: Some(accesskit::Tree::new(ROOT_NODE_ID)),
tree_id: accesskit::TreeId::ROOT,
focus: button_id,
};
let repaired = A11yNodeBuilder::repair_tree_update(update);
let root = repaired
.nodes
.iter()
.find(|(id, _)| *id == ROOT_NODE_ID)
.map(|(_, node)| node)
.unwrap();
let button = repaired
.nodes
.iter()
.find(|(id, _)| *id == button_id)
.map(|(_, node)| node)
.unwrap();
assert_eq!(repaired.focus, button_id);
assert_eq!(root.children(), &[button_id, label_id, controlled_id]);
assert_eq!(button.controls(), &[controlled_id]);
assert_eq!(button.labelled_by(), &[label_id]);
assert_eq!(button.active_descendant(), Some(controlled_id));
}
#[test]
fn repair_tree_update_clears_invalid_single_node_references() {
let input_id = NodeId(1);
let missing_error = NodeId(42);
let missing_popup = NodeId(43);
let mut root = accesskit::Node::new(accesskit::Role::Window);
let mut input = accesskit::Node::new(accesskit::Role::TextInput);
root.set_children([input_id]);
input.set_error_message(missing_error);
input.set_popup_for(missing_popup);
let update = accesskit::TreeUpdate {
nodes: vec![(ROOT_NODE_ID, root), (input_id, input)],
tree: Some(accesskit::Tree::new(ROOT_NODE_ID)),
tree_id: accesskit::TreeId::ROOT,
focus: input_id,
};
let repaired = A11yNodeBuilder::repair_tree_update(update);
let input = repaired
.nodes
.iter()
.find(|(id, _)| *id == input_id)
.map(|(_, node)| node)
.unwrap();
assert_eq!(repaired.focus, input_id);
assert_eq!(input.error_message(), None);
assert_eq!(input.popup_for(), None);
}
}