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}