1#[derive(Debug, thiserror::Error)]
28pub enum UiaError {
29 #[error("UIA element not found: {0}")]
30 NotFound(String),
31 #[error("UIA COM error: {0}")]
32 Com(String),
33 #[error("UIA not supported on this platform")]
34 Unsupported,
35 #[error("{0}")]
36 Other(String),
37}
38
39pub type Result<T> = std::result::Result<T, UiaError>;
40
41#[derive(Debug, Clone)]
43pub enum UiaSelector {
44 Name(String),
46 AutomationId(String),
48 ClassName(String),
50}
51
52impl UiaSelector {
53 pub fn from_name(s: impl Into<String>) -> Self {
54 Self::Name(s.into())
55 }
56 pub fn from_id(s: impl Into<String>) -> Self {
57 Self::AutomationId(s.into())
58 }
59 pub fn from_class(s: impl Into<String>) -> Self {
60 Self::ClassName(s.into())
61 }
62}
63
64pub struct UiaElement {
66 #[cfg(target_os = "windows")]
67 inner: windows_impl::Element,
68 #[cfg(not(target_os = "windows"))]
69 _phantom: (),
70}
71
72pub struct UiaFinder {
74 #[cfg(target_os = "windows")]
75 inner: windows_impl::Finder,
76 #[cfg(not(target_os = "windows"))]
77 _phantom: (),
78}
79
80impl UiaFinder {
81 pub fn new() -> Result<Self> {
82 #[cfg(target_os = "windows")]
83 {
84 Ok(Self {
85 inner: windows_impl::Finder::new()?,
86 })
87 }
88 #[cfg(not(target_os = "windows"))]
89 Err(UiaError::Unsupported)
90 }
91
92 pub fn find(&self, selector: &UiaSelector) -> Result<UiaElement> {
94 #[cfg(target_os = "windows")]
95 {
96 let el = self.inner.find(selector)?;
97 Ok(UiaElement { inner: el })
98 }
99 #[cfg(not(target_os = "windows"))]
100 {
101 let _ = selector;
102 Err(UiaError::Unsupported)
103 }
104 }
105}
106
107impl UiaElement {
108 pub fn get_name(&self) -> Result<String> {
110 #[cfg(target_os = "windows")]
111 return self.inner.get_name();
112 #[cfg(not(target_os = "windows"))]
113 Err(UiaError::Unsupported)
114 }
115
116 pub fn get_value(&self) -> Result<String> {
118 #[cfg(target_os = "windows")]
119 return self.inner.get_value();
120 #[cfg(not(target_os = "windows"))]
121 Err(UiaError::Unsupported)
122 }
123
124 pub fn set_value(&self, value: &str) -> Result<()> {
126 #[cfg(target_os = "windows")]
127 return self.inner.set_value(value);
128 #[cfg(not(target_os = "windows"))]
129 {
130 let _ = value;
131 Err(UiaError::Unsupported)
132 }
133 }
134
135 pub fn invoke(&self) -> Result<()> {
137 #[cfg(target_os = "windows")]
138 return self.inner.invoke();
139 #[cfg(not(target_os = "windows"))]
140 Err(UiaError::Unsupported)
141 }
142
143 pub fn bounding_rect(&self) -> Result<(i32, i32, i32, i32)> {
145 #[cfg(target_os = "windows")]
146 return self.inner.bounding_rect();
147 #[cfg(not(target_os = "windows"))]
148 Err(UiaError::Unsupported)
149 }
150
151 pub fn children(&self) -> Result<Vec<UiaElement>> {
153 #[cfg(target_os = "windows")]
154 {
155 let children = self.inner.children()?;
156 Ok(children
157 .into_iter()
158 .map(|el| UiaElement { inner: el })
159 .collect())
160 }
161 #[cfg(not(target_os = "windows"))]
162 Err(UiaError::Unsupported)
163 }
164
165 pub fn is_enabled(&self) -> Result<bool> {
167 #[cfg(target_os = "windows")]
168 return self.inner.is_enabled();
169 #[cfg(not(target_os = "windows"))]
170 Err(UiaError::Unsupported)
171 }
172
173 pub fn is_offscreen(&self) -> Result<bool> {
175 #[cfg(target_os = "windows")]
176 return self.inner.is_offscreen();
177 #[cfg(not(target_os = "windows"))]
178 Err(UiaError::Unsupported)
179 }
180
181 pub fn get_class_name(&self) -> Result<String> {
183 #[cfg(target_os = "windows")]
184 return self.inner.get_class_name();
185 #[cfg(not(target_os = "windows"))]
186 Err(UiaError::Unsupported)
187 }
188
189 pub fn select_item(&self, item_name: &str) -> Result<()> {
194 #[cfg(target_os = "windows")]
195 return self.inner.select_item(item_name);
196 #[cfg(not(target_os = "windows"))]
197 {
198 let _ = item_name;
199 Err(UiaError::Unsupported)
200 }
201 }
202
203 pub fn set_checked(&self, checked: bool) -> Result<()> {
205 #[cfg(target_os = "windows")]
206 return self.inner.set_checked(checked);
207 #[cfg(not(target_os = "windows"))]
208 {
209 let _ = checked;
210 Err(UiaError::Unsupported)
211 }
212 }
213}
214
215#[cfg(target_os = "windows")]
218mod windows_impl {
219 use super::{UiaError, UiaSelector};
220 use windows::{
221 core::{Interface, BSTR},
222 Win32::{
223 System::{
224 Com::{
225 CoCreateInstance, CoInitializeEx, CLSCTX_INPROC_SERVER, COINIT_MULTITHREADED,
226 },
227 Variant::VARIANT,
228 },
229 UI::Accessibility::{
230 CUIAutomation, IUIAutomation, IUIAutomationElement, IUIAutomationValuePattern,
231 TreeScope_Descendants, UIA_AutomationIdPropertyId, UIA_ClassNamePropertyId,
232 UIA_NamePropertyId, UIA_ValuePatternId,
233 },
234 },
235 };
236
237 pub struct Finder {
238 automation: IUIAutomation,
239 }
240
241 pub struct Element {
242 pub(crate) el: IUIAutomationElement,
243 automation: IUIAutomation,
244 }
245
246 impl Finder {
247 pub fn new() -> super::Result<Self> {
248 unsafe {
249 CoInitializeEx(None, COINIT_MULTITHREADED)
250 .ok()
251 .map_err(|e| UiaError::Com(e.to_string()))?;
252 let automation: IUIAutomation =
253 CoCreateInstance(&CUIAutomation, None, CLSCTX_INPROC_SERVER)
254 .map_err(|e| UiaError::Com(e.to_string()))?;
255 Ok(Self { automation })
256 }
257 }
258
259 pub fn find(&self, selector: &UiaSelector) -> super::Result<Element> {
260 unsafe {
261 let root = self
262 .automation
263 .GetRootElement()
264 .map_err(|e| UiaError::Com(e.to_string()))?;
265
266 let (prop_id, value) = match selector {
267 UiaSelector::Name(s) => (UIA_NamePropertyId, s.clone()),
268 UiaSelector::AutomationId(s) => (UIA_AutomationIdPropertyId, s.clone()),
269 UiaSelector::ClassName(s) => (UIA_ClassNamePropertyId, s.clone()),
270 };
271
272 let variant = VARIANT::from(BSTR::from(value.as_str()));
273 let condition = self
274 .automation
275 .CreatePropertyCondition(prop_id, &variant)
276 .map_err(|e| UiaError::Com(e.to_string()))?;
277
278 let el = root
279 .FindFirst(TreeScope_Descendants, &condition)
280 .map_err(|e| UiaError::Com(e.to_string()))?;
281
282 Ok(Element {
283 el,
284 automation: self.automation.clone(),
285 })
286 }
287 }
288 }
289
290 impl Element {
291 pub fn get_name(&self) -> super::Result<String> {
292 unsafe {
293 let bstr = self
294 .el
295 .CurrentName()
296 .map_err(|e| UiaError::Com(e.to_string()))?;
297 Ok(bstr.to_string())
298 }
299 }
300
301 pub fn get_value(&self) -> super::Result<String> {
302 unsafe {
303 let pattern: IUIAutomationValuePattern = self
304 .el
305 .GetCurrentPattern(UIA_ValuePatternId)
306 .map_err(|e| UiaError::Com(e.to_string()))?
307 .cast()
308 .map_err(|e| UiaError::Com(e.to_string()))?;
309 let bstr = pattern
310 .CurrentValue()
311 .map_err(|e| UiaError::Com(e.to_string()))?;
312 Ok(bstr.to_string())
313 }
314 }
315
316 pub fn set_value(&self, value: &str) -> super::Result<()> {
317 unsafe {
318 let pattern: IUIAutomationValuePattern = self
319 .el
320 .GetCurrentPattern(UIA_ValuePatternId)
321 .map_err(|e| UiaError::Com(e.to_string()))?
322 .cast()
323 .map_err(|e| UiaError::Com(e.to_string()))?;
324 pattern
325 .SetValue(&BSTR::from(value))
326 .map_err(|e| UiaError::Com(e.to_string()))
327 }
328 }
329
330 pub fn invoke(&self) -> super::Result<()> {
331 use windows::Win32::UI::Accessibility::{
332 IUIAutomationInvokePattern, UIA_InvokePatternId,
333 };
334 unsafe {
335 let pattern: IUIAutomationInvokePattern = self
336 .el
337 .GetCurrentPattern(UIA_InvokePatternId)
338 .map_err(|e| UiaError::Com(e.to_string()))?
339 .cast()
340 .map_err(|e| UiaError::Com(e.to_string()))?;
341 pattern.Invoke().map_err(|e| UiaError::Com(e.to_string()))
342 }
343 }
344
345 pub fn bounding_rect(&self) -> super::Result<(i32, i32, i32, i32)> {
346 unsafe {
347 let rect = self
348 .el
349 .CurrentBoundingRectangle()
350 .map_err(|e| UiaError::Com(e.to_string()))?;
351 Ok((
352 rect.left,
353 rect.top,
354 rect.right - rect.left,
355 rect.bottom - rect.top,
356 ))
357 }
358 }
359
360 pub fn children(&self) -> super::Result<Vec<Element>> {
361 use windows::Win32::UI::Accessibility::TreeScope_Children;
362 unsafe {
363 let true_cond = self
364 .automation
365 .CreateTrueCondition()
366 .map_err(|e| UiaError::Com(e.to_string()))?;
367 let el_array = self
368 .el
369 .FindAll(TreeScope_Children, &true_cond)
370 .map_err(|e| UiaError::Com(e.to_string()))?;
371 let count = el_array
372 .Length()
373 .map_err(|e| UiaError::Com(e.to_string()))?;
374 let mut result = Vec::with_capacity(count as usize);
375 for i in 0..count {
376 let child = el_array
377 .GetElement(i)
378 .map_err(|e| UiaError::Com(e.to_string()))?;
379 result.push(Element {
380 el: child,
381 automation: self.automation.clone(),
382 });
383 }
384 Ok(result)
385 }
386 }
387
388 pub fn is_enabled(&self) -> super::Result<bool> {
389 unsafe {
390 let b = self
391 .el
392 .CurrentIsEnabled()
393 .map_err(|e| UiaError::Com(e.to_string()))?;
394 Ok(b.as_bool())
395 }
396 }
397
398 pub fn is_offscreen(&self) -> super::Result<bool> {
399 unsafe {
400 let b = self
401 .el
402 .CurrentIsOffscreen()
403 .map_err(|e| UiaError::Com(e.to_string()))?;
404 Ok(b.as_bool())
405 }
406 }
407
408 pub fn get_class_name(&self) -> super::Result<String> {
409 unsafe {
410 let bstr = self
411 .el
412 .CurrentClassName()
413 .map_err(|e| UiaError::Com(e.to_string()))?;
414 Ok(bstr.to_string())
415 }
416 }
417
418 pub fn select_item(&self, item_name: &str) -> super::Result<()> {
419 use windows::Win32::UI::Accessibility::{
420 IUIAutomationExpandCollapsePattern, IUIAutomationSelectionItemPattern,
421 UIA_ExpandCollapsePatternId, UIA_SelectionItemPatternId,
422 };
423 unsafe {
424 if let Ok(p) = self.el.GetCurrentPattern(UIA_ExpandCollapsePatternId) {
426 if let Ok(ecp) = p.cast::<IUIAutomationExpandCollapsePattern>() {
427 let _ = ecp.Expand();
428 }
429 }
430 let true_cond = self
431 .automation
432 .CreateTrueCondition()
433 .map_err(|e| UiaError::Com(e.to_string()))?;
434 let el_array = self
435 .el
436 .FindAll(TreeScope_Descendants, &true_cond)
437 .map_err(|e| UiaError::Com(e.to_string()))?;
438 let count = el_array
439 .Length()
440 .map_err(|e| UiaError::Com(e.to_string()))?;
441 for i in 0..count {
442 let child = el_array
443 .GetElement(i)
444 .map_err(|e| UiaError::Com(e.to_string()))?;
445 let name = child
446 .CurrentName()
447 .map_err(|e| UiaError::Com(e.to_string()))?;
448 if name == item_name {
449 if let Ok(p) = child.GetCurrentPattern(UIA_SelectionItemPatternId) {
450 let sip = p
451 .cast::<IUIAutomationSelectionItemPattern>()
452 .map_err(|e| UiaError::Com(e.to_string()))?;
453 sip.Select().map_err(|e| UiaError::Com(e.to_string()))?;
454 return Ok(());
455 }
456 }
457 }
458 Err(UiaError::NotFound(format!("item '{item_name}'")))
459 }
460 }
461
462 pub fn set_checked(&self, checked: bool) -> super::Result<()> {
463 use windows::Win32::UI::Accessibility::{
464 IUIAutomationTogglePattern, ToggleState_On, UIA_TogglePatternId,
465 };
466 unsafe {
467 let p = self
468 .el
469 .GetCurrentPattern(UIA_TogglePatternId)
470 .map_err(|e| UiaError::Com(e.to_string()))?;
471 let tp = p
472 .cast::<IUIAutomationTogglePattern>()
473 .map_err(|e| UiaError::Com(e.to_string()))?;
474 let state = tp
475 .CurrentToggleState()
476 .map_err(|e| UiaError::Com(e.to_string()))?;
477 let is_on = state == ToggleState_On;
478 if is_on != checked {
479 tp.Toggle().map_err(|e| UiaError::Com(e.to_string()))?;
480 }
481 Ok(())
482 }
483 }
484 }
485}