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