1use crate::element::Semantics;
25use crate::frame::{AccessNode, Frame};
26
27#[derive(Debug, Clone, PartialEq, Eq)]
29pub enum TextMatch {
30 Exact(String),
32 Contains(String),
34}
35
36impl TextMatch {
37 fn matches(&self, hay: Option<&str>) -> bool {
38 match (self, hay) {
39 (Self::Exact(needle), Some(hay)) => hay == needle,
40 (Self::Contains(needle), Some(hay)) => hay.contains(needle),
41 (_, None) => false,
42 }
43 }
44}
45
46impl std::fmt::Display for TextMatch {
47 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
48 match self {
49 Self::Exact(s) => write!(f, "{s:?}"),
50 Self::Contains(s) => write!(f, "*{s:?}*"),
51 }
52 }
53}
54
55#[derive(Debug, Clone)]
59pub struct Query {
60 role: Option<Semantics>,
61 label: Option<TextMatch>,
62 value: Option<TextMatch>,
63 key: Option<String>,
64}
65
66pub mod by {
68 use super::{Query, TextMatch};
69 use crate::element::Semantics;
70
71 pub fn role(role: Semantics) -> Query {
75 Query {
76 role: Some(role),
77 label: None,
78 value: None,
79 key: None,
80 }
81 }
82
83 pub fn label(label: impl Into<String>) -> Query {
86 Query {
87 role: None,
88 label: Some(TextMatch::Exact(label.into())),
89 value: None,
90 key: None,
91 }
92 }
93
94 pub fn label_contains(label: impl Into<String>) -> Query {
96 Query {
97 role: None,
98 label: Some(TextMatch::Contains(label.into())),
99 value: None,
100 key: None,
101 }
102 }
103
104 pub fn value(value: impl Into<String>) -> Query {
106 Query {
107 role: None,
108 label: None,
109 value: Some(TextMatch::Exact(value.into())),
110 key: None,
111 }
112 }
113
114 pub fn value_contains(value: impl Into<String>) -> Query {
116 Query {
117 role: None,
118 label: None,
119 value: Some(TextMatch::Contains(value.into())),
120 key: None,
121 }
122 }
123
124 pub fn id(key: impl Into<String>) -> Query {
127 Query {
128 role: None,
129 label: None,
130 value: None,
131 key: Some(key.into()),
132 }
133 }
134}
135
136impl Query {
137 pub fn name(mut self, label: impl Into<String>) -> Self {
140 self.label = Some(TextMatch::Exact(label.into()));
141 self
142 }
143
144 pub fn name_contains(mut self, label: impl Into<String>) -> Self {
146 self.label = Some(TextMatch::Contains(label.into()));
147 self
148 }
149
150 fn matches(&self, node: &AccessNode) -> bool {
151 if let Some(role) = &self.role {
152 let Some(semantics) = &node.semantics else {
153 return false;
154 };
155 if std::mem::discriminant(role) != std::mem::discriminant(semantics) {
156 return false;
157 }
158 }
159 if let Some(label) = &self.label
160 && !label.matches(node.label.as_deref())
161 {
162 return false;
163 }
164 if let Some(value) = &self.value
165 && !value.matches(node.value.as_deref())
166 {
167 return false;
168 }
169 if let Some(key) = &self.key
170 && node.key.as_deref() != Some(key.as_str())
171 {
172 return false;
173 }
174 true
175 }
176}
177
178impl std::fmt::Display for Query {
179 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180 let mut parts = Vec::new();
181 if let Some(role) = &self.role {
182 parts.push(format!("role={}", role_name(role)));
183 }
184 if let Some(label) = &self.label {
185 parts.push(format!("name={label}"));
186 }
187 if let Some(value) = &self.value {
188 parts.push(format!("value={value}"));
189 }
190 if let Some(key) = &self.key {
191 parts.push(format!("id={key:?}"));
192 }
193 write!(f, "{}", parts.join(" "))
194 }
195}
196
197pub(crate) fn role_name(semantics: &Semantics) -> &'static str {
200 match semantics {
201 Semantics::Button => "button",
202 Semantics::Checkbox { .. } => "checkbox",
203 Semantics::Switch { .. } => "switch",
204 Semantics::Radio { .. } => "radio",
205 Semantics::Slider { .. } => "slider",
206 Semantics::TextInput { .. } => "textbox",
207 Semantics::ComboBox => "combobox",
208 Semantics::Dialog => "dialog",
209 Semantics::Tab { .. } => "tab",
210 Semantics::Alert => "alert",
211 Semantics::Label => "text",
212 Semantics::Image => "image",
213 }
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
219pub enum QueryError {
220 NoMatch,
222 Ambiguous(usize),
224}
225
226impl std::fmt::Display for QueryError {
227 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
228 match self {
229 Self::NoMatch => write!(f, "no node matches"),
230 Self::Ambiguous(n) => write!(f, "{n} nodes match (ambiguous)"),
231 }
232 }
233}
234
235fn collect<'t>(node: &'t AccessNode, q: &Query, out: &mut Vec<&'t AccessNode>) {
236 if q.matches(node) {
237 out.push(node);
238 }
239 for child in &node.children {
240 collect(child, q, out);
241 }
242}
243
244impl Frame {
245 pub fn get_all(&self, q: &Query) -> Vec<AccessNode> {
247 let root = self.access_tree();
248 let mut out = Vec::new();
249 collect(&root, q, &mut out);
250 out.into_iter().cloned().collect()
251 }
252
253 pub fn try_get(&self, q: &Query) -> Result<AccessNode, QueryError> {
260 let mut all = self.get_all(q);
261 match all.len() {
262 0 => Err(QueryError::NoMatch),
263 1 => Ok(all.pop().expect("len checked")),
264 n => Err(QueryError::Ambiguous(n)),
265 }
266 }
267
268 pub fn query(&self, q: &Query) -> Option<AccessNode> {
275 match self.try_get(q) {
276 Ok(node) => Some(node),
277 Err(QueryError::NoMatch) => None,
278 Err(QueryError::Ambiguous(n)) => panic!(
279 "query [{q}] is ambiguous: {n} matches\n\
280 accessibility tree:\n{}",
281 self.access_yaml()
282 ),
283 }
284 }
285
286 pub fn get(&self, q: &Query) -> AccessNode {
293 match self.query(q) {
294 Some(node) => node,
295 None => panic!(
296 "no node matches [{q}]\naccessibility tree:\n{}",
297 self.access_yaml()
298 ),
299 }
300 }
301
302 pub fn access_yaml(&self) -> String {
308 fn attrs(node: &AccessNode) -> String {
309 let mut out = String::new();
310 match node.semantics {
311 Some(Semantics::Checkbox { checked: true }) => out.push_str(" [checked]"),
312 Some(Semantics::Switch { on: true }) => out.push_str(" [on]"),
313 Some(Semantics::Radio { selected: true })
314 | Some(Semantics::Tab { selected: true }) => out.push_str(" [selected]"),
315 Some(Semantics::Slider { value, min, max }) => {
316 out.push_str(&format!(" [value={value} min={min} max={max}]"));
317 }
318 Some(Semantics::TextInput { multiline: true }) => out.push_str(" [multiline]"),
319 _ => {}
320 }
321 if let Some(value) = &node.value
322 && !value.is_empty()
323 {
324 out.push_str(&format!(" [value={value:?}]"));
325 }
326 if node.live {
327 out.push_str(" [live]");
328 }
329 out
330 }
331 fn emit(node: &AccessNode, depth: usize, out: &mut String) {
332 let interesting = node.semantics.is_some()
333 || node.label.is_some()
334 || node.value.is_some()
335 || node.key.is_some()
336 || node.live;
337 let child_depth = if interesting {
338 let role = node
339 .semantics
340 .as_ref()
341 .map_or("generic", crate::query::role_name);
342 out.push_str(&" ".repeat(depth));
343 out.push('-');
344 out.push(' ');
345 out.push_str(role);
346 if let Some(label) = &node.label {
347 out.push_str(&format!(" {label:?}"));
348 }
349 out.push_str(&attrs(node));
350 if let Some(key) = &node.key {
351 out.push_str(&format!(" #{key}"));
352 }
353 out.push('\n');
354 depth + 1
355 } else {
356 depth
357 };
358 for child in &node.children {
359 emit(child, child_depth, out);
360 }
361 }
362 let mut out = String::new();
363 emit(&self.access_tree(), 0, &mut out);
364 if out.is_empty() {
365 out.push_str("(empty accessibility tree)\n");
366 }
367 out
368 }
369}