use std::collections::VecDeque;
use std::fmt::Debug;
use std::iter::repeat;
use std::sync::atomic::AtomicU64;
use std::sync::{LazyLock, atomic};
use accesskit::{NodeId, Role};
use bitflags::bitflags;
use layout_api::{AccessibilityDamage, 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;
use crate::cell::WeakRefCell;
bitflags! {
#[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
struct LocalAccessibilityDamage: u16 {
const SubtreeChanged = 0b0001;
const RoleChanged = 0b0010;
const TextChanged = 0b0100;
}
}
struct AccessibilityUpdate {
changed_nodes: FxHashSet<NodeId>,
tree_changes: FxHashMap<NodeId, TreeChange>,
counters: UpdateCounters,
rooted_nodes: Option<FxHashSet<OpaqueNode>>,
}
#[derive(Debug, Default)]
pub struct UpdateCounters {
pub nodes_updated_from_dom: u32,
pub nodes_updated_from_tree: u32,
pub nodes_in_tree_update: u32,
}
bitflags! {
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct DirtyState : u16 {
const DescendantHasDamage = 0b0001;
const HasDamage = 0b0010;
const Updated = 0b0100;
}
}
struct AccessibilityNode {
id: NodeId,
accesskit_node: accesskit::Node,
parent_node: Option<WeakRefCell<AccessibilityNode>>,
child_nodes: Vec<ArcRefCell<AccessibilityNode>>,
opaque_node: Option<OpaqueNode>,
dirty_state: DirtyState,
}
#[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: Option<ArcRefCell<AccessibilityNode>>,
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: None,
embedder_epoch,
debug: opts::get().debug.clone(),
}
}
pub(super) fn update_tree<'dom>(
&mut self,
root_dom_node: &ServoLayoutNode<'dom>,
mut damage_from_dom: VecDeque<(ServoLayoutNode<'dom>, AccessibilityDamage)>,
rooted_nodes: Option<FxHashSet<OpaqueNode>>,
) -> (Option<accesskit::TreeUpdate>, UpdateCounters) {
let mut update = AccessibilityUpdate::new(rooted_nodes);
self.ensure_root_node(root_dom_node, &mut damage_from_dom, &mut update);
self.apply_changes_from_dom_tree(damage_from_dom, &mut update);
update.finalize(self)
}
fn ensure_root_node<'dom>(
&mut self,
root_dom_node: &ServoLayoutNode<'dom>,
damage_from_dom: &mut VecDeque<(ServoLayoutNode<'dom>, AccessibilityDamage)>,
update: &mut AccessibilityUpdate,
) {
let (root_id, root_node) = self.get_or_create_node(root_dom_node, update);
if update.is_new(&root_id) {
damage_from_dom.push_front((*root_dom_node, AccessibilityDamage::Rebuild));
}
self.root_node = Some(root_node);
}
fn apply_changes_from_dom_tree<'dom>(
&mut self,
damage_from_dom: VecDeque<(ServoLayoutNode<'dom>, AccessibilityDamage)>,
update: &mut AccessibilityUpdate,
) {
let mut dom_damage_map = FxHashMap::from(
damage_from_dom
.into_iter()
.filter_map(|(dom_node, dom_node_damage)| {
let id = self.existing_id_for_opaque(dom_node.opaque())?;
Some((id, (dom_node, dom_node_damage)))
})
.collect(),
);
let damage_root = self.mark_nodes_and_ancestors_dirty(dom_damage_map.keys().cloned());
let Some(damage_root) = damage_root else {
return;
};
let local_damage = damage_root.borrow_mut().update_subtree(
damage_root.clone(),
&mut dom_damage_map,
self,
update,
);
damage_root.borrow().update_ancestors(local_damage, update);
}
fn mark_nodes_and_ancestors_dirty(
&mut self,
mut dirty_node_ids: impl Iterator<Item = NodeId>,
) -> Option<ArcRefCell<AccessibilityNode>> {
let mut common_ancestors: Vec<NodeId> = Vec::new();
{
let first_node = self.assert_node_for_id(&dirty_node_ids.next()?);
let mut first_node = first_node.borrow_mut();
first_node.dirty_state |= DirtyState::HasDamage;
common_ancestors.push(first_node.id);
common_ancestors.extend(first_node.ancestors().map(|ancestor| {
let mut ancestor = ancestor.borrow_mut();
ancestor.dirty_state |= DirtyState::DescendantHasDamage;
ancestor.id
}));
common_ancestors.reverse();
}
let mut truncate_ancestors = |node: &AccessibilityNode| -> bool {
if node.dirty_state.descendant_has_damage() {
if let Some(pos) = common_ancestors.iter().position(|&id| id == node.id) {
common_ancestors.truncate(pos + 1);
}
return true;
}
false
};
for node_id in dirty_node_ids {
let node = self.assert_node_for_id(&node_id);
let mut node = node.borrow_mut();
node.dirty_state |= DirtyState::HasDamage;
if truncate_ancestors(&node) {
continue;
}
for ancestor in node.ancestors() {
let mut ancestor = ancestor.borrow_mut();
if truncate_ancestors(&ancestor) {
break;
}
ancestor.dirty_state |= DirtyState::DescendantHasDamage;
}
}
self.nodes.get(common_ancestors.last()?).cloned()
}
fn get_or_create_node(
&mut self,
dom_node: &ServoLayoutNode<'_>,
update: &mut AccessibilityUpdate,
) -> (NodeId, ArcRefCell<AccessibilityNode>) {
let id = self.get_or_create_id_for_opaque(dom_node.opaque());
let node_ref = self.get_or_create_node_with_id(id, update);
if update.is_new(&id) {
let mut node = node_ref.borrow_mut();
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();
node.set_html_tag(&local_name);
}
}
(id, node_ref)
}
fn get_or_create_node_with_id(
&mut self,
id: NodeId,
update: &mut AccessibilityUpdate,
) -> ArcRefCell<AccessibilityNode> {
if let Some(node) = self.nodes.get(&id) {
return node.clone();
}
let node = ArcRefCell::new(AccessibilityNode::new(id));
update.set_tree_state_change(id, TreeChange::New);
self.nodes.insert(id, node.clone());
node
}
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 drop_removed_nodes(&mut self, mut update: AccessibilityUpdate) {
let mut rooted_nodes = std::mem::take(&mut update.rooted_nodes);
if let Some(rooted_nodes) = rooted_nodes.as_mut() {
self.assert_removed_nodes_were_rooted(&update, rooted_nodes);
}
let mut ids_to_remove: Vec<_> = update
.tree_changes
.iter()
.filter_map(|(id, change)| match change {
TreeChange::Removed => Some(id),
TreeChange::PendingMove => None,
TreeChange::New => None,
TreeChange::Moved => None,
})
.cloned()
.collect();
while let Some(id) = ids_to_remove.pop() {
if update.tree_changes.get(&id) == Some(&TreeChange::PendingMove) {
update.set_tree_state_change(id, TreeChange::Removed);
continue;
}
if let Some(opaque_node) = self.id_to_opaque_node.remove(&id) {
self.opaque_node_to_id.remove(&opaque_node);
}
let node = self.nodes.remove(&id).expect("Node {id:?} already removed");
ids_to_remove.extend(node.borrow().child_ids());
}
update
.tree_changes
.drain()
.for_each(|(id, change)| match change {
TreeChange::PendingMove => unreachable!(
"Pending move found for node id {id:?} when draining tree state changes"
),
TreeChange::Removed => (),
TreeChange::New => (),
TreeChange::Moved => (),
});
if let Some(rooted_nodes) = rooted_nodes {
self.assert_remaining_rooted_nodes_not_in_tree(rooted_nodes);
}
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,
rooted_nodes: &mut 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:?}"
);
};
}
}
fn assert_remaining_rooted_nodes_not_in_tree(&self, rooted_nodes: FxHashSet<OpaqueNode>) {
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: {:#x}",
leftover_node.0
);
}
}
fn get_or_create_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
}
fn existing_id_for_opaque(&self, opaque: OpaqueNode) -> Option<NodeId> {
self.opaque_node_to_id.get(&opaque).cloned()
}
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) = self.root_node.clone() else {
return;
};
let mut nodes = vec![(root_node, None)];
let mut seen_node_ids = FxHashSet::default();
while let Some((node, expected_parent)) = nodes.pop() {
let node = node.borrow();
assert!(
seen_node_ids.insert(node.id),
"Tree contains {:?} in multiple places",
node.id
);
node.assert_integrity(expected_parent);
let weak_node = Some(self.assert_node_for_id(&node.id).downgrade());
nodes.extend(node.children().cloned().zip(repeat(weak_node)));
}
assert_eq!(seen_node_ids, self.nodes.keys().copied().collect());
}
fn print(&self) {
let Some(root_node) = self.root_node.clone() else {
return;
};
let mut print_tree = PrintTree::new("Accessibility Tree");
root_node.borrow().print(&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
}
}
struct AccessibilityNodeIterator<I>
where
I: Fn(&AccessibilityNode) -> Option<ArcRefCell<AccessibilityNode>>,
{
next_value: Option<ArcRefCell<AccessibilityNode>>,
next_fn: I,
}
impl<I> AccessibilityNodeIterator<I>
where
I: Fn(&AccessibilityNode) -> Option<ArcRefCell<AccessibilityNode>>,
{
fn new(next_value: Option<ArcRefCell<AccessibilityNode>>, next_fn: I) -> Self {
AccessibilityNodeIterator {
next_value,
next_fn,
}
}
}
impl<I> Iterator for AccessibilityNodeIterator<I>
where
I: Fn(&AccessibilityNode) -> Option<ArcRefCell<AccessibilityNode>>,
{
type Item = ArcRefCell<AccessibilityNode>;
fn next(&mut self) -> Option<Self::Item> {
let next_value = self.next_value.take();
self.next_value = next_value
.as_ref()
.and_then(|node| (self.next_fn)(&node.borrow()));
next_value
}
}
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),
parent_node: None,
child_nodes: vec![],
opaque_node: None,
dirty_state: DirtyState::empty(),
}
}
fn update_subtree<'dom>(
&mut self,
ref_self: ArcRefCell<Self>,
dom_damage_map: &mut FxHashMap<NodeId, (ServoLayoutNode<'dom>, AccessibilityDamage)>,
tree: &mut AccessibilityTree,
update: &mut AccessibilityUpdate,
) -> LocalAccessibilityDamage {
let mut local_damage = LocalAccessibilityDamage::empty();
if let Some((dom_node, dom_damage)) = dom_damage_map.get(&self.id) {
local_damage.insert(self.update_node_and_populate_new_descendants_from_dom_node(
ref_self,
dom_node,
*dom_damage,
tree,
update,
));
self.dirty_state -= DirtyState::HasDamage;
}
if self.dirty_state.descendant_has_damage() {
for child_node in self.children() {
let strong_child_node = child_node.clone();
let mut child_node = child_node.borrow_mut();
if !child_node.dirty_state.self_or_descendant_has_damage() {
continue;
}
let child_damage =
child_node.update_subtree(strong_child_node, dom_damage_map, tree, update);
if !child_damage.is_empty() {
local_damage.insert(LocalAccessibilityDamage::SubtreeChanged);
}
}
self.dirty_state -= DirtyState::DescendantHasDamage;
}
local_damage.insert(self.update_node_local(local_damage, update));
if self.dirty_state.updated() {
update.add(self);
}
local_damage
}
fn update_ancestors(
&self,
local_damage: LocalAccessibilityDamage,
update: &mut AccessibilityUpdate,
) {
if local_damage.is_empty() {
return;
}
for node in self.ancestors() {
let mut node = node.borrow_mut();
node.update_node_local(LocalAccessibilityDamage::SubtreeChanged, update);
node.dirty_state -= DirtyState::DescendantHasDamage;
if node.dirty_state.updated() {
update.add(&mut node);
}
}
}
fn update_node_and_populate_new_descendants_from_dom_node<'dom>(
&mut self,
ref_self: ArcRefCell<Self>,
dom_node: &ServoLayoutNode<'dom>,
dom_damage: AccessibilityDamage,
tree: &mut AccessibilityTree,
update: &mut AccessibilityUpdate,
) -> LocalAccessibilityDamage {
update.counters.nodes_updated_from_dom += 1;
let mut local_damage = LocalAccessibilityDamage::empty();
local_damage.insert(self.update_properties_from_dom_node(dom_node, dom_damage));
local_damage.insert(
self.update_children_and_populate_new_descendants_from_dom_node(
ref_self, dom_node, dom_damage, tree, update,
),
);
local_damage
}
fn update_children_and_populate_new_descendants_from_dom_node<'dom>(
&mut self,
ref_self: ArcRefCell<AccessibilityNode>,
dom_node: &ServoLayoutNode<'dom>,
dom_damage: AccessibilityDamage,
tree: &mut AccessibilityTree,
update: &mut AccessibilityUpdate,
) -> LocalAccessibilityDamage {
if !dom_damage.contains(AccessibilityDamage::Children) {
return LocalAccessibilityDamage::empty();
}
let mut remaining_dom_children = dom_node.flat_tree_children().peekable();
let mut old_child_ids = self.child_ids().iter().peekable();
let mut unchanged_count = 0usize;
while let Some(&old_id) = old_child_ids.peek() &&
let Some(dom_child) = remaining_dom_children.peek()
{
if tree.existing_id_for_opaque(dom_child.opaque()) == Some(*old_id) {
unchanged_count += 1;
old_child_ids.next();
remaining_dom_children.next();
} else {
break;
}
}
if old_child_ids.peek().is_none() && remaining_dom_children.peek().is_none() {
return LocalAccessibilityDamage::empty();
}
self.child_nodes.truncate(unchanged_count);
let mut new_child_ids = Vec::from(self.child_ids());
for removed_child_id in new_child_ids.split_off(unchanged_count) {
update.set_tree_state_change(removed_child_id, TreeChange::Removed);
}
let weak_self = ref_self.downgrade();
for dom_child in remaining_dom_children {
let (child_id, child_ref) = tree.get_or_create_node(&dom_child, update);
self.child_nodes.push(child_ref.clone());
new_child_ids.push(child_id);
let mut child = child_ref.borrow_mut();
child.parent_node = Some(weak_self.clone());
if update.is_new(&child_id) {
let child_damage = child.update_node_and_populate_new_descendants_from_dom_node(
child_ref.clone(),
&dom_child,
AccessibilityDamage::Rebuild,
tree,
update,
);
child.update_node_local(child_damage, update);
update.add(&mut child);
} else {
update.set_tree_state_change(child_id, TreeChange::PendingMove);
}
self.dirty_state
.propagate_descendant_has_damage(child.dirty_state);
}
self.accesskit_node.set_children(new_child_ids);
self.dirty_state |= DirtyState::Updated;
LocalAccessibilityDamage::SubtreeChanged
}
fn update_properties_from_dom_node(
&mut self,
dom_node: &ServoLayoutNode<'_>,
dom_damage: AccessibilityDamage,
) -> LocalAccessibilityDamage {
let mut local_damage = LocalAccessibilityDamage::empty();
if !dom_damage.contains(AccessibilityDamage::Text) {
return local_damage;
}
local_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:?}");
local_damage.insert(self.set_value(&text_content));
}
local_damage
}
fn update_node_local(
&mut self,
local_damage: LocalAccessibilityDamage,
update: &mut AccessibilityUpdate,
) -> LocalAccessibilityDamage {
update.counters.nodes_updated_from_tree += 1;
let mut new_damage = LocalAccessibilityDamage::empty();
if local_damage.contains(LocalAccessibilityDamage::SubtreeChanged) ||
local_damage.contains(LocalAccessibilityDamage::RoleChanged)
{
if let Some(text) = self.label_from_descendants() {
new_damage.insert(self.set_label(text.as_str()));
} else {
new_damage.insert(self.clear_label());
}
}
new_damage
}
fn label_from_descendants(&self) -> Option<String> {
if !NAME_FROM_CONTENTS_ROLES.contains(&self.role()) {
return None;
}
let mut children = VecDeque::from_iter(self.children().cloned());
let mut text = String::new();
while let Some(child) = children.pop_front() {
let child = child.borrow();
match child.role() {
Role::TextRun => {
if let Some(child_text) = child.value() {
text.push_str(child_text);
}
},
_ => {
for node in child.children().rev() {
children.push_front(node.clone());
}
},
}
}
Some(text.trim().to_owned())
}
fn print(&self, print_tree: &mut PrintTree) {
if self.child_nodes.is_empty() {
print_tree.add_item(format!("{self:?}"));
return;
}
print_tree.new_level(format!("{self:?}"));
for child in self.children() {
child.borrow().print(print_tree);
}
print_tree.end_level();
}
fn parent(&self) -> Option<ArcRefCell<AccessibilityNode>> {
self.parent_node.as_ref().and_then(|weak| weak.upgrade())
}
fn children(&self) -> impl DoubleEndedIterator<Item = &ArcRefCell<AccessibilityNode>> {
self.child_nodes.iter()
}
fn ancestors(&self) -> impl Iterator<Item = ArcRefCell<AccessibilityNode>> {
AccessibilityNodeIterator::new(self.parent(), |node| node.parent_node.clone()?.upgrade())
}
fn child_ids(&self) -> &[NodeId] {
self.accesskit_node.children()
}
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.dirty_state |= DirtyState::Updated;
LocalAccessibilityDamage::RoleChanged
}
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.dirty_state |= DirtyState::Updated;
LocalAccessibilityDamage::TextChanged
}
fn clear_label(&mut self) -> LocalAccessibilityDamage {
if self.accesskit_node.label().is_none() {
return LocalAccessibilityDamage::empty();
}
self.accesskit_node.clear_label();
self.dirty_state |= DirtyState::Updated;
LocalAccessibilityDamage::TextChanged
}
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.dirty_state |= DirtyState::Updated;
}
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.dirty_state |= DirtyState::Updated;
LocalAccessibilityDamage::TextChanged
}
fn assert_integrity(&self, expected_parent: Option<WeakRefCell<AccessibilityNode>>) {
debug_assert!(pref!(expensive_accessibility_test_assertions_enabled));
if let Some(actual_parent) = &self.parent_node {
let expected = expected_parent.expect("Actual parent but no expected parent");
let expected = expected.upgrade().expect("Expected parent was dropped");
let actual = actual_parent.upgrade().expect("Actual parent was dropped");
assert!(actual.ptr_eq(&expected));
} else {
assert!(
expected_parent.is_none(),
"Expected parent but no actual parent"
);
}
assert!(
self.dirty_state.is_empty(),
"{self:?} has dirty state {:?}",
self.dirty_state
);
let children_ids: Vec<_> = self.children().map(|child| child.borrow().id).collect();
assert_eq!(
children_ids,
self.child_ids(),
"children() IDs didn't match child_ids() for {self:?}"
);
}
}
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.child_ids().is_empty() {
write!(f, "\nchildren: {:?}", self.child_ids())?;
}
Ok(())
}
}
impl AccessibilityUpdate {
fn new(rooted_nodes: Option<FxHashSet<OpaqueNode>>) -> Self {
Self {
changed_nodes: FxHashSet::default(),
tree_changes: FxHashMap::default(),
counters: UpdateCounters::default(),
rooted_nodes,
}
}
fn add(&mut self, node: &mut AccessibilityNode) {
self.changed_nodes.insert(node.id);
node.dirty_state -= DirtyState::Updated;
}
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 resolved_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, resolved_change);
}
fn is_new(&mut self, node_id: &NodeId) -> bool {
self.tree_changes.get(node_id) == Some(&TreeChange::New)
}
fn finalize(
mut self,
tree: &mut AccessibilityTree,
) -> (Option<accesskit::TreeUpdate>, UpdateCounters) {
let root_node_id = tree
.root_node
.clone()
.expect("AccessibilityUpdate::finalize() called but no root_node set in tree")
.borrow()
.id;
if self.changed_nodes.is_empty() {
assert!(self.tree_changes.is_empty());
return (None, self.counters);
}
let changed_nodes = std::mem::take(&mut self.changed_nodes);
let mut counters = std::mem::take(&mut self.counters);
tree.drop_removed_nodes(self);
let changed_nodes: Vec<_> = changed_nodes
.into_iter()
.filter_map(|id| Some((id, tree.node_for_id(id)?.borrow().accesskit_node.clone())))
.collect();
counters.nodes_in_tree_update = changed_nodes.len().try_into().unwrap_or_default();
let accesskit_tree = accesskit::Tree::new(root_node_id);
let tree_update = accesskit::TreeUpdate {
nodes: changed_nodes,
tree: Some(accesskit_tree),
focus: NodeId(1),
tree_id: tree.tree_id,
};
(Some(tree_update), counters)
}
}
impl DirtyState {
fn updated(&self) -> bool {
self.contains(DirtyState::Updated)
}
fn descendant_has_damage(&self) -> bool {
self.contains(DirtyState::DescendantHasDamage)
}
fn propagate_descendant_has_damage(&mut self, child_dirty_state: DirtyState) {
if child_dirty_state.self_or_descendant_has_damage() {
self.insert(DirtyState::DescendantHasDamage)
}
}
fn self_or_descendant_has_damage(&self) -> bool {
self.intersects(DirtyState::HasDamage | DirtyState::DescendantHasDamage)
}
}
#[cfg(test)]
#[test]
fn test_accessibility_update_add_some_nodes_twice() {
let mut tree = AccessibilityTree::new(accesskit::TreeId::ROOT, Epoch::default());
let mut root_update = AccessibilityUpdate::new(None);
let root_node = tree.get_or_create_node_with_id(NodeId(2), &mut root_update);
tree.root_node = Some(root_node.clone());
let nodes: Vec<_> = [
(3, Role::GenericContainer),
(4, Role::Heading),
(5, Role::Paragraph),
]
.into_iter()
.map(|(id, role)| {
let id = NodeId(id);
let node = tree.get_or_create_node_with_id(id, &mut root_update);
node.borrow_mut().set_role(role);
(id, node)
})
.collect();
{
let (child_node_ids, child_nodes): (Vec<_>, Vec<_>) = nodes.iter().cloned().unzip();
let mut root_node = root_node.borrow_mut();
root_node.accesskit_node.set_children(child_node_ids);
root_node.child_nodes = child_nodes;
}
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 (tree_update, _) = update.finalize(&mut tree);
let mut tree_update = tree_update.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());