use accesskit_consumer::{Node, Tree};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, JsonSchema)]
pub struct NodeView {
pub id: String,
pub role: String,
pub label: Option<String>,
pub value: Option<String>,
pub bounds: Option<RectF>,
pub focused: bool,
pub disabled: bool,
pub hidden: bool,
pub parent_id: Option<String>,
}
#[derive(Debug, Clone, Copy, Serialize, JsonSchema)]
pub struct RectF {
pub x: f64,
pub y: f64,
pub w: f64,
pub h: f64,
}
impl RectF {
fn from_physical(r: accesskit::Rect, pixels_per_point: f32) -> Self {
let s = 1.0 / f64::from(pixels_per_point);
Self {
x: r.x0 * s,
y: r.y0 * s,
w: (r.x1 - r.x0) * s,
h: (r.y1 - r.y0) * s,
}
}
pub fn center(&self) -> (f64, f64) {
(self.x + self.w / 2.0, self.y + self.h / 2.0)
}
}
#[derive(Debug, Clone, Default, Deserialize, JsonSchema)]
pub struct Query {
pub content_contains: Option<String>,
pub role: Option<String>,
pub label_contains: Option<String>,
pub value_contains: Option<String>,
}
impl Query {
pub fn is_empty(&self) -> bool {
self.content_contains.is_none()
&& self.role.is_none()
&& self.label_contains.is_none()
&& self.value_contains.is_none()
}
fn matches(&self, node: &Node<'_>) -> bool {
if let Some(needle) = &self.content_contains
&& !contains_ci(&node.label().unwrap_or_default(), needle)
&& !contains_ci(&node.value().unwrap_or_default(), needle)
{
return false;
}
if let Some(role) = &self.role
&& !role.eq_ignore_ascii_case(&format!("{:?}", node.role()))
{
return false;
}
if let Some(needle) = &self.label_contains
&& !contains_ci(&node.label().unwrap_or_default(), needle)
{
return false;
}
if let Some(needle) = &self.value_contains
&& !contains_ci(&node.value().unwrap_or_default(), needle)
{
return false;
}
true
}
fn describe(&self) -> String {
let mut clauses = Vec::new();
if let Some(c) = &self.content_contains {
clauses.push(format!("label or value containing `{c}`"));
}
if let Some(r) = &self.role {
clauses.push(format!("role `{r}`"));
}
if let Some(l) = &self.label_contains {
clauses.push(format!("label containing `{l}`"));
}
if let Some(v) = &self.value_contains {
clauses.push(format!("value containing `{v}`"));
}
if clauses.is_empty() {
"the locator".to_owned()
} else {
clauses.join(" and ")
}
}
}
#[derive(Debug, Clone, Deserialize, JsonSchema)]
pub struct QueryFilter {
#[serde(flatten)]
pub query: Query,
#[serde(default = "default_true")]
pub visible_only: bool,
#[serde(default = "default_limit")]
pub limit: usize,
}
fn default_true() -> bool {
true
}
fn default_limit() -> usize {
200
}
impl Default for QueryFilter {
fn default() -> Self {
Self {
query: Query::default(),
visible_only: true,
limit: default_limit(),
}
}
}
pub fn query(tree: &Tree, filter: &QueryFilter, pixels_per_point: f32) -> Vec<NodeView> {
let root = tree.state().root();
let mut out = Vec::new();
walk(&root, filter, pixels_per_point, &mut out);
if out.len() > filter.limit {
out.truncate(filter.limit);
}
out
}
fn walk(node: &Node<'_>, filter: &QueryFilter, pixels_per_point: f32, out: &mut Vec<NodeView>) {
if matches(node, filter) {
out.push(node_view(node, pixels_per_point));
}
for child in node.children() {
walk(&child, filter, pixels_per_point, out);
}
}
pub fn validate_role(role: &str, tree: Option<&Tree>) -> Result<(), String> {
let valid = (0u8..=u8::MAX)
.map_while(accesskit::Role::n)
.any(|r| role.eq_ignore_ascii_case(&format!("{r:?}")));
if valid {
return Ok(());
}
let present = tree.map(roles_in_tree).unwrap_or_default();
let hint = if present.is_empty() {
"(no nodes in the current tree)".to_owned()
} else {
present.join(", ")
};
Err(format!(
"unknown role `{role}` — roles present in the current tree: {hint}"
))
}
fn roles_in_tree(tree: &Tree) -> Vec<String> {
let mut roles = std::collections::BTreeSet::new();
collect_roles(&tree.state().root(), &mut roles);
roles.into_iter().collect()
}
fn collect_roles(node: &Node<'_>, out: &mut std::collections::BTreeSet<String>) {
out.insert(format!("{:?}", node.role()));
for child in node.children() {
collect_roles(&child, out);
}
}
fn matches(node: &Node<'_>, filter: &QueryFilter) -> bool {
if filter.visible_only && node.is_hidden() {
return false;
}
filter.query.matches(node)
}
fn contains_ci(hay: &str, needle: &str) -> bool {
hay.to_ascii_lowercase()
.contains(&needle.to_ascii_lowercase())
}
pub fn node_view(node: &Node<'_>, pixels_per_point: f32) -> NodeView {
NodeView {
id: accesskit_id(node).to_string(),
role: format!("{:?}", node.role()),
label: node.label(),
value: node.value(),
bounds: node
.bounding_box()
.map(|r| RectF::from_physical(r, pixels_per_point)),
focused: node.is_focused_in_tree(),
disabled: node.is_disabled(),
hidden: node.is_hidden(),
parent_id: node.parent().map(|p| accesskit_id(&p).to_string()),
}
}
pub fn accesskit_id(node: &Node<'_>) -> u64 {
let (local, _tree) = node.locate();
local.0
}
#[derive(Debug, Clone)]
pub enum Locator {
Id { id: u64 },
Match { query: Query },
}
impl Locator {
pub fn from_fields(id: Option<&str>, query: Query) -> Option<Self> {
if let Some(id) = id.and_then(|s| s.trim().parse::<u64>().ok()) {
return Some(Self::Id { id });
}
if !query.is_empty() {
return Some(Self::Match { query });
}
None
}
}
pub fn resolve_unique<'a>(
tree: &'a Tree,
locator: &Locator,
pixels_per_point: f32,
) -> Result<Node<'a>, String> {
let root = tree.state().root();
match locator {
Locator::Id { id } => {
let mut found = Vec::new();
find_all(&root, &|n| accesskit_id(n) == *id, &mut found);
one(found, pixels_per_point, &format!("id `{id}`"))
}
Locator::Match { query } => {
let filter = QueryFilter {
query: query.clone(),
visible_only: true,
limit: usize::MAX,
};
let mut found = Vec::new();
find_all(&root, &|n| matches(n, &filter), &mut found);
one(found, pixels_per_point, &query.describe())
}
}
}
fn one<'a>(
mut found: Vec<Node<'a>>,
pixels_per_point: f32,
what: &str,
) -> Result<Node<'a>, String> {
match found.len() {
0 => Err(format!("no node found matching {what}")),
1 => Ok(found.remove(0)),
n => {
let views: Vec<NodeView> = found
.iter()
.map(|node| node_view(node, pixels_per_point))
.collect();
let list =
serde_json::to_string_pretty(&views).unwrap_or_else(|_| format!("{n} nodes"));
Err(format!(
"{what} matched {n} nodes — narrow the locator (`content_contains`/`role`/`label_contains`/`value_contains`), or target a specific `id`. Matches:\n{list}"
))
}
}
}
fn find_all<'a>(node: &Node<'a>, pred: &impl Fn(&Node<'_>) -> bool, out: &mut Vec<Node<'a>>) {
if pred(node) {
out.push(*node);
}
for child in node.children() {
find_all(&child, pred, out);
}
}