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::{
11    Code, InputEvent, KeyState, KeyboardEvent, KeyboardKey, Location, Modifiers, NamedKey, Offset,
12    PointerPhase, Rect,
13};
14use incular_platform::WindowId;
15use incular_semantics::SemanticActionKind;
16use std::collections::{HashMap, HashSet};
17use std::fmt;
18use std::sync::atomic::{AtomicBool, Ordering};
19use std::sync::{Arc, Mutex, mpsc};
20use std::time::{Duration, Instant};
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)]
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    Disconnected,
95    /// The target event loop did not service the request in time.
96    Timeout,
97    /// The requested retained window no longer exists.
98    WindowNotFound(WindowId),
99    /// The requested retained window was closed while the command was queued.
100    WindowClosed(WindowId),
101    /// No semantic node with the requested exact label was found.
102    ElementNotFound(String),
103    /// The command contained a value that cannot represent native input.
104    InvalidInput(String),
105    /// The renderer could not produce a window frame for a capture request.
106    CaptureUnavailable(String),
107    /// The runtime or renderer failed while servicing a frame request.
108    FrameFailed(String),
109}
110
111impl fmt::Display for SimulationError {
112    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
113        match self {
114            Self::Disconnected => {
115                formatter.write_str("the Incular simulation bridge is disconnected")
116            }
117            Self::Timeout => formatter.write_str("the Incular simulation request timed out"),
118            Self::WindowNotFound(id) => write!(formatter, "simulation window {id} was not found"),
119            Self::WindowClosed(id) => write!(formatter, "simulation window {id} was closed"),
120            Self::ElementNotFound(label) => {
121                write!(formatter, "no semantic element labeled {label:?} was found")
122            }
123            Self::InvalidInput(message) => write!(formatter, "invalid simulation input: {message}"),
124            Self::CaptureUnavailable(message) => {
125                write!(formatter, "window capture unavailable: {message}")
126            }
127            Self::FrameFailed(message) => write!(formatter, "simulation frame failed: {message}"),
128        }
129    }
130}
131
132impl std::error::Error for SimulationError {}
133
134type Reply<T> = mpsc::SyncSender<Result<T, SimulationError>>;
135
136pub(crate) enum SimulationRequest {
137    Input {
138        window_id: WindowId,
139        event: InputEvent,
140        reply: Reply<()>,
141    },
142    ClickAt {
143        window_id: WindowId,
144        position: Offset,
145        reply: Reply<()>,
146    },
147    ClickLabel {
148        window_id: WindowId,
149        label: String,
150        reply: Reply<()>,
151    },
152    WaitForFrame {
153        window_id: WindowId,
154        reply: Reply<()>,
155    },
156    Capture {
157        window_id: WindowId,
158        reply: Reply<Screenshot>,
159    },
160    GpuResources {
161        window_id: WindowId,
162        reply: Reply<GpuResourceSummary>,
163    },
164}
165
166pub(crate) struct SimulationBridge {
167    sender: mpsc::Sender<SimulationRequest>,
168    wake: Mutex<Option<Arc<dyn super::RuntimeWake>>>,
169    accepting: AtomicBool,
170}
171
172impl SimulationBridge {
173    pub(crate) fn new(sender: mpsc::Sender<SimulationRequest>) -> Self {
174        Self {
175            sender,
176            wake: Mutex::new(None),
177            accepting: AtomicBool::new(true),
178        }
179    }
180
181    fn enqueue(&self, request: SimulationRequest) -> Result<(), SimulationError> {
182        if !self.accepting.load(Ordering::Acquire) {
183            return Err(SimulationError::Disconnected);
184        }
185        self.sender
186            .send(request)
187            .map_err(|_| SimulationError::Disconnected)?;
188        if let Some(wake) = self.wake.lock().expect("simulation wake mutex").as_ref() {
189            wake.wake();
190        }
191        Ok(())
192    }
193
194    pub(crate) fn set_wake(&self, wake: Arc<dyn super::RuntimeWake>) {
195        *self.wake.lock().expect("simulation wake mutex") = Some(wake);
196    }
197
198    pub(crate) fn stop(&self) {
199        self.accepting.store(false, Ordering::Release);
200    }
201}
202
203#[derive(Default)]
204struct KeyboardState {
205    modifiers: Modifiers,
206    pressed: HashSet<Code>,
207}
208
209/// Owns per-window simulation waiter settlement: frame, capture, and
210/// GPU-resource replies. Every settle path removes a window's waiters
211/// before replying, so each waiter settles exactly once and window
212/// close/shutdown settle all three kinds together. Frame failure leaves
213/// GPU queries pending by design — they hold no frame debt and resolve
214/// through maintenance, close, or shutdown instead.
215#[derive(Default)]
216pub(crate) struct SimulationWaiters {
217    frame: HashMap<WindowId, Vec<mpsc::SyncSender<Result<(), SimulationError>>>>,
218    capture: HashMap<WindowId, Vec<mpsc::SyncSender<Result<Screenshot, SimulationError>>>>,
219    gpu: HashMap<WindowId, Vec<mpsc::SyncSender<Result<GpuResourceSummary, SimulationError>>>>,
220}
221
222impl SimulationWaiters {
223    fn settle<T: Clone>(
224        waiters: &mut HashMap<WindowId, Vec<mpsc::SyncSender<Result<T, SimulationError>>>>,
225        window_id: WindowId,
226        result: Result<T, SimulationError>,
227    ) {
228        if let Some(pending) = waiters.remove(&window_id) {
229            for reply in pending {
230                let _ = reply.send(result.clone());
231            }
232        }
233    }
234
235    /// Reports whether a simulator is waiting for the next presented frame.
236    #[must_use]
237    pub(crate) fn frame_pending(&self, window_id: WindowId) -> bool {
238        self.frame
239            .get(&window_id)
240            .is_some_and(|waiters| !waiters.is_empty())
241    }
242
243    /// Reports whether a simulator is waiting for a capture.
244    #[must_use]
245    pub(crate) fn capture_pending(&self, window_id: WindowId) -> bool {
246        self.capture
247            .get(&window_id)
248            .is_some_and(|waiters| !waiters.is_empty())
249    }
250
251    /// Registers a frame waiter enlisted by simulation request handling.
252    pub(crate) fn insert_frame(
253        &mut self,
254        window_id: WindowId,
255        reply: mpsc::SyncSender<Result<(), SimulationError>>,
256    ) {
257        self.frame.entry(window_id).or_default().push(reply);
258    }
259
260    /// Registers a capture waiter enlisted by simulation request handling.
261    pub(crate) fn insert_capture(
262        &mut self,
263        window_id: WindowId,
264        reply: mpsc::SyncSender<Result<Screenshot, SimulationError>>,
265    ) {
266        self.capture.entry(window_id).or_default().push(reply);
267    }
268
269    /// Registers a GPU-resource waiter enlisted by simulation request handling.
270    pub(crate) fn insert_gpu(
271        &mut self,
272        window_id: WindowId,
273        reply: mpsc::SyncSender<Result<GpuResourceSummary, SimulationError>>,
274    ) {
275        self.gpu.entry(window_id).or_default().push(reply);
276    }
277
278    /// Completes simulator waiters after the native renderer has presented a
279    /// frame. `capture` is supplied only when a renderer serviced a pending
280    /// capture request.
281    pub(crate) fn complete_frame(
282        &mut self,
283        window_id: WindowId,
284        presented: bool,
285        capture: Option<Result<Screenshot, String>>,
286    ) {
287        if !presented {
288            return;
289        }
290        Self::settle(&mut self.frame, window_id, Ok(()));
291        let result = capture
292            .map(|result| result.map_err(SimulationError::CaptureUnavailable))
293            .unwrap_or_else(|| {
294                Err(SimulationError::CaptureUnavailable(
295                    "the renderer did not return a capture".into(),
296                ))
297            });
298        Self::settle(&mut self.capture, window_id, result);
299    }
300
301    /// Fails simulator requests when a native frame cannot be produced.
302    pub(crate) fn fail_frame(&mut self, window_id: WindowId, message: impl Into<String>) {
303        let error = SimulationError::FrameFailed(message.into());
304        Self::settle(&mut self.frame, window_id, Err(error.clone()));
305        let capture_error = SimulationError::CaptureUnavailable(error.to_string());
306        Self::settle(&mut self.capture, window_id, Err(capture_error));
307    }
308
309    /// Fails all three waiter kinds when a window closes or the
310    /// application shuts down.
311    pub(crate) fn fail_window(&mut self, window_id: WindowId) {
312        let error = SimulationError::WindowClosed(window_id);
313        Self::settle(&mut self.frame, window_id, Err(error.clone()));
314        Self::settle(&mut self.capture, window_id, Err(error.clone()));
315        // Resource queries hold no frame debt: closing settles them here,
316        // through the same close/shutdown paths as every other waiter, so
317        // no maintenance pass is needed to release them.
318        Self::settle(&mut self.gpu, window_id, Err(error));
319    }
320
321    /// Completes pending GPU-resource waiters after the native adapter has
322    /// snapshotted current residency. Never renders.
323    pub(crate) fn complete_gpu(&mut self, window_id: WindowId, summary: GpuResourceSummary) {
324        Self::settle(&mut self.gpu, window_id, Ok(summary));
325    }
326
327    /// Windows holding GPU-resource waiters, for liveness filtering by
328    /// the caller: liveness lives in the window registry, not here.
329    pub(crate) fn gpu_windows(&self) -> Vec<WindowId> {
330        self.gpu.keys().copied().collect()
331    }
332
333    /// Releases one window's GPU-resource waiters for out-of-band
334    /// settlement by the caller. Empty when nothing was pending.
335    pub(crate) fn take_gpu_waiters(
336        &mut self,
337        window_id: WindowId,
338    ) -> Vec<Reply<GpuResourceSummary>> {
339        self.gpu.remove(&window_id).unwrap_or_default()
340    }
341}
342
343/// A cloneable in-process controller for one Incular application window.
344///
345/// Calls block until the application's UI thread services the command. Use
346/// this handle from a test/automation thread, not from an application build or
347/// event callback, because those already run on the UI thread.
348#[derive(Clone)]
349pub struct Simulation {
350    bridge: Arc<SimulationBridge>,
351    window_id: WindowId,
352    keyboard: Arc<Mutex<KeyboardState>>,
353}
354
355impl Simulation {
356    pub(crate) fn new(bridge: Arc<SimulationBridge>, window_id: WindowId) -> Self {
357        Self {
358            bridge,
359            window_id,
360            keyboard: Arc::new(Mutex::new(KeyboardState::default())),
361        }
362    }
363
364    /// Returns a controller targeting the window represented by `handle`.
365    #[must_use]
366    pub fn window(&self, handle: &WindowHandle) -> Self {
367        self.for_window(handle.id())
368    }
369
370    /// Returns a controller targeting a known Incular window ID.
371    #[must_use]
372    pub fn for_window(&self, window_id: WindowId) -> Self {
373        Self {
374            bridge: self.bridge.clone(),
375            window_id,
376            keyboard: self.keyboard.clone(),
377        }
378    }
379
380    #[must_use]
381    pub const fn window_id(&self) -> WindowId {
382        self.window_id
383    }
384
385    /// Returns the mouse command group for this window.
386    #[must_use]
387    pub fn mouse(&self) -> SimulationMouse {
388        SimulationMouse {
389            simulation: self.clone(),
390        }
391    }
392
393    /// Returns the keyboard command group for this window.
394    #[must_use]
395    pub fn keyboard(&self) -> SimulationKeyboard {
396        SimulationKeyboard {
397            simulation: self.clone(),
398        }
399    }
400
401    /// Clicks the first enabled semantic node whose label exactly matches
402    /// `label`, using move → down → up through the ordinary pointer pipeline.
403    pub fn click(&self, label: impl Into<String>) -> Result<(), SimulationError> {
404        self.mouse().click(label)
405    }
406
407    pub fn click_at(&self, position: Offset) -> Result<(), SimulationError> {
408        self.mouse().click_at(position)
409    }
410
411    pub fn move_mouse_to(&self, position: Offset) -> Result<(), SimulationError> {
412        self.mouse().move_to(position)
413    }
414
415    pub fn scroll(&self, delta: Offset) -> Result<(), SimulationError> {
416        self.mouse().scroll(delta)
417    }
418
419    pub fn key_down(&self, code: Code) -> Result<(), SimulationError> {
420        self.keyboard().key_down(code)
421    }
422
423    pub fn key_up(&self, code: Code) -> Result<(), SimulationError> {
424        self.keyboard().key_up(code)
425    }
426
427    pub fn press(&self, code: Code) -> Result<(), SimulationError> {
428        self.keyboard().press(code)
429    }
430
431    pub fn press_key(&self, key: KeyboardKey, code: Code) -> Result<(), SimulationError> {
432        self.keyboard().press_key(key, code)
433    }
434
435    pub fn type_text(&self, text: impl Into<String>) -> Result<(), SimulationError> {
436        self.keyboard().type_text(text)
437    }
438
439    /// Waits until the next frame for this window has been presented.
440    pub fn wait_for_frame(&self) -> Result<(), SimulationError> {
441        self.request(|reply| SimulationRequest::WaitForFrame {
442            window_id: self.window_id,
443            reply,
444        })
445    }
446
447    /// Requests and returns the next rendered frame for this window.
448    pub fn capture(&self) -> Result<Screenshot, SimulationError> {
449        self.request(|reply| SimulationRequest::Capture {
450            window_id: self.window_id,
451            reply,
452        })
453    }
454
455    /// Reads the current GPU residency snapshot for this window's device
456    /// without presenting a frame. Resolves once the native adapter's
457    /// maintenance path fulfills it from current state.
458    pub fn query_gpu_resources(&self) -> Result<GpuResourceSummary, SimulationError> {
459        self.request(|reply| SimulationRequest::GpuResources {
460            window_id: self.window_id,
461            reply,
462        })
463    }
464
465    /// Releases every modifier/key held by this simulation keyboard.
466    pub fn release_all_keys(&self) -> Result<(), SimulationError> {
467        self.keyboard().release_all()
468    }
469
470    fn request<T>(
471        &self,
472        build: impl FnOnce(Reply<T>) -> SimulationRequest,
473    ) -> Result<T, SimulationError> {
474        let (sender, receiver) = mpsc::sync_channel(1);
475        self.bridge.enqueue(build(sender))?;
476        match receiver.recv_timeout(SIMULATION_REPLY_TIMEOUT) {
477            Ok(result) => result,
478            Err(mpsc::RecvTimeoutError::Timeout) => Err(SimulationError::Timeout),
479            Err(mpsc::RecvTimeoutError::Disconnected) => Err(SimulationError::Disconnected),
480        }
481    }
482
483    fn input(&self, event: InputEvent) -> Result<(), SimulationError> {
484        self.request(|reply| SimulationRequest::Input {
485            window_id: self.window_id,
486            event,
487            reply,
488        })
489    }
490}
491
492/// Mouse/pointer operations for a [`Simulation`] window. Only the primary
493/// pointer/left-button path is exposed here; multi-pointer tests can use the
494/// lower-level runtime input API directly.
495#[derive(Clone)]
496pub struct SimulationMouse {
497    simulation: Simulation,
498}
499
500impl SimulationMouse {
501    pub fn move_to(&self, position: Offset) -> Result<(), SimulationError> {
502        self.simulation.input(InputEvent::Pointer {
503            phase: PointerPhase::Move,
504            position,
505        })
506    }
507
508    pub fn down(&self, position: Offset) -> Result<(), SimulationError> {
509        self.simulation.input(InputEvent::Pointer {
510            phase: PointerPhase::Down,
511            position,
512        })
513    }
514
515    pub fn up(&self, position: Offset) -> Result<(), SimulationError> {
516        self.simulation.input(InputEvent::Pointer {
517            phase: PointerPhase::Up,
518            position,
519        })
520    }
521
522    pub fn click(&self, label: impl Into<String>) -> Result<(), SimulationError> {
523        self.simulation
524            .request(|reply| SimulationRequest::ClickLabel {
525                window_id: self.simulation.window_id,
526                label: label.into(),
527                reply,
528            })
529    }
530
531    pub fn click_at(&self, position: Offset) -> Result<(), SimulationError> {
532        self.simulation.request(|reply| SimulationRequest::ClickAt {
533            window_id: self.simulation.window_id,
534            position,
535            reply,
536        })
537    }
538
539    pub fn scroll(&self, delta: Offset) -> Result<(), SimulationError> {
540        self.simulation.input(InputEvent::Scroll { delta })
541    }
542}
543
544/// Keyboard operations for a [`Simulation`] window. Key events use Incular's
545/// platform-neutral physical [`Code`] plus the logical key supplied to
546/// [`Self::press_key`]. Text entry is a separate committed-text event, just as
547/// it is for native IME-aware adapters.
548#[derive(Clone)]
549pub struct SimulationKeyboard {
550    simulation: Simulation,
551}
552
553impl SimulationKeyboard {
554    pub fn key_down(&self, code: Code) -> Result<(), SimulationError> {
555        self.send_key(KeyState::Down, None, code)
556    }
557
558    pub fn key_up(&self, code: Code) -> Result<(), SimulationError> {
559        self.send_key(KeyState::Up, None, code)
560    }
561
562    pub fn press(&self, code: Code) -> Result<(), SimulationError> {
563        self.key_down(code)?;
564        self.key_up(code)
565    }
566
567    pub fn press_key(&self, key: KeyboardKey, code: Code) -> Result<(), SimulationError> {
568        self.send_key(KeyState::Down, Some(key.clone()), code)?;
569        self.send_key(KeyState::Up, Some(key), code)
570    }
571
572    pub fn type_text(&self, text: impl Into<String>) -> Result<(), SimulationError> {
573        let text = text.into();
574        if text.chars().any(char::is_control) {
575            return Err(SimulationError::InvalidInput(
576                "type_text accepts printable committed text; use press for commands".into(),
577            ));
578        }
579        self.simulation.input(InputEvent::Text(text))
580    }
581
582    pub fn release_all(&self) -> Result<(), SimulationError> {
583        let pressed = self
584            .simulation
585            .keyboard
586            .lock()
587            .expect("simulation keyboard mutex")
588            .pressed
589            .iter()
590            .copied()
591            .collect::<Vec<_>>();
592        for code in pressed {
593            self.key_up(code)?;
594        }
595        Ok(())
596    }
597
598    fn send_key(
599        &self,
600        state: KeyState,
601        explicit_key: Option<KeyboardKey>,
602        code: Code,
603    ) -> Result<(), SimulationError> {
604        let (key, modifiers, location, repeat, was_pressed) = {
605            let mut keyboard = self
606                .simulation
607                .keyboard
608                .lock()
609                .expect("simulation keyboard mutex");
610            let was_pressed = keyboard.pressed.contains(&code);
611            if state.is_down() {
612                if let Some(modifier) = modifier_for_code(code) {
613                    keyboard.modifiers.insert(modifier);
614                }
615                keyboard.pressed.insert(code);
616            }
617            let values = (
618                explicit_key.unwrap_or_else(|| logical_key(code)),
619                keyboard.modifiers,
620                location_for_code(code),
621                state.is_down() && was_pressed,
622                was_pressed,
623            );
624            if state.is_up() {
625                // Key-up carries the modifier as pressed, then the local
626                // simulator state releases it after the event is delivered.
627                keyboard.pressed.remove(&code);
628                if let Some(modifier) = modifier_for_code(code) {
629                    keyboard.modifiers.remove(modifier);
630                }
631            }
632            values
633        };
634        let event = KeyboardEvent {
635            state,
636            key,
637            code,
638            location,
639            modifiers,
640            repeat,
641            is_composing: false,
642        };
643        let result = self.simulation.input(InputEvent::Key(event));
644        if result.is_err() {
645            // Keep local key state usable after a disconnected/failed request.
646            let mut keyboard = self
647                .simulation
648                .keyboard
649                .lock()
650                .expect("simulation keyboard mutex");
651            if state.is_down() {
652                if !was_pressed {
653                    keyboard.pressed.remove(&code);
654                }
655            } else {
656                keyboard.pressed.insert(code);
657            }
658            keyboard.modifiers = modifiers_for_pressed(&keyboard.pressed);
659        }
660        result
661    }
662}
663
664fn modifier_for_code(code: Code) -> Option<Modifiers> {
665    match code {
666        Code::AltLeft | Code::AltRight => Some(Modifiers::ALT),
667        Code::ControlLeft | Code::ControlRight => Some(Modifiers::CONTROL),
668        Code::MetaLeft | Code::MetaRight => Some(Modifiers::META),
669        Code::ShiftLeft | Code::ShiftRight => Some(Modifiers::SHIFT),
670        _ => None,
671    }
672}
673
674fn modifiers_for_pressed(pressed: &HashSet<Code>) -> Modifiers {
675    pressed
676        .iter()
677        .fold(Modifiers::default(), |mut modifiers, code| {
678            if let Some(modifier) = modifier_for_code(*code) {
679                modifiers.insert(modifier);
680            }
681            modifiers
682        })
683}
684
685fn location_for_code(code: Code) -> Location {
686    match code {
687        Code::AltLeft | Code::ControlLeft | Code::MetaLeft | Code::ShiftLeft => Location::Left,
688        Code::AltRight | Code::ControlRight | Code::MetaRight | Code::ShiftRight => Location::Right,
689        Code::NumpadEnter => Location::Numpad,
690        _ => Location::Standard,
691    }
692}
693
694fn logical_key(code: Code) -> KeyboardKey {
695    let character = match code {
696        Code::Backquote => Some("`"),
697        Code::Backslash => Some("\\"),
698        Code::BracketLeft => Some("["),
699        Code::BracketRight => Some("]"),
700        Code::Comma => Some(","),
701        Code::Digit0 => Some("0"),
702        Code::Digit1 => Some("1"),
703        Code::Digit2 => Some("2"),
704        Code::Digit3 => Some("3"),
705        Code::Digit4 => Some("4"),
706        Code::Digit5 => Some("5"),
707        Code::Digit6 => Some("6"),
708        Code::Digit7 => Some("7"),
709        Code::Digit8 => Some("8"),
710        Code::Digit9 => Some("9"),
711        Code::Equal => Some("="),
712        Code::KeyA => Some("a"),
713        Code::KeyB => Some("b"),
714        Code::KeyC => Some("c"),
715        Code::KeyD => Some("d"),
716        Code::KeyE => Some("e"),
717        Code::KeyF => Some("f"),
718        Code::KeyG => Some("g"),
719        Code::KeyH => Some("h"),
720        Code::KeyI => Some("i"),
721        Code::KeyJ => Some("j"),
722        Code::KeyK => Some("k"),
723        Code::KeyL => Some("l"),
724        Code::KeyM => Some("m"),
725        Code::KeyN => Some("n"),
726        Code::KeyO => Some("o"),
727        Code::KeyP => Some("p"),
728        Code::KeyQ => Some("q"),
729        Code::KeyR => Some("r"),
730        Code::KeyS => Some("s"),
731        Code::KeyT => Some("t"),
732        Code::KeyU => Some("u"),
733        Code::KeyV => Some("v"),
734        Code::KeyW => Some("w"),
735        Code::KeyX => Some("x"),
736        Code::KeyY => Some("y"),
737        Code::KeyZ => Some("z"),
738        Code::Minus => Some("-"),
739        Code::Period => Some("."),
740        Code::Quote => Some("'"),
741        Code::Semicolon => Some(";"),
742        Code::Slash => Some("/"),
743        Code::Space => Some(" "),
744        Code::Numpad0 => Some("0"),
745        Code::Numpad1 => Some("1"),
746        Code::Numpad2 => Some("2"),
747        Code::Numpad3 => Some("3"),
748        Code::Numpad4 => Some("4"),
749        Code::Numpad5 => Some("5"),
750        Code::Numpad6 => Some("6"),
751        Code::Numpad7 => Some("7"),
752        Code::Numpad8 => Some("8"),
753        Code::Numpad9 => Some("9"),
754        Code::NumpadAdd => Some("+"),
755        Code::NumpadComma => Some(","),
756        Code::NumpadDecimal => Some("."),
757        Code::NumpadDivide => Some("/"),
758        Code::NumpadEqual => Some("="),
759        Code::NumpadMultiply | Code::NumpadStar => Some("*"),
760        Code::NumpadSubtract => Some("-"),
761        _ => None,
762    };
763    if let Some(character) = character {
764        return KeyboardKey::Character(character.into());
765    }
766    let named = match code {
767        Code::AltLeft | Code::AltRight => NamedKey::Alt,
768        Code::CapsLock => NamedKey::CapsLock,
769        Code::ControlLeft | Code::ControlRight => NamedKey::Control,
770        Code::Enter | Code::NumpadEnter => NamedKey::Enter,
771        Code::Escape => NamedKey::Escape,
772        Code::MetaLeft | Code::MetaRight => NamedKey::Meta,
773        Code::NumLock => NamedKey::NumLock,
774        Code::ScrollLock => NamedKey::ScrollLock,
775        Code::ShiftLeft | Code::ShiftRight => NamedKey::Shift,
776        Code::Tab => NamedKey::Tab,
777        Code::Backspace => NamedKey::Backspace,
778        Code::Delete => NamedKey::Delete,
779        Code::Insert => NamedKey::Insert,
780        Code::PageDown => NamedKey::PageDown,
781        Code::PageUp => NamedKey::PageUp,
782        Code::ArrowDown => NamedKey::ArrowDown,
783        Code::ArrowLeft => NamedKey::ArrowLeft,
784        Code::ArrowRight => NamedKey::ArrowRight,
785        Code::ArrowUp => NamedKey::ArrowUp,
786        Code::End => NamedKey::End,
787        Code::Home => NamedKey::Home,
788        _ => NamedKey::Unidentified,
789    };
790    KeyboardKey::Named(named)
791}
792
793fn valid_offset(offset: Offset) -> Result<(), SimulationError> {
794    if offset.x.is_finite() && offset.y.is_finite() {
795        Ok(())
796    } else {
797        Err(SimulationError::InvalidInput(
798            "pointer and scroll offsets must be finite".into(),
799        ))
800    }
801}
802
803impl Application {
804    /// Services queued in-process simulation requests on the application UI
805    /// thread. Native adapters call this from their event-loop wake path.
806    pub fn process_simulation_requests(&mut self) {
807        for _ in 0..MAX_SIMULATION_REQUESTS_PER_TURN {
808            let request = match self.simulation_receiver.try_recv() {
809                Ok(request) => request,
810                Err(mpsc::TryRecvError::Empty | mpsc::TryRecvError::Disconnected) => break,
811            };
812            self.handle_simulation_request(request);
813        }
814    }
815
816    fn handle_simulation_request(&mut self, request: SimulationRequest) {
817        match request {
818            SimulationRequest::Input {
819                window_id,
820                event,
821                reply,
822            } => {
823                let result = self
824                    .ensure_simulation_layout(window_id)
825                    .and_then(|_| self.dispatch_simulation_input(window_id, event));
826                let _ = reply.send(result);
827            }
828            SimulationRequest::ClickAt {
829                window_id,
830                position,
831                reply,
832            } => {
833                let _ = reply.send(self.dispatch_simulation_click(window_id, position));
834            }
835            SimulationRequest::ClickLabel {
836                window_id,
837                label,
838                reply,
839            } => {
840                let trigger = format!("click({label:?})");
841                let result = self
842                    .simulation_label_bounds(window_id, &label)
843                    .and_then(|bounds| {
844                        self.dispatch_simulation_click_prepared(
845                            window_id,
846                            bounds.center(),
847                            &trigger,
848                        )
849                    });
850                let _ = reply.send(result);
851            }
852            SimulationRequest::WaitForFrame { window_id, reply } => {
853                if self.contains_window(window_id) {
854                    let _ =
855                        self.with_window_mut(window_id, |record| record.runtime.request_frame());
856                    self.simulation_waiters.insert_frame(window_id, reply);
857                } else {
858                    let _ = reply.send(Err(SimulationError::WindowNotFound(window_id)));
859                }
860            }
861            SimulationRequest::Capture { window_id, reply } => {
862                if self.contains_window(window_id) {
863                    let _ =
864                        self.with_window_mut(window_id, |record| record.runtime.request_frame());
865                    self.simulation_waiters.insert_capture(window_id, reply);
866                } else {
867                    let _ = reply.send(Err(SimulationError::WindowNotFound(window_id)));
868                }
869            }
870            SimulationRequest::GpuResources { window_id, reply } => {
871                // Deliberately no frame request: resource observation must
872                // not present. The native adapter fulfills pending queries
873                // from current state on its maintenance path.
874                if self.contains_window(window_id) {
875                    self.simulation_waiters.insert_gpu(window_id, reply);
876                } else {
877                    let _ = reply.send(Err(SimulationError::WindowNotFound(window_id)));
878                }
879            }
880        }
881    }
882
883    fn dispatch_simulation_input(
884        &mut self,
885        window_id: WindowId,
886        event: InputEvent,
887    ) -> Result<(), SimulationError> {
888        let trigger = format!("simulation::{}", super::diagnostic_input_trigger(&event));
889        self.dispatch_simulation_input_with_trigger(window_id, event, &trigger)
890    }
891
892    fn dispatch_simulation_input_with_trigger(
893        &mut self,
894        window_id: WindowId,
895        event: InputEvent,
896        trigger: &str,
897    ) -> Result<(), SimulationError> {
898        if let InputEvent::Pointer { position, .. } | InputEvent::PointerWithId { position, .. } =
899            &event
900        {
901            valid_offset(*position)?;
902        }
903        if let InputEvent::Scroll { delta } = &event {
904            valid_offset(*delta)?;
905        }
906        if !self.contains_window(window_id) {
907            return Err(SimulationError::WindowNotFound(window_id));
908        }
909        let applied = self.with_window_mut(window_id, |record| {
910            record.input_events = record.input_events.wrapping_add(1);
911            let _ = record.runtime.handle_input_with_trigger(event, trigger);
912        });
913        applied
914            .map(|_| ())
915            .ok_or(SimulationError::WindowNotFound(window_id))
916    }
917
918    fn dispatch_simulation_click(
919        &mut self,
920        window_id: WindowId,
921        position: Offset,
922    ) -> Result<(), SimulationError> {
923        valid_offset(position)?;
924        self.ensure_simulation_layout(window_id)?;
925        self.dispatch_simulation_click_prepared(
926            window_id,
927            position,
928            &format!("click_at({:.1}, {:.1})", position.x, position.y),
929        )
930    }
931
932    fn dispatch_simulation_click_prepared(
933        &mut self,
934        window_id: WindowId,
935        position: Offset,
936        trigger: &str,
937    ) -> Result<(), SimulationError> {
938        self.dispatch_simulation_input_with_trigger(
939            window_id,
940            InputEvent::Pointer {
941                phase: PointerPhase::Move,
942                position,
943            },
944            trigger,
945        )?;
946        self.dispatch_simulation_input_with_trigger(
947            window_id,
948            InputEvent::Pointer {
949                phase: PointerPhase::Down,
950                position,
951            },
952            trigger,
953        )?;
954        self.dispatch_simulation_input_with_trigger(
955            window_id,
956            InputEvent::Pointer {
957                phase: PointerPhase::Up,
958                position,
959            },
960            trigger,
961        )
962    }
963
964    fn ensure_simulation_layout(&mut self, window_id: WindowId) -> Result<(), SimulationError> {
965        let diagnostics = self
966            .window_diagnostics(window_id)
967            .ok_or(SimulationError::WindowNotFound(window_id))?;
968        if diagnostics.physical_size.is_zero() {
969            return Err(SimulationError::FrameFailed(
970                "the target window has a zero-sized surface".into(),
971            ));
972        }
973        self.run_window_frame_at(
974            window_id,
975            Constraints::tight(diagnostics.logical_size),
976            Instant::now(),
977        )
978        .map_err(|error| SimulationError::FrameFailed(format!("{error:?}")))?;
979        // The preparation frame is not presented by this runtime-only step;
980        // leave the native adapter a real redraw to present afterward.
981        let _ = self.with_window_mut(window_id, |record| record.runtime.request_frame());
982        Ok(())
983    }
984
985    fn simulation_label_bounds(
986        &mut self,
987        window_id: WindowId,
988        label: &str,
989    ) -> Result<Rect, SimulationError> {
990        // Targets resolve through semantics, which a native host leaves off
991        // until something needs them.
992        self.set_semantics_enabled(window_id, true);
993        self.ensure_simulation_layout(window_id)?;
994        let viewport = self
995            .window_diagnostics(window_id)
996            .map(|diagnostics| Rect::from_origin_size(Offset::ZERO, diagnostics.logical_size))
997            .ok_or(SimulationError::WindowNotFound(window_id))?;
998        let result = self.registry.borrow().get(window_id).and_then(|record| {
999            let mut fallback = None;
1000            record
1001                .runtime
1002                .tree()
1003                .semantics()
1004                .iter()
1005                .find_map(|(_, node)| {
1006                    if node.label.as_deref() != Some(label) || !node.state.enabled {
1007                        return None;
1008                    }
1009                    // Semantic trees can retain children just outside a
1010                    // viewport (especially sliver children). Resolve a
1011                    // pointer target only from the visible intersection so a
1012                    // simulation never reports a click that the pointer
1013                    // pipeline cannot receive.
1014                    let visible_bounds = node.bounds.intersection(viewport)?;
1015                    if visible_bounds.size.width <= 0.0 || visible_bounds.size.height <= 0.0 {
1016                        return None;
1017                    }
1018                    if node.actions.contains(&SemanticActionKind::Activate) {
1019                        return Some(visible_bounds);
1020                    }
1021                    fallback = Some(visible_bounds);
1022                    None
1023                })
1024                .or(fallback)
1025        });
1026        result.ok_or_else(|| SimulationError::ElementNotFound(label.to_owned()))
1027    }
1028
1029    pub fn simulation_capture_pending(&self, window_id: WindowId) -> bool {
1030        self.simulation_waiters.capture_pending(window_id)
1031    }
1032
1033    /// Returns the live windows with pending GPU-resource queries so the
1034    /// native adapter can fulfill them from current state without
1035    /// presenting. Live waiters stay queued until
1036    /// [`Self::complete_gpu_resource_query`] drains them; waiters for
1037    /// windows that no longer exist fail immediately instead of hanging
1038    /// the querier. Window close and application shutdown settle pending
1039    /// waiters through the shared close/shutdown cleanup, never through
1040    /// this method.
1041    pub fn take_gpu_resource_queries(&mut self) -> Vec<WindowId> {
1042        let mut live = Vec::new();
1043        for window_id in self.simulation_waiters.gpu_windows() {
1044            if self.contains_window(window_id) {
1045                live.push(window_id);
1046            } else {
1047                for reply in self.simulation_waiters.take_gpu_waiters(window_id) {
1048                    let _ = reply.send(Err(SimulationError::WindowNotFound(window_id)));
1049                }
1050            }
1051        }
1052        live
1053    }
1054
1055    /// Completes pending GPU-resource waiters after the native adapter has
1056    /// snapshotted current residency. Never renders.
1057    pub fn complete_gpu_resource_query(
1058        &mut self,
1059        window_id: WindowId,
1060        summary: GpuResourceSummary,
1061    ) {
1062        self.simulation_waiters.complete_gpu(window_id, summary);
1063    }
1064
1065    /// Reports whether a simulator is waiting for the next presented frame.
1066    #[must_use]
1067    pub fn simulation_frame_pending(&self, window_id: WindowId) -> bool {
1068        self.simulation_waiters.frame_pending(window_id)
1069    }
1070
1071    /// Completes simulator waiters after the native renderer has presented a
1072    /// frame. `capture` is supplied only when a renderer serviced a pending
1073    /// capture request.
1074    pub fn complete_simulation_frame(
1075        &mut self,
1076        window_id: WindowId,
1077        presented: bool,
1078        capture: Option<Result<Screenshot, String>>,
1079    ) {
1080        self.simulation_waiters
1081            .complete_frame(window_id, presented, capture);
1082    }
1083
1084    /// Fails simulator requests when a native frame cannot be produced.
1085    pub fn fail_simulation_frame(&mut self, window_id: WindowId, message: impl Into<String>) {
1086        self.simulation_waiters.fail_frame(window_id, message);
1087    }
1088
1089    pub(crate) fn fail_simulation_window(&mut self, window_id: WindowId) {
1090        self.simulation_waiters.fail_window(window_id);
1091    }
1092
1093    pub(crate) fn stop_simulation(&self) {
1094        self.simulation_bridge.stop();
1095    }
1096}
1097
1098trait RectCenter {
1099    fn center(self) -> Offset;
1100}
1101
1102impl RectCenter for Rect {
1103    fn center(self) -> Offset {
1104        Offset::new(
1105            self.origin.x + self.size.width * 0.5,
1106            self.origin.y + self.size.height * 0.5,
1107        )
1108    }
1109}
1110
1111// Keep the imported input types intentionally visible in this module's API
1112// documentation and make accidental future requests for native IME payloads
1113// explicit at the type boundary.