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 Semantics::Spinbutton { .. } => "spinbutton",
214 Semantics::Meter { .. } => "meter",
215 Semantics::ProgressBar { .. } => "progressbar",
216 }
217}
218
219#[derive(Debug, Clone, Copy, PartialEq, Eq)]
222pub enum QueryError {
223 NoMatch,
225 Ambiguous(usize),
227}
228
229impl std::fmt::Display for QueryError {
230 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
231 match self {
232 Self::NoMatch => write!(f, "no node matches"),
233 Self::Ambiguous(n) => write!(f, "{n} nodes match (ambiguous)"),
234 }
235 }
236}
237
238fn collect<'t>(node: &'t AccessNode, q: &Query, out: &mut Vec<&'t AccessNode>) {
239 if q.matches(node) {
240 out.push(node);
241 }
242 for child in &node.children {
243 collect(child, q, out);
244 }
245}
246
247impl Frame {
248 pub fn get_all(&self, q: &Query) -> Vec<AccessNode> {
250 let root = self.access_tree();
251 let mut out = Vec::new();
252 collect(&root, q, &mut out);
253 out.into_iter().cloned().collect()
254 }
255
256 pub fn try_get(&self, q: &Query) -> Result<AccessNode, QueryError> {
263 let mut all = self.get_all(q);
264 match all.len() {
265 0 => Err(QueryError::NoMatch),
266 1 => Ok(all.pop().expect("len checked")),
267 n => Err(QueryError::Ambiguous(n)),
268 }
269 }
270
271 pub fn query(&self, q: &Query) -> Option<AccessNode> {
278 match self.try_get(q) {
279 Ok(node) => Some(node),
280 Err(QueryError::NoMatch) => None,
281 Err(QueryError::Ambiguous(n)) => panic!(
282 "query [{q}] is ambiguous: {n} matches\n\
283 accessibility tree:\n{}",
284 self.access_yaml()
285 ),
286 }
287 }
288
289 pub fn get(&self, q: &Query) -> AccessNode {
296 match self.query(q) {
297 Some(node) => node,
298 None => panic!(
299 "no node matches [{q}]\naccessibility tree:\n{}",
300 self.access_yaml()
301 ),
302 }
303 }
304
305 pub fn access_yaml(&self) -> String {
311 fn attrs(node: &AccessNode) -> String {
312 let mut out = String::new();
313 match node.semantics {
314 Some(Semantics::Checkbox { mixed: true, .. }) => out.push_str(" [mixed]"),
315 Some(Semantics::Checkbox { checked: true, .. }) => out.push_str(" [checked]"),
316 Some(Semantics::Switch { on: true }) => out.push_str(" [on]"),
317 Some(Semantics::Radio { selected: true })
318 | Some(Semantics::Tab { selected: true }) => out.push_str(" [selected]"),
319 Some(Semantics::Slider { value, min, max })
320 | Some(Semantics::Spinbutton { value, min, max })
321 | Some(Semantics::Meter { value, min, max }) => {
322 out.push_str(&format!(" [value={value} min={min} max={max}]"));
323 }
324 Some(Semantics::ProgressBar { value: Some(v) }) => {
325 out.push_str(&format!(" [value={v}]"));
326 }
327 Some(Semantics::ProgressBar { value: None }) => out.push_str(" [indeterminate]"),
328 Some(Semantics::TextInput { multiline: true }) => out.push_str(" [multiline]"),
329 _ => {}
330 }
331 if let Some(value) = &node.value
332 && !value.is_empty()
333 {
334 out.push_str(&format!(" [value={value:?}]"));
335 }
336 if node.live {
337 out.push_str(" [live]");
338 }
339 if node.invalid {
340 out.push_str(" [invalid]");
341 }
342 out
343 }
344 fn emit(node: &AccessNode, depth: usize, out: &mut String) {
345 let interesting = node.semantics.is_some()
346 || node.label.is_some()
347 || node.value.is_some()
348 || node.key.is_some()
349 || node.live;
350 let child_depth = if interesting {
351 let role = node
352 .semantics
353 .as_ref()
354 .map_or("generic", crate::query::role_name);
355 out.push_str(&" ".repeat(depth));
356 out.push('-');
357 out.push(' ');
358 out.push_str(role);
359 if let Some(label) = &node.label {
360 out.push_str(&format!(" {label:?}"));
361 }
362 out.push_str(&attrs(node));
363 if let Some(key) = &node.key {
364 out.push_str(&format!(" #{key}"));
365 }
366 out.push('\n');
367 depth + 1
368 } else {
369 depth
370 };
371 for child in &node.children {
372 emit(child, child_depth, out);
373 }
374 }
375 let mut out = String::new();
376 emit(&self.access_tree(), 0, &mut out);
377 if out.is_empty() {
378 out.push_str("(empty accessibility tree)\n");
379 }
380 out
381 }
382}