use std::cell::RefCell;
use std::collections::VecDeque;
use std::fmt::Debug;
use std::iter::repeat;
use std::sync::atomic::AtomicU64;
use std::sync::{LazyLock, atomic};
use accesskit::{Affine, NodeId, Role};
use app_units::Au;
use bitflags::bitflags;
use euclid::Rect;
use layout_api::{
AccessibilityDamage, BoxAreaType, LayoutElement, LayoutNode, LayoutNodeType,
node_id_from_scroll_id,
};
use log::trace;
use num_traits::ToPrimitive;
use paint_api::display_list::SpatialTreeNodeInfo;
use rustc_hash::{FxHashMap, FxHashSet};
use script::layout_dom::{ServoLayoutElement, ServoLayoutNode};
use servo_base::Epoch;
use servo_base::print_tree::PrintTree;
use servo_config::opts::{self, DiagnosticsLogging, DiagnosticsLoggingOption};
use servo_config::pref;
use style::Atom;
use style::dom::OpaqueNode;
use style_traits::CSSPixel;
use web_atoms::{LocalName, local_name, ns};
use webrender_api::ExternalScrollId;
use webrender_api::units::LayoutVector2D;
use crate::ArcRefCell;
use crate::cell::WeakRefCell;
use crate::display_list::StackingContextTree;
use crate::layout_impl::LayoutThread;
use crate::query::process_box_area_request;
bitflags! {
#[derive(Clone, Copy, Default, Debug, Eq, PartialEq)]
struct LocalAccessibilityDamage: u16 {
const SubtreeChanged = 0b0001;
const RoleChanged = 0b0010;
const TextChanged = 0b0100;
}
}
pub(super) struct AccessibilityContext<'update> {
pub(super) layout_thread: &'update LayoutThread,
pub(super) stacking_context_tree: &'update StackingContextTree,
}
pub(super) type AccessibilityDamageMap<'a> =
FxHashMap<OpaqueNode, (ServoLayoutNode<'a>, AccessibilityDamage)>;
fn au_rect_to_accesskit_rect(rect: Rect<Au, CSSPixel>) -> accesskit::Rect {
accesskit::Rect::new(
rect.min_x().to_f64_px(),
rect.min_y().to_f64_px(),
rect.max_x().to_f64_px(),
rect.max_y().to_f64_px(),
)
}
fn scroll_offset_to_affine(layout_vector: LayoutVector2D) -> Affine {
Affine::translate((
-layout_vector.x.to_f64().unwrap_or(0.),
-layout_vector.y.to_f64().unwrap_or(0.),
))
}
struct AccessibilityUpdate<'update> {
changed_nodes: FxHashSet<NodeId>,
tree_changes: FxHashMap<NodeId, TreeChange>,
counters: UpdateCounters,
damage_map: FxHashMap<NodeId, AccessibilityDamage>,
dom_node_map: RefCell<FxHashMap<NodeId, ServoLayoutNode<'update>>>,
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_updated_bounds: 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>,
scroll_offset: Option<LayoutVector2D>,
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>>,
pending_scroll_updates: FxHashMap<ExternalScrollId, LayoutVector2D>,
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,
pending_scroll_updates: FxHashMap::default(),
embedder_epoch,
debug: opts::get().debug.clone(),
}
}
pub(super) fn update_tree<'update>(
&mut self,
root_dom_node: &ServoLayoutNode<'update>,
damage_from_dom: AccessibilityDamageMap<'update>,
context: AccessibilityContext<'update>,
rooted_nodes: Option<FxHashSet<OpaqueNode>>,
) -> (Option<accesskit::TreeUpdate>, UpdateCounters) {
let mut update = AccessibilityUpdate::new(damage_from_dom, rooted_nodes, self);
self.ensure_root_node(root_dom_node, &context, &mut update);
self.apply_changes_from_dom_tree(&context, &mut update);
self.handle_pending_scroll_updates(&mut update);
update.finalize(self)
}
pub(super) fn add_pending_scroll_updates(
&mut self,
scroll_states: FxHashMap<ExternalScrollId, LayoutVector2D>,
) {
self.pending_scroll_updates.extend(scroll_states);
}
pub(super) fn add_pending_scroll_update(
&mut self,
external_scroll_id: ExternalScrollId,
offset: LayoutVector2D,
) {
self.pending_scroll_updates
.insert(external_scroll_id, offset);
}
fn ensure_root_node<'update>(
&mut self,
root_dom_node: &ServoLayoutNode<'update>,
context: &AccessibilityContext<'update>,
update: &mut AccessibilityUpdate<'update>,
) {
let (root_id, root_node) = self.get_or_create_node(root_dom_node, update);
if update.is_new(&root_id) {
update.clear_damage();
update.insert_damage(root_id, AccessibilityDamage::Rebuild);
update.insert_dom_node(root_id, *root_dom_node);
self.populate_pending_scroll_updates_from_scroll_tree(context);
} else {
update.insert_damage(root_id, AccessibilityDamage::Subtree);
update.insert_dom_node(root_id, *root_dom_node);
}
self.root_node = Some(root_node);
}
fn apply_changes_from_dom_tree(
&mut self,
context: &AccessibilityContext,
update: &mut AccessibilityUpdate,
) {
let Some(damage_root_id) = self.mark_nodes_and_ancestors_dirty(update) else {
return;
};
let damage_root = self.assert_node_for_id(&damage_root_id);
let local_damage =
damage_root
.borrow_mut()
.update_subtree(damage_root.clone(), context, self, update);
damage_root.borrow().update_ancestors(local_damage, update);
}
fn populate_pending_scroll_updates_from_scroll_tree(&mut self, context: &AccessibilityContext) {
let scroll_tree = &context.stacking_context_tree.paint_info.scroll_tree;
let scroll_updates = scroll_tree
.nodes
.iter()
.filter_map(|node| match node.info {
SpatialTreeNodeInfo::Scroll(ref info) => {
let offset = info.offset;
Some((info.external_id, offset))
},
_ => None,
})
.collect();
self.pending_scroll_updates = scroll_updates;
}
fn handle_pending_scroll_updates(&mut self, update: &mut AccessibilityUpdate) {
let pending_scroll_updates = std::mem::take(&mut self.pending_scroll_updates);
for (opaque, offset) in
pending_scroll_updates
.into_iter()
.filter_map(|(scroll_id, translate)| {
if scroll_id.is_root() {
let root_node_opaque =
self.root_node.as_ref()?.clone().borrow().opaque_node?;
return Some((root_node_opaque, translate));
}
let node_id = node_id_from_scroll_id(scroll_id.0 as usize);
let opaque = OpaqueNode(node_id);
Some((opaque, translate))
})
{
let Some(node) = self.node_for_opaque(opaque) else {
continue;
};
node.borrow_mut().set_scroll_offset(offset, update);
}
}
fn mark_nodes_and_ancestors_dirty(
&mut self,
update: &mut AccessibilityUpdate,
) -> Option<NodeId> {
let mut dirty_node_ids = update.damage_map.keys();
let mut common_ancestors: Vec<NodeId> = Vec::new();
{
let node_id = dirty_node_ids.next()?;
update.collect_dom_node_ancestors(node_id, self);
let first_node = self.assert_node_for_id(node_id);
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;
}
}
common_ancestors.pop()
}
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);
}
update.insert_damage(id, AccessibilityDamage::Rebuild);
node.dirty_state |= DirtyState::HasDamage;
}
(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 node_for_opaque(&self, opaque: OpaqueNode) -> Option<ArcRefCell<AccessibilityNode>> {
self.nodes
.get(&self.existing_id_for_opaque(opaque)?)
.cloned()
}
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_role_attribute(dom_element: &ServoLayoutElement<'_>) -> Option<Role> {
let role_attribute = dom_element.attribute(&ns!(), &local_name!("role"))?;
role_attribute
.as_tokens()
.iter()
.filter_map(|role_name_in_attribute| SUPPORTED_ARIA_ROLES.get(role_name_in_attribute))
.next()
.cloned()
}
fn role_from_dom_node(dom_node: &ServoLayoutNode<'_>) -> Role {
if let Some(dom_element) = dom_node.as_element() {
role_from_role_attribute(&dom_element).unwrap_or_else(|| {
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,
scroll_offset: None,
dirty_state: DirtyState::empty(),
}
}
fn update_subtree<'update>(
&mut self,
ref_self: ArcRefCell<Self>,
context: &AccessibilityContext,
tree: &mut AccessibilityTree,
update: &mut AccessibilityUpdate<'update>,
) -> LocalAccessibilityDamage {
let mut local_damage = LocalAccessibilityDamage::empty();
let damage = self.compute_damage(update);
if let Some(dom_node) = update.take_dom_node(&self.id) {
local_damage.insert(self.update_properties_and_children_from_dom_node(
ref_self, &dom_node, damage, tree, update,
));
self.update_bounds_from_dom_node(&dom_node, context, update);
if local_damage.contains(LocalAccessibilityDamage::SubtreeChanged) &&
let Some(scroll_offset) = self.scroll_offset
{
self.set_scroll_offset(scroll_offset, update);
}
self.dirty_state -= DirtyState::HasDamage;
}
for child_node in self.children() {
let child_node_ref = child_node.clone();
let mut child_node = child_node.borrow_mut();
let child_local_damage =
child_node.update_subtree(child_node_ref, context, tree, update);
if !child_local_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_properties_and_children_from_dom_node<'update>(
&mut self,
ref_self: ArcRefCell<Self>,
dom_node: &ServoLayoutNode<'update>,
dom_damage: AccessibilityDamage,
tree: &mut AccessibilityTree,
update: &mut AccessibilityUpdate<'update>,
) -> LocalAccessibilityDamage {
let mut local_damage = LocalAccessibilityDamage::empty();
update.counters.nodes_updated_from_dom += 1;
local_damage.insert(self.update_properties_from_dom_node(dom_node, dom_damage));
local_damage.insert(
self.update_children_from_dom_node(ref_self, dom_node, dom_damage, tree, update),
);
local_damage
}
fn update_children_from_dom_node<'update>(
&mut self,
ref_self: ArcRefCell<AccessibilityNode>,
dom_node: &ServoLayoutNode<'update>,
_dom_damage: AccessibilityDamage,
tree: &mut AccessibilityTree,
update: &mut AccessibilityUpdate<'update>,
) -> LocalAccessibilityDamage {
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) {
update.insert_dom_node(*old_id, *dom_child);
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);
update.insert_dom_node(child_id, dom_child);
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) {
self.dirty_state |= DirtyState::DescendantHasDamage;
} 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::Node) {
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_bounds_from_dom_node(
&mut self,
dom_node: &ServoLayoutNode,
context: &AccessibilityContext,
update: &mut AccessibilityUpdate,
) {
update.counters.nodes_updated_bounds += 1;
let bounds = process_box_area_request(
context.layout_thread,
context.stacking_context_tree,
*dom_node,
BoxAreaType::Border,
true,
)
.map(au_rect_to_accesskit_rect);
match bounds {
Some(bounds) => self.set_bounds(bounds),
None => self.clear_bounds(),
}
}
fn update_node_local(
&mut self,
local_damage: LocalAccessibilityDamage,
update: &mut AccessibilityUpdate,
) -> LocalAccessibilityDamage {
let mut new_damage = LocalAccessibilityDamage::empty();
if local_damage.is_empty() {
return new_damage;
}
update.counters.nodes_updated_from_tree += 1;
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 set_scroll_offset(&mut self, offset: LayoutVector2D, update: &mut AccessibilityUpdate) {
self.scroll_offset = Some(offset);
let transform = scroll_offset_to_affine(offset);
for child in self.children() {
let mut child = child.borrow_mut();
child.set_transform(transform);
if child.dirty_state.updated() {
update.add(&mut child);
}
}
}
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 bounds(&self) -> Option<accesskit::Rect> {
self.accesskit_node.bounds()
}
fn set_bounds(&mut self, bounds: accesskit::Rect) {
if Some(bounds) == self.accesskit_node.bounds() {
return;
}
self.accesskit_node.set_bounds(bounds);
self.dirty_state |= DirtyState::Updated;
}
fn clear_bounds(&mut self) {
if self.accesskit_node.bounds().is_none() {
return;
}
self.accesskit_node.clear_bounds();
self.dirty_state |= DirtyState::Updated;
}
fn set_transform(&mut self, transform: Affine) {
if self.accesskit_node.transform() == Some(&transform) {
return;
}
if transform == Affine::IDENTITY {
self.clear_transform();
return;
}
self.accesskit_node.set_transform(transform);
self.dirty_state |= DirtyState::Updated;
}
fn clear_transform(&mut self) {
if self.accesskit_node.transform().is_none() {
return;
}
self.accesskit_node.clear_transform();
self.dirty_state |= DirtyState::Updated;
}
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:?}"
);
}
fn compute_damage(&self, update: &mut AccessibilityUpdate) -> AccessibilityDamage {
let mut damage = AccessibilityDamage::empty();
if self.dirty_state.has_damage() {
damage |= update.take_damage(&self.id);
}
damage
}
}
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 let Some(bounds) = self.bounds() {
write!(f, "\nbounds: {bounds:?}")?;
}
if !self.child_ids().is_empty() {
write!(f, "\nchildren: {:?}", self.child_ids())?;
}
Ok(())
}
}
impl<'update> AccessibilityUpdate<'update> {
fn new(
dom_damage: AccessibilityDamageMap<'update>,
rooted_nodes: Option<FxHashSet<OpaqueNode>>,
tree: &AccessibilityTree,
) -> Self {
let damage_map = dom_damage
.iter()
.filter_map(|(&opaque, &(_dom_node, damage))| {
let id = tree.existing_id_for_opaque(opaque)?;
Some((id, damage))
})
.collect();
let dom_node_map = dom_damage
.into_iter()
.filter_map(|(opaque, (dom_node, _damage))| {
let id = tree.existing_id_for_opaque(opaque)?;
Some((id, dom_node))
})
.collect();
Self {
changed_nodes: FxHashSet::default(),
tree_changes: FxHashMap::default(),
counters: UpdateCounters::default(),
damage_map,
dom_node_map: RefCell::new(dom_node_map),
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)
}
fn clear_damage(&mut self) {
self.damage_map.clear();
}
fn insert_damage(&mut self, node_id: NodeId, damage: AccessibilityDamage) {
self.damage_map.insert(node_id, damage);
}
fn insert_dom_node(&self, node_id: NodeId, dom_node: ServoLayoutNode<'update>) {
self.dom_node_map.borrow_mut().insert(node_id, dom_node);
}
fn take_damage(&mut self, node_id: &NodeId) -> AccessibilityDamage {
self.damage_map
.remove(node_id)
.unwrap_or(AccessibilityDamage::empty())
}
fn take_dom_node(&mut self, node_id: &NodeId) -> Option<ServoLayoutNode<'update>> {
self.dom_node_map.borrow_mut().remove(node_id)
}
#[expect(unsafe_code)]
fn collect_dom_node_ancestors(&self, node_id: &NodeId, tree: &AccessibilityTree) {
let mut dom_node_map = self.dom_node_map.borrow_mut();
let dom_node = dom_node_map
.get(node_id)
.expect("collect_dom_node_ancestors should be called for a known DOM node");
let mut parent = unsafe { dom_node.dangerous_flat_tree_parent() };
while let Some(node) = parent {
if let Some(node_id) = tree.existing_id_for_opaque(node.opaque()) {
dom_node_map.insert(node_id, node);
}
parent = unsafe { node.dangerous_flat_tree_parent() };
}
}
}
impl DirtyState {
fn updated(&self) -> bool {
self.contains(DirtyState::Updated)
}
fn has_damage(&self) -> bool {
self.contains(DirtyState::HasDamage)
}
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(AccessibilityDamageMap::default(), None, &tree);
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(AccessibilityDamageMap::default(), None, &tree);
{
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 SUPPORTED_ARIA_ROLES: LazyLock<FxHashMap<Atom, Role>> = LazyLock::new(|| {
[
(Atom::from("alert"), Role::Alert),
(Atom::from("banner"), Role::Banner),
(Atom::from("blockquote"), Role::Blockquote),
(Atom::from("caption"), Role::Caption),
(Atom::from("code"), Role::Code),
(Atom::from("complementary"), Role::Complementary),
(Atom::from("contentinfo"), Role::ContentInfo),
(Atom::from("definition"), Role::Definition),
(Atom::from("deletion"), Role::ContentDeletion),
(Atom::from("directory"), Role::Unknown),
(Atom::from("document"), Role::Document),
(Atom::from("emphasis"), Role::Emphasis),
(Atom::from("feed"), Role::Feed),
(Atom::from("figure"), Role::Figure),
(Atom::from("generic"), Role::GenericContainer),
(Atom::from("insertion"), Role::ContentInsertion),
(Atom::from("list"), Role::List),
(Atom::from("log"), Role::Log),
(Atom::from("main"), Role::Main),
(Atom::from("math"), Role::Math),
(Atom::from("navigation"), Role::Navigation),
(Atom::from("none"), Role::GenericContainer),
(Atom::from("note"), Role::Note),
(Atom::from("paragraph"), Role::Paragraph),
(Atom::from("presentation"), Role::GenericContainer),
(Atom::from("rowgroup"), Role::RowGroup),
(Atom::from("search"), Role::Search),
(Atom::from("status"), Role::Status),
(Atom::from("strong"), Role::Strong),
(Atom::from("term"), Role::Term),
(Atom::from("time"), Role::Time),
(Atom::from("timer"), Role::Timer),
]
.into_iter()
.collect()
});
static NAME_FROM_CONTENTS_ROLES: LazyLock<FxHashSet<Role>> =
LazyLock::new(|| [(Role::Heading)].into_iter().collect());