use super::{Application, WindowHandle, profiling::GpuResourceSummary};
use incular_config::Constraints;
use incular_core::time::Instant;
use incular_core::{
Code, InputEvent, KeyState, KeyboardEvent, KeyboardKey, Location, Modifiers, NamedKey, Offset,
PointerPhase, Rect,
};
use incular_platform::WindowId;
use incular_semantics::{SemanticAction, SemanticActionKind};
use std::collections::{HashMap, HashSet};
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex, mpsc};
use std::time::Duration;
const SIMULATION_REPLY_TIMEOUT: Duration = Duration::from_secs(30);
const MAX_SIMULATION_REQUESTS_PER_TURN: usize = 64;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Screenshot {
width: u32,
height: u32,
rgba8: Vec<u8>,
}
impl Screenshot {
pub fn from_rgba8(width: u32, height: u32, rgba8: Vec<u8>) -> Result<Self, SimulationError> {
let expected = usize::try_from(width)
.ok()
.and_then(|width| {
usize::try_from(height)
.ok()
.and_then(|height| width.checked_mul(height))
})
.and_then(|pixels| pixels.checked_mul(4))
.ok_or_else(|| {
SimulationError::InvalidInput("screenshot dimensions overflow".into())
})?;
if expected == 0 || rgba8.len() != expected {
return Err(SimulationError::InvalidInput(format!(
"screenshot pixel length {} does not match {}x{} RGBA8",
rgba8.len(),
width,
height
)));
}
Ok(Self {
width,
height,
rgba8,
})
}
#[must_use]
pub const fn width(&self) -> u32 {
self.width
}
#[must_use]
pub const fn height(&self) -> u32 {
self.height
}
#[must_use]
pub fn pixels(&self) -> &[u8] {
&self.rgba8
}
#[must_use]
pub fn into_pixels(self) -> Vec<u8> {
self.rgba8
}
}
#[derive(Clone, Debug, PartialEq, Eq, thiserror::Error)]
pub enum SimulationError {
#[error("the Incular simulation bridge is disconnected")]
Disconnected,
#[error("the Incular simulation request timed out")]
Timeout,
#[error("simulation window {0} was not found")]
WindowNotFound(WindowId),
#[error("simulation window {0} was closed")]
WindowClosed(WindowId),
#[error("no semantic element labeled {0:?} was found")]
ElementNotFound(String),
#[error("invalid simulation input: {0}")]
InvalidInput(String),
#[error("window capture unavailable: {0}")]
CaptureUnavailable(String),
#[error("simulation frame failed: {0}")]
FrameFailed(String),
#[error("the application panicked: {0}")]
AppPanicked(String),
}
type Reply<T> = mpsc::SyncSender<Result<T, SimulationError>>;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum PointSpace {
#[default]
Logical,
Physical,
}
pub(crate) enum SimulationRequest {
Input {
window_id: WindowId,
event: InputEvent,
reply: Reply<()>,
},
ClickAt {
window_id: WindowId,
position: Offset,
space: PointSpace,
reply: Reply<()>,
},
ScaleFactor {
window_id: WindowId,
reply: Reply<f64>,
},
ClickLabel {
window_id: WindowId,
label: String,
reply: Reply<()>,
},
FocusLabel {
window_id: WindowId,
label: String,
reply: Reply<()>,
},
WaitForFrame {
window_id: WindowId,
reply: Reply<()>,
},
Capture {
window_id: WindowId,
reply: Reply<Screenshot>,
},
GpuResources {
window_id: WindowId,
reply: Reply<GpuResourceSummary>,
},
}
pub(crate) struct SimulationBridge {
sender: mpsc::Sender<SimulationRequest>,
wake: Mutex<Option<Arc<dyn super::RuntimeWake>>>,
accepting: AtomicBool,
panic: Arc<Mutex<Option<String>>>,
}
thread_local! {
static PANIC_SLOTS: std::cell::RefCell<Vec<std::sync::Weak<Mutex<Option<String>>>>> =
const { std::cell::RefCell::new(Vec::new()) };
}
fn install_panic_recorder() {
static INSTALL: std::sync::Once = std::sync::Once::new();
INSTALL.call_once(|| {
let previous = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
let message = info
.payload()
.downcast_ref::<&str>()
.map(|message| (*message).to_owned())
.or_else(|| info.payload().downcast_ref::<String>().cloned())
.unwrap_or_else(|| "non-string panic payload".to_owned());
let message = match info.location() {
Some(location) => format!("{message} at {location}"),
None => message,
};
let _ = PANIC_SLOTS.try_with(|slots| {
slots.borrow_mut().retain(|slot| {
let Some(slot) = slot.upgrade() else {
return false;
};
if let Ok(mut recorded) = slot.lock() {
recorded.get_or_insert_with(|| message.clone());
}
true
});
});
previous(info);
}));
});
}
impl SimulationBridge {
pub(crate) fn new(sender: mpsc::Sender<SimulationRequest>) -> Self {
install_panic_recorder();
let panic = Arc::new(Mutex::new(None));
let _ = PANIC_SLOTS.try_with(|slots| slots.borrow_mut().push(Arc::downgrade(&panic)));
Self {
sender,
wake: Mutex::new(None),
accepting: AtomicBool::new(true),
panic,
}
}
fn panicked(&self) -> Option<SimulationError> {
self.panic
.lock()
.ok()
.and_then(|recorded| recorded.clone())
.map(SimulationError::AppPanicked)
}
fn enqueue(&self, request: SimulationRequest) -> Result<(), SimulationError> {
if let Some(error) = self.panicked() {
return Err(error);
}
if !self.accepting.load(Ordering::Acquire) {
return Err(SimulationError::Disconnected);
}
self.sender
.send(request)
.map_err(|_| SimulationError::Disconnected)?;
if let Some(wake) = self.wake.lock().expect("simulation wake mutex").as_ref() {
wake.wake();
}
Ok(())
}
pub(crate) fn set_wake(&self, wake: Arc<dyn super::RuntimeWake>) {
*self.wake.lock().expect("simulation wake mutex") = Some(wake);
}
pub(crate) fn stop(&self) {
self.accepting.store(false, Ordering::Release);
}
}
#[derive(Default)]
struct KeyboardState {
modifiers: Modifiers,
pressed: HashSet<Code>,
}
#[derive(Default)]
pub(crate) struct SimulationWaiters {
frame: HashMap<WindowId, Vec<mpsc::SyncSender<Result<(), SimulationError>>>>,
capture: HashMap<WindowId, Vec<mpsc::SyncSender<Result<Screenshot, SimulationError>>>>,
gpu: HashMap<WindowId, Vec<mpsc::SyncSender<Result<GpuResourceSummary, SimulationError>>>>,
}
impl SimulationWaiters {
fn settle<T: Clone>(
waiters: &mut HashMap<WindowId, Vec<mpsc::SyncSender<Result<T, SimulationError>>>>,
window_id: WindowId,
result: Result<T, SimulationError>,
) {
if let Some(pending) = waiters.remove(&window_id) {
for reply in pending {
let _ = reply.send(result.clone());
}
}
}
#[must_use]
pub(crate) fn frame_pending(&self, window_id: WindowId) -> bool {
self.frame
.get(&window_id)
.is_some_and(|waiters| !waiters.is_empty())
}
#[must_use]
pub(crate) fn capture_pending(&self, window_id: WindowId) -> bool {
self.capture
.get(&window_id)
.is_some_and(|waiters| !waiters.is_empty())
}
pub(crate) fn insert_frame(
&mut self,
window_id: WindowId,
reply: mpsc::SyncSender<Result<(), SimulationError>>,
) {
self.frame.entry(window_id).or_default().push(reply);
}
pub(crate) fn insert_capture(
&mut self,
window_id: WindowId,
reply: mpsc::SyncSender<Result<Screenshot, SimulationError>>,
) {
self.capture.entry(window_id).or_default().push(reply);
}
pub(crate) fn insert_gpu(
&mut self,
window_id: WindowId,
reply: mpsc::SyncSender<Result<GpuResourceSummary, SimulationError>>,
) {
self.gpu.entry(window_id).or_default().push(reply);
}
pub(crate) fn complete_frame(
&mut self,
window_id: WindowId,
presented: bool,
capture: Option<Result<Screenshot, String>>,
) {
if !presented {
return;
}
Self::settle(&mut self.frame, window_id, Ok(()));
let result = capture
.map(|result| result.map_err(SimulationError::CaptureUnavailable))
.unwrap_or_else(|| {
Err(SimulationError::CaptureUnavailable(
"the renderer did not return a capture".into(),
))
});
Self::settle(&mut self.capture, window_id, result);
}
pub(crate) fn fail_frame(&mut self, window_id: WindowId, message: impl Into<String>) {
let error = SimulationError::FrameFailed(message.into());
Self::settle(&mut self.frame, window_id, Err(error.clone()));
let capture_error = SimulationError::CaptureUnavailable(error.to_string());
Self::settle(&mut self.capture, window_id, Err(capture_error));
}
pub(crate) fn fail_window(&mut self, window_id: WindowId) {
let error = SimulationError::WindowClosed(window_id);
Self::settle(&mut self.frame, window_id, Err(error.clone()));
Self::settle(&mut self.capture, window_id, Err(error.clone()));
Self::settle(&mut self.gpu, window_id, Err(error));
}
pub(crate) fn complete_gpu(&mut self, window_id: WindowId, summary: GpuResourceSummary) {
Self::settle(&mut self.gpu, window_id, Ok(summary));
}
pub(crate) fn gpu_windows(&self) -> Vec<WindowId> {
self.gpu.keys().copied().collect()
}
pub(crate) fn take_gpu_waiters(
&mut self,
window_id: WindowId,
) -> Vec<Reply<GpuResourceSummary>> {
self.gpu.remove(&window_id).unwrap_or_default()
}
}
#[derive(Clone)]
pub struct Simulation {
bridge: Arc<SimulationBridge>,
window_id: WindowId,
keyboard: Arc<Mutex<KeyboardState>>,
}
impl Simulation {
pub(crate) fn new(bridge: Arc<SimulationBridge>, window_id: WindowId) -> Self {
Self {
bridge,
window_id,
keyboard: Arc::new(Mutex::new(KeyboardState::default())),
}
}
#[must_use]
pub fn window(&self, handle: &WindowHandle) -> Self {
self.for_window(handle.id())
}
#[must_use]
pub fn for_window(&self, window_id: WindowId) -> Self {
Self {
bridge: self.bridge.clone(),
window_id,
keyboard: self.keyboard.clone(),
}
}
#[must_use]
pub const fn window_id(&self) -> WindowId {
self.window_id
}
#[must_use]
pub fn mouse(&self) -> SimulationMouse {
SimulationMouse {
simulation: self.clone(),
}
}
#[must_use]
pub fn keyboard(&self) -> SimulationKeyboard {
SimulationKeyboard {
simulation: self.clone(),
}
}
pub fn click(&self, label: impl Into<String>) -> Result<(), SimulationError> {
self.mouse().click(label)
}
pub fn click_at(&self, position: Offset) -> Result<(), SimulationError> {
self.mouse().click_logical(position)
}
pub fn click_logical(&self, position: Offset) -> Result<(), SimulationError> {
self.mouse().click_logical(position)
}
pub fn click_physical(&self, position: Offset) -> Result<(), SimulationError> {
self.mouse().click_physical(position)
}
pub fn click_in(&self, position: Offset, space: PointSpace) -> Result<(), SimulationError> {
self.mouse().click_in(position, space)
}
pub fn scale_factor(&self) -> Result<f64, SimulationError> {
self.request(|reply| SimulationRequest::ScaleFactor {
window_id: self.window_id,
reply,
})
}
pub fn focus(&self, label: impl Into<String>) -> Result<(), SimulationError> {
self.request(|reply| SimulationRequest::FocusLabel {
window_id: self.window_id,
label: label.into(),
reply,
})
}
pub fn type_into(
&self,
label: impl Into<String>,
text: impl Into<String>,
) -> Result<(), SimulationError> {
self.focus(label)?;
self.type_text(text)
}
pub fn move_mouse_to(&self, position: Offset) -> Result<(), SimulationError> {
self.mouse().move_to(position)
}
pub fn scroll(&self, delta: Offset) -> Result<(), SimulationError> {
self.mouse().scroll(delta)
}
pub fn key_down(&self, code: Code) -> Result<(), SimulationError> {
self.keyboard().key_down(code)
}
pub fn key_up(&self, code: Code) -> Result<(), SimulationError> {
self.keyboard().key_up(code)
}
pub fn press(&self, code: Code) -> Result<(), SimulationError> {
self.keyboard().press(code)
}
pub fn press_key(&self, key: KeyboardKey, code: Code) -> Result<(), SimulationError> {
self.keyboard().press_key(key, code)
}
pub fn type_text(&self, text: impl Into<String>) -> Result<(), SimulationError> {
self.keyboard().type_text(text)
}
pub fn wait_for_frame(&self) -> Result<(), SimulationError> {
self.request(|reply| SimulationRequest::WaitForFrame {
window_id: self.window_id,
reply,
})
}
pub fn capture(&self) -> Result<Screenshot, SimulationError> {
self.request(|reply| SimulationRequest::Capture {
window_id: self.window_id,
reply,
})
}
pub fn query_gpu_resources(&self) -> Result<GpuResourceSummary, SimulationError> {
self.request(|reply| SimulationRequest::GpuResources {
window_id: self.window_id,
reply,
})
}
pub fn release_all_keys(&self) -> Result<(), SimulationError> {
self.keyboard().release_all()
}
fn request<T>(
&self,
build: impl FnOnce(Reply<T>) -> SimulationRequest,
) -> Result<T, SimulationError> {
let (sender, receiver) = mpsc::sync_channel(1);
self.bridge.enqueue(build(sender))?;
match receiver.recv_timeout(SIMULATION_REPLY_TIMEOUT) {
Ok(result) => result,
Err(mpsc::RecvTimeoutError::Timeout) => {
Err(self.bridge.panicked().unwrap_or(SimulationError::Timeout))
}
Err(mpsc::RecvTimeoutError::Disconnected) => Err(self
.bridge
.panicked()
.unwrap_or(SimulationError::Disconnected)),
}
}
fn input(&self, event: InputEvent) -> Result<(), SimulationError> {
self.request(|reply| SimulationRequest::Input {
window_id: self.window_id,
event,
reply,
})
}
}
#[derive(Clone)]
pub struct SimulationMouse {
simulation: Simulation,
}
impl SimulationMouse {
pub fn move_to(&self, position: Offset) -> Result<(), SimulationError> {
self.simulation.input(InputEvent::Pointer {
phase: PointerPhase::Move,
position,
})
}
pub fn down(&self, position: Offset) -> Result<(), SimulationError> {
self.simulation.input(InputEvent::Pointer {
phase: PointerPhase::Down,
position,
})
}
pub fn up(&self, position: Offset) -> Result<(), SimulationError> {
self.simulation.input(InputEvent::Pointer {
phase: PointerPhase::Up,
position,
})
}
pub fn click(&self, label: impl Into<String>) -> Result<(), SimulationError> {
self.simulation
.request(|reply| SimulationRequest::ClickLabel {
window_id: self.simulation.window_id,
label: label.into(),
reply,
})
}
pub fn click_at(&self, position: Offset) -> Result<(), SimulationError> {
self.click_logical(position)
}
pub fn click_logical(&self, position: Offset) -> Result<(), SimulationError> {
self.click_in(position, PointSpace::Logical)
}
pub fn click_physical(&self, position: Offset) -> Result<(), SimulationError> {
self.click_in(position, PointSpace::Physical)
}
pub fn click_in(&self, position: Offset, space: PointSpace) -> Result<(), SimulationError> {
self.simulation.request(|reply| SimulationRequest::ClickAt {
window_id: self.simulation.window_id,
position,
space,
reply,
})
}
pub fn scroll(&self, delta: Offset) -> Result<(), SimulationError> {
self.simulation.input(InputEvent::Scroll { delta })
}
}
#[derive(Clone)]
pub struct SimulationKeyboard {
simulation: Simulation,
}
impl SimulationKeyboard {
pub fn key_down(&self, code: Code) -> Result<(), SimulationError> {
self.send_key(KeyState::Down, None, code)
}
pub fn key_up(&self, code: Code) -> Result<(), SimulationError> {
self.send_key(KeyState::Up, None, code)
}
pub fn press(&self, code: Code) -> Result<(), SimulationError> {
self.key_down(code)?;
self.key_up(code)
}
pub fn press_key(&self, key: KeyboardKey, code: Code) -> Result<(), SimulationError> {
self.send_key(KeyState::Down, Some(key.clone()), code)?;
self.send_key(KeyState::Up, Some(key), code)
}
pub fn type_text(&self, text: impl Into<String>) -> Result<(), SimulationError> {
let text = text.into();
if text.chars().any(char::is_control) {
return Err(SimulationError::InvalidInput(
"type_text accepts printable committed text; use press for commands".into(),
));
}
self.simulation.input(InputEvent::Text(text))
}
pub fn release_all(&self) -> Result<(), SimulationError> {
let pressed = self
.simulation
.keyboard
.lock()
.expect("simulation keyboard mutex")
.pressed
.iter()
.copied()
.collect::<Vec<_>>();
for code in pressed {
self.key_up(code)?;
}
Ok(())
}
fn send_key(
&self,
state: KeyState,
explicit_key: Option<KeyboardKey>,
code: Code,
) -> Result<(), SimulationError> {
let (key, modifiers, location, repeat, was_pressed) = {
let mut keyboard = self
.simulation
.keyboard
.lock()
.expect("simulation keyboard mutex");
let was_pressed = keyboard.pressed.contains(&code);
if state.is_down() {
if let Some(modifier) = modifier_for_code(code) {
keyboard.modifiers.insert(modifier);
}
keyboard.pressed.insert(code);
}
let values = (
explicit_key.unwrap_or_else(|| logical_key(code)),
keyboard.modifiers,
location_for_code(code),
state.is_down() && was_pressed,
was_pressed,
);
if state.is_up() {
keyboard.pressed.remove(&code);
if let Some(modifier) = modifier_for_code(code) {
keyboard.modifiers.remove(modifier);
}
}
values
};
let event = KeyboardEvent {
state,
key,
code,
location,
modifiers,
repeat,
is_composing: false,
};
let result = self.simulation.input(InputEvent::Key(event));
if result.is_err() {
let mut keyboard = self
.simulation
.keyboard
.lock()
.expect("simulation keyboard mutex");
if state.is_down() {
if !was_pressed {
keyboard.pressed.remove(&code);
}
} else {
keyboard.pressed.insert(code);
}
keyboard.modifiers = modifiers_for_pressed(&keyboard.pressed);
}
result
}
}
fn modifier_for_code(code: Code) -> Option<Modifiers> {
match code {
Code::AltLeft | Code::AltRight => Some(Modifiers::ALT),
Code::ControlLeft | Code::ControlRight => Some(Modifiers::CONTROL),
Code::MetaLeft | Code::MetaRight => Some(Modifiers::META),
Code::ShiftLeft | Code::ShiftRight => Some(Modifiers::SHIFT),
_ => None,
}
}
fn modifiers_for_pressed(pressed: &HashSet<Code>) -> Modifiers {
pressed
.iter()
.fold(Modifiers::default(), |mut modifiers, code| {
if let Some(modifier) = modifier_for_code(*code) {
modifiers.insert(modifier);
}
modifiers
})
}
fn location_for_code(code: Code) -> Location {
match code {
Code::AltLeft | Code::ControlLeft | Code::MetaLeft | Code::ShiftLeft => Location::Left,
Code::AltRight | Code::ControlRight | Code::MetaRight | Code::ShiftRight => Location::Right,
Code::NumpadEnter => Location::Numpad,
_ => Location::Standard,
}
}
fn logical_key(code: Code) -> KeyboardKey {
let character = match code {
Code::Backquote => Some("`"),
Code::Backslash => Some("\\"),
Code::BracketLeft => Some("["),
Code::BracketRight => Some("]"),
Code::Comma => Some(","),
Code::Digit0 => Some("0"),
Code::Digit1 => Some("1"),
Code::Digit2 => Some("2"),
Code::Digit3 => Some("3"),
Code::Digit4 => Some("4"),
Code::Digit5 => Some("5"),
Code::Digit6 => Some("6"),
Code::Digit7 => Some("7"),
Code::Digit8 => Some("8"),
Code::Digit9 => Some("9"),
Code::Equal => Some("="),
Code::KeyA => Some("a"),
Code::KeyB => Some("b"),
Code::KeyC => Some("c"),
Code::KeyD => Some("d"),
Code::KeyE => Some("e"),
Code::KeyF => Some("f"),
Code::KeyG => Some("g"),
Code::KeyH => Some("h"),
Code::KeyI => Some("i"),
Code::KeyJ => Some("j"),
Code::KeyK => Some("k"),
Code::KeyL => Some("l"),
Code::KeyM => Some("m"),
Code::KeyN => Some("n"),
Code::KeyO => Some("o"),
Code::KeyP => Some("p"),
Code::KeyQ => Some("q"),
Code::KeyR => Some("r"),
Code::KeyS => Some("s"),
Code::KeyT => Some("t"),
Code::KeyU => Some("u"),
Code::KeyV => Some("v"),
Code::KeyW => Some("w"),
Code::KeyX => Some("x"),
Code::KeyY => Some("y"),
Code::KeyZ => Some("z"),
Code::Minus => Some("-"),
Code::Period => Some("."),
Code::Quote => Some("'"),
Code::Semicolon => Some(";"),
Code::Slash => Some("/"),
Code::Space => Some(" "),
Code::Numpad0 => Some("0"),
Code::Numpad1 => Some("1"),
Code::Numpad2 => Some("2"),
Code::Numpad3 => Some("3"),
Code::Numpad4 => Some("4"),
Code::Numpad5 => Some("5"),
Code::Numpad6 => Some("6"),
Code::Numpad7 => Some("7"),
Code::Numpad8 => Some("8"),
Code::Numpad9 => Some("9"),
Code::NumpadAdd => Some("+"),
Code::NumpadComma => Some(","),
Code::NumpadDecimal => Some("."),
Code::NumpadDivide => Some("/"),
Code::NumpadEqual => Some("="),
Code::NumpadMultiply | Code::NumpadStar => Some("*"),
Code::NumpadSubtract => Some("-"),
_ => None,
};
if let Some(character) = character {
return KeyboardKey::Character(character.into());
}
let named = match code {
Code::AltLeft | Code::AltRight => NamedKey::Alt,
Code::CapsLock => NamedKey::CapsLock,
Code::ControlLeft | Code::ControlRight => NamedKey::Control,
Code::Enter | Code::NumpadEnter => NamedKey::Enter,
Code::Escape => NamedKey::Escape,
Code::MetaLeft | Code::MetaRight => NamedKey::Meta,
Code::NumLock => NamedKey::NumLock,
Code::ScrollLock => NamedKey::ScrollLock,
Code::ShiftLeft | Code::ShiftRight => NamedKey::Shift,
Code::Tab => NamedKey::Tab,
Code::Backspace => NamedKey::Backspace,
Code::Delete => NamedKey::Delete,
Code::Insert => NamedKey::Insert,
Code::PageDown => NamedKey::PageDown,
Code::PageUp => NamedKey::PageUp,
Code::ArrowDown => NamedKey::ArrowDown,
Code::ArrowLeft => NamedKey::ArrowLeft,
Code::ArrowRight => NamedKey::ArrowRight,
Code::ArrowUp => NamedKey::ArrowUp,
Code::End => NamedKey::End,
Code::Home => NamedKey::Home,
_ => NamedKey::Unidentified,
};
KeyboardKey::Named(named)
}
fn valid_offset(offset: Offset) -> Result<(), SimulationError> {
if offset.x.is_finite() && offset.y.is_finite() {
Ok(())
} else {
Err(SimulationError::InvalidInput(
"pointer and scroll offsets must be finite".into(),
))
}
}
impl Application {
pub fn process_simulation_requests(&mut self) {
for _ in 0..MAX_SIMULATION_REQUESTS_PER_TURN {
let request = match self.simulation_receiver.try_recv() {
Ok(request) => request,
Err(mpsc::TryRecvError::Empty | mpsc::TryRecvError::Disconnected) => break,
};
self.handle_simulation_request(request);
}
}
fn handle_simulation_request(&mut self, request: SimulationRequest) {
match request {
SimulationRequest::Input {
window_id,
event,
reply,
} => {
let result = self
.ensure_simulation_layout(window_id)
.and_then(|_| self.dispatch_simulation_input(window_id, event));
let _ = reply.send(result);
}
SimulationRequest::ClickAt {
window_id,
position,
space,
reply,
} => {
let result = self
.simulation_logical_point(window_id, position, space)
.and_then(|position| self.dispatch_simulation_click(window_id, position));
let _ = reply.send(result);
}
SimulationRequest::ScaleFactor { window_id, reply } => {
let _ = reply.send(self.simulation_scale_factor(window_id));
}
SimulationRequest::ClickLabel {
window_id,
label,
reply,
} => {
let trigger = format!("click({label:?})");
let result = self
.simulation_label_target(window_id, &label)
.and_then(|target| {
self.dispatch_simulation_click_prepared(
window_id,
target.bounds.center(),
&trigger,
)
});
let _ = reply.send(result);
}
SimulationRequest::FocusLabel {
window_id,
label,
reply,
} => {
let _ = reply.send(self.simulation_focus_label(window_id, &label));
}
SimulationRequest::WaitForFrame { window_id, reply } => {
if self.contains_window(window_id) {
let _ =
self.with_window_mut(window_id, |record| record.runtime.request_frame());
self.simulation_waiters.insert_frame(window_id, reply);
} else {
let _ = reply.send(Err(SimulationError::WindowNotFound(window_id)));
}
}
SimulationRequest::Capture { window_id, reply } => {
if self.contains_window(window_id) {
let _ =
self.with_window_mut(window_id, |record| record.runtime.request_frame());
self.simulation_waiters.insert_capture(window_id, reply);
} else {
let _ = reply.send(Err(SimulationError::WindowNotFound(window_id)));
}
}
SimulationRequest::GpuResources { window_id, reply } => {
if self.contains_window(window_id) {
self.simulation_waiters.insert_gpu(window_id, reply);
} else {
let _ = reply.send(Err(SimulationError::WindowNotFound(window_id)));
}
}
}
}
fn dispatch_simulation_input(
&mut self,
window_id: WindowId,
event: InputEvent,
) -> Result<(), SimulationError> {
self.dispatch_simulation_input_with_trigger(window_id, event, None)
}
fn dispatch_simulation_input_with_trigger(
&mut self,
window_id: WindowId,
event: InputEvent,
trigger: Option<&str>,
) -> Result<(), SimulationError> {
if let InputEvent::Pointer { position, .. } | InputEvent::PointerWithId { position, .. } =
&event
{
valid_offset(*position)?;
}
if let InputEvent::Scroll { delta } = &event {
valid_offset(*delta)?;
}
if !self.contains_window(window_id) {
return Err(SimulationError::WindowNotFound(window_id));
}
let applied = self.with_window_mut(window_id, |record| {
record.input_events = record.input_events.wrapping_add(1);
let _ = match trigger {
Some(trigger) => record.runtime.handle_input_with_trigger(event, trigger),
None => {
record.runtime.tree().set_diagnostic_input_trigger(
Some("simulation::"),
&event,
super::diagnostic_input_trigger,
);
record.runtime.handle_input_timed(event)
}
};
});
applied
.map(|_| ())
.ok_or(SimulationError::WindowNotFound(window_id))
}
fn simulation_scale_factor(&mut self, window_id: WindowId) -> Result<f64, SimulationError> {
self.with_window_mut(window_id, |record| record.metrics.scale_factor)
.ok_or(SimulationError::WindowNotFound(window_id))
}
fn simulation_logical_point(
&mut self,
window_id: WindowId,
position: Offset,
space: PointSpace,
) -> Result<Offset, SimulationError> {
valid_offset(position)?;
match space {
PointSpace::Logical => Ok(position),
PointSpace::Physical => {
let scale = self.simulation_scale_factor(window_id)?;
if !(scale.is_finite() && scale > 0.0) {
return Err(SimulationError::InvalidInput(format!(
"window scale factor {scale} cannot convert physical pixels"
)));
}
let scale = scale as f32;
Ok(Offset::new(position.x / scale, position.y / scale))
}
}
}
fn dispatch_simulation_click(
&mut self,
window_id: WindowId,
position: Offset,
) -> Result<(), SimulationError> {
valid_offset(position)?;
self.ensure_simulation_layout(window_id)?;
self.dispatch_simulation_click_prepared(
window_id,
position,
&format!("click_at({:.1}, {:.1})", position.x, position.y),
)
}
fn dispatch_simulation_click_prepared(
&mut self,
window_id: WindowId,
position: Offset,
trigger: &str,
) -> Result<(), SimulationError> {
self.dispatch_simulation_input_with_trigger(
window_id,
InputEvent::Pointer {
phase: PointerPhase::Move,
position,
},
Some(trigger),
)?;
self.dispatch_simulation_input_with_trigger(
window_id,
InputEvent::Pointer {
phase: PointerPhase::Down,
position,
},
Some(trigger),
)?;
self.dispatch_simulation_input_with_trigger(
window_id,
InputEvent::Pointer {
phase: PointerPhase::Up,
position,
},
Some(trigger),
)
}
fn ensure_simulation_layout(&mut self, window_id: WindowId) -> Result<(), SimulationError> {
let diagnostics = self
.window_diagnostics(window_id)
.ok_or(SimulationError::WindowNotFound(window_id))?;
if diagnostics.physical_size.is_zero() {
return Err(SimulationError::FrameFailed(
"the target window has a zero-sized surface".into(),
));
}
self.run_window_frame_at(
window_id,
Constraints::tight(diagnostics.logical_size),
Instant::now(),
)
.map_err(|error| SimulationError::FrameFailed(format!("{error:?}")))?;
let _ = self.with_window_mut(window_id, |record| record.runtime.request_frame());
Ok(())
}
fn simulation_label_target(
&mut self,
window_id: WindowId,
label: &str,
) -> Result<LabelTarget, SimulationError> {
self.set_semantics_enabled(window_id, true);
self.ensure_simulation_layout(window_id)?;
let viewport = self
.window_diagnostics(window_id)
.map(|diagnostics| Rect::from_origin_size(Offset::ZERO, diagnostics.logical_size))
.ok_or(SimulationError::WindowNotFound(window_id))?;
let result = self.registry.borrow().get(window_id).and_then(|record| {
let tree = record.runtime.tree();
let mut fallback = None;
tree.semantics()
.iter()
.find_map(|(id, node)| {
let matches = node.label.as_deref() == Some(label)
|| tree
.element_for_semantic_node(id)
.and_then(|element| tree.text_field_placeholder(element))
== Some(label);
if !matches || (!node.state.enabled && carries_enabled_state(node.role)) {
return None;
}
let visible_bounds = node.bounds.intersection(viewport)?;
if visible_bounds.size.width <= 0.0 || visible_bounds.size.height <= 0.0 {
return None;
}
let target = LabelTarget {
node: id,
bounds: visible_bounds,
focusable: node.actions.contains(&SemanticActionKind::Focus),
};
if node.actions.contains(&SemanticActionKind::Activate) {
return Some(target);
}
fallback.get_or_insert(target);
None
})
.or(fallback)
});
result.ok_or_else(|| SimulationError::ElementNotFound(label.to_owned()))
}
fn simulation_focus_label(
&mut self,
window_id: WindowId,
label: &str,
) -> Result<(), SimulationError> {
let target = self.simulation_label_target(window_id, label)?;
if target.focusable {
let focused = self
.with_window_mut(window_id, |record| {
let focused = record
.runtime
.dispatch_semantic_action(target.node, SemanticAction::Focus);
record.runtime.request_frame();
focused
})
.ok_or(SimulationError::WindowNotFound(window_id))?;
if focused {
return Ok(());
}
}
self.dispatch_simulation_click_prepared(
window_id,
target.bounds.center(),
&format!("focus({label:?})"),
)
}
pub fn simulation_capture_pending(&self, window_id: WindowId) -> bool {
self.simulation_waiters.capture_pending(window_id)
}
pub fn take_gpu_resource_queries(&mut self) -> Vec<WindowId> {
let mut live = Vec::new();
for window_id in self.simulation_waiters.gpu_windows() {
if self.contains_window(window_id) {
live.push(window_id);
} else {
for reply in self.simulation_waiters.take_gpu_waiters(window_id) {
let _ = reply.send(Err(SimulationError::WindowNotFound(window_id)));
}
}
}
live
}
pub fn complete_gpu_resource_query(
&mut self,
window_id: WindowId,
summary: GpuResourceSummary,
) {
self.simulation_waiters.complete_gpu(window_id, summary);
}
#[must_use]
pub fn simulation_frame_pending(&self, window_id: WindowId) -> bool {
self.simulation_waiters.frame_pending(window_id)
}
pub fn complete_simulation_frame(
&mut self,
window_id: WindowId,
presented: bool,
capture: Option<Result<Screenshot, String>>,
) {
self.simulation_waiters
.complete_frame(window_id, presented, capture);
}
pub fn fail_simulation_frame(&mut self, window_id: WindowId, message: impl Into<String>) {
self.simulation_waiters.fail_frame(window_id, message);
}
pub(crate) fn fail_simulation_window(&mut self, window_id: WindowId) {
self.simulation_waiters.fail_window(window_id);
}
pub(crate) fn stop_simulation(&self) {
self.simulation_bridge.stop();
}
}
struct LabelTarget {
node: incular_semantics::SemanticNodeId,
bounds: Rect,
focusable: bool,
}
fn carries_enabled_state(role: incular_semantics::Role) -> bool {
use incular_semantics::Role;
matches!(
role,
Role::Button
| Role::TextField
| Role::TextArea
| Role::SearchField
| Role::Checkbox
| Role::Radio
| Role::Switch
| Role::Slider
| Role::MenuItem
| Role::Tab
| Role::Link
)
}
trait RectCenter {
fn center(self) -> Offset;
}
impl RectCenter for Rect {
fn center(self) -> Offset {
Offset::new(
self.origin.x + self.size.width * 0.5,
self.origin.y + self.size.height * 0.5,
)
}
}