Skip to main content

incular_runtime/
simulation.rs

1//! High-level, in-process interaction and capture for Incular applications.
2//!
3//! The simulator is deliberately below the native desktop boundary. It sends
4//! the same normalized [`InputEvent`] values that a platform adapter sends,
5//! after resolving semantic labels to retained layout bounds. No OS cursor,
6//! keyboard device, focus, or desktop screenshot API is involved.
7
8use super::{Application, WindowHandle, profiling::GpuResourceSummary};
9use incular_config::Constraints;
10use incular_core::time::Instant;
11use incular_core::{
12    Code, InputEvent, KeyState, KeyboardEvent, KeyboardKey, Location, Modifiers, NamedKey, Offset,
13    PointerPhase, Rect,
14};
15use incular_platform::WindowId;
16use incular_semantics::{SemanticAction, SemanticActionKind};
17use std::collections::{HashMap, HashSet};
18use std::sync::atomic::{AtomicBool, Ordering};
19use std::sync::{Arc, Mutex, mpsc};
20use std::time::Duration;
21
22const SIMULATION_REPLY_TIMEOUT: Duration = Duration::from_secs(30);
23const MAX_SIMULATION_REQUESTS_PER_TURN: usize = 64;
24
25/// A screenshot of an Incular window's rendered surface.
26///
27/// Pixels are tightly packed, top-to-bottom, straight-alpha RGBA8 values.
28/// This is an application frame, not a capture of the desktop or another
29/// application.
30#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct Screenshot {
32    width: u32,
33    height: u32,
34    rgba8: Vec<u8>,
35}
36
37impl Screenshot {
38    /// Creates a screenshot from tightly packed RGBA8 pixels.
39    pub fn from_rgba8(width: u32, height: u32, rgba8: Vec<u8>) -> Result<Self, SimulationError> {
40        let expected = usize::try_from(width)
41            .ok()
42            .and_then(|width| {
43                usize::try_from(height)
44                    .ok()
45                    .and_then(|height| width.checked_mul(height))
46            })
47            .and_then(|pixels| pixels.checked_mul(4))
48            .ok_or_else(|| {
49                SimulationError::InvalidInput("screenshot dimensions overflow".into())
50            })?;
51        if expected == 0 || rgba8.len() != expected {
52            return Err(SimulationError::InvalidInput(format!(
53                "screenshot pixel length {} does not match {}x{} RGBA8",
54                rgba8.len(),
55                width,
56                height
57            )));
58        }
59        Ok(Self {
60            width,
61            height,
62            rgba8,
63        })
64    }
65
66    #[must_use]
67    pub const fn width(&self) -> u32 {
68        self.width
69    }
70
71    #[must_use]
72    pub const fn height(&self) -> u32 {
73        self.height
74    }
75
76    /// Returns the tightly packed RGBA8 pixel bytes.
77    #[must_use]
78    pub fn pixels(&self) -> &[u8] {
79        &self.rgba8
80    }
81
82    /// Consumes the screenshot and returns its tightly packed RGBA8 pixels.
83    #[must_use]
84    pub fn into_pixels(self) -> Vec<u8> {
85        self.rgba8
86    }
87}
88
89/// Failure returned by an in-process simulation command.
90#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
91pub enum SimulationError {
92    /// The target application's event loop is not running, has stopped, or
93    /// the application was dropped before the request was serviced.
94    #[error("the Incular simulation bridge is disconnected")]
95    Disconnected,
96    /// The target event loop did not service the request in time.
97    #[error("the Incular simulation request timed out")]
98    Timeout,
99    /// The requested retained window no longer exists.
100    #[error("simulation window {0} was not found")]
101    WindowNotFound(WindowId),
102    /// The requested retained window was closed while the command was queued.
103    #[error("simulation window {0} was closed")]
104    WindowClosed(WindowId),
105    /// No semantic node with the requested exact label was found.
106    #[error("no semantic element labeled {0:?} was found")]
107    ElementNotFound(String),
108    /// The command contained a value that cannot represent native input.
109    #[error("invalid simulation input: {0}")]
110    InvalidInput(String),
111    /// The renderer could not produce a window frame for a capture request.
112    #[error("window capture unavailable: {0}")]
113    CaptureUnavailable(String),
114    /// The runtime or renderer failed while servicing a frame request.
115    #[error("simulation frame failed: {0}")]
116    FrameFailed(String),
117    /// The application panicked on its UI thread; the message is the panic's.
118    /// Every later call returns it too, so a script stops at the failure
119    /// instead of reading a stale frame.
120    #[error("the application panicked: {0}")]
121    AppPanicked(String),
122}
123
124type Reply<T> = mpsc::SyncSender<Result<T, SimulationError>>;
125
126/// Coordinate space of a simulated pointer position.
127///
128/// Layout, semantics and pointer events use logical pixels;
129/// [`Simulation::capture`] returns physical pixels. A physical point is
130/// divided by the window's current scale factor on the UI thread, so the
131/// conversion always uses the scale the frame was laid out with.
132#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
133pub enum PointSpace {
134    /// Logical pixels: the space of layout bounds and widget sizes.
135    #[default]
136    Logical,
137    /// Physical pixels: the space of [`Screenshot`] pixels.
138    Physical,
139}
140
141pub(crate) enum SimulationRequest {
142    Input {
143        window_id: WindowId,
144        event: InputEvent,
145        reply: Reply<()>,
146    },
147    ClickAt {
148        window_id: WindowId,
149        position: Offset,
150        space: PointSpace,
151        reply: Reply<()>,
152    },
153    ScaleFactor {
154        window_id: WindowId,
155        reply: Reply<f64>,
156    },
157    ClickLabel {
158        window_id: WindowId,
159        label: String,
160        reply: Reply<()>,
161    },
162    FocusLabel {
163        window_id: WindowId,
164        label: String,
165        reply: Reply<()>,
166    },
167    WaitForFrame {
168        window_id: WindowId,
169        reply: Reply<()>,
170    },
171    Capture {
172        window_id: WindowId,
173        reply: Reply<Screenshot>,
174    },
175    GpuResources {
176        window_id: WindowId,
177        reply: Reply<GpuResourceSummary>,
178    },
179}
180
181pub(crate) struct SimulationBridge {
182    sender: mpsc::Sender<SimulationRequest>,
183    wake: Mutex<Option<Arc<dyn super::RuntimeWake>>>,
184    accepting: AtomicBool,
185    /// The message of a panic on the application's UI thread.
186    panic: Arc<Mutex<Option<String>>>,
187}
188
189thread_local! {
190    /// Panic slots of the simulation bridges created on this thread (the
191    /// UI thread of their applications).
192    static PANIC_SLOTS: std::cell::RefCell<Vec<std::sync::Weak<Mutex<Option<String>>>>> =
193        const { std::cell::RefCell::new(Vec::new()) };
194}
195
196/// Records a UI-thread panic in the bridges created on the panicking
197/// thread, then runs the previous hook. Installed once per process.
198fn install_panic_recorder() {
199    static INSTALL: std::sync::Once = std::sync::Once::new();
200    INSTALL.call_once(|| {
201        let previous = std::panic::take_hook();
202        std::panic::set_hook(Box::new(move |info| {
203            let message = info
204                .payload()
205                .downcast_ref::<&str>()
206                .map(|message| (*message).to_owned())
207                .or_else(|| info.payload().downcast_ref::<String>().cloned())
208                .unwrap_or_else(|| "non-string panic payload".to_owned());
209            let message = match info.location() {
210                Some(location) => format!("{message} at {location}"),
211                None => message,
212            };
213            let _ = PANIC_SLOTS.try_with(|slots| {
214                slots.borrow_mut().retain(|slot| {
215                    let Some(slot) = slot.upgrade() else {
216                        return false;
217                    };
218                    if let Ok(mut recorded) = slot.lock() {
219                        recorded.get_or_insert_with(|| message.clone());
220                    }
221                    true
222                });
223            });
224            previous(info);
225        }));
226    });
227}
228
229impl SimulationBridge {
230    pub(crate) fn new(sender: mpsc::Sender<SimulationRequest>) -> Self {
231        install_panic_recorder();
232        let panic = Arc::new(Mutex::new(None));
233        let _ = PANIC_SLOTS.try_with(|slots| slots.borrow_mut().push(Arc::downgrade(&panic)));
234        Self {
235            sender,
236            wake: Mutex::new(None),
237            accepting: AtomicBool::new(true),
238            panic,
239        }
240    }
241
242    /// The recorded UI-thread panic, as the error to report.
243    fn panicked(&self) -> Option<SimulationError> {
244        self.panic
245            .lock()
246            .ok()
247            .and_then(|recorded| recorded.clone())
248            .map(SimulationError::AppPanicked)
249    }
250
251    fn enqueue(&self, request: SimulationRequest) -> Result<(), SimulationError> {
252        if let Some(error) = self.panicked() {
253            return Err(error);
254        }
255        if !self.accepting.load(Ordering::Acquire) {
256            return Err(SimulationError::Disconnected);
257        }
258        self.sender
259            .send(request)
260            .map_err(|_| SimulationError::Disconnected)?;
261        if let Some(wake) = self.wake.lock().expect("simulation wake mutex").as_ref() {
262            wake.wake();
263        }
264        Ok(())
265    }
266
267    pub(crate) fn set_wake(&self, wake: Arc<dyn super::RuntimeWake>) {
268        *self.wake.lock().expect("simulation wake mutex") = Some(wake);
269    }
270
271    pub(crate) fn stop(&self) {
272        self.accepting.store(false, Ordering::Release);
273    }
274}
275
276#[derive(Default)]
277struct KeyboardState {
278    modifiers: Modifiers,
279    pressed: HashSet<Code>,
280}
281
282/// Owns per-window simulation waiter settlement: frame, capture, and
283/// GPU-resource replies. Every settle path removes a window's waiters
284/// before replying, so each waiter settles exactly once and window
285/// close/shutdown settle all three kinds together. Frame failure leaves
286/// GPU queries pending by design — they hold no frame debt and resolve
287/// through maintenance, close, or shutdown instead.
288#[derive(Default)]
289pub(crate) struct SimulationWaiters {
290    frame: HashMap<WindowId, Vec<mpsc::SyncSender<Result<(), SimulationError>>>>,
291    capture: HashMap<WindowId, Vec<mpsc::SyncSender<Result<Screenshot, SimulationError>>>>,
292    gpu: HashMap<WindowId, Vec<mpsc::SyncSender<Result<GpuResourceSummary, SimulationError>>>>,
293}
294
295impl SimulationWaiters {
296    fn settle<T: Clone>(
297        waiters: &mut HashMap<WindowId, Vec<mpsc::SyncSender<Result<T, SimulationError>>>>,
298        window_id: WindowId,
299        result: Result<T, SimulationError>,
300    ) {
301        if let Some(pending) = waiters.remove(&window_id) {
302            for reply in pending {
303                let _ = reply.send(result.clone());
304            }
305        }
306    }
307
308    /// Reports whether a simulator is waiting for the next presented frame.
309    #[must_use]
310    pub(crate) fn frame_pending(&self, window_id: WindowId) -> bool {
311        self.frame
312            .get(&window_id)
313            .is_some_and(|waiters| !waiters.is_empty())
314    }
315
316    /// Reports whether a simulator is waiting for a capture.
317    #[must_use]
318    pub(crate) fn capture_pending(&self, window_id: WindowId) -> bool {
319        self.capture
320            .get(&window_id)
321            .is_some_and(|waiters| !waiters.is_empty())
322    }
323
324    /// Registers a frame waiter enlisted by simulation request handling.
325    pub(crate) fn insert_frame(
326        &mut self,
327        window_id: WindowId,
328        reply: mpsc::SyncSender<Result<(), SimulationError>>,
329    ) {
330        self.frame.entry(window_id).or_default().push(reply);
331    }
332
333    /// Registers a capture waiter enlisted by simulation request handling.
334    pub(crate) fn insert_capture(
335        &mut self,
336        window_id: WindowId,
337        reply: mpsc::SyncSender<Result<Screenshot, SimulationError>>,
338    ) {
339        self.capture.entry(window_id).or_default().push(reply);
340    }
341
342    /// Registers a GPU-resource waiter enlisted by simulation request handling.
343    pub(crate) fn insert_gpu(
344        &mut self,
345        window_id: WindowId,
346        reply: mpsc::SyncSender<Result<GpuResourceSummary, SimulationError>>,
347    ) {
348        self.gpu.entry(window_id).or_default().push(reply);
349    }
350
351    /// Completes simulator waiters after the native renderer has presented a
352    /// frame. `capture` is supplied only when a renderer serviced a pending
353    /// capture request.
354    pub(crate) fn complete_frame(
355        &mut self,
356        window_id: WindowId,
357        presented: bool,
358        capture: Option<Result<Screenshot, String>>,
359    ) {
360        if !presented {
361            return;
362        }
363        Self::settle(&mut self.frame, window_id, Ok(()));
364        let result = capture
365            .map(|result| result.map_err(SimulationError::CaptureUnavailable))
366            .unwrap_or_else(|| {
367                Err(SimulationError::CaptureUnavailable(
368                    "the renderer did not return a capture".into(),
369                ))
370            });
371        Self::settle(&mut self.capture, window_id, result);
372    }
373
374    /// Fails simulator requests when a native frame cannot be produced.
375    pub(crate) fn fail_frame(&mut self, window_id: WindowId, message: impl Into<String>) {
376        let error = SimulationError::FrameFailed(message.into());
377        Self::settle(&mut self.frame, window_id, Err(error.clone()));
378        let capture_error = SimulationError::CaptureUnavailable(error.to_string());
379        Self::settle(&mut self.capture, window_id, Err(capture_error));
380    }
381
382    /// Fails all three waiter kinds when a window closes or the
383    /// application shuts down.
384    pub(crate) fn fail_window(&mut self, window_id: WindowId) {
385        let error = SimulationError::WindowClosed(window_id);
386        Self::settle(&mut self.frame, window_id, Err(error.clone()));
387        Self::settle(&mut self.capture, window_id, Err(error.clone()));
388        // Resource queries hold no frame debt: closing settles them here,
389        // through the same close/shutdown paths as every other waiter, so
390        // no maintenance pass is needed to release them.
391        Self::settle(&mut self.gpu, window_id, Err(error));
392    }
393
394    /// Completes pending GPU-resource waiters after the native adapter has
395    /// snapshotted current residency. Never renders.
396    pub(crate) fn complete_gpu(&mut self, window_id: WindowId, summary: GpuResourceSummary) {
397        Self::settle(&mut self.gpu, window_id, Ok(summary));
398    }
399
400    /// Windows holding GPU-resource waiters, for liveness filtering by
401    /// the caller: liveness lives in the window registry, not here.
402    pub(crate) fn gpu_windows(&self) -> Vec<WindowId> {
403        self.gpu.keys().copied().collect()
404    }
405
406    /// Releases one window's GPU-resource waiters for out-of-band
407    /// settlement by the caller. Empty when nothing was pending.
408    pub(crate) fn take_gpu_waiters(
409        &mut self,
410        window_id: WindowId,
411    ) -> Vec<Reply<GpuResourceSummary>> {
412        self.gpu.remove(&window_id).unwrap_or_default()
413    }
414}
415
416/// A cloneable in-process controller for one Incular application window.
417///
418/// Calls block until the application's UI thread services the command. Use
419/// this handle from a test/automation thread, not from an application build or
420/// event callback, because those already run on the UI thread.
421#[derive(Clone)]
422pub struct Simulation {
423    bridge: Arc<SimulationBridge>,
424    window_id: WindowId,
425    keyboard: Arc<Mutex<KeyboardState>>,
426}
427
428impl Simulation {
429    pub(crate) fn new(bridge: Arc<SimulationBridge>, window_id: WindowId) -> Self {
430        Self {
431            bridge,
432            window_id,
433            keyboard: Arc::new(Mutex::new(KeyboardState::default())),
434        }
435    }
436
437    /// Returns a controller targeting the window represented by `handle`.
438    #[must_use]
439    pub fn window(&self, handle: &WindowHandle) -> Self {
440        self.for_window(handle.id())
441    }
442
443    /// Returns a controller targeting a known Incular window ID.
444    #[must_use]
445    pub fn for_window(&self, window_id: WindowId) -> Self {
446        Self {
447            bridge: self.bridge.clone(),
448            window_id,
449            keyboard: self.keyboard.clone(),
450        }
451    }
452
453    #[must_use]
454    pub const fn window_id(&self) -> WindowId {
455        self.window_id
456    }
457
458    /// Returns the mouse command group for this window.
459    #[must_use]
460    pub fn mouse(&self) -> SimulationMouse {
461        SimulationMouse {
462            simulation: self.clone(),
463        }
464    }
465
466    /// Returns the keyboard command group for this window.
467    #[must_use]
468    pub fn keyboard(&self) -> SimulationKeyboard {
469        SimulationKeyboard {
470            simulation: self.clone(),
471        }
472    }
473
474    /// Clicks the first enabled semantic node whose label exactly matches
475    /// `label`, using move → down → up through the ordinary pointer pipeline.
476    pub fn click(&self, label: impl Into<String>) -> Result<(), SimulationError> {
477        self.mouse().click(label)
478    }
479
480    /// Clicks at `position` in logical pixels; the same as
481    /// [`Self::click_logical`]. Use [`Self::click_physical`] for a point read
482    /// off a [`Self::capture`] screenshot.
483    pub fn click_at(&self, position: Offset) -> Result<(), SimulationError> {
484        self.mouse().click_logical(position)
485    }
486
487    /// Clicks at `position` in logical pixels, the space layout bounds and
488    /// widget sizes use.
489    pub fn click_logical(&self, position: Offset) -> Result<(), SimulationError> {
490        self.mouse().click_logical(position)
491    }
492
493    /// Clicks at `position` in physical pixels, the space of
494    /// [`Self::capture`] screenshots. The runtime divides it by the window's
495    /// current scale factor before dispatch.
496    pub fn click_physical(&self, position: Offset) -> Result<(), SimulationError> {
497        self.mouse().click_physical(position)
498    }
499
500    /// Clicks at `position` in the given coordinate space.
501    pub fn click_in(&self, position: Offset, space: PointSpace) -> Result<(), SimulationError> {
502        self.mouse().click_in(position, space)
503    }
504
505    /// The window's current scale factor: physical pixels per logical pixel.
506    /// Divide a screenshot point by it to get the logical point.
507    pub fn scale_factor(&self) -> Result<f64, SimulationError> {
508        self.request(|reply| SimulationRequest::ScaleFactor {
509            window_id: self.window_id,
510            reply,
511        })
512    }
513
514    /// Gives keyboard focus to the first enabled semantic node whose label
515    /// exactly matches `label`; a text field also matches by its placeholder.
516    /// Focusable nodes take focus through their semantic focus action (no
517    /// pointer event, so no tap side effects); anything else is clicked at
518    /// its center, as a user would to focus it.
519    pub fn focus(&self, label: impl Into<String>) -> Result<(), SimulationError> {
520        self.request(|reply| SimulationRequest::FocusLabel {
521            window_id: self.window_id,
522            label: label.into(),
523            reply,
524        })
525    }
526
527    /// Focuses the field labeled `label` (see [`Self::focus`]) and types
528    /// `text` into it at the caret. [`Self::type_text`] alone delivers text
529    /// only to an already focused field.
530    pub fn type_into(
531        &self,
532        label: impl Into<String>,
533        text: impl Into<String>,
534    ) -> Result<(), SimulationError> {
535        self.focus(label)?;
536        self.type_text(text)
537    }
538
539    pub fn move_mouse_to(&self, position: Offset) -> Result<(), SimulationError> {
540        self.mouse().move_to(position)
541    }
542
543    pub fn scroll(&self, delta: Offset) -> Result<(), SimulationError> {
544        self.mouse().scroll(delta)
545    }
546
547    pub fn key_down(&self, code: Code) -> Result<(), SimulationError> {
548        self.keyboard().key_down(code)
549    }
550
551    pub fn key_up(&self, code: Code) -> Result<(), SimulationError> {
552        self.keyboard().key_up(code)
553    }
554
555    pub fn press(&self, code: Code) -> Result<(), SimulationError> {
556        self.keyboard().press(code)
557    }
558
559    pub fn press_key(&self, key: KeyboardKey, code: Code) -> Result<(), SimulationError> {
560        self.keyboard().press_key(key, code)
561    }
562
563    pub fn type_text(&self, text: impl Into<String>) -> Result<(), SimulationError> {
564        self.keyboard().type_text(text)
565    }
566
567    /// Waits until the next frame for this window has been presented.
568    pub fn wait_for_frame(&self) -> Result<(), SimulationError> {
569        self.request(|reply| SimulationRequest::WaitForFrame {
570            window_id: self.window_id,
571            reply,
572        })
573    }
574
575    /// Requests and returns the next rendered frame for this window.
576    ///
577    /// The screenshot is in physical pixels. Click a point read off it with
578    /// [`Self::click_physical`], or convert it with [`Self::scale_factor`].
579    pub fn capture(&self) -> Result<Screenshot, SimulationError> {
580        self.request(|reply| SimulationRequest::Capture {
581            window_id: self.window_id,
582            reply,
583        })
584    }
585
586    /// Reads the current GPU residency snapshot for this window's device
587    /// without presenting a frame. Resolves once the native adapter's
588    /// maintenance path fulfills it from current state.
589    pub fn query_gpu_resources(&self) -> Result<GpuResourceSummary, SimulationError> {
590        self.request(|reply| SimulationRequest::GpuResources {
591            window_id: self.window_id,
592            reply,
593        })
594    }
595
596    /// Releases every modifier/key held by this simulation keyboard.
597    pub fn release_all_keys(&self) -> Result<(), SimulationError> {
598        self.keyboard().release_all()
599    }
600
601    fn request<T>(
602        &self,
603        build: impl FnOnce(Reply<T>) -> SimulationRequest,
604    ) -> Result<T, SimulationError> {
605        let (sender, receiver) = mpsc::sync_channel(1);
606        self.bridge.enqueue(build(sender))?;
607        // A panic on the UI thread drops the pending reply (or stalls it):
608        // report the panic rather than a bare disconnect or timeout.
609        match receiver.recv_timeout(SIMULATION_REPLY_TIMEOUT) {
610            Ok(result) => result,
611            Err(mpsc::RecvTimeoutError::Timeout) => {
612                Err(self.bridge.panicked().unwrap_or(SimulationError::Timeout))
613            }
614            Err(mpsc::RecvTimeoutError::Disconnected) => Err(self
615                .bridge
616                .panicked()
617                .unwrap_or(SimulationError::Disconnected)),
618        }
619    }
620
621    fn input(&self, event: InputEvent) -> Result<(), SimulationError> {
622        self.request(|reply| SimulationRequest::Input {
623            window_id: self.window_id,
624            event,
625            reply,
626        })
627    }
628}
629
630/// Mouse/pointer operations for a [`Simulation`] window. Only the primary
631/// pointer/left-button path is exposed here; multi-pointer tests can use the
632/// lower-level runtime input API directly.
633#[derive(Clone)]
634pub struct SimulationMouse {
635    simulation: Simulation,
636}
637
638impl SimulationMouse {
639    pub fn move_to(&self, position: Offset) -> Result<(), SimulationError> {
640        self.simulation.input(InputEvent::Pointer {
641            phase: PointerPhase::Move,
642            position,
643        })
644    }
645
646    pub fn down(&self, position: Offset) -> Result<(), SimulationError> {
647        self.simulation.input(InputEvent::Pointer {
648            phase: PointerPhase::Down,
649            position,
650        })
651    }
652
653    pub fn up(&self, position: Offset) -> Result<(), SimulationError> {
654        self.simulation.input(InputEvent::Pointer {
655            phase: PointerPhase::Up,
656            position,
657        })
658    }
659
660    pub fn click(&self, label: impl Into<String>) -> Result<(), SimulationError> {
661        self.simulation
662            .request(|reply| SimulationRequest::ClickLabel {
663                window_id: self.simulation.window_id,
664                label: label.into(),
665                reply,
666            })
667    }
668
669    /// Clicks at `position` in logical pixels (see [`Self::click_logical`]).
670    pub fn click_at(&self, position: Offset) -> Result<(), SimulationError> {
671        self.click_logical(position)
672    }
673
674    /// Clicks at `position` in logical pixels.
675    pub fn click_logical(&self, position: Offset) -> Result<(), SimulationError> {
676        self.click_in(position, PointSpace::Logical)
677    }
678
679    /// Clicks at `position` in physical (screenshot) pixels.
680    pub fn click_physical(&self, position: Offset) -> Result<(), SimulationError> {
681        self.click_in(position, PointSpace::Physical)
682    }
683
684    /// Clicks at `position` in the given coordinate space.
685    pub fn click_in(&self, position: Offset, space: PointSpace) -> Result<(), SimulationError> {
686        self.simulation.request(|reply| SimulationRequest::ClickAt {
687            window_id: self.simulation.window_id,
688            position,
689            space,
690            reply,
691        })
692    }
693
694    pub fn scroll(&self, delta: Offset) -> Result<(), SimulationError> {
695        self.simulation.input(InputEvent::Scroll { delta })
696    }
697}
698
699/// Keyboard operations for a [`Simulation`] window. Key events use Incular's
700/// platform-neutral physical [`Code`] plus the logical key supplied to
701/// [`Self::press_key`]. Text entry is a separate committed-text event, just as
702/// it is for native IME-aware adapters.
703#[derive(Clone)]
704pub struct SimulationKeyboard {
705    simulation: Simulation,
706}
707
708impl SimulationKeyboard {
709    pub fn key_down(&self, code: Code) -> Result<(), SimulationError> {
710        self.send_key(KeyState::Down, None, code)
711    }
712
713    pub fn key_up(&self, code: Code) -> Result<(), SimulationError> {
714        self.send_key(KeyState::Up, None, code)
715    }
716
717    pub fn press(&self, code: Code) -> Result<(), SimulationError> {
718        self.key_down(code)?;
719        self.key_up(code)
720    }
721
722    pub fn press_key(&self, key: KeyboardKey, code: Code) -> Result<(), SimulationError> {
723        self.send_key(KeyState::Down, Some(key.clone()), code)?;
724        self.send_key(KeyState::Up, Some(key), code)
725    }
726
727    pub fn type_text(&self, text: impl Into<String>) -> Result<(), SimulationError> {
728        let text = text.into();
729        if text.chars().any(char::is_control) {
730            return Err(SimulationError::InvalidInput(
731                "type_text accepts printable committed text; use press for commands".into(),
732            ));
733        }
734        self.simulation.input(InputEvent::Text(text))
735    }
736
737    pub fn release_all(&self) -> Result<(), SimulationError> {
738        let pressed = self
739            .simulation
740            .keyboard
741            .lock()
742            .expect("simulation keyboard mutex")
743            .pressed
744            .iter()
745            .copied()
746            .collect::<Vec<_>>();
747        for code in pressed {
748            self.key_up(code)?;
749        }
750        Ok(())
751    }
752
753    fn send_key(
754        &self,
755        state: KeyState,
756        explicit_key: Option<KeyboardKey>,
757        code: Code,
758    ) -> Result<(), SimulationError> {
759        let (key, modifiers, location, repeat, was_pressed) = {
760            let mut keyboard = self
761                .simulation
762                .keyboard
763                .lock()
764                .expect("simulation keyboard mutex");
765            let was_pressed = keyboard.pressed.contains(&code);
766            if state.is_down() {
767                if let Some(modifier) = modifier_for_code(code) {
768                    keyboard.modifiers.insert(modifier);
769                }
770                keyboard.pressed.insert(code);
771            }
772            let values = (
773                explicit_key.unwrap_or_else(|| logical_key(code)),
774                keyboard.modifiers,
775                location_for_code(code),
776                state.is_down() && was_pressed,
777                was_pressed,
778            );
779            if state.is_up() {
780                // Key-up carries the modifier as pressed, then the local
781                // simulator state releases it after the event is delivered.
782                keyboard.pressed.remove(&code);
783                if let Some(modifier) = modifier_for_code(code) {
784                    keyboard.modifiers.remove(modifier);
785                }
786            }
787            values
788        };
789        let event = KeyboardEvent {
790            state,
791            key,
792            code,
793            location,
794            modifiers,
795            repeat,
796            is_composing: false,
797        };
798        let result = self.simulation.input(InputEvent::Key(event));
799        if result.is_err() {
800            // Keep local key state usable after a disconnected/failed request.
801            let mut keyboard = self
802                .simulation
803                .keyboard
804                .lock()
805                .expect("simulation keyboard mutex");
806            if state.is_down() {
807                if !was_pressed {
808                    keyboard.pressed.remove(&code);
809                }
810            } else {
811                keyboard.pressed.insert(code);
812            }
813            keyboard.modifiers = modifiers_for_pressed(&keyboard.pressed);
814        }
815        result
816    }
817}
818
819fn modifier_for_code(code: Code) -> Option<Modifiers> {
820    match code {
821        Code::AltLeft | Code::AltRight => Some(Modifiers::ALT),
822        Code::ControlLeft | Code::ControlRight => Some(Modifiers::CONTROL),
823        Code::MetaLeft | Code::MetaRight => Some(Modifiers::META),
824        Code::ShiftLeft | Code::ShiftRight => Some(Modifiers::SHIFT),
825        _ => None,
826    }
827}
828
829fn modifiers_for_pressed(pressed: &HashSet<Code>) -> Modifiers {
830    pressed
831        .iter()
832        .fold(Modifiers::default(), |mut modifiers, code| {
833            if let Some(modifier) = modifier_for_code(*code) {
834                modifiers.insert(modifier);
835            }
836            modifiers
837        })
838}
839
840fn location_for_code(code: Code) -> Location {
841    match code {
842        Code::AltLeft | Code::ControlLeft | Code::MetaLeft | Code::ShiftLeft => Location::Left,
843        Code::AltRight | Code::ControlRight | Code::MetaRight | Code::ShiftRight => Location::Right,
844        Code::NumpadEnter => Location::Numpad,
845        _ => Location::Standard,
846    }
847}
848
849fn logical_key(code: Code) -> KeyboardKey {
850    let character = match code {
851        Code::Backquote => Some("`"),
852        Code::Backslash => Some("\\"),
853        Code::BracketLeft => Some("["),
854        Code::BracketRight => Some("]"),
855        Code::Comma => Some(","),
856        Code::Digit0 => Some("0"),
857        Code::Digit1 => Some("1"),
858        Code::Digit2 => Some("2"),
859        Code::Digit3 => Some("3"),
860        Code::Digit4 => Some("4"),
861        Code::Digit5 => Some("5"),
862        Code::Digit6 => Some("6"),
863        Code::Digit7 => Some("7"),
864        Code::Digit8 => Some("8"),
865        Code::Digit9 => Some("9"),
866        Code::Equal => Some("="),
867        Code::KeyA => Some("a"),
868        Code::KeyB => Some("b"),
869        Code::KeyC => Some("c"),
870        Code::KeyD => Some("d"),
871        Code::KeyE => Some("e"),
872        Code::KeyF => Some("f"),
873        Code::KeyG => Some("g"),
874        Code::KeyH => Some("h"),
875        Code::KeyI => Some("i"),
876        Code::KeyJ => Some("j"),
877        Code::KeyK => Some("k"),
878        Code::KeyL => Some("l"),
879        Code::KeyM => Some("m"),
880        Code::KeyN => Some("n"),
881        Code::KeyO => Some("o"),
882        Code::KeyP => Some("p"),
883        Code::KeyQ => Some("q"),
884        Code::KeyR => Some("r"),
885        Code::KeyS => Some("s"),
886        Code::KeyT => Some("t"),
887        Code::KeyU => Some("u"),
888        Code::KeyV => Some("v"),
889        Code::KeyW => Some("w"),
890        Code::KeyX => Some("x"),
891        Code::KeyY => Some("y"),
892        Code::KeyZ => Some("z"),
893        Code::Minus => Some("-"),
894        Code::Period => Some("."),
895        Code::Quote => Some("'"),
896        Code::Semicolon => Some(";"),
897        Code::Slash => Some("/"),
898        Code::Space => Some(" "),
899        Code::Numpad0 => Some("0"),
900        Code::Numpad1 => Some("1"),
901        Code::Numpad2 => Some("2"),
902        Code::Numpad3 => Some("3"),
903        Code::Numpad4 => Some("4"),
904        Code::Numpad5 => Some("5"),
905        Code::Numpad6 => Some("6"),
906        Code::Numpad7 => Some("7"),
907        Code::Numpad8 => Some("8"),
908        Code::Numpad9 => Some("9"),
909        Code::NumpadAdd => Some("+"),
910        Code::NumpadComma => Some(","),
911        Code::NumpadDecimal => Some("."),
912        Code::NumpadDivide => Some("/"),
913        Code::NumpadEqual => Some("="),
914        Code::NumpadMultiply | Code::NumpadStar => Some("*"),
915        Code::NumpadSubtract => Some("-"),
916        _ => None,
917    };
918    if let Some(character) = character {
919        return KeyboardKey::Character(character.into());
920    }
921    let named = match code {
922        Code::AltLeft | Code::AltRight => NamedKey::Alt,
923        Code::CapsLock => NamedKey::CapsLock,
924        Code::ControlLeft | Code::ControlRight => NamedKey::Control,
925        Code::Enter | Code::NumpadEnter => NamedKey::Enter,
926        Code::Escape => NamedKey::Escape,
927        Code::MetaLeft | Code::MetaRight => NamedKey::Meta,
928        Code::NumLock => NamedKey::NumLock,
929        Code::ScrollLock => NamedKey::ScrollLock,
930        Code::ShiftLeft | Code::ShiftRight => NamedKey::Shift,
931        Code::Tab => NamedKey::Tab,
932        Code::Backspace => NamedKey::Backspace,
933        Code::Delete => NamedKey::Delete,
934        Code::Insert => NamedKey::Insert,
935        Code::PageDown => NamedKey::PageDown,
936        Code::PageUp => NamedKey::PageUp,
937        Code::ArrowDown => NamedKey::ArrowDown,
938        Code::ArrowLeft => NamedKey::ArrowLeft,
939        Code::ArrowRight => NamedKey::ArrowRight,
940        Code::ArrowUp => NamedKey::ArrowUp,
941        Code::End => NamedKey::End,
942        Code::Home => NamedKey::Home,
943        _ => NamedKey::Unidentified,
944    };
945    KeyboardKey::Named(named)
946}
947
948fn valid_offset(offset: Offset) -> Result<(), SimulationError> {
949    if offset.x.is_finite() && offset.y.is_finite() {
950        Ok(())
951    } else {
952        Err(SimulationError::InvalidInput(
953            "pointer and scroll offsets must be finite".into(),
954        ))
955    }
956}
957
958impl Application {
959    /// Services queued in-process simulation requests on the application UI
960    /// thread. Native adapters call this from their event-loop wake path.
961    pub fn process_simulation_requests(&mut self) {
962        for _ in 0..MAX_SIMULATION_REQUESTS_PER_TURN {
963            let request = match self.simulation_receiver.try_recv() {
964                Ok(request) => request,
965                Err(mpsc::TryRecvError::Empty | mpsc::TryRecvError::Disconnected) => break,
966            };
967            self.handle_simulation_request(request);
968        }
969    }
970
971    fn handle_simulation_request(&mut self, request: SimulationRequest) {
972        match request {
973            SimulationRequest::Input {
974                window_id,
975                event,
976                reply,
977            } => {
978                let result = self
979                    .ensure_simulation_layout(window_id)
980                    .and_then(|_| self.dispatch_simulation_input(window_id, event));
981                let _ = reply.send(result);
982            }
983            SimulationRequest::ClickAt {
984                window_id,
985                position,
986                space,
987                reply,
988            } => {
989                let result = self
990                    .simulation_logical_point(window_id, position, space)
991                    .and_then(|position| self.dispatch_simulation_click(window_id, position));
992                let _ = reply.send(result);
993            }
994            SimulationRequest::ScaleFactor { window_id, reply } => {
995                let _ = reply.send(self.simulation_scale_factor(window_id));
996            }
997            SimulationRequest::ClickLabel {
998                window_id,
999                label,
1000                reply,
1001            } => {
1002                let trigger = format!("click({label:?})");
1003                let result = self
1004                    .simulation_label_target(window_id, &label)
1005                    .and_then(|target| {
1006                        self.dispatch_simulation_click_prepared(
1007                            window_id,
1008                            target.bounds.center(),
1009                            &trigger,
1010                        )
1011                    });
1012                let _ = reply.send(result);
1013            }
1014            SimulationRequest::FocusLabel {
1015                window_id,
1016                label,
1017                reply,
1018            } => {
1019                let _ = reply.send(self.simulation_focus_label(window_id, &label));
1020            }
1021            SimulationRequest::WaitForFrame { window_id, reply } => {
1022                if self.contains_window(window_id) {
1023                    let _ =
1024                        self.with_window_mut(window_id, |record| record.runtime.request_frame());
1025                    self.simulation_waiters.insert_frame(window_id, reply);
1026                } else {
1027                    let _ = reply.send(Err(SimulationError::WindowNotFound(window_id)));
1028                }
1029            }
1030            SimulationRequest::Capture { window_id, reply } => {
1031                if self.contains_window(window_id) {
1032                    let _ =
1033                        self.with_window_mut(window_id, |record| record.runtime.request_frame());
1034                    self.simulation_waiters.insert_capture(window_id, reply);
1035                } else {
1036                    let _ = reply.send(Err(SimulationError::WindowNotFound(window_id)));
1037                }
1038            }
1039            SimulationRequest::GpuResources { window_id, reply } => {
1040                // Deliberately no frame request: resource observation must
1041                // not present. The native adapter fulfills pending queries
1042                // from current state on its maintenance path.
1043                if self.contains_window(window_id) {
1044                    self.simulation_waiters.insert_gpu(window_id, reply);
1045                } else {
1046                    let _ = reply.send(Err(SimulationError::WindowNotFound(window_id)));
1047                }
1048            }
1049        }
1050    }
1051
1052    fn dispatch_simulation_input(
1053        &mut self,
1054        window_id: WindowId,
1055        event: InputEvent,
1056    ) -> Result<(), SimulationError> {
1057        self.dispatch_simulation_input_with_trigger(window_id, event, None)
1058    }
1059
1060    fn dispatch_simulation_input_with_trigger(
1061        &mut self,
1062        window_id: WindowId,
1063        event: InputEvent,
1064        trigger: Option<&str>,
1065    ) -> Result<(), SimulationError> {
1066        if let InputEvent::Pointer { position, .. } | InputEvent::PointerWithId { position, .. } =
1067            &event
1068        {
1069            valid_offset(*position)?;
1070        }
1071        if let InputEvent::Scroll { delta } = &event {
1072            valid_offset(*delta)?;
1073        }
1074        if !self.contains_window(window_id) {
1075            return Err(SimulationError::WindowNotFound(window_id));
1076        }
1077        let applied = self.with_window_mut(window_id, |record| {
1078            record.input_events = record.input_events.wrapping_add(1);
1079            let _ = match trigger {
1080                Some(trigger) => record.runtime.handle_input_with_trigger(event, trigger),
1081                None => {
1082                    record.runtime.tree().set_diagnostic_input_trigger(
1083                        Some("simulation::"),
1084                        &event,
1085                        super::diagnostic_input_trigger,
1086                    );
1087                    record.runtime.handle_input_timed(event)
1088                }
1089            };
1090        });
1091        applied
1092            .map(|_| ())
1093            .ok_or(SimulationError::WindowNotFound(window_id))
1094    }
1095
1096    fn simulation_scale_factor(&mut self, window_id: WindowId) -> Result<f64, SimulationError> {
1097        self.with_window_mut(window_id, |record| record.metrics.scale_factor)
1098            .ok_or(SimulationError::WindowNotFound(window_id))
1099    }
1100
1101    fn simulation_logical_point(
1102        &mut self,
1103        window_id: WindowId,
1104        position: Offset,
1105        space: PointSpace,
1106    ) -> Result<Offset, SimulationError> {
1107        valid_offset(position)?;
1108        match space {
1109            PointSpace::Logical => Ok(position),
1110            PointSpace::Physical => {
1111                let scale = self.simulation_scale_factor(window_id)?;
1112                if !(scale.is_finite() && scale > 0.0) {
1113                    return Err(SimulationError::InvalidInput(format!(
1114                        "window scale factor {scale} cannot convert physical pixels"
1115                    )));
1116                }
1117                let scale = scale as f32;
1118                Ok(Offset::new(position.x / scale, position.y / scale))
1119            }
1120        }
1121    }
1122
1123    fn dispatch_simulation_click(
1124        &mut self,
1125        window_id: WindowId,
1126        position: Offset,
1127    ) -> Result<(), SimulationError> {
1128        valid_offset(position)?;
1129        self.ensure_simulation_layout(window_id)?;
1130        self.dispatch_simulation_click_prepared(
1131            window_id,
1132            position,
1133            &format!("click_at({:.1}, {:.1})", position.x, position.y),
1134        )
1135    }
1136
1137    fn dispatch_simulation_click_prepared(
1138        &mut self,
1139        window_id: WindowId,
1140        position: Offset,
1141        trigger: &str,
1142    ) -> Result<(), SimulationError> {
1143        self.dispatch_simulation_input_with_trigger(
1144            window_id,
1145            InputEvent::Pointer {
1146                phase: PointerPhase::Move,
1147                position,
1148            },
1149            Some(trigger),
1150        )?;
1151        self.dispatch_simulation_input_with_trigger(
1152            window_id,
1153            InputEvent::Pointer {
1154                phase: PointerPhase::Down,
1155                position,
1156            },
1157            Some(trigger),
1158        )?;
1159        self.dispatch_simulation_input_with_trigger(
1160            window_id,
1161            InputEvent::Pointer {
1162                phase: PointerPhase::Up,
1163                position,
1164            },
1165            Some(trigger),
1166        )
1167    }
1168
1169    fn ensure_simulation_layout(&mut self, window_id: WindowId) -> Result<(), SimulationError> {
1170        let diagnostics = self
1171            .window_diagnostics(window_id)
1172            .ok_or(SimulationError::WindowNotFound(window_id))?;
1173        if diagnostics.physical_size.is_zero() {
1174            return Err(SimulationError::FrameFailed(
1175                "the target window has a zero-sized surface".into(),
1176            ));
1177        }
1178        self.run_window_frame_at(
1179            window_id,
1180            Constraints::tight(diagnostics.logical_size),
1181            Instant::now(),
1182        )
1183        .map_err(|error| SimulationError::FrameFailed(format!("{error:?}")))?;
1184        // The preparation frame is not presented by this runtime-only step;
1185        // leave the native adapter a real redraw to present afterward.
1186        let _ = self.with_window_mut(window_id, |record| record.runtime.request_frame());
1187        Ok(())
1188    }
1189
1190    /// Resolves `label` to a visible semantic node: an exact label match,
1191    /// or a text field whose placeholder matches. Nodes that activate win
1192    /// over static ones (a button over its own text). Only controls carry an
1193    /// enabled state; plain text, images and headings are never "disabled",
1194    /// so a card is found by the title text inside it.
1195    fn simulation_label_target(
1196        &mut self,
1197        window_id: WindowId,
1198        label: &str,
1199    ) -> Result<LabelTarget, SimulationError> {
1200        // Targets resolve through semantics, which a native host leaves off
1201        // until something needs them.
1202        self.set_semantics_enabled(window_id, true);
1203        self.ensure_simulation_layout(window_id)?;
1204        let viewport = self
1205            .window_diagnostics(window_id)
1206            .map(|diagnostics| Rect::from_origin_size(Offset::ZERO, diagnostics.logical_size))
1207            .ok_or(SimulationError::WindowNotFound(window_id))?;
1208        let result = self.registry.borrow().get(window_id).and_then(|record| {
1209            let tree = record.runtime.tree();
1210            let mut fallback = None;
1211            tree.semantics()
1212                .iter()
1213                .find_map(|(id, node)| {
1214                    let matches = node.label.as_deref() == Some(label)
1215                        || tree
1216                            .element_for_semantic_node(id)
1217                            .and_then(|element| tree.text_field_placeholder(element))
1218                            == Some(label);
1219                    if !matches || (!node.state.enabled && carries_enabled_state(node.role)) {
1220                        return None;
1221                    }
1222                    // Semantic trees can retain children just outside a
1223                    // viewport (especially sliver children). Resolve a
1224                    // pointer target only from the visible intersection so a
1225                    // simulation never reports a click that the pointer
1226                    // pipeline cannot receive.
1227                    let visible_bounds = node.bounds.intersection(viewport)?;
1228                    if visible_bounds.size.width <= 0.0 || visible_bounds.size.height <= 0.0 {
1229                        return None;
1230                    }
1231                    let target = LabelTarget {
1232                        node: id,
1233                        bounds: visible_bounds,
1234                        focusable: node.actions.contains(&SemanticActionKind::Focus),
1235                    };
1236                    if node.actions.contains(&SemanticActionKind::Activate) {
1237                        return Some(target);
1238                    }
1239                    fallback.get_or_insert(target);
1240                    None
1241                })
1242                .or(fallback)
1243        });
1244        result.ok_or_else(|| SimulationError::ElementNotFound(label.to_owned()))
1245    }
1246
1247    fn simulation_focus_label(
1248        &mut self,
1249        window_id: WindowId,
1250        label: &str,
1251    ) -> Result<(), SimulationError> {
1252        let target = self.simulation_label_target(window_id, label)?;
1253        if target.focusable {
1254            let focused = self
1255                .with_window_mut(window_id, |record| {
1256                    let focused = record
1257                        .runtime
1258                        .dispatch_semantic_action(target.node, SemanticAction::Focus);
1259                    record.runtime.request_frame();
1260                    focused
1261                })
1262                .ok_or(SimulationError::WindowNotFound(window_id))?;
1263            if focused {
1264                return Ok(());
1265            }
1266        }
1267        self.dispatch_simulation_click_prepared(
1268            window_id,
1269            target.bounds.center(),
1270            &format!("focus({label:?})"),
1271        )
1272    }
1273
1274    pub fn simulation_capture_pending(&self, window_id: WindowId) -> bool {
1275        self.simulation_waiters.capture_pending(window_id)
1276    }
1277
1278    /// Returns the live windows with pending GPU-resource queries so the
1279    /// native adapter can fulfill them from current state without
1280    /// presenting. Live waiters stay queued until
1281    /// [`Self::complete_gpu_resource_query`] drains them; waiters for
1282    /// windows that no longer exist fail immediately instead of hanging
1283    /// the querier. Window close and application shutdown settle pending
1284    /// waiters through the shared close/shutdown cleanup, never through
1285    /// this method.
1286    pub fn take_gpu_resource_queries(&mut self) -> Vec<WindowId> {
1287        let mut live = Vec::new();
1288        for window_id in self.simulation_waiters.gpu_windows() {
1289            if self.contains_window(window_id) {
1290                live.push(window_id);
1291            } else {
1292                for reply in self.simulation_waiters.take_gpu_waiters(window_id) {
1293                    let _ = reply.send(Err(SimulationError::WindowNotFound(window_id)));
1294                }
1295            }
1296        }
1297        live
1298    }
1299
1300    /// Completes pending GPU-resource waiters after the native adapter has
1301    /// snapshotted current residency. Never renders.
1302    pub fn complete_gpu_resource_query(
1303        &mut self,
1304        window_id: WindowId,
1305        summary: GpuResourceSummary,
1306    ) {
1307        self.simulation_waiters.complete_gpu(window_id, summary);
1308    }
1309
1310    /// Reports whether a simulator is waiting for the next presented frame.
1311    #[must_use]
1312    pub fn simulation_frame_pending(&self, window_id: WindowId) -> bool {
1313        self.simulation_waiters.frame_pending(window_id)
1314    }
1315
1316    /// Completes simulator waiters after the native renderer has presented a
1317    /// frame. `capture` is supplied only when a renderer serviced a pending
1318    /// capture request.
1319    pub fn complete_simulation_frame(
1320        &mut self,
1321        window_id: WindowId,
1322        presented: bool,
1323        capture: Option<Result<Screenshot, String>>,
1324    ) {
1325        self.simulation_waiters
1326            .complete_frame(window_id, presented, capture);
1327    }
1328
1329    /// Fails simulator requests when a native frame cannot be produced.
1330    pub fn fail_simulation_frame(&mut self, window_id: WindowId, message: impl Into<String>) {
1331        self.simulation_waiters.fail_frame(window_id, message);
1332    }
1333
1334    pub(crate) fn fail_simulation_window(&mut self, window_id: WindowId) {
1335        self.simulation_waiters.fail_window(window_id);
1336    }
1337
1338    pub(crate) fn stop_simulation(&self) {
1339        self.simulation_bridge.stop();
1340    }
1341}
1342
1343/// A semantic node a label resolved to.
1344struct LabelTarget {
1345    node: incular_semantics::SemanticNodeId,
1346    bounds: Rect,
1347    focusable: bool,
1348}
1349
1350/// Roles whose `enabled` state is meaningful. Every other role (text,
1351/// images, headings, containers) has no enabled state, so its default
1352/// `enabled: false` must not hide it from label lookup.
1353fn carries_enabled_state(role: incular_semantics::Role) -> bool {
1354    use incular_semantics::Role;
1355    matches!(
1356        role,
1357        Role::Button
1358            | Role::TextField
1359            | Role::TextArea
1360            | Role::SearchField
1361            | Role::Checkbox
1362            | Role::Radio
1363            | Role::Switch
1364            | Role::Slider
1365            | Role::MenuItem
1366            | Role::Tab
1367            | Role::Link
1368    )
1369}
1370
1371trait RectCenter {
1372    fn center(self) -> Offset;
1373}
1374
1375impl RectCenter for Rect {
1376    fn center(self) -> Offset {
1377        Offset::new(
1378            self.origin.x + self.size.width * 0.5,
1379            self.origin.y + self.size.height * 0.5,
1380        )
1381    }
1382}
1383
1384// Keep the imported input types intentionally visible in this module's API
1385// documentation and make accidental future requests for native IME payloads
1386// explicit at the type boundary.