use ahash::AHashMap;
use crate::node::{A11yNodeInfo, A11yRole, LiveRegion};
pub struct A11yTreeBuilder {
nodes: AHashMap<u64, A11yNodeInfo>,
root: Option<u64>,
focused: Option<u64>,
}
impl Default for A11yTreeBuilder {
fn default() -> Self {
Self::new()
}
}
impl A11yTreeBuilder {
#[inline]
pub fn new() -> Self {
Self {
nodes: AHashMap::new(),
root: None,
focused: None,
}
}
#[inline]
pub fn with_capacity(capacity: usize) -> Self {
Self {
nodes: AHashMap::with_capacity(capacity),
root: None,
focused: None,
}
}
#[inline]
pub fn add_node(&mut self, node: A11yNodeInfo) {
self.nodes.insert(node.id, node);
}
#[inline]
pub fn set_root(&mut self, id: u64) {
self.root = Some(id);
}
#[inline]
pub fn set_focused(&mut self, id: Option<u64>) {
self.focused = id;
}
#[inline]
pub fn build(self) -> A11yTree {
A11yTree {
nodes: self.nodes,
root: self.root,
focused: self.focused,
}
}
}
pub struct A11yTree {
nodes: AHashMap<u64, A11yNodeInfo>,
root: Option<u64>,
focused: Option<u64>,
}
impl Default for A11yTree {
fn default() -> Self {
Self::empty()
}
}
impl A11yTree {
#[inline]
pub fn empty() -> Self {
Self {
nodes: AHashMap::new(),
root: None,
focused: None,
}
}
#[inline]
pub fn node(&self, id: u64) -> Option<&A11yNodeInfo> {
self.nodes.get(&id)
}
#[inline]
pub fn root(&self) -> Option<&A11yNodeInfo> {
self.root.and_then(|id| self.nodes.get(&id))
}
#[inline]
pub fn root_id(&self) -> Option<u64> {
self.root
}
#[inline]
pub fn focused(&self) -> Option<&A11yNodeInfo> {
self.focused.and_then(|id| self.nodes.get(&id))
}
#[inline]
pub fn focused_id(&self) -> Option<u64> {
self.focused
}
#[inline]
pub fn nodes(&self) -> impl Iterator<Item = &A11yNodeInfo> {
self.nodes.values()
}
#[inline]
pub fn node_count(&self) -> usize {
self.nodes.len()
}
#[inline]
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
pub fn children_of(&self, id: u64) -> Vec<&A11yNodeInfo> {
self.nodes
.get(&id)
.map(|n| {
n.children
.iter()
.filter_map(|cid| self.nodes.get(cid))
.collect()
})
.unwrap_or_default()
}
pub fn ancestors(&self, id: u64) -> Vec<u64> {
const MAX_DEPTH: usize = 1000;
let mut path = Vec::new();
let mut visited = ahash::AHashSet::new();
let mut current = Some(id);
while let Some(cid) = current {
if path.len() >= MAX_DEPTH || !visited.insert(cid) {
break;
}
if let Some(node) = self.nodes.get(&cid) {
path.push(cid);
current = node.parent;
} else {
break;
}
}
path
}
pub fn diff(&self, previous: &A11yTree) -> A11yTreeDiff {
let mut added = Vec::new();
let mut removed = Vec::new();
let mut changed = Vec::new();
for (&id, node) in &self.nodes {
match previous.nodes.get(&id) {
None => added.push(id),
Some(old) => {
let changes = diff_node(old, node);
if !changes.is_empty() {
changed.push((id, changes));
}
}
}
}
for &id in previous.nodes.keys() {
if !self.nodes.contains_key(&id) {
removed.push(id);
}
}
let focus_changed = if self.focused != previous.focused {
Some((previous.focused, self.focused))
} else {
None
};
A11yTreeDiff {
added,
removed,
changed,
focus_changed,
}
}
}
#[derive(Debug, Clone)]
pub struct A11yTreeDiff {
pub added: Vec<u64>,
pub removed: Vec<u64>,
pub changed: Vec<(u64, Vec<A11yChange>)>,
pub focus_changed: Option<(Option<u64>, Option<u64>)>,
}
impl A11yTreeDiff {
#[inline]
pub fn is_empty(&self) -> bool {
self.added.is_empty()
&& self.removed.is_empty()
&& self.changed.is_empty()
&& self.focus_changed.is_none()
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum A11yChange {
NameChanged {
old: Option<String>,
new: Option<String>,
},
RoleChanged { old: A11yRole, new: A11yRole },
StateChanged { field: String, description: String },
BoundsChanged,
ChildrenChanged,
LiveRegionChanged {
old: Option<LiveRegion>,
new: Option<LiveRegion>,
},
DescriptionChanged {
old: Option<String>,
new: Option<String>,
},
ShortcutChanged {
old: Option<String>,
new: Option<String>,
},
ParentChanged { old: Option<u64>, new: Option<u64> },
}
fn diff_node(old: &A11yNodeInfo, new: &A11yNodeInfo) -> Vec<A11yChange> {
let mut changes = Vec::new();
if old.name != new.name {
changes.push(A11yChange::NameChanged {
old: old.name.clone(),
new: new.name.clone(),
});
}
if old.role != new.role {
changes.push(A11yChange::RoleChanged {
old: old.role,
new: new.role,
});
}
if old.bounds != new.bounds {
changes.push(A11yChange::BoundsChanged);
}
if old.children != new.children {
changes.push(A11yChange::ChildrenChanged);
}
if old.live_region != new.live_region {
changes.push(A11yChange::LiveRegionChanged {
old: old.live_region,
new: new.live_region,
});
}
if old.description != new.description {
changes.push(A11yChange::DescriptionChanged {
old: old.description.clone(),
new: new.description.clone(),
});
}
if old.shortcut != new.shortcut {
changes.push(A11yChange::ShortcutChanged {
old: old.shortcut.clone(),
new: new.shortcut.clone(),
});
}
if old.parent != new.parent {
changes.push(A11yChange::ParentChanged {
old: old.parent,
new: new.parent,
});
}
diff_state(&old.state, &new.state, &mut changes);
changes
}
fn diff_state(
old: &crate::node::A11yState,
new: &crate::node::A11yState,
changes: &mut Vec<A11yChange>,
) {
macro_rules! check_bool {
($field:ident) => {
if old.$field != new.$field {
changes.push(A11yChange::StateChanged {
field: stringify!($field).to_owned(),
description: new.$field.to_string(),
});
}
};
}
macro_rules! check_option {
($field:ident) => {
if old.$field != new.$field {
changes.push(A11yChange::StateChanged {
field: stringify!($field).to_owned(),
description: format!("{:?}", new.$field),
});
}
};
}
check_bool!(focused);
check_bool!(disabled);
check_option!(checked);
check_option!(expanded);
check_bool!(selected);
check_bool!(readonly);
check_bool!(required);
check_bool!(busy);
check_option!(value_now);
check_option!(value_min);
check_option!(value_max);
if old.value_text != new.value_text {
changes.push(A11yChange::StateChanged {
field: "value_text".to_owned(),
description: new.value_text.as_deref().unwrap_or("<none>").to_owned(),
});
}
}