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