Skip to main content

xa11y_core/
input.rs

1//! Input simulation: synthesised pointer and keyboard events.
2//!
3//! Input simulation is **separate from** the accessibility action layer
4//! ([`crate::Provider`], [`crate::Element`], [`crate::Locator`]). The two
5//! mechanisms are fundamentally different:
6//!
7//! - **Accessibility actions** (`element.press()`, `locator.toggle()`) call
8//!   the platform's a11y API directly. They work without the target window
9//!   being focused or visible, are deterministic, and are the preferred way
10//!   to drive a UI.
11//! - **Input simulation** ([`InputSim`]) generates OS-level pointer/keyboard
12//!   events at the system event layer. Use it only for interactions that have
13//!   no a11y equivalent (drag-and-drop, scroll wheels, complex shortcut
14//!   sequences). Most platforms require the target window to be foregrounded
15//!   and require additional permissions (Accessibility + Input Monitoring on
16//!   macOS, Wayland portal grants on Linux, etc.).
17//!
18//! There is **no implicit bridge** between the two: an accessibility-action
19//! failure never falls back to input simulation, and [`InputSim`] never
20//! inspects or auto-resolves the a11y tree on behalf of the caller. If you
21//! want to click an element, you compute its bounds (via the a11y API) and
22//! pass them in — see [`IntoPoint`] and [`point_for`].
23//!
24//! # Layout
25//!
26//! [`InputSim`] exposes two sub-handles:
27//!
28//! - [`InputSim::mouse`] → [`Mouse`] for pointer operations (`click`, `drag`,
29//!   `scroll`, `down`/`up`).
30//! - [`InputSim::keyboard`] → [`Keyboard`] for key operations (`press`,
31//!   `chord`, `down`/`up`, `type_text`).
32//!
33//! Modifier keys (`Shift`, `Ctrl`, `Alt`, `Meta`) are regular variants of
34//! [`Key`] — there is no separate `Modifier` type. `Key::Char(c)` represents
35//! the unshifted physical key; use [`Key::Shift`] explicitly for uppercase
36//! or shifted symbols (see [`Key`] for the rationale).
37
38use std::sync::Arc;
39use std::time::Duration;
40
41use crate::element::{Element, Rect};
42use crate::error::{Error, Result};
43
44// ── Geometry ────────────────────────────────────────────────────────
45
46/// A 2D point in desktop coordinates: physical pixels on Windows and logical
47/// points on macOS and Linux.
48///
49/// This is the same coordinate space as [`crate::element::Rect`] in
50/// `Element::bounds`, so anchor points computed from an element's bounds are
51/// already in the right space. Origin is top-left of the primary display;
52/// negative values are valid on multi-monitor setups.
53///
54/// Each [`InputProvider`] uses the same units as its element bounds. Windows
55/// and macOS pass these coordinates to native input without DPI conversion;
56/// Linux converts logical points when its display scale is known. A point
57/// that lands on an element's centre is
58/// `anchor_point(&element.bounds, Anchor::Center)`.
59#[allow(
60    clippy::exhaustive_structs,
61    reason = "Closed domain: a 2D screen point is an x and a y. Literal \
62              construction is the point of the type."
63)]
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub struct Point {
66    pub x: i32,
67    pub y: i32,
68}
69
70impl Point {
71    pub const fn new(x: i32, y: i32) -> Self {
72        Self { x, y }
73    }
74
75    /// Convert this **logical** point to **physical** device pixels by
76    /// multiplying by `scale` (the physical-to-logical ratio). Used by input
77    /// backends at the OS boundary. A non-finite or non-positive `scale`
78    /// collapses to `1.0` (identity) — see [`crate::element::Rect::to_physical`].
79    #[must_use]
80    pub fn to_physical(self, scale: f64) -> Point {
81        let s = crate::element::sane_scale(scale);
82        Point {
83            x: (f64::from(self.x) * s).round() as i32,
84            y: (f64::from(self.y) * s).round() as i32,
85        }
86    }
87
88    /// Convert this **physical** point to **logical** coordinates by dividing
89    /// by `scale`. Inverse of [`Point::to_physical`].
90    #[must_use]
91    pub fn to_logical(self, scale: f64) -> Point {
92        self.to_physical(1.0 / crate::element::sane_scale(scale))
93    }
94}
95
96/// Where on an element to land a pointer event.
97///
98/// All anchors are computed against the element's [`Rect`] *at the time of the
99/// input call*, not at element-fetch time — but only if the caller supplies a
100/// fresh element. `InputSim` will not re-traverse the a11y tree on its own.
101///
102/// `#[non_exhaustive]`: the named anchors are the four corners and the centre,
103/// which is five of the standard nine-point grid — `TopCenter`, `BottomCenter`,
104/// `LeftCenter`, and `RightCenter` are the obvious next additions.
105#[derive(Debug, Clone, Copy, PartialEq, Default)]
106#[non_exhaustive]
107pub enum Anchor {
108    #[default]
109    Center,
110    TopLeft,
111    TopRight,
112    BottomLeft,
113    BottomRight,
114    /// Pixel offset from the element's top-left corner.
115    Offset {
116        dx: i32,
117        dy: i32,
118    },
119}
120
121/// Compute a [`Point`] inside a [`Rect`] using the given [`Anchor`].
122pub fn anchor_point(rect: &Rect, anchor: Anchor) -> Point {
123    let x = rect.x;
124    let y = rect.y;
125    // Rectangles are half-open. Named right/bottom anchors target the last
126    // point inside the bounds, never the exclusive far edge (which can be on
127    // a different display and is not part of the element).
128    let w = rect.width.saturating_sub(1).min(i32::MAX as u32) as i32;
129    let h = rect.height.saturating_sub(1).min(i32::MAX as u32) as i32;
130    match anchor {
131        Anchor::Center => Point::new(
132            x.saturating_add((rect.width / 2).min(i32::MAX as u32) as i32),
133            y.saturating_add((rect.height / 2).min(i32::MAX as u32) as i32),
134        ),
135        Anchor::TopLeft => Point::new(x, y),
136        Anchor::TopRight => Point::new(x.saturating_add(w), y),
137        Anchor::BottomLeft => Point::new(x, y.saturating_add(h)),
138        Anchor::BottomRight => Point::new(x.saturating_add(w), y.saturating_add(h)),
139        Anchor::Offset { dx, dy } => Point::new(x.saturating_add(dx), y.saturating_add(dy)),
140    }
141}
142
143/// Resolve an [`Element`]'s current bounds to a screen [`Point`] using `anchor`.
144///
145/// Reads `element.bounds`. Returns [`Error::NoElementBounds`] if the element
146/// has no bounds (e.g. an off-screen or virtual node).
147///
148/// **Staleness:** `Element` is a snapshot — its bounds were captured when the
149/// caller fetched it from the provider. If the UI may have moved since then,
150/// re-fetch the element first (e.g. via [`crate::Locator`]).
151pub fn point_for(element: &Element, anchor: Anchor) -> Result<Point> {
152    let bounds = element.bounds.ok_or(Error::NoElementBounds)?;
153    Ok(anchor_point(&bounds, anchor))
154}
155
156// ── Targets ─────────────────────────────────────────────────────────
157
158/// A target that can be lowered to a screen [`Point`].
159///
160/// Implemented for:
161/// - [`Point`] and `(i32, i32)` — used as-is.
162/// - `&`[`Element`] — uses the element's `bounds` field at the call site, with
163///   [`Anchor::Center`]. For a non-default anchor, call [`point_for`] yourself
164///   and pass the resulting `Point`.
165///
166/// Not implemented for [`crate::Locator`]: the caller must explicitly resolve
167/// the locator to an `Element` first. This keeps the cost of provider traffic
168/// (and the failure mode) visible at the call site.
169pub trait IntoPoint {
170    fn into_point(self) -> Result<Point>;
171}
172
173impl IntoPoint for Point {
174    fn into_point(self) -> Result<Point> {
175        Ok(self)
176    }
177}
178
179impl IntoPoint for (i32, i32) {
180    fn into_point(self) -> Result<Point> {
181        Ok(Point::new(self.0, self.1))
182    }
183}
184
185impl IntoPoint for &Element {
186    fn into_point(self) -> Result<Point> {
187        point_for(self, Anchor::Center)
188    }
189}
190
191// ── Pointer ─────────────────────────────────────────────────────────
192
193/// A mouse button.
194///
195/// Deliberately **exhaustive**, for the same reason as [`Key`]: every variant
196/// has to be mapped to an X11 button number, an evdev code, a CoreGraphics
197/// event type, and a `MOUSEEVENTF_*` flag pair. Adding a side button (X1/X2)
198/// is real work in four backends — Windows alone expresses them through
199/// `MOUSEEVENTF_XDOWN` plus a `mouseData` field rather than a distinct flag —
200/// and the build should say so.
201#[allow(
202    clippy::exhaustive_enums,
203    reason = "Capability enum: each variant must be mapped by every input \
204              backend, so adding one has to break their builds. See Key."
205)]
206#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
207pub enum MouseButton {
208    #[default]
209    Left,
210    Right,
211    Middle,
212}
213
214/// Direction and magnitude of a scroll event, in platform "ticks" (typically
215/// one notch of a physical scroll wheel). Positive `dy` scrolls content
216/// downward (i.e. moves the viewport up); positive `dx` scrolls right.
217#[allow(
218    clippy::exhaustive_structs,
219    reason = "Closed domain: scrolling has two axes. A third would need a \
220              third scroll axis to exist."
221)]
222#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
223pub struct ScrollDelta {
224    pub dx: i32,
225    pub dy: i32,
226}
227
228impl ScrollDelta {
229    pub const fn new(dx: i32, dy: i32) -> Self {
230        Self { dx, dy }
231    }
232
233    pub const fn vertical(dy: i32) -> Self {
234        Self { dx: 0, dy }
235    }
236
237    pub const fn horizontal(dx: i32) -> Self {
238        Self { dx, dy: 0 }
239    }
240}
241
242// ── Keyboard ────────────────────────────────────────────────────────
243
244/// A keyboard key.
245///
246/// Modifier keys (`Shift`, `Ctrl`, `Alt`, `Meta`) are regular variants of this
247/// enum — they are not a separate type. This matches the physical reality that
248/// modifiers are keys like any other, and the convention of Playwright,
249/// Puppeteer, Selenium, pyautogui, `SendInput`, and `XTest`.
250///
251/// # `Key::Char` semantics
252///
253/// `Key::Char(c)` represents **the physical key labeled with the unshifted
254/// character `c`**. It does **not** auto-synthesise `Shift`. To produce an
255/// uppercase letter or shifted symbol, hold [`Key::Shift`] explicitly:
256///
257/// ```ignore
258/// // Cmd+A (select all):
259/// keyboard.chord(Key::Char('a'), &[Key::Meta]);
260///
261/// // Uppercase 'A':
262/// keyboard.chord(Key::Char('a'), &[Key::Shift]);
263/// ```
264///
265/// For this reason, `Key::Char` **rejects ASCII uppercase letters at the API
266/// boundary** ([`Error::InvalidActionData`]). This prevents the common
267/// footgun where `chord(Key::Char('K'), &[Key::Meta])` is read as "Cmd+K"
268/// but would silently mean "Cmd+Shift+K" under auto-shift semantics.
269///
270/// To type arbitrary text (with IME support and correct case handling), use
271/// [`Keyboard::type_text`] — `Key` is for single-key presses.
272///
273/// # `Meta`
274///
275/// `Meta` is the platform's "command" modifier: Cmd on macOS, Win on Windows,
276/// Super on Linux. Backends are responsible for the platform mapping.
277///
278/// # Extensibility
279///
280/// Deliberately **exhaustive**. This enum names platform capabilities: every
281/// variant has to be translated to a keysym, a virtual-key code, and an evdev
282/// keycode by four separate input backends. Growth here *should* be breaking,
283/// because the compile error in each backend is the only thing that forces a
284/// per-platform decision. `#[non_exhaustive]` would turn "nobody mapped this
285/// key" into a runtime `Error::Unsupported` from a library whose type system
286/// advertises the key — a worse outcome than a build failure at the point the
287/// variant is added.
288#[allow(
289    clippy::exhaustive_enums,
290    reason = "Capability enum: each variant must be mapped by every input \
291              backend, so adding one has to break their builds. See the \
292              Extensibility note above."
293)]
294#[derive(Debug, Clone, PartialEq, Eq, Hash)]
295pub enum Key {
296    /// A printable character (lowercase, no shifted symbols). Backends
297    /// translate this to the matching physical key. See the type-level docs
298    /// for the rationale on rejecting uppercase letters.
299    Char(char),
300
301    // Modifiers (held-key form — combine with other keys via `chord`).
302    Shift,
303    Ctrl,
304    Alt,
305    Meta,
306
307    Enter,
308    Escape,
309    Backspace,
310    Tab,
311    Space,
312    Delete,
313    Insert,
314
315    ArrowUp,
316    ArrowDown,
317    ArrowLeft,
318    ArrowRight,
319
320    Home,
321    End,
322    PageUp,
323    PageDown,
324
325    /// A function key. `n` is 1-based (`F(1)` = F1).
326    F(u8),
327}
328
329impl Key {
330    /// Validate a key for use at the API boundary.
331    ///
332    /// Returns [`Error::InvalidActionData`] for `Key::Char` with an ASCII
333    /// uppercase letter — callers must lowercase and hold [`Key::Shift`]
334    /// explicitly. See the type-level docs.
335    pub(crate) fn validate(&self) -> Result<()> {
336        if let Key::Char(c) = self {
337            if c.is_ascii_uppercase() {
338                return Err(Error::InvalidActionData {
339                    message: format!(
340                        "Key::Char('{c}') is uppercase; use Key::Char('{}') \
341                         with Key::Shift held to produce an uppercase letter",
342                        c.to_ascii_lowercase()
343                    ),
344                });
345            }
346        }
347        Ok(())
348    }
349}
350
351// ── Click / drag option structs ─────────────────────────────────────
352
353/// Options for [`Mouse::click_with`].
354///
355/// `#[non_exhaustive]`: an options struct exists to grow. Start from
356/// [`ClickOptions::new`] and chain only what you need —
357/// `ClickOptions::new().button(MouseButton::Right).count(2)`.
358#[derive(Debug, Clone)]
359#[non_exhaustive]
360pub struct ClickOptions {
361    pub button: MouseButton,
362    /// Number of consecutive clicks (1 = single, 2 = double, …).
363    pub count: u32,
364    /// Keys held (pressed but not released) for the duration of the click —
365    /// typically modifier keys like [`Key::Shift`] or [`Key::Meta`].
366    pub held: Vec<Key>,
367    /// Anchor used when the target is an [`Element`]. Ignored for raw points.
368    pub anchor: Anchor,
369}
370
371impl Default for ClickOptions {
372    fn default() -> Self {
373        Self {
374            button: MouseButton::Left,
375            count: 1,
376            held: Vec::new(),
377            anchor: Anchor::Center,
378        }
379    }
380}
381
382impl ClickOptions {
383    /// Default options: left button, single click, nothing held, centre anchor.
384    pub fn new() -> Self {
385        Self::default()
386    }
387
388    /// Set the button to click with.
389    #[must_use]
390    pub fn button(mut self, button: MouseButton) -> Self {
391        self.button = button;
392        self
393    }
394
395    /// Set the number of consecutive clicks (`2` is a double-click).
396    #[must_use]
397    pub fn count(mut self, count: u32) -> Self {
398        self.count = count;
399        self
400    }
401
402    /// Set the keys held for the duration of the click.
403    #[must_use]
404    pub fn held(mut self, held: impl IntoIterator<Item = Key>) -> Self {
405        self.held = held.into_iter().collect();
406        self
407    }
408
409    /// Set the anchor used when the target is an [`Element`].
410    #[must_use]
411    pub fn anchor(mut self, anchor: Anchor) -> Self {
412        self.anchor = anchor;
413        self
414    }
415}
416
417/// Options for [`Mouse::drag_with`].
418///
419/// `#[non_exhaustive]`: see [`ClickOptions`]. Start from
420/// [`DragOptions::new`] and chain what you need.
421#[derive(Debug, Clone)]
422#[non_exhaustive]
423pub struct DragOptions {
424    pub button: MouseButton,
425    /// Keys held for the duration of the drag.
426    pub held: Vec<Key>,
427    /// Total time over which the drag is performed. Backends interpolate
428    /// pointer movement across this duration.
429    pub duration: Duration,
430}
431
432impl Default for DragOptions {
433    fn default() -> Self {
434        Self {
435            button: MouseButton::Left,
436            held: Vec::new(),
437            duration: Duration::from_millis(150),
438        }
439    }
440}
441
442impl DragOptions {
443    /// Default options: left button, nothing held, 150ms.
444    pub fn new() -> Self {
445        Self::default()
446    }
447
448    /// Set the button held down for the drag.
449    #[must_use]
450    pub fn button(mut self, button: MouseButton) -> Self {
451        self.button = button;
452        self
453    }
454
455    /// Set the keys held for the duration of the drag.
456    #[must_use]
457    pub fn held(mut self, held: impl IntoIterator<Item = Key>) -> Self {
458        self.held = held.into_iter().collect();
459        self
460    }
461
462    /// Set the total time over which the drag is performed.
463    #[must_use]
464    pub fn duration(mut self, duration: Duration) -> Self {
465        self.duration = duration;
466        self
467    }
468}
469
470// ── Backend trait ───────────────────────────────────────────────────
471
472/// Platform backend trait for synthesised user input.
473///
474/// Implementors generate OS-level pointer and keyboard events. Most methods
475/// correspond to a single low-level operation; a few (marked "provided") are
476/// synthesised by default but may be overridden when a platform has a
477/// higher-fidelity primitive.
478///
479/// **This trait is intentionally separate from [`crate::Provider`].** A
480/// backend that only knows how to read the accessibility tree should not
481/// implement `InputProvider`, and vice versa. Crates may implement both for
482/// the same platform but the two surfaces never call into each other.
483///
484/// # Errors
485///
486/// Implementations should return:
487/// - [`Error::PermissionDenied`] when the OS denies the synthesis permission.
488/// - [`Error::Unsupported`] when the operation has no platform implementation
489///   (e.g. pointer warp on a session that disallows it). Do **not** silently
490///   degrade — surface the missing capability per Tenet 1.
491/// - [`Error::Platform`] for raw OS failures.
492pub trait InputProvider: Send + Sync {
493    // ── Pointer (required) ──────────────────────────────────────────
494
495    /// Move the pointer to `to` without pressing any buttons.
496    fn pointer_move(&self, to: Point) -> Result<()>;
497
498    /// Press `button` at the current pointer location (no release).
499    fn pointer_down(&self, button: MouseButton) -> Result<()>;
500
501    /// Release `button` at the current pointer location.
502    fn pointer_up(&self, button: MouseButton) -> Result<()>;
503
504    /// Click `button` at `at`, repeated `count` times. The backend is
505    /// responsible for honouring the OS double-click interval when
506    /// `count > 1` and for any platform-specific click-state bookkeeping
507    /// (e.g. `kCGMouseEventClickState` on macOS).
508    fn pointer_click(&self, at: Point, button: MouseButton, count: u32) -> Result<()>;
509
510    /// Scroll by `delta` ticks at `at`.
511    fn pointer_scroll(&self, at: Point, delta: ScrollDelta) -> Result<()>;
512
513    // ── Keyboard (required) ─────────────────────────────────────────
514
515    /// Press `key` (no release). Use [`key_up`](Self::key_up) to release.
516    ///
517    /// Modifiers are just keys: hold `Key::Shift` via `key_down(&Key::Shift)`.
518    fn key_down(&self, key: &Key) -> Result<()>;
519
520    /// Release `key`.
521    fn key_up(&self, key: &Key) -> Result<()>;
522
523    /// Type `text` as literal user input.
524    ///
525    /// Backends should prefer the OS's text-input path (with IME support)
526    /// over synthesising individual key presses where possible.
527    fn type_text(&self, text: &str) -> Result<()>;
528
529    // ── Pointer (provided, override for platform fidelity) ──────────
530
531    /// Press `button` at `from`, interpolate to `to` over `duration`, release.
532    ///
533    /// The default synthesis posts `pointer_down` → a series of `pointer_move`
534    /// calls (≈60 Hz cadence) → `pointer_up`. Backends **should override** to
535    /// emit platform-specific drag events where they differ from move events
536    /// — on macOS, drag-and-drop source apps filter for
537    /// `kCGEventLeftMouseDragged`, which is distinct from
538    /// `kCGEventMouseMoved`. On Windows and X11 the default synthesis is
539    /// usually sufficient.
540    fn pointer_drag(
541        &self,
542        from: Point,
543        to: Point,
544        button: MouseButton,
545        duration: Duration,
546    ) -> Result<()> {
547        const STEP: Duration = Duration::from_millis(16);
548        self.pointer_move(from)?;
549        self.pointer_down(button)?;
550        let steps = (duration.as_millis() / STEP.as_millis().max(1)).max(1) as i32;
551        for i in 1..=steps {
552            let t = i as f64 / steps as f64;
553            let x = from.x + ((to.x - from.x) as f64 * t).round() as i32;
554            let y = from.y + ((to.y - from.y) as f64 * t).round() as i32;
555            self.pointer_move(Point::new(x, y))?;
556            if i < steps {
557                std::thread::sleep(STEP);
558            }
559        }
560        self.pointer_up(button)
561    }
562}
563
564// ── Public façade ───────────────────────────────────────────────────
565
566/// Synthesises OS-level pointer and keyboard events.
567///
568/// `InputSim` is a thin façade over an [`InputProvider`] backend. Methods are
569/// organised by input device: [`InputSim::mouse`] returns a [`Mouse`] handle
570/// with pointer operations, [`InputSim::keyboard`] returns a [`Keyboard`]
571/// handle with key operations. This structure matches Playwright and
572/// Puppeteer's `page.mouse.*` / `page.keyboard.*` layout and keeps the combo
573/// verbs (`click`, `press`) unambiguous even though `Element::press` exists
574/// at the a11y layer.
575///
576/// Use this only when the accessibility action layer cannot express the
577/// interaction you need — see the [module docs](self) for the rationale.
578///
579/// `InputSim` is cheap to clone (it shares the backend via `Arc`).
580///
581/// # Example
582///
583/// ```ignore
584/// # use xa11y_core::{input::*, Element};
585/// # fn go(sim: InputSim, button: Element) -> xa11y_core::Result<()> {
586/// sim.mouse().click(&button)?;
587/// sim.keyboard().chord(Key::Char('a'), &[Key::Meta])?; // Cmd/Ctrl+A
588/// sim.keyboard().type_text("hello")?;
589/// # Ok(()) }
590/// ```
591#[derive(Clone)]
592pub struct InputSim {
593    backend: Arc<dyn InputProvider>,
594}
595
596impl InputSim {
597    /// Build an `InputSim` over an explicit input backend.
598    ///
599    /// Most callers should use the platform-detected singleton instead —
600    /// `xa11y::input_sim()` in Rust, `input_sim()` / `inputSim()` in the
601    /// bindings. This constructor exists for tests and for embedders that
602    /// supply their own [`InputProvider`].
603    pub fn new(backend: Arc<dyn InputProvider>) -> Self {
604        Self { backend }
605    }
606
607    /// Get the backing provider for advanced or composite sequences.
608    pub fn backend(&self) -> &Arc<dyn InputProvider> {
609        &self.backend
610    }
611
612    /// Handle for pointer operations.
613    pub fn mouse(&self) -> Mouse<'_> {
614        Mouse {
615            backend: &self.backend,
616        }
617    }
618
619    /// Handle for keyboard operations.
620    pub fn keyboard(&self) -> Keyboard<'_> {
621        Keyboard {
622            backend: &self.backend,
623        }
624    }
625
626    /// Resolve an element's current bounds to a screen point using `anchor`.
627    /// Equivalent to the free function [`point_for`].
628    pub fn point_for(&self, element: &Element, anchor: Anchor) -> Result<Point> {
629        point_for(element, anchor)
630    }
631}
632
633/// Pointer operations. Obtain via [`InputSim::mouse`].
634pub struct Mouse<'a> {
635    backend: &'a Arc<dyn InputProvider>,
636}
637
638impl Mouse<'_> {
639    /// Left-click `target` once at its [`Anchor::Center`] (for elements) or
640    /// at the literal point.
641    pub fn click(&self, target: impl IntoPoint) -> Result<()> {
642        let pt = target.into_point()?;
643        self.backend.pointer_click(pt, MouseButton::Left, 1)
644    }
645
646    /// Click with explicit options (button, count, held keys, anchor).
647    ///
648    /// `opts.anchor` is used only when `target` is an [`Element`]; for raw
649    /// points it is ignored.
650    pub fn click_with(&self, target: ClickTarget<'_>, opts: ClickOptions) -> Result<()> {
651        for k in &opts.held {
652            k.validate()?;
653        }
654        let pt = match target {
655            ClickTarget::Point(p) => p,
656            ClickTarget::Element(el) => point_for(el, opts.anchor)?,
657        };
658        with_keys_held(self.backend.as_ref(), &opts.held, || {
659            self.backend.pointer_click(pt, opts.button, opts.count)
660        })
661    }
662
663    /// Convenience for a left double-click at `target`.
664    pub fn double_click(&self, target: impl IntoPoint) -> Result<()> {
665        let pt = target.into_point()?;
666        self.backend.pointer_click(pt, MouseButton::Left, 2)
667    }
668
669    /// Convenience for a right-click at `target`.
670    pub fn right_click(&self, target: impl IntoPoint) -> Result<()> {
671        let pt = target.into_point()?;
672        self.backend.pointer_click(pt, MouseButton::Right, 1)
673    }
674
675    /// Press `button` at the current pointer location (no release).
676    pub fn down(&self, button: MouseButton) -> Result<()> {
677        self.backend.pointer_down(button)
678    }
679
680    /// Release `button` at the current pointer location.
681    pub fn up(&self, button: MouseButton) -> Result<()> {
682        self.backend.pointer_up(button)
683    }
684
685    /// Move the pointer to `target` without pressing any buttons.
686    pub fn move_to(&self, target: impl IntoPoint) -> Result<()> {
687        let pt = target.into_point()?;
688        self.backend.pointer_move(pt)
689    }
690
691    /// Press the left button at `from`, move to `to`, release. Default
692    /// duration: 150 ms. Use [`drag_with`](Self::drag_with) to customise.
693    pub fn drag(&self, from: impl IntoPoint, to: impl IntoPoint) -> Result<()> {
694        let from = from.into_point()?;
695        let to = to.into_point()?;
696        self.backend
697            .pointer_drag(from, to, MouseButton::Left, Duration::from_millis(150))
698    }
699
700    /// Drag with explicit options.
701    pub fn drag_with(
702        &self,
703        from: impl IntoPoint,
704        to: impl IntoPoint,
705        opts: DragOptions,
706    ) -> Result<()> {
707        for k in &opts.held {
708            k.validate()?;
709        }
710        let from = from.into_point()?;
711        let to = to.into_point()?;
712        with_keys_held(self.backend.as_ref(), &opts.held, || {
713            self.backend
714                .pointer_drag(from, to, opts.button, opts.duration)
715        })
716    }
717
718    /// Scroll at `target` by `delta` ticks.
719    pub fn scroll(&self, target: impl IntoPoint, delta: ScrollDelta) -> Result<()> {
720        let pt = target.into_point()?;
721        self.backend.pointer_scroll(pt, delta)
722    }
723}
724
725/// Keyboard operations. Obtain via [`InputSim::keyboard`].
726pub struct Keyboard<'a> {
727    backend: &'a Arc<dyn InputProvider>,
728}
729
730impl Keyboard<'_> {
731    /// Tap `key` (press + release) with no other keys held.
732    pub fn press(&self, key: Key) -> Result<()> {
733        key.validate()?;
734        self.backend.key_down(&key)?;
735        self.backend.key_up(&key)
736    }
737
738    /// Tap `key` while `held` are held down.
739    ///
740    /// Modifiers are ordinary keys in this API — pass `Key::Shift`,
741    /// `Key::Ctrl`, `Key::Alt`, or `Key::Meta` via `held`.
742    ///
743    /// ```ignore
744    /// // Cmd/Ctrl+A:
745    /// keyboard.chord(Key::Char('a'), &[Key::Meta])?;
746    /// ```
747    pub fn chord(&self, key: Key, held: &[Key]) -> Result<()> {
748        key.validate()?;
749        for k in held {
750            k.validate()?;
751        }
752        with_keys_held(self.backend.as_ref(), held, || {
753            self.backend.key_down(&key)?;
754            self.backend.key_up(&key)
755        })
756    }
757
758    /// Press `key` without releasing. Pair with [`up`](Self::up).
759    pub fn down(&self, key: Key) -> Result<()> {
760        key.validate()?;
761        self.backend.key_down(&key)
762    }
763
764    /// Release a previously pressed key.
765    pub fn up(&self, key: Key) -> Result<()> {
766        key.validate()?;
767        self.backend.key_up(&key)
768    }
769
770    /// Type literal text into whichever element currently has keyboard focus.
771    ///
772    /// `Keyboard` does not focus the target for you — call the appropriate
773    /// accessibility action (e.g. `Element::focus` via the provider) first.
774    ///
775    /// Unlike [`press`](Self::press), this accepts any text (including
776    /// uppercase and shifted symbols); backends handle the case/shift synthesis.
777    pub fn type_text(&self, text: &str) -> Result<()> {
778        self.backend.type_text(text)
779    }
780}
781
782/// Explicit target for [`Mouse::click_with`]: either a raw point or an
783/// element to anchor against.
784#[allow(
785    clippy::exhaustive_enums,
786    reason = "Closed domain: a click lands at a screen position, which the \
787              caller either states outright or derives from an element's \
788              bounds. Locator is deliberately not a third variant — see the \
789              IntoPoint docs on why resolving stays explicit."
790)]
791pub enum ClickTarget<'a> {
792    Point(Point),
793    Element(&'a Element),
794}
795
796impl From<Point> for ClickTarget<'_> {
797    fn from(p: Point) -> Self {
798        Self::Point(p)
799    }
800}
801
802impl From<(i32, i32)> for ClickTarget<'_> {
803    fn from(t: (i32, i32)) -> Self {
804        Self::Point(Point::new(t.0, t.1))
805    }
806}
807
808impl<'a> From<&'a Element> for ClickTarget<'a> {
809    fn from(el: &'a Element) -> Self {
810        Self::Element(el)
811    }
812}
813
814/// Run `body` with each key in `keys` held down, releasing them all (in
815/// reverse order) before returning. Errors during release are returned only
816/// when `body` succeeded — a body failure takes precedence.
817fn with_keys_held<F>(backend: &dyn InputProvider, keys: &[Key], body: F) -> Result<()>
818where
819    F: FnOnce() -> Result<()>,
820{
821    for k in keys {
822        backend.key_down(k)?;
823    }
824    let result = body();
825    let mut release_err: Option<Error> = None;
826    for k in keys.iter().rev() {
827        if let Err(e) = backend.key_up(k) {
828            // Keep the first release error so we can surface it if the body
829            // succeeded; if the body already failed, the body error wins.
830            if release_err.is_none() {
831                release_err = Some(e);
832            }
833        }
834    }
835    match (result, release_err) {
836        (Err(e), _) => Err(e),
837        (Ok(()), Some(e)) => Err(e),
838        (Ok(()), None) => Ok(()),
839    }
840}
841
842#[cfg(test)]
843mod point_scale_tests {
844    use super::{anchor_point, Anchor, Point};
845    use crate::element::Rect;
846
847    #[test]
848    fn scale_one_is_identity() {
849        let p = Point::new(100, -50);
850        assert_eq!(p.to_physical(1.0), p);
851        assert_eq!(p.to_logical(1.0), p);
852    }
853
854    #[test]
855    fn logical_to_physical_scales() {
856        assert_eq!(Point::new(100, 200).to_physical(1.5), Point::new(150, 300));
857        assert_eq!(Point::new(-40, 10).to_physical(2.0), Point::new(-80, 20));
858    }
859
860    #[test]
861    fn physical_to_logical_is_inverse() {
862        assert_eq!(Point::new(150, 300).to_logical(1.5), Point::new(100, 200));
863    }
864
865    #[test]
866    fn bad_scale_degrades_to_identity() {
867        let p = Point::new(7, 9);
868        assert_eq!(p.to_physical(0.0), p);
869        assert_eq!(p.to_physical(f64::NAN), p);
870    }
871
872    #[test]
873    fn anchor_center_of_logical_bounds_scales_to_physical() {
874        // A button whose logical bounds are 200x40 at (100, 100). Its centre
875        // is (200, 120) logically; on a 2x display the physical pixel is
876        // (400, 240) — what an input backend would feed to the OS.
877        let bounds = Rect {
878            x: 100,
879            y: 100,
880            width: 200,
881            height: 40,
882        };
883        let center = anchor_point(&bounds, Anchor::Center);
884        assert_eq!(center, Point::new(200, 120));
885        assert_eq!(center.to_physical(2.0), Point::new(400, 240));
886    }
887
888    #[test]
889    fn corner_anchors_stay_inside_half_open_bounds() {
890        let bounds = Rect {
891            x: 10,
892            y: 20,
893            width: 3,
894            height: 2,
895        };
896        assert_eq!(anchor_point(&bounds, Anchor::TopRight), Point::new(12, 20));
897        assert_eq!(
898            anchor_point(&bounds, Anchor::BottomLeft),
899            Point::new(10, 21)
900        );
901        assert_eq!(
902            anchor_point(&bounds, Anchor::BottomRight),
903            Point::new(12, 21)
904        );
905    }
906}