use crate::element::Semantics;
use crate::frame::{AccessNode, Frame};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TextMatch {
Exact(String),
Contains(String),
}
impl TextMatch {
fn matches(&self, hay: Option<&str>) -> bool {
match (self, hay) {
(Self::Exact(needle), Some(hay)) => hay == needle,
(Self::Contains(needle), Some(hay)) => hay.contains(needle),
(_, None) => false,
}
}
}
impl std::fmt::Display for TextMatch {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Exact(s) => write!(f, "{s:?}"),
Self::Contains(s) => write!(f, "*{s:?}*"),
}
}
}
#[derive(Debug, Clone)]
pub struct Query {
role: Option<Semantics>,
label: Option<TextMatch>,
value: Option<TextMatch>,
key: Option<String>,
}
pub mod by {
use super::{Query, TextMatch};
use crate::element::Semantics;
pub fn role(role: Semantics) -> Query {
Query {
role: Some(role),
label: None,
value: None,
key: None,
}
}
pub fn label(label: impl Into<String>) -> Query {
Query {
role: None,
label: Some(TextMatch::Exact(label.into())),
value: None,
key: None,
}
}
pub fn label_contains(label: impl Into<String>) -> Query {
Query {
role: None,
label: Some(TextMatch::Contains(label.into())),
value: None,
key: None,
}
}
pub fn value(value: impl Into<String>) -> Query {
Query {
role: None,
label: None,
value: Some(TextMatch::Exact(value.into())),
key: None,
}
}
pub fn value_contains(value: impl Into<String>) -> Query {
Query {
role: None,
label: None,
value: Some(TextMatch::Contains(value.into())),
key: None,
}
}
pub fn id(key: impl Into<String>) -> Query {
Query {
role: None,
label: None,
value: None,
key: Some(key.into()),
}
}
}
impl Query {
pub fn name(mut self, label: impl Into<String>) -> Self {
self.label = Some(TextMatch::Exact(label.into()));
self
}
pub fn name_contains(mut self, label: impl Into<String>) -> Self {
self.label = Some(TextMatch::Contains(label.into()));
self
}
fn matches(&self, node: &AccessNode) -> bool {
if let Some(role) = &self.role {
let Some(semantics) = &node.semantics else {
return false;
};
if std::mem::discriminant(role) != std::mem::discriminant(semantics) {
return false;
}
}
if let Some(label) = &self.label
&& !label.matches(node.label.as_deref())
{
return false;
}
if let Some(value) = &self.value
&& !value.matches(node.value.as_deref())
{
return false;
}
if let Some(key) = &self.key
&& node.key.as_deref() != Some(key.as_str())
{
return false;
}
true
}
}
impl std::fmt::Display for Query {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut parts = Vec::new();
if let Some(role) = &self.role {
parts.push(format!("role={}", role_name(role)));
}
if let Some(label) = &self.label {
parts.push(format!("name={label}"));
}
if let Some(value) = &self.value {
parts.push(format!("value={value}"));
}
if let Some(key) = &self.key {
parts.push(format!("id={key:?}"));
}
write!(f, "{}", parts.join(" "))
}
}
pub(crate) fn role_name(semantics: &Semantics) -> &'static str {
match semantics {
Semantics::Button => "button",
Semantics::Checkbox { .. } => "checkbox",
Semantics::Switch { .. } => "switch",
Semantics::Radio { .. } => "radio",
Semantics::Slider { .. } => "slider",
Semantics::TextInput { .. } => "textbox",
Semantics::ComboBox => "combobox",
Semantics::Dialog => "dialog",
Semantics::Tab { .. } => "tab",
Semantics::Alert => "alert",
Semantics::Label => "text",
Semantics::Image => "image",
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum QueryError {
NoMatch,
Ambiguous(usize),
}
impl std::fmt::Display for QueryError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::NoMatch => write!(f, "no node matches"),
Self::Ambiguous(n) => write!(f, "{n} nodes match (ambiguous)"),
}
}
}
fn collect<'t>(node: &'t AccessNode, q: &Query, out: &mut Vec<&'t AccessNode>) {
if q.matches(node) {
out.push(node);
}
for child in &node.children {
collect(child, q, out);
}
}
impl Frame {
pub fn get_all(&self, q: &Query) -> Vec<AccessNode> {
let root = self.access_tree();
let mut out = Vec::new();
collect(&root, q, &mut out);
out.into_iter().cloned().collect()
}
pub fn try_get(&self, q: &Query) -> Result<AccessNode, QueryError> {
let mut all = self.get_all(q);
match all.len() {
0 => Err(QueryError::NoMatch),
1 => Ok(all.pop().expect("len checked")),
n => Err(QueryError::Ambiguous(n)),
}
}
pub fn query(&self, q: &Query) -> Option<AccessNode> {
match self.try_get(q) {
Ok(node) => Some(node),
Err(QueryError::NoMatch) => None,
Err(QueryError::Ambiguous(n)) => panic!(
"query [{q}] is ambiguous: {n} matches\n\
accessibility tree:\n{}",
self.access_yaml()
),
}
}
pub fn get(&self, q: &Query) -> AccessNode {
match self.query(q) {
Some(node) => node,
None => panic!(
"no node matches [{q}]\naccessibility tree:\n{}",
self.access_yaml()
),
}
}
pub fn access_yaml(&self) -> String {
fn attrs(node: &AccessNode) -> String {
let mut out = String::new();
match node.semantics {
Some(Semantics::Checkbox { mixed: true, .. }) => out.push_str(" [mixed]"),
Some(Semantics::Checkbox { checked: true, .. }) => out.push_str(" [checked]"),
Some(Semantics::Switch { on: true }) => out.push_str(" [on]"),
Some(Semantics::Radio { selected: true })
| Some(Semantics::Tab { selected: true }) => out.push_str(" [selected]"),
Some(Semantics::Slider { value, min, max }) => {
out.push_str(&format!(" [value={value} min={min} max={max}]"));
}
Some(Semantics::TextInput { multiline: true }) => out.push_str(" [multiline]"),
_ => {}
}
if let Some(value) = &node.value
&& !value.is_empty()
{
out.push_str(&format!(" [value={value:?}]"));
}
if node.live {
out.push_str(" [live]");
}
out
}
fn emit(node: &AccessNode, depth: usize, out: &mut String) {
let interesting = node.semantics.is_some()
|| node.label.is_some()
|| node.value.is_some()
|| node.key.is_some()
|| node.live;
let child_depth = if interesting {
let role = node
.semantics
.as_ref()
.map_or("generic", crate::query::role_name);
out.push_str(&" ".repeat(depth));
out.push('-');
out.push(' ');
out.push_str(role);
if let Some(label) = &node.label {
out.push_str(&format!(" {label:?}"));
}
out.push_str(&attrs(node));
if let Some(key) = &node.key {
out.push_str(&format!(" #{key}"));
}
out.push('\n');
depth + 1
} else {
depth
};
for child in &node.children {
emit(child, child_depth, out);
}
}
let mut out = String::new();
emit(&self.access_tree(), 0, &mut out);
if out.is_empty() {
out.push_str("(empty accessibility tree)\n");
}
out
}
}