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}