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}