Skip to main content

ui_automata/
platform.rs

1use schemars::JsonSchema;
2use serde::Deserialize;
3
4use crate::AutomataError;
5
6// ── Browser / TabInfo ─────────────────────────────────────────────────────────
7
8/// Basic info about a browser tab.
9#[derive(Debug, Clone)]
10pub struct TabInfo {
11    pub title: String,
12    pub url: String,
13}
14
15/// CDP browser: operations for controlling browser sessions and tabs.
16pub trait Browser: Send + Sync + 'static {
17    /// Ensure the browser is running with CDP enabled.
18    fn ensure(&self) -> Result<(), AutomataError>;
19    /// Open a new tab at `url` (or about:blank). Returns the CDP tab ID.
20    fn open_tab(&self, url: Option<&str>) -> Result<String, AutomataError>;
21    /// Close a tab by CDP target ID.
22    fn close_tab(&self, tab_id: &str) -> Result<(), AutomataError>;
23    /// Bring a tab to the foreground (switch to it).
24    fn activate_tab(&self, tab_id: &str) -> Result<(), AutomataError>;
25    /// Navigate a tab to a URL.
26    fn navigate(&self, tab_id: &str, url: &str) -> Result<(), AutomataError>;
27    /// Evaluate a JS expression in a tab. Returns the string result.
28    fn eval(&self, tab_id: &str, expr: &str) -> Result<String, AutomataError>;
29    /// Title + URL of a specific tab.
30    fn tab_info(&self, tab_id: &str) -> Result<TabInfo, AutomataError>;
31    /// All open tabs: (tab_id, TabInfo).
32    fn tabs(&self) -> Result<Vec<(String, TabInfo)>, AutomataError>;
33}
34
35/// Type of mouse click to perform.
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, JsonSchema)]
37#[serde(rename_all = "snake_case")]
38pub enum ClickType {
39    Left,
40    Double,
41    Triple,
42    Right,
43    Middle,
44}
45
46/// A UI element handle. Implementations wrap platform-specific COM/UIA objects.
47///
48/// All property methods return `None` / empty on a stale handle; interaction
49/// methods return `Err(AutomataError::Platform(...))` on staleness or failure.
50pub trait Element: Clone + 'static {
51    // ── Properties ───────────────────────────────────────────────────────────
52
53    /// Localized name / label of the element. `None` if empty or unavailable.
54    fn name(&self) -> Option<String>;
55
56    /// Localized role string (e.g. "Button", "Pane", "ToolBar").
57    fn role(&self) -> String;
58
59    /// Value text (ValuePattern) or name fallback.
60    fn text(&self) -> Result<String, AutomataError>;
61
62    /// Direct children's names joined by newlines, excluding the element's own name.
63    fn inner_text(&self) -> Result<String, AutomataError>;
64
65    fn is_enabled(&self) -> Result<bool, AutomataError>;
66    fn is_visible(&self) -> Result<bool, AutomataError>;
67    fn process_id(&self) -> Result<u32, AutomataError>;
68    /// Process name (without .exe) for this element's owning process.
69    /// Returns `None` on non-Windows or if the lookup fails.
70    fn process_name(&self) -> Option<String> {
71        None
72    }
73    /// Native window handle (HWND as u64) for this element's owning window.
74    /// Returns `None` on non-Windows or if the lookup fails.
75    fn hwnd(&self) -> Option<u64> {
76        None
77    }
78
79    /// UIA AutomationId property. `None` if empty or unavailable.
80    fn automation_id(&self) -> Option<String> {
81        None
82    }
83
84    /// Bounding box as `(x, y, width, height)`.
85    fn bounds(&self) -> Result<(i32, i32, i32, i32), AutomataError>;
86
87    /// Direct children. Returns `Err` on a stale handle.
88    fn children(&self) -> Result<Vec<Self>, AutomataError>;
89
90    /// Returns `false` when the element has been detached from the accessibility
91    /// tree (e.g. a dismissed dialog). Root windows have the desktop as parent so
92    /// they return `true`. Default returns `true` for platforms that don't implement it.
93    fn has_parent(&self) -> bool {
94        true
95    }
96
97    /// Navigate to this element's parent. Returns `None` at the root or on error.
98    fn parent(&self) -> Option<Self> {
99        None
100    }
101
102    // ── Interactions ─────────────────────────────────────────────────────────
103
104    fn click(&self) -> Result<(), AutomataError>;
105    fn double_click(&self) -> Result<(), AutomataError>;
106
107    /// Move the mouse cursor to the centre of the element without clicking.
108    fn hover(&self) -> Result<(), AutomataError>;
109
110    /// Click at a position expressed as fractions of the element's bounding box.
111    fn click_at(&self, x_pct: f64, y_pct: f64, kind: ClickType) -> Result<(), AutomataError>;
112
113    fn type_text(&self, text: &str) -> Result<(), AutomataError>;
114    fn press_key(&self, key: &str) -> Result<(), AutomataError>;
115
116    /// Set a field's value directly via IValuePattern (avoids needing to
117    /// select-all + type). Preferred over `type_text` for pre-filled fields.
118    fn set_value(&self, value: &str) -> Result<(), AutomataError>;
119
120    fn focus(&self) -> Result<(), AutomataError>;
121
122    /// Activate this element via UIA's `IInvokePattern::Invoke()`.
123    ///
124    /// Unlike `click()`, `invoke()` does not require a valid bounding rect —
125    /// it works on off-screen elements whose bounds are `(0,0,1,1)` because they
126    /// are scrolled out of view.  Prefer this over `Click` + `ScrollIntoView`
127    /// for items in virtualised or scrollable lists (e.g. Settings nav items,
128    /// WinUI ListView rows) where mouse-wheel scrolling causes elastic snap-back.
129    ///
130    /// Falls back to `click()` when the element does not support `InvokePattern`.
131    fn invoke(&self) -> Result<(), AutomataError> {
132        // Default: fall back to click for platforms that don't override this.
133        self.click()
134    }
135
136    /// Scroll ancestor containers until this element is within their visible
137    /// viewport. Uses `ScrollItemPattern` when supported; falls back to a
138    /// geometric ancestor walk with `ScrollPattern`.
139    fn scroll_into_view(&self) -> Result<(), AutomataError>;
140
141    fn activate_window(&self) -> Result<(), AutomataError>;
142    fn minimize_window(&self) -> Result<(), AutomataError>;
143    fn close(&self) -> Result<(), AutomataError>;
144}
145
146/// Platform desktop: discovers windows and provides foreground state.
147pub trait Desktop: Send + 'static {
148    type Elem: Element;
149    type Browser: Browser;
150
151    /// CDP browser handle. Used by the workflow engine for browser automation.
152    fn browser(&self) -> &Self::Browser;
153
154    /// All top-level application windows currently visible.
155    fn application_windows(&self) -> Result<Vec<Self::Elem>, AutomataError>;
156
157    /// Launch an executable by name or full path. Returns the process ID.
158    fn open_application(&self, exe: &str) -> Result<u32, AutomataError>;
159
160    /// The element that currently has keyboard focus (topmost modal if a dialog
161    /// is present). Returns `None` if the foreground window is unknown.
162    fn foreground_window(&self) -> Option<Self::Elem>;
163
164    /// Raw HWND as `u64` for process-ownership checks without a full element
165    /// query. Returns `None` on non-Windows or when nothing is focused.
166    fn foreground_hwnd(&self) -> Option<u64>;
167
168    /// All currently visible tooltip windows on the desktop.
169    /// Returns an empty vec on non-Windows or if none are present.
170    fn tooltip_windows(&self) -> Vec<Self::Elem> {
171        vec![]
172    }
173
174    /// Top-level window handles in Z-order (topmost first).
175    ///
176    /// Used by the anchor resolver to prefer the topmost window of a process
177    /// when multiple windows match the same filter. Returns an empty vec on
178    /// non-Windows or if the enumeration fails.
179    fn hwnd_z_order(&self) -> Vec<u64> {
180        vec![]
181    }
182}