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