1use 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#[derive(Clone, Debug, PartialEq, Eq)]
31pub struct Screenshot {
32 width: u32,
33 height: u32,
34 rgba8: Vec<u8>,
35}
36
37impl Screenshot {
38 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 #[must_use]
78 pub fn pixels(&self) -> &[u8] {
79 &self.rgba8
80 }
81
82 #[must_use]
84 pub fn into_pixels(self) -> Vec<u8> {
85 self.rgba8
86 }
87}
88
89#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
91pub enum SimulationError {
92 #[error("the Incular simulation bridge is disconnected")]
95 Disconnected,
96 #[error("the Incular simulation request timed out")]
98 Timeout,
99 #[error("simulation window {0} was not found")]
101 WindowNotFound(WindowId),
102 #[error("simulation window {0} was closed")]
104 WindowClosed(WindowId),
105 #[error("no semantic element labeled {0:?} was found")]
107 ElementNotFound(String),
108 #[error("invalid simulation input: {0}")]
110 InvalidInput(String),
111 #[error("window capture unavailable: {0}")]
113 CaptureUnavailable(String),
114 #[error("simulation frame failed: {0}")]
116 FrameFailed(String),
117 #[error("the application panicked: {0}")]
121 AppPanicked(String),
122}
123
124type Reply<T> = mpsc::SyncSender<Result<T, SimulationError>>;
125
126#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
133pub enum PointSpace {
134 #[default]
136 Logical,
137 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 panic: Arc<Mutex<Option<String>>>,
187}
188
189thread_local! {
190 static PANIC_SLOTS: std::cell::RefCell<Vec<std::sync::Weak<Mutex<Option<String>>>>> =
193 const { std::cell::RefCell::new(Vec::new()) };
194}
195
196fn 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 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#[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 #[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 #[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 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 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 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 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 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 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 Self::settle(&mut self.gpu, window_id, Err(error));
392 }
393
394 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 pub(crate) fn gpu_windows(&self) -> Vec<WindowId> {
403 self.gpu.keys().copied().collect()
404 }
405
406 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#[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 #[must_use]
439 pub fn window(&self, handle: &WindowHandle) -> Self {
440 self.for_window(handle.id())
441 }
442
443 #[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 #[must_use]
460 pub fn mouse(&self) -> SimulationMouse {
461 SimulationMouse {
462 simulation: self.clone(),
463 }
464 }
465
466 #[must_use]
468 pub fn keyboard(&self) -> SimulationKeyboard {
469 SimulationKeyboard {
470 simulation: self.clone(),
471 }
472 }
473
474 pub fn click(&self, label: impl Into<String>) -> Result<(), SimulationError> {
477 self.mouse().click(label)
478 }
479
480 pub fn click_at(&self, position: Offset) -> Result<(), SimulationError> {
484 self.mouse().click_logical(position)
485 }
486
487 pub fn click_logical(&self, position: Offset) -> Result<(), SimulationError> {
490 self.mouse().click_logical(position)
491 }
492
493 pub fn click_physical(&self, position: Offset) -> Result<(), SimulationError> {
497 self.mouse().click_physical(position)
498 }
499
500 pub fn click_in(&self, position: Offset, space: PointSpace) -> Result<(), SimulationError> {
502 self.mouse().click_in(position, space)
503 }
504
505 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 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 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 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 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 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 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 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#[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 pub fn click_at(&self, position: Offset) -> Result<(), SimulationError> {
671 self.click_logical(position)
672 }
673
674 pub fn click_logical(&self, position: Offset) -> Result<(), SimulationError> {
676 self.click_in(position, PointSpace::Logical)
677 }
678
679 pub fn click_physical(&self, position: Offset) -> Result<(), SimulationError> {
681 self.click_in(position, PointSpace::Physical)
682 }
683
684 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#[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 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 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 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 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 let _ = self.with_window_mut(window_id, |record| record.runtime.request_frame());
1187 Ok(())
1188 }
1189
1190 fn simulation_label_target(
1196 &mut self,
1197 window_id: WindowId,
1198 label: &str,
1199 ) -> Result<LabelTarget, SimulationError> {
1200 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 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 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 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 #[must_use]
1312 pub fn simulation_frame_pending(&self, window_id: WindowId) -> bool {
1313 self.simulation_waiters.frame_pending(window_id)
1314 }
1315
1316 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 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
1343struct LabelTarget {
1345 node: incular_semantics::SemanticNodeId,
1346 bounds: Rect,
1347 focusable: bool,
1348}
1349
1350fn 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