use accesskit::{NodeId, Role, TreeUpdate};
use accesskit_consumer::{NodeRef, Tree};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct LabelLeak {
pub owner: NodeId,
pub owner_role: Role,
pub name: String,
pub label: NodeId,
}
#[derive(Debug, Clone, PartialEq)]
pub enum DivergenceKind {
TextDiffersFromValue { announced: String, reviewed: String },
NoGeometry,
GeometryOutsideNode,
FocusOnSyntheticNode,
}
#[derive(Debug, Clone, PartialEq)]
pub struct TextRangeDivergence {
pub node: NodeId,
pub kind: DivergenceKind,
}
fn locate(node: &NodeRef<'_>) -> NodeId {
node.locate().0
}
fn announced_name(node: &NodeRef<'_>) -> Option<String> {
let name = node
.value()
.filter(|_| node.role() == Role::Label)
.or_else(|| node.label())
.or_else(|| node.value())?;
let trimmed = name.trim();
(!trimmed.is_empty()).then(|| trimmed.to_string())
}
pub fn duplicate_label_leaks(update: &TreeUpdate) -> Vec<LabelLeak> {
let tree = Tree::new(update.clone(), false);
let state = tree.state();
let mut leaks = Vec::new();
let mut stack: Vec<(NodeRef<'_>, Vec<(NodeId, Role, String)>)> =
vec![(state.root(), Vec::new())];
while let Some((node, names)) = stack.pop() {
if node.role() == Role::Label
&& let Some(text) = announced_name(&node)
{
let id = locate(&node);
if let Some((owner, owner_role, name)) = names.iter().rev().find(|(_, _, n)| *n == text)
&& !is_labelled_by_target(&state.root(), *owner, id)
{
leaks.push(LabelLeak {
owner: *owner,
owner_role: *owner_role,
name: name.clone(),
label: id,
});
}
}
let mut inherited = names;
if node.role() != Role::Label
&& let Some(name) = announced_name(&node)
{
inherited.push((locate(&node), node.role(), name));
}
for child in node.children() {
stack.push((child, inherited.clone()));
}
}
leaks
}
fn is_labelled_by_target(root: &NodeRef<'_>, owner: NodeId, label: NodeId) -> bool {
let mut stack = vec![*root];
while let Some(node) = stack.pop() {
if locate(&node) == owner {
return node.data().labelled_by().contains(&label);
}
for child in node.children() {
stack.push(child);
}
}
false
}
pub fn labels_without_text_ranges(update: &TreeUpdate) -> Vec<NodeId> {
let tree = Tree::new(update.clone(), false);
let state = tree.state();
let mut out = Vec::new();
let mut stack = vec![state.root()];
while let Some(node) = stack.pop() {
if node.role() == Role::Label
&& announced_name(&node).is_some()
&& !node.is_hidden()
&& !node.supports_text_ranges()
{
out.push(locate(&node));
}
for child in node.children() {
stack.push(child);
}
}
out
}
pub fn text_range_divergences(update: &TreeUpdate) -> Vec<TextRangeDivergence> {
let focus = update.focus;
let tree = Tree::new(update.clone(), false);
let state = tree.state();
let mut out = Vec::new();
if super::is_synthetic(focus) {
out.push(TextRangeDivergence {
node: focus,
kind: DivergenceKind::FocusOnSyntheticNode,
});
}
let mut stack = vec![state.root()];
while let Some(node) = stack.pop() {
for child in node.children() {
stack.push(child);
}
if !node.supports_text_ranges() {
continue;
}
let owns_its_text = node
.data()
.children()
.iter()
.any(|child| super::is_synthetic(*child));
if !owns_its_text {
continue;
}
let id = locate(&node);
let range = node.document_range();
let reviewed = range.text();
if let Some(announced) = node.value().map(|v| v.to_string())
&& reviewed != announced
{
out.push(TextRangeDivergence {
node: id,
kind: DivergenceKind::TextDiffersFromValue {
announced,
reviewed,
},
});
}
let boxes = range.bounding_boxes();
if boxes.is_empty() {
out.push(TextRangeDivergence {
node: id,
kind: DivergenceKind::NoGeometry,
});
continue;
}
if let Some(own) = node.bounding_box()
&& !boxes.iter().any(|b| intersects(*b, own))
{
out.push(TextRangeDivergence {
node: id,
kind: DivergenceKind::GeometryOutsideNode,
});
}
}
out
}
fn intersects(a: accesskit::Rect, b: accesskit::Rect) -> bool {
a.x0 <= b.x1 && b.x0 <= a.x1 && a.y0 <= b.y1 && b.y0 <= a.y1
}
#[derive(Debug, Clone, PartialEq)]
pub struct NodeTextInfo {
pub run_count: usize,
pub document_text: String,
pub has_geometry: bool,
pub direction: Option<accesskit::TextDirection>,
}
pub fn text_infos(update: &TreeUpdate) -> std::collections::HashMap<NodeId, NodeTextInfo> {
let tree = Tree::new(update.clone(), false);
let state = tree.state();
let mut out = std::collections::HashMap::new();
let mut stack = vec![state.root()];
while let Some(node) = stack.pop() {
for child in node.children() {
stack.push(child);
}
if !node.supports_text_ranges() {
continue;
}
let range = node.document_range();
out.insert(
locate(&node),
NodeTextInfo {
run_count: node
.data()
.children()
.iter()
.filter(|id| {
update
.nodes
.iter()
.any(|(n, data)| n == *id && data.role() == Role::TextRun)
})
.count(),
document_text: range.text(),
has_geometry: !range.bounding_boxes().is_empty(),
direction: node.data().text_direction(),
},
);
}
out
}
pub fn resolved_names(update: &TreeUpdate) -> std::collections::HashMap<NodeId, String> {
let tree = Tree::new(update.clone(), false);
let state = tree.state();
let mut out = std::collections::HashMap::new();
let mut stack = vec![state.root()];
while let Some(node) = stack.pop() {
for child in node.children() {
stack.push(child);
}
if let Some(label) = node.label() {
out.insert(locate(&node), label);
}
}
out
}