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    pub fn new(backend: Arc<dyn InputProvider>) -> Self {
473        Self { backend }
474    }
475
476    /// Get the backing provider for advanced or composite sequences.
477    pub fn backend(&self) -> &Arc<dyn InputProvider> {
478        &self.backend
479    }
480
481    /// Handle for pointer operations.
482    pub fn mouse(&self) -> Mouse<'_> {
483        Mouse {
484            backend: &self.backend,
485        }
486    }
487
488    /// Handle for keyboard operations.
489    pub fn keyboard(&self) -> Keyboard<'_> {
490        Keyboard {
491            backend: &self.backend,
492        }
493    }
494
495    /// Resolve an element's current bounds to a screen point using `anchor`.
496    /// Equivalent to the free function [`point_for`].
497    pub fn point_for(&self, element: &Element, anchor: Anchor) -> Result<Point> {
498        point_for(element, anchor)
499    }
500}
501
502/// Pointer operations. Obtain via [`InputSim::mouse`].
503pub struct Mouse<'a> {
504    backend: &'a Arc<dyn InputProvider>,
505}
506
507impl Mouse<'_> {
508    /// Left-click `target` once at its [`Anchor::Center`] (for elements) or
509    /// at the literal point.
510    pub fn click(&self, target: impl IntoPoint) -> Result<()> {
511        let pt = target.into_point()?;
512        self.backend.pointer_click(pt, MouseButton::Left, 1)
513    }
514
515    /// Click with explicit options (button, count, held keys, anchor).
516    ///
517    /// `opts.anchor` is used only when `target` is an [`Element`]; for raw
518    /// points it is ignored.
519    pub fn click_with(&self, target: ClickTarget<'_>, opts: ClickOptions) -> Result<()> {
520        for k in &opts.held {
521            k.validate()?;
522        }
523        let pt = match target {
524            ClickTarget::Point(p) => p,
525            ClickTarget::Element(el) => point_for(el, opts.anchor)?,
526        };
527        with_keys_held(self.backend.as_ref(), &opts.held, || {
528            self.backend.pointer_click(pt, opts.button, opts.count)
529        })
530    }
531
532    /// Convenience for a left double-click at `target`.
533    pub fn double_click(&self, target: impl IntoPoint) -> Result<()> {
534        let pt = target.into_point()?;
535        self.backend.pointer_click(pt, MouseButton::Left, 2)
536    }
537
538    /// Convenience for a right-click at `target`.
539    pub fn right_click(&self, target: impl IntoPoint) -> Result<()> {
540        let pt = target.into_point()?;
541        self.backend.pointer_click(pt, MouseButton::Right, 1)
542    }
543
544    /// Press `button` at the current pointer location (no release).
545    pub fn down(&self, button: MouseButton) -> Result<()> {
546        self.backend.pointer_down(button)
547    }
548
549    /// Release `button` at the current pointer location.
550    pub fn up(&self, button: MouseButton) -> Result<()> {
551        self.backend.pointer_up(button)
552    }
553
554    /// Move the pointer to `target` without pressing any buttons.
555    pub fn move_to(&self, target: impl IntoPoint) -> Result<()> {
556        let pt = target.into_point()?;
557        self.backend.pointer_move(pt)
558    }
559
560    /// Press the left button at `from`, move to `to`, release. Default
561    /// duration: 150 ms. Use [`drag_with`](Self::drag_with) to customise.
562    pub fn drag(&self, from: impl IntoPoint, to: impl IntoPoint) -> Result<()> {
563        let from = from.into_point()?;
564        let to = to.into_point()?;
565        self.backend
566            .pointer_drag(from, to, MouseButton::Left, Duration::from_millis(150))
567    }
568
569    /// Drag with explicit options.
570    pub fn drag_with(
571        &self,
572        from: impl IntoPoint,
573        to: impl IntoPoint,
574        opts: DragOptions,
575    ) -> Result<()> {
576        for k in &opts.held {
577            k.validate()?;
578        }
579        let from = from.into_point()?;
580        let to = to.into_point()?;
581        with_keys_held(self.backend.as_ref(), &opts.held, || {
582            self.backend
583                .pointer_drag(from, to, opts.button, opts.duration)
584        })
585    }
586
587    /// Scroll at `target` by `delta` ticks.
588    pub fn scroll(&self, target: impl IntoPoint, delta: ScrollDelta) -> Result<()> {
589        let pt = target.into_point()?;
590        self.backend.pointer_scroll(pt, delta)
591    }
592}
593
594/// Keyboard operations. Obtain via [`InputSim::keyboard`].
595pub struct Keyboard<'a> {
596    backend: &'a Arc<dyn InputProvider>,
597}
598
599impl Keyboard<'_> {
600    /// Tap `key` (press + release) with no other keys held.
601    pub fn press(&self, key: Key) -> Result<()> {
602        key.validate()?;
603        self.backend.key_down(&key)?;
604        self.backend.key_up(&key)
605    }
606
607    /// Tap `key` while `held` are held down.
608    ///
609    /// Modifiers are ordinary keys in this API — pass `Key::Shift`,
610    /// `Key::Ctrl`, `Key::Alt`, or `Key::Meta` via `held`.
611    ///
612    /// ```ignore
613    /// // Cmd/Ctrl+A:
614    /// keyboard.chord(Key::Char('a'), &[Key::Meta])?;
615    /// ```
616    pub fn chord(&self, key: Key, held: &[Key]) -> Result<()> {
617        key.validate()?;
618        for k in held {
619            k.validate()?;
620        }
621        with_keys_held(self.backend.as_ref(), held, || {
622            self.backend.key_down(&key)?;
623            self.backend.key_up(&key)
624        })
625    }
626
627    /// Press `key` without releasing. Pair with [`up`](Self::up).
628    pub fn down(&self, key: Key) -> Result<()> {
629        key.validate()?;
630        self.backend.key_down(&key)
631    }
632
633    /// Release a previously pressed key.
634    pub fn up(&self, key: Key) -> Result<()> {
635        key.validate()?;
636        self.backend.key_up(&key)
637    }
638
639    /// Type literal text into whichever element currently has keyboard focus.
640    ///
641    /// `Keyboard` does not focus the target for you — call the appropriate
642    /// accessibility action (e.g. `Element::focus` via the provider) first.
643    ///
644    /// Unlike [`press`](Self::press), this accepts any text (including
645    /// uppercase and shifted symbols); backends handle the case/shift synthesis.
646    pub fn type_text(&self, text: &str) -> Result<()> {
647        self.backend.type_text(text)
648    }
649}
650
651/// Explicit target for [`Mouse::click_with`]: either a raw point or an
652/// element to anchor against.
653pub enum ClickTarget<'a> {
654    Point(Point),
655    Element(&'a Element),
656}
657
658impl From<Point> for ClickTarget<'_> {
659    fn from(p: Point) -> Self {
660        Self::Point(p)
661    }
662}
663
664impl From<(i32, i32)> for ClickTarget<'_> {
665    fn from(t: (i32, i32)) -> Self {
666        Self::Point(Point::new(t.0, t.1))
667    }
668}
669
670impl<'a> From<&'a Element> for ClickTarget<'a> {
671    fn from(el: &'a Element) -> Self {
672        Self::Element(el)
673    }
674}
675
676/// Run `body` with each key in `keys` held down, releasing them all (in
677/// reverse order) before returning. Errors during release are returned only
678/// when `body` succeeded — a body failure takes precedence.
679fn with_keys_held<F>(backend: &dyn InputProvider, keys: &[Key], body: F) -> Result<()>
680where
681    F: FnOnce() -> Result<()>,
682{
683    for k in keys {
684        backend.key_down(k)?;
685    }
686    let result = body();
687    let mut release_err: Option<Error> = None;
688    for k in keys.iter().rev() {
689        if let Err(e) = backend.key_up(k) {
690            // Keep the first release error so we can surface it if the body
691            // succeeded; if the body already failed, the body error wins.
692            if release_err.is_none() {
693                release_err = Some(e);
694            }
695        }
696    }
697    match (result, release_err) {
698        (Err(e), _) => Err(e),
699        (Ok(()), Some(e)) => Err(e),
700        (Ok(()), None) => Ok(()),
701    }
702}
703
704#[cfg(test)]
705mod point_scale_tests {
706    use super::{anchor_point, Anchor, Point};
707    use crate::element::Rect;
708
709    #[test]
710    fn scale_one_is_identity() {
711        let p = Point::new(100, -50);
712        assert_eq!(p.to_physical(1.0), p);
713        assert_eq!(p.to_logical(1.0), p);
714    }
715
716    #[test]
717    fn logical_to_physical_scales() {
718        assert_eq!(Point::new(100, 200).to_physical(1.5), Point::new(150, 300));
719        assert_eq!(Point::new(-40, 10).to_physical(2.0), Point::new(-80, 20));
720    }
721
722    #[test]
723    fn physical_to_logical_is_inverse() {
724        assert_eq!(Point::new(150, 300).to_logical(1.5), Point::new(100, 200));
725    }
726
727    #[test]
728    fn bad_scale_degrades_to_identity() {
729        let p = Point::new(7, 9);
730        assert_eq!(p.to_physical(0.0), p);
731        assert_eq!(p.to_physical(f64::NAN), p);
732    }
733
734    #[test]
735    fn anchor_center_of_logical_bounds_scales_to_physical() {
736        // A button whose logical bounds are 200x40 at (100, 100). Its centre
737        // is (200, 120) logically; on a 2x display the physical pixel is
738        // (400, 240) — what an input backend would feed to the OS.
739        let bounds = Rect {
740            x: 100,
741            y: 100,
742            width: 200,
743            height: 40,
744        };
745        let center = anchor_point(&bounds, Anchor::Center);
746        assert_eq!(center, Point::new(200, 120));
747        assert_eq!(center.to_physical(2.0), Point::new(400, 240));
748    }
749}