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