xa11y_core/provider.rs
1use crate::element::ElementData;
2use crate::error::{Error, Result};
3use crate::event_provider::Subscription;
4use crate::selector::{matches_simple, Combinator, Selector, SelectorGroup, SelectorSegment};
5
6/// Platform backend trait for accessibility tree access.
7///
8/// Providers implement lazy, on-demand tree navigation. Elements are identified
9/// by their [`ElementData`] (which contains a provider-specific `handle` for
10/// looking up the underlying platform object).
11///
12/// # Action model
13///
14/// Common actions are first-class methods with proper typed signatures.
15/// Platform-specific or custom actions use [`perform_action`](Self::perform_action)
16/// as an escape hatch — it takes a `snake_case` action name string.
17///
18/// Providers should check platform permissions in their constructor (`new()`)
19/// and return [`Error::PermissionDenied`](crate::Error::PermissionDenied) if
20/// required permissions are not granted.
21pub trait Provider: Send + Sync {
22 // ── Tree navigation ─────────────────────────────────────────────
23
24 /// Get direct children of an element.
25 ///
26 /// If `element` is `None`, returns top-level application elements.
27 fn get_children(&self, element: Option<&ElementData>) -> Result<Vec<ElementData>>;
28
29 /// Get the parent of an element.
30 ///
31 /// Returns `None` for top-level (application) elements.
32 fn get_parent(&self, element: &ElementData) -> Result<Option<ElementData>>;
33
34 /// Enumerate top-level applications visible to this provider.
35 ///
36 /// Backends return one `ElementData` per application — typically with
37 /// `role=Application`, but Windows returns `role=Window` for top-level
38 /// HWNDs because UIA exposes applications as their top-level window.
39 /// This is the dedicated discovery primitive: it replaces the previous
40 /// `find_elements(None, application_selector, .., depth=0)` idiom and
41 /// lets each backend batch the platform-specific enumeration (CGWindowList
42 /// on macOS, the AT-SPI registry on Linux, UIA's desktop root on Windows)
43 /// in one place.
44 ///
45 /// Required: every `Provider` implements this explicitly. There's no
46 /// default impl — discovery and subtree search have different cost
47 /// profiles, and silently routing app discovery through `get_children`
48 /// would hide the difference from implementors.
49 fn list_apps(&self) -> Result<Vec<ElementData>>;
50
51 /// Find the application owning process `pid` — one attempt, no polling.
52 ///
53 /// This is the single-shot discovery primitive behind `App::by_pid`; the
54 /// core's lookup loop owns the retry/timeout policy. Returns
55 /// [`Error::SelectorNotMatched`] when no accessible application for `pid`
56 /// exists *yet* — the signal the poll loop retries on — and any other
57 /// error when the lookup itself failed (permissions, platform API
58 /// failure), which short-circuits the poll.
59 ///
60 /// The default implementation filters [`list_apps`](Self::list_apps) by
61 /// `pid`, and its not-matched error carries enumeration diagnostics (how
62 /// many apps were visible and how many had no resolvable pid) so a
63 /// timeout on an opaque CI runner says *why* the app was invisible.
64 /// Backends override this where the platform supports reaching a process
65 /// directly (`AXUIElementCreateApplication` on macOS, a UIA `ProcessId`
66 /// property search on Windows): direct attach is immune to enumeration
67 /// blind spots such as top-level windows that are still unnamed while the
68 /// app starts up.
69 fn app_by_pid(&self, pid: u32) -> Result<ElementData> {
70 let apps = self.list_apps()?;
71 let total = apps.len();
72 let unresolved = apps.iter().filter(|a| a.pid.is_none()).count();
73 if let Some(data) = apps.into_iter().find(|a| a.pid == Some(pid)) {
74 return Ok(data);
75 }
76 // The enumeration counts are structured diagnosis fields (tenet 6),
77 // not prose baked into the selector — bindings expose them as
78 // attributes, and `App::by_pid_with` merges in the full candidate
79 // list at its terminal failure site. Cheap to build (two counters),
80 // so attaching it per poll tick is fine.
81 Err(
82 Error::selector_not_matched(format!("application[pid={pid}]")).diagnose(
83 crate::error::Diagnosis {
84 last_observed: Some(format!(
85 "enumerated {total} running apps, {unresolved} without a resolvable pid"
86 )),
87 ..Default::default()
88 },
89 ),
90 )
91 }
92
93 /// Search for elements matching a selector.
94 ///
95 /// The selector is already parsed by the core — providers match against it
96 /// during traversal and can prune subtrees that can't match.
97 ///
98 /// `root` is the subtree root to search under (always present; use
99 /// [`list_apps`](Self::list_apps) to discover application roots first).
100 /// If `limit` is `Some(n)`, stops after finding `n` matches.
101 /// If `max_depth` is `Some(d)`, does not descend deeper than `d` levels.
102 ///
103 /// This is a thin convenience wrapper that re-uses
104 /// [`find_elements_group`](Self::find_elements_group) with a single-clause
105 /// group. Backends do **not** override this method — they override
106 /// `find_elements_group`, and the single-clause case runs through the
107 /// same native code path as multi-clause queries.
108 fn find_elements(
109 &self,
110 root: &ElementData,
111 selector: &Selector,
112 limit: Option<usize>,
113 max_depth: Option<u32>,
114 ) -> Result<Vec<ElementData>> {
115 let group = SelectorGroup {
116 clauses: vec![selector.clone()],
117 };
118 self.find_elements_group(root, &group, limit, max_depth)
119 }
120
121 /// Search for elements matching any clause of a comma-separated selector
122 /// group, scoped to `root`'s subtree.
123 ///
124 /// This is the **primary search primitive** each backend should override.
125 /// A native implementation performs ONE platform-level subtree query/walk
126 /// (e.g. `FindAllBuildCache(TreeScope_Subtree)` on Windows, one DFS over
127 /// AT-SPI children on Linux, one AX walk on macOS) and evaluates every
128 /// clause inline against each visited element. This avoids the
129 /// per-clause-walk perf cliff and also keeps the cross-clause dedup
130 /// correct: because everything happens inside one call, each platform
131 /// node is visited once and platform identity is stable.
132 ///
133 /// `root` is non-optional: app discovery now goes through
134 /// [`list_apps`](Self::list_apps) and any rootless multi-app search is
135 /// performed by the caller (e.g. `Locator`) by enumerating apps and
136 /// invoking this method per app.
137 ///
138 /// The default implementation traverses via [`get_children`](Self::get_children)
139 /// and uses tree-path identity for cross-clause merging. It's the
140 /// fallback for backends that haven't shipped a native override yet —
141 /// the same path the test/mock provider exercises.
142 fn find_elements_group(
143 &self,
144 root: &ElementData,
145 group: &SelectorGroup,
146 limit: Option<usize>,
147 max_depth: Option<u32>,
148 ) -> Result<Vec<ElementData>> {
149 crate::selector::find_elements_in_tree_group(
150 |el| self.get_children(el),
151 Some(root),
152 group,
153 limit,
154 max_depth,
155 )
156 }
157
158 /// Narrow candidates through remaining selector segments (Child/Descendant
159 /// combinators), deduplicate, apply final :nth and limit.
160 fn narrow_multi_segment(
161 &self,
162 mut candidates: Vec<ElementData>,
163 segments: &[SelectorSegment],
164 max_depth: u32,
165 limit: Option<usize>,
166 ) -> Result<Vec<ElementData>> {
167 for segment in segments {
168 let mut next_candidates = Vec::new();
169 for candidate in &candidates {
170 match segment.combinator {
171 Combinator::Child => {
172 let children = self.get_children(Some(candidate))?;
173 for child in children {
174 if matches_simple(&child, &segment.simple) {
175 next_candidates.push(child);
176 }
177 }
178 }
179 Combinator::Descendant => {
180 let sub_selector = Selector {
181 segments: vec![SelectorSegment {
182 combinator: Combinator::Root,
183 simple: segment.simple.clone(),
184 }],
185 };
186 let mut sub_results =
187 self.find_elements(candidate, &sub_selector, None, Some(max_depth))?;
188 next_candidates.append(&mut sub_results);
189 }
190 Combinator::Root => unreachable!(),
191 }
192 }
193 let mut seen = std::collections::HashSet::new();
194 next_candidates.retain(|e| seen.insert(e.handle));
195 candidates = next_candidates;
196 }
197
198 // Apply :nth on last segment
199 if let Some(nth) = segments.last().and_then(|s| s.simple.nth) {
200 if nth <= candidates.len() {
201 candidates = vec![candidates.remove(nth - 1)];
202 } else {
203 candidates.clear();
204 }
205 }
206
207 if let Some(limit) = limit {
208 candidates.truncate(limit);
209 }
210
211 Ok(candidates)
212 }
213
214 // ── Common actions ──────────────────────────────────────────────
215
216 /// Click / tap / invoke the element.
217 fn press(&self, element: &ElementData) -> Result<()>;
218
219 /// Set keyboard focus to the element.
220 fn focus(&self, element: &ElementData) -> Result<()>;
221
222 /// Remove keyboard focus from the element.
223 fn blur(&self, element: &ElementData) -> Result<()>;
224
225 /// Toggle a checkbox or switch.
226 fn toggle(&self, element: &ElementData) -> Result<()>;
227
228 /// Select an item in a list, tab group, or menu.
229 fn select(&self, element: &ElementData) -> Result<()>;
230
231 /// Expand a collapsible element (combo box, tree item, disclosure).
232 fn expand(&self, element: &ElementData) -> Result<()>;
233
234 /// Collapse an expanded element.
235 fn collapse(&self, element: &ElementData) -> Result<()>;
236
237 /// Show the element's context menu or dropdown.
238 fn show_menu(&self, element: &ElementData) -> Result<()>;
239
240 /// Increment a slider or spinner by one step.
241 fn increment(&self, element: &ElementData) -> Result<()>;
242
243 /// Decrement a slider or spinner by one step.
244 fn decrement(&self, element: &ElementData) -> Result<()>;
245
246 /// Scroll the element into the visible area.
247 fn scroll_into_view(&self, element: &ElementData) -> Result<()>;
248
249 // ── Typed operations ────────────────────────────────────────────
250
251 /// Set the text value of the element.
252 fn set_value(&self, element: &ElementData, value: &str) -> Result<()>;
253
254 /// Set the numeric value of the element (slider, spinner).
255 fn set_numeric_value(&self, element: &ElementData, value: f64) -> Result<()>;
256
257 /// Insert text at the current cursor position.
258 fn type_text(&self, element: &ElementData, text: &str) -> Result<()>;
259
260 /// Select a text range (0-based character offsets).
261 fn set_text_selection(&self, element: &ElementData, start: u32, end: u32) -> Result<()>;
262
263 // ── Generic action escape hatch ─────────────────────────────────
264
265 /// Perform an action by `snake_case` name.
266 ///
267 /// This is the escape hatch for platform-specific actions not covered by
268 /// the first-class methods above. The provider converts the name to the
269 /// platform's convention (e.g. `"custom_thing"` → `"AXCustomThing"` on
270 /// macOS) and invokes it.
271 ///
272 /// Well-known action names (`"press"`, `"focus"`, etc.) should also work
273 /// here — providers should delegate to the corresponding method.
274 fn perform_action(&self, element: &ElementData, action: &str) -> Result<()>;
275
276 // ── Events ──────────────────────────────────────────────────────
277
278 /// Subscribe to all accessibility events for an application.
279 ///
280 /// The element should be an application-level element (role=Application).
281 /// The provider extracts the PID from `element.pid`.
282 ///
283 /// Returns a [`Subscription`] that receives events until dropped.
284 fn subscribe(&self, element: &ElementData) -> Result<Subscription>;
285}
286
287// Blanket impl so shared references to a provider act as providers themselves.
288// Used by the umbrella crate's singleton (a `&'static dyn Provider` wrapped in
289// `Arc<_>`) and by any caller that wants to share a provider via `&T`. The
290// orphan rules keep this collision-free for downstream crates because `xa11y-core`
291// owns the `Provider` trait.
292impl<T: Provider + ?Sized> Provider for &T {
293 fn get_children(&self, element: Option<&ElementData>) -> Result<Vec<ElementData>> {
294 (**self).get_children(element)
295 }
296 fn get_parent(&self, element: &ElementData) -> Result<Option<ElementData>> {
297 (**self).get_parent(element)
298 }
299 fn list_apps(&self) -> Result<Vec<ElementData>> {
300 (**self).list_apps()
301 }
302 // Delegated explicitly (despite having a default impl) so a concrete
303 // provider's PID-direct override isn't bypassed when it's used through a
304 // shared reference — the default body on `&T` would re-route through
305 // `list_apps` and silently lose the override.
306 fn app_by_pid(&self, pid: u32) -> Result<ElementData> {
307 (**self).app_by_pid(pid)
308 }
309 fn find_elements(
310 &self,
311 root: &ElementData,
312 selector: &Selector,
313 limit: Option<usize>,
314 max_depth: Option<u32>,
315 ) -> Result<Vec<ElementData>> {
316 (**self).find_elements(root, selector, limit, max_depth)
317 }
318 fn find_elements_group(
319 &self,
320 root: &ElementData,
321 group: &SelectorGroup,
322 limit: Option<usize>,
323 max_depth: Option<u32>,
324 ) -> Result<Vec<ElementData>> {
325 (**self).find_elements_group(root, group, limit, max_depth)
326 }
327 fn narrow_multi_segment(
328 &self,
329 candidates: Vec<ElementData>,
330 segments: &[SelectorSegment],
331 max_depth: u32,
332 limit: Option<usize>,
333 ) -> Result<Vec<ElementData>> {
334 (**self).narrow_multi_segment(candidates, segments, max_depth, limit)
335 }
336 fn press(&self, element: &ElementData) -> Result<()> {
337 (**self).press(element)
338 }
339 fn focus(&self, element: &ElementData) -> Result<()> {
340 (**self).focus(element)
341 }
342 fn blur(&self, element: &ElementData) -> Result<()> {
343 (**self).blur(element)
344 }
345 fn toggle(&self, element: &ElementData) -> Result<()> {
346 (**self).toggle(element)
347 }
348 fn select(&self, element: &ElementData) -> Result<()> {
349 (**self).select(element)
350 }
351 fn expand(&self, element: &ElementData) -> Result<()> {
352 (**self).expand(element)
353 }
354 fn collapse(&self, element: &ElementData) -> Result<()> {
355 (**self).collapse(element)
356 }
357 fn show_menu(&self, element: &ElementData) -> Result<()> {
358 (**self).show_menu(element)
359 }
360 fn increment(&self, element: &ElementData) -> Result<()> {
361 (**self).increment(element)
362 }
363 fn decrement(&self, element: &ElementData) -> Result<()> {
364 (**self).decrement(element)
365 }
366 fn scroll_into_view(&self, element: &ElementData) -> Result<()> {
367 (**self).scroll_into_view(element)
368 }
369 fn set_value(&self, element: &ElementData, value: &str) -> Result<()> {
370 (**self).set_value(element, value)
371 }
372 fn set_numeric_value(&self, element: &ElementData, value: f64) -> Result<()> {
373 (**self).set_numeric_value(element, value)
374 }
375 fn type_text(&self, element: &ElementData, text: &str) -> Result<()> {
376 (**self).type_text(element, text)
377 }
378 fn set_text_selection(&self, element: &ElementData, start: u32, end: u32) -> Result<()> {
379 (**self).set_text_selection(element, start, end)
380 }
381 fn perform_action(&self, element: &ElementData, action: &str) -> Result<()> {
382 (**self).perform_action(element, action)
383 }
384 fn subscribe(&self, element: &ElementData) -> Result<Subscription> {
385 (**self).subscribe(element)
386 }
387}