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}