accessibility_ng/
lib.rs

1pub mod action;
2pub mod attribute;
3pub mod observer;
4pub mod ui_element;
5mod util;
6pub mod value;
7
8use accessibility_sys_ng::{error_string, AXError};
9use core_foundation::{
10    array::CFArray,
11    base::CFTypeID,
12    base::{CFCopyTypeIDDescription, TCFType},
13    string::CFString,
14};
15use std::{
16    cell::{Cell, RefCell},
17    thread,
18    time::{Duration, Instant},
19};
20use thiserror::Error as TError;
21
22pub use action::*;
23pub use attribute::*;
24pub use observer::*;
25pub use ui_element::*;
26pub use value::*;
27
28#[non_exhaustive]
29#[derive(Debug, TError)]
30pub enum Error {
31    #[error("element not found")]
32    NotFound,
33    #[error(
34        "expected attribute type {} but got {}",
35        type_name(*expected),
36        type_name(*received),
37    )]
38    UnexpectedType {
39        expected: CFTypeID,
40        received: CFTypeID,
41    },
42    #[error("accessibility error {}", error_string(*.0))]
43    Ax(AXError),
44}
45
46fn type_name(type_id: CFTypeID) -> CFString {
47    unsafe { CFString::wrap_under_create_rule(CFCopyTypeIDDescription(type_id)) }
48}
49
50pub trait TreeVisitor {
51    fn enter_element(&self, element: &AXUIElement) -> TreeWalkerFlow;
52    fn exit_element(&self, element: &AXUIElement);
53}
54
55pub struct TreeWalker {
56    attr_children: AXAttribute<CFArray<AXUIElement>>,
57}
58
59#[derive(Copy, Clone, PartialEq, Eq)]
60pub enum TreeWalkerFlow {
61    Continue,
62    SkipSubtree,
63    Exit,
64}
65
66impl TreeWalker {
67    pub fn new() -> Self {
68        Self {
69            attr_children: AXAttribute::children(),
70        }
71    }
72
73    pub fn walk(&self, root: &AXUIElement, visitor: &dyn TreeVisitor) {
74        let _ = self.walk_one(root, visitor);
75    }
76
77    fn walk_one(&self, root: &AXUIElement, visitor: &dyn TreeVisitor) -> TreeWalkerFlow {
78        let mut flow = visitor.enter_element(root);
79
80        if flow == TreeWalkerFlow::Continue {
81            if let Ok(children) = root.attribute(&self.attr_children) {
82                for child in children.into_iter() {
83                    let child_flow = self.walk_one(&*child, visitor);
84
85                    if child_flow == TreeWalkerFlow::Exit {
86                        flow = child_flow;
87                        break;
88                    }
89                }
90            }
91        }
92
93        visitor.exit_element(root);
94        flow
95    }
96}
97
98pub struct ElementFinder {
99    root: AXUIElement,
100    implicit_wait: Option<Duration>,
101    predicate: Box<dyn Fn(&AXUIElement) -> bool>,
102    depth: Cell<usize>,
103    cached: RefCell<Option<AXUIElement>>,
104}
105
106impl ElementFinder {
107    pub fn new<F>(root: &AXUIElement, predicate: F, implicit_wait: Option<Duration>) -> Self
108    where
109        F: 'static + Fn(&AXUIElement) -> bool,
110    {
111        Self {
112            root: root.clone(),
113            predicate: Box::new(predicate),
114            implicit_wait,
115            depth: Cell::new(0),
116            cached: RefCell::new(None),
117        }
118    }
119
120    pub fn find(&self) -> Result<AXUIElement, Error> {
121        if let Some(result) = &*self.cached.borrow() {
122            return Ok(result.clone());
123        }
124
125        let mut deadline = Instant::now();
126        let walker = TreeWalker::new();
127
128        if let Some(implicit_wait) = &self.implicit_wait {
129            deadline += *implicit_wait;
130        }
131
132        loop {
133            if let Some(result) = &*self.cached.borrow() {
134                return Ok(result.clone());
135            }
136
137            walker.walk(&self.root, self);
138            let now = Instant::now();
139
140            if now >= deadline {
141                return Err(Error::NotFound);
142            } else {
143                let time_left = deadline.saturating_duration_since(now);
144                thread::sleep(std::cmp::min(time_left, Duration::from_millis(250)));
145            }
146        }
147    }
148
149    pub fn reset(&self) {
150        self.cached.replace(None);
151    }
152
153    pub fn attribute<T: TCFType>(&self, attribute: &AXAttribute<T>) -> Result<T, Error> {
154        self.find()?.attribute(attribute)
155    }
156
157    pub fn set_attribute<T: TCFType>(
158        &self,
159        attribute: &AXAttribute<T>,
160        value: impl Into<T>,
161    ) -> Result<(), Error> {
162        self.find()?.set_attribute(attribute, value)
163    }
164
165    pub fn perform_action(&self, name: &CFString) -> Result<(), Error> {
166        self.find()?.perform_action(name)
167    }
168}
169
170const MAX_DEPTH: usize = 100;
171
172impl TreeVisitor for ElementFinder {
173    fn enter_element(&self, element: &AXUIElement) -> TreeWalkerFlow {
174        self.depth.set(self.depth.get() + 1);
175
176        if (self.predicate)(element) {
177            self.cached.replace(Some(element.clone()));
178            return TreeWalkerFlow::Exit;
179        }
180
181        if self.depth.get() > MAX_DEPTH {
182            TreeWalkerFlow::SkipSubtree
183        } else {
184            TreeWalkerFlow::Continue
185        }
186    }
187
188    fn exit_element(&self, _element: &AXUIElement) {
189        self.depth.set(self.depth.get() - 1)
190    }
191}