use std::collections::VecDeque;
use std::fmt::Debug;
use std::sync::atomic::AtomicU64;
use std::sync::{LazyLock, atomic};
use accesskit::{NodeId, Role};
use bitflags::bitflags;
use layout_api::{LayoutElement, LayoutNode, LayoutNodeType};
use log::trace;
use rustc_hash::{FxHashMap, FxHashSet};
use script::layout_dom::ServoLayoutNode;
use servo_base::Epoch;
use servo_base::print_tree::PrintTree;
use servo_config::opts::{self, DiagnosticsLogging, DiagnosticsLoggingOption};
use servo_config::pref;
use style::dom::OpaqueNode;
use web_atoms::{LocalName, local_name};
use crate::ArcRefCell;
bitflags! {
#[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
struct LocalAccessibilityDamage: u16 {
const SUBTREE_CHANGED = 0b0001;
const ROLE_CHANGED = 0b0010;
const TEXT_CHANGED = 0b0100;
}
}
struct AccessibilityUpdate {
changed_nodes: FxHashSet<NodeId>,
tree_changes: FxHashMap<NodeId, TreeChange>,
rooted_nodes: Option<FxHashSet<OpaqueNode>>,
}
struct AccessibilityNode {
id: NodeId,
accesskit_node: accesskit::Node,
opaque_node: Option<OpaqueNode>,
updated: bool,
}
#[derive(Debug)]
pub struct AccessibilityTree {
nodes: FxHashMap<NodeId, ArcRefCell<AccessibilityNode>>,
opaque_node_to_id: FxHashMap<OpaqueNode, NodeId>,
id_to_opaque_node: FxHashMap<NodeId, OpaqueNode>,
tree_id: accesskit::TreeId,
root_node_id: Option<NodeId>,
embedder_epoch: Epoch,
debug: DiagnosticsLogging,
}
#[derive(Debug, PartialEq, Copy, Clone)]
enum TreeChange {
New,
Moved,
PendingMove,
Removed,
}
impl AccessibilityTree {
pub(super) fn new(tree_id: accesskit::TreeId, embedder_epoch: Epoch) -> Self {
Self {
nodes: FxHashMap::default(),
opaque_node_to_id: FxHashMap::default(),
id_to_opaque_node: FxHashMap::default(),
tree_id,
root_node_id: None,
embedder_epoch,
debug: opts::get().debug.clone(),
}
}
pub(super) fn update_tree(
&mut self,
root_dom_node: &ServoLayoutNode<'_>,
rooted_nodes: Option<FxHashSet<OpaqueNode>>,
) -> Option<accesskit::TreeUpdate> {
let mut update = AccessibilityUpdate::new(rooted_nodes);
let (root_node_id, root_node) = self.get_or_create_node(root_dom_node, &mut update);
self.root_node_id = Some(root_node_id);
self.update_node_and_descendants_from_dom_node(&root_node, root_dom_node, &mut update);
update.finalize(self)
}
fn update_node_and_descendants_from_dom_node(
&mut self,
node: &ArcRefCell<AccessibilityNode>,
dom_node: &ServoLayoutNode<'_>,
update: &mut AccessibilityUpdate,
) -> LocalAccessibilityDamage {
let mut node = node.borrow_mut();
let mut damage = LocalAccessibilityDamage::empty();
damage.insert(node.update_node_from_dom_node(dom_node));
damage.insert(node.update_descendants_from_dom_node(dom_node, self, update));
damage.insert(node.update_node_local(damage, self));
if node.updated {
update.add(&mut node);
}
damage
}
fn get_or_create_node(
&mut self,
dom_node: &ServoLayoutNode<'_>,
update: &mut AccessibilityUpdate,
) -> (NodeId, ArcRefCell<AccessibilityNode>) {
let id = self.id_for_opaque(dom_node.opaque());
let node = self.nodes.entry(id).or_insert_with(|| {
update.set_tree_state_change(id, TreeChange::New);
ArcRefCell::new(AccessibilityNode::new(id))
});
let mut new_node = node.borrow_mut();
new_node.opaque_node = Some(dom_node.opaque());
if let Some(dom_element) = dom_node.as_element() {
let local_name = dom_element.local_name().to_ascii_lowercase();
new_node.set_html_tag(&local_name);
}
(id, node.clone())
}
fn node_for_id(&self, id: &NodeId) -> Option<ArcRefCell<AccessibilityNode>> {
self.nodes.get(id).cloned()
}
fn assert_node_for_id(&self, id: &NodeId) -> ArcRefCell<AccessibilityNode> {
let Some(node) = self.nodes.get(id) else {
panic!("{id:?} does not exist in tree");
};
node.clone()
}
fn remove_stale_nodes(&mut self, mut update: AccessibilityUpdate) {
if let Some(rooted_nodes) = std::mem::take(&mut update.rooted_nodes) {
self.assert_removed_nodes_were_rooted(&update, rooted_nodes);
}
for id in update
.tree_changes
.drain()
.filter_map(|(id, change)| match change {
TreeChange::PendingMove => {
unreachable!(
"Pending move found for node id {id:?} when draining tree state changes"
);
},
TreeChange::Removed => Some(id),
_ => None,
})
{
let node = self.nodes.remove(&id);
debug_assert!(node.is_some(), "Node for id {id:?} was already removed");
if let Some(opaque_node) = self.id_to_opaque_node.remove(&id) {
self.opaque_node_to_id.remove(&opaque_node);
}
}
if self
.debug
.is_enabled(DiagnosticsLoggingOption::AccessibilityTree)
{
self.print();
}
if pref!(expensive_accessibility_test_assertions_enabled) {
self.assert_integrity();
}
}
fn assert_removed_nodes_were_rooted(
&mut self,
update: &AccessibilityUpdate,
mut rooted_nodes: FxHashSet<OpaqueNode>,
) {
debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
for (id, change) in update.tree_changes.iter() {
if change == &TreeChange::Removed {
let Some(&opaque_node) = self.id_to_opaque_node.get(id) else {
panic!("No opaque node found for removed node: id {id:?}");
};
assert!(
rooted_nodes.remove(&opaque_node),
"Node removed from accessibility tree wasn't rooted: id {id:?}"
);
};
}
for leftover_node in rooted_nodes {
assert!(
!self.opaque_node_to_id.contains_key(&leftover_node),
"Found node removed from DOM tree but not accessibility tree"
);
}
}
fn id_for_opaque(&mut self, opaque: OpaqueNode) -> NodeId {
let id = self.opaque_node_to_id.entry(opaque).or_insert_with(|| {
static LAST_ID: AtomicU64 = AtomicU64::new(0);
let id = LAST_ID.fetch_add(1, atomic::Ordering::SeqCst).into();
self.id_to_opaque_node.insert(id, opaque);
id
});
*id
}
pub(crate) fn embedder_epoch(&self) -> Epoch {
self.embedder_epoch
}
fn assert_integrity(&self) {
debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
let Some(root_node_id) = self.root_node_id else {
return;
};
let mut node_ids = vec![root_node_id];
let mut seen_node_ids = FxHashSet::default();
while let Some(node_id) = node_ids.pop() {
assert!(
seen_node_ids.insert(node_id),
"Tree contains {node_id:?} in multiple places"
);
let node = self.assert_node_for_id(&node_id);
let node = node.borrow();
node_ids.extend(node.children().iter().rev());
}
assert_eq!(seen_node_ids, self.nodes.keys().copied().collect());
}
fn print(&self) {
let Some(root_node_id) = self.root_node_id else {
return;
};
let mut print_tree = PrintTree::new("Accessibility Tree");
let node = self.assert_node_for_id(&root_node_id);
node.borrow().print(self, &mut print_tree);
print_tree.end_level();
}
}
fn role_from_dom_node(dom_node: &ServoLayoutNode<'_>) -> Role {
if let Some(dom_element) = dom_node.as_element() {
let local_name = dom_element.local_name().to_ascii_lowercase();
*HTML_ELEMENT_ROLE_MAPPINGS
.get(&local_name)
.unwrap_or(&Role::GenericContainer)
} else if dom_node.type_id() == Some(LayoutNodeType::Text) {
Role::TextRun
} else {
Role::GenericContainer
}
}
impl AccessibilityNode {
fn new(id: NodeId) -> Self {
Self::new_with_role(id, Role::Unknown)
}
fn new_with_role(id: NodeId, role: Role) -> Self {
Self {
id,
accesskit_node: accesskit::Node::new(role),
opaque_node: None,
updated: true,
}
}
fn update_descendants_from_dom_node<'dom>(
&mut self,
dom_node: &ServoLayoutNode<'dom>,
tree: &mut AccessibilityTree,
update: &mut AccessibilityUpdate,
) -> LocalAccessibilityDamage {
let mut damage = LocalAccessibilityDamage::empty();
let dom_children: Vec<ServoLayoutNode> = dom_node.flat_tree_children().collect();
let new_children: Vec<NodeId> = dom_children
.iter()
.map(|dom_child| tree.id_for_opaque(dom_child.opaque()))
.collect();
damage.insert(self.set_children(new_children, tree, update));
let mut damage_from_children = LocalAccessibilityDamage::empty();
for dom_child in dom_children {
let (_, child_node) = tree.get_or_create_node(&dom_child, update);
let child_damage =
tree.update_node_and_descendants_from_dom_node(&child_node, &dom_child, update);
damage_from_children.insert(child_damage);
}
if !damage_from_children.is_empty() {
damage.insert(LocalAccessibilityDamage::SUBTREE_CHANGED);
}
damage
}
fn set_subtree_state_change(
&self,
change: TreeChange,
tree: &mut AccessibilityTree,
update: &mut AccessibilityUpdate,
) {
assert!(
change != TreeChange::New,
"New shouldn't be set recursively"
);
update.set_tree_state_change(self.id, change);
for child_id in self.children().iter() {
let child = tree.assert_node_for_id(child_id);
child
.borrow()
.set_subtree_state_change(change, tree, update);
}
}
fn update_node_from_dom_node(
&mut self,
dom_node: &ServoLayoutNode<'_>,
) -> LocalAccessibilityDamage {
let mut damage = LocalAccessibilityDamage::empty();
damage.insert(self.set_role(role_from_dom_node(dom_node)));
if dom_node.type_id() == Some(LayoutNodeType::Text) {
let text_content = dom_node.text_content();
trace!("node text content = {text_content:?}");
damage.insert(self.set_value(&text_content));
}
damage
}
fn update_node_local(
&mut self,
damage: LocalAccessibilityDamage,
tree: &mut AccessibilityTree,
) -> LocalAccessibilityDamage {
let mut new_damage = LocalAccessibilityDamage::empty();
if damage.contains(LocalAccessibilityDamage::SUBTREE_CHANGED) ||
damage.contains(LocalAccessibilityDamage::ROLE_CHANGED)
{
if let Some(text) = self.label_from_descendants(tree) {
new_damage.insert(self.set_label(text.as_str()));
} else {
new_damage.insert(self.clear_label());
}
}
new_damage
}
fn label_from_descendants(&self, tree: &AccessibilityTree) -> Option<String> {
if !NAME_FROM_CONTENTS_ROLES.contains(&self.role()) {
return None;
}
let mut children = VecDeque::from_iter(self.children().iter().copied());
let mut text = String::new();
while let Some(child_id) = children.pop_front() {
let child = tree.assert_node_for_id(&child_id);
let child = child.borrow();
match child.role() {
Role::TextRun => {
if let Some(child_text) = child.value() {
text.push_str(child_text);
}
},
_ => {
for id in child.children().iter().rev() {
children.push_front(*id);
}
},
}
}
Some(text.trim().to_owned())
}
fn print(&self, tree: &AccessibilityTree, print_tree: &mut PrintTree) {
if self.children().is_empty() {
print_tree.add_item(format!("{self:?}"));
return;
}
print_tree.new_level(format!("{self:?}"));
for child_id in self.children() {
let child = tree.assert_node_for_id(child_id);
child.borrow().print(tree, print_tree);
}
print_tree.end_level();
}
fn children(&self) -> &[NodeId] {
self.accesskit_node.children()
}
fn set_children(
&mut self,
children: Vec<NodeId>,
tree: &mut AccessibilityTree,
update: &mut AccessibilityUpdate,
) -> LocalAccessibilityDamage {
if children == self.children() {
return LocalAccessibilityDamage::empty();
}
let old_children = self.children();
for old_child_id in old_children {
if !children.contains(old_child_id) {
let removed_child = tree.assert_node_for_id(old_child_id);
removed_child
.borrow()
.set_subtree_state_change(TreeChange::Removed, tree, update);
}
}
for new_child_id in children.iter() {
if !old_children.contains(new_child_id) &&
let Some(moved_child) = tree.node_for_id(new_child_id)
{
moved_child.borrow().set_subtree_state_change(
TreeChange::PendingMove,
tree,
update,
);
}
}
self.accesskit_node.set_children(children);
self.updated = true;
LocalAccessibilityDamage::SUBTREE_CHANGED
}
fn role(&self) -> Role {
self.accesskit_node.role()
}
fn set_role(&mut self, role: Role) -> LocalAccessibilityDamage {
if role == self.accesskit_node.role() {
return LocalAccessibilityDamage::empty();
}
self.accesskit_node.set_role(role);
self.updated = true;
LocalAccessibilityDamage::ROLE_CHANGED
}
fn label(&self) -> Option<&str> {
self.accesskit_node.label()
}
fn set_label(&mut self, label: &str) -> LocalAccessibilityDamage {
if Some(label) == self.accesskit_node.label() {
return LocalAccessibilityDamage::empty();
}
self.accesskit_node.set_label(label);
self.updated = true;
LocalAccessibilityDamage::TEXT_CHANGED
}
fn clear_label(&mut self) -> LocalAccessibilityDamage {
if self.accesskit_node.label().is_none() {
return LocalAccessibilityDamage::empty();
}
self.accesskit_node.clear_label();
self.updated = true;
LocalAccessibilityDamage::TEXT_CHANGED
}
fn html_tag(&self) -> Option<&str> {
self.accesskit_node.html_tag()
}
fn set_html_tag(&mut self, html_tag: &str) {
if Some(html_tag) == self.accesskit_node.html_tag() {
return;
}
self.accesskit_node.set_html_tag(html_tag);
self.updated = true;
}
fn value(&self) -> Option<&str> {
self.accesskit_node.value()
}
fn set_value(&mut self, value: &str) -> LocalAccessibilityDamage {
if Some(value) == self.accesskit_node.value() {
return LocalAccessibilityDamage::empty();
}
self.accesskit_node.set_value(value);
self.updated = true;
LocalAccessibilityDamage::TEXT_CHANGED
}
}
impl Debug for AccessibilityNode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}: {:?}", self.id, self.role())?;
if let Some(html_tag) = self.html_tag() {
write!(f, " (html_tag: {html_tag:?})")?;
}
if let Some(label) = self.label() {
write!(f, "\nlabel: {label:?}")?;
}
if !self.children().is_empty() {
write!(f, "\nchildren: {:?}", self.children())?;
}
Ok(())
}
}
impl AccessibilityUpdate {
fn new(rooted_nodes: Option<FxHashSet<OpaqueNode>>) -> Self {
Self {
changed_nodes: FxHashSet::default(),
tree_changes: FxHashMap::default(),
rooted_nodes,
}
}
fn add(&mut self, node: &mut AccessibilityNode) {
self.changed_nodes.insert(node.id);
node.updated = false;
}
fn set_tree_state_change(&mut self, node_id: NodeId, change: TreeChange) {
let old_change = self.tree_changes.get(&node_id);
assert!(
change != TreeChange::Moved,
"Incoming change must never be Moved"
);
let new_change = old_change
.map(|old_change| match (old_change, change) {
(TreeChange::PendingMove, TreeChange::Removed) => TreeChange::Moved,
(TreeChange::Removed, TreeChange::PendingMove) => TreeChange::Moved,
_ => {
unreachable!("Logically impossible state change: {old_change:?} → {change:?}")
},
})
.unwrap_or(change);
self.tree_changes.insert(node_id, new_change);
}
fn finalize(mut self, tree: &mut AccessibilityTree) -> Option<accesskit::TreeUpdate> {
let root_node_id = tree
.root_node_id
.expect("AccessibilityUpdate::finalize() called but no root_node_id set in tree");
if self.changed_nodes.is_empty() {
assert!(self.tree_changes.is_empty());
return None;
}
let accesskit_tree = accesskit::Tree::new(root_node_id);
let tree_update = accesskit::TreeUpdate {
nodes: std::mem::take(&mut self.changed_nodes)
.into_iter()
.map(|id| {
(
id,
tree.assert_node_for_id(&id).borrow().accesskit_node.clone(),
)
})
.collect(),
tree: Some(accesskit_tree),
focus: NodeId(1),
tree_id: tree.tree_id,
};
tree.remove_stale_nodes(self);
Some(tree_update)
}
}
#[cfg(test)]
#[test]
fn test_accessibility_update_add_some_nodes_twice() {
let mut tree = AccessibilityTree::new(accesskit::TreeId::ROOT, Epoch::default());
tree.root_node_id = Some(NodeId(2));
for (id, role) in [
(3, Role::GenericContainer),
(4, Role::Heading),
(5, Role::Paragraph),
] {
let id = NodeId(id);
tree.nodes.insert(
id,
ArcRefCell::new(AccessibilityNode::new_with_role(id, role)),
);
}
let mut update = AccessibilityUpdate::new(None);
{
let node_3 = tree.assert_node_for_id(&NodeId(3));
let mut node_3 = node_3.borrow_mut();
let node_4 = tree.assert_node_for_id(&NodeId(4));
let mut node_4 = node_4.borrow_mut();
let node_5 = tree.assert_node_for_id(&NodeId(5));
let mut node_5 = node_5.borrow_mut();
update.add(&mut node_5);
update.add(&mut node_3);
update.add(&mut node_4);
update.add(&mut node_4);
node_3.set_role(Role::ScrollView);
update.add(&mut node_3);
}
let mut tree_update = update
.finalize(&mut tree)
.expect("finalize should produce a tree update");
tree_update.nodes.sort_by_key(|(node_id, _node)| *node_id);
assert_eq!(
tree_update,
accesskit::TreeUpdate {
nodes: vec![
(NodeId(3), accesskit::Node::new(Role::ScrollView)),
(NodeId(4), accesskit::Node::new(Role::Heading)),
(NodeId(5), accesskit::Node::new(Role::Paragraph)),
],
tree: Some(accesskit::Tree {
root: NodeId(2),
toolkit_name: None,
toolkit_version: None
}),
tree_id: accesskit::TreeId::ROOT,
focus: NodeId(1),
}
);
}
static HTML_ELEMENT_ROLE_MAPPINGS: LazyLock<FxHashMap<LocalName, Role>> = LazyLock::new(|| {
[
(local_name!("article"), Role::Article),
(local_name!("aside"), Role::Complementary),
(local_name!("body"), Role::RootWebArea),
(local_name!("footer"), Role::ContentInfo),
(local_name!("h1"), Role::Heading),
(local_name!("h2"), Role::Heading),
(local_name!("h3"), Role::Heading),
(local_name!("h4"), Role::Heading),
(local_name!("h5"), Role::Heading),
(local_name!("h6"), Role::Heading),
(local_name!("header"), Role::Banner),
(local_name!("hr"), Role::Splitter),
(local_name!("main"), Role::Main),
(local_name!("nav"), Role::Navigation),
(local_name!("p"), Role::Paragraph),
]
.into_iter()
.collect()
});
static NAME_FROM_CONTENTS_ROLES: LazyLock<FxHashSet<Role>> =
LazyLock::new(|| [(Role::Heading)].into_iter().collect());