use std::cell::{Cell, RefCell};
use std::collections::VecDeque;
use std::rc::Rc;
use core::time::Duration;
use crcbl_core::{
EventTime, KeyCode, SurfaceTarget,
input::{Keysym, Scancode},
};
use crate::{
ClipboardOffer, ClipboardRequestId, CloseReply, CursorIcon, DisplayMode, MonitorId,
MonitorInfo, PhysicalPoint, PhysicalRect, PhysicalSize, PointerMode, Shell, ShellBackend,
ShellCaps, ShellError, ShellEvent, SizeConstraints, WindowConfiguration, WindowDesc, WindowId,
WindowState,
};
#[derive(Debug, Default)]
struct Bridge {
queue: VecDeque<ShellEvent>,
window: Option<WindowId>,
canvas: u32,
frame_ms: f64,
clock_offset_ms: f64,
fullscreen: bool,
pointer_lock_wanted: bool,
cursor_css: &'static str,
pointer_locked: bool,
}
type Shared = Rc<RefCell<Bridge>>;
thread_local! {
static BRIDGE: RefCell<Option<Shared>> = const { RefCell::new(None) };
static CANVAS: Cell<u32> = const { Cell::new(0) };
static KEY_SCRATCH: Cell<[u8; KEY_SCRATCH_CAPACITY]> =
const { Cell::new([0; KEY_SCRATCH_CAPACITY]) };
}
pub const KEY_SCRATCH_CAPACITY: usize = 256;
const HIDDEN_CURSOR_CSS: &str = "none";
fn with_bridge(f: impl FnOnce(&mut Bridge)) -> bool {
BRIDGE.with(|slot| match slot.borrow().as_ref() {
Some(shared) => {
f(&mut shared.borrow_mut());
true
}
None => false,
})
}
fn queue_for(canvas: u32, make: impl FnOnce(WindowId, &Bridge) -> ShellEvent) {
let mut queued = false;
let had_shell = with_bridge(|bridge| {
if bridge.canvas != canvas {
return;
}
let Some(window) = bridge.window else {
return;
};
let event = make(window, bridge);
bridge.queue.push_back(event);
queued = true;
});
if !queued {
crcbl_core::log::debug!(
"web shim event for canvas {canvas} dropped: {}",
if had_shell {
"no live window on that canvas"
} else {
"no shell on this thread"
}
);
}
}
pub fn canvas_id() -> u32 {
CANVAS.with(Cell::get)
}
fn event_time(bridge: &Bridge, time_ms: f64) -> EventTime {
let millis = time_ms + bridge.clock_offset_ms;
if millis.is_finite() && millis > 0.0 {
EventTime::from_duration(Duration::from_secs_f64(millis / 1000.0))
} else {
EventTime::ZERO
}
}
fn scancode_of(code: &str) -> Scancode {
let mut hash: u32 = 0x811c_9dc5;
for byte in code.as_bytes() {
hash ^= u32::from(*byte);
hash = hash.wrapping_mul(0x0100_0193);
}
Scancode(hash)
}
fn key_code_of(code: &str) -> Option<KeyCode> {
KeyCode::from_name(code)
}
fn keysym_of(key: &str) -> Keysym {
let mut chars = key.chars();
match (chars.next(), chars.next()) {
(Some(character), None) => Keysym::from_char(character),
_ => Keysym::NONE,
}
}
fn text_of(key: &str) -> Option<String> {
let mut chars = key.chars();
match (chars.next(), chars.next()) {
(Some(character), None) if !character.is_control() => Some(character.to_string()),
_ => None,
}
}
pub(crate) mod shim {
use super::{
CANVAS, KEY_SCRATCH, KEY_SCRATCH_CAPACITY, PhysicalPoint, PhysicalSize, ShellEvent,
event_time, key_code_of, keysym_of, queue_for, scancode_of, text_of, with_bridge,
};
use crcbl_core::input::{
ButtonState, ContactId, DeviceId, Modifiers, PointerButton, ScrollDelta, TouchPhase,
};
pub const STATE_CTRL: u32 = 1 << 0;
pub const STATE_SHIFT: u32 = 1 << 1;
pub const STATE_ALT: u32 = 1 << 2;
pub const STATE_SUPER: u32 = 1 << 3;
pub const STATE_EDGE: u32 = 1 << 4;
pub const STATE_REPEAT: u32 = 1 << 5;
pub const STATE_ALT_GRAPH: u32 = 1 << 6;
pub const TOUCH_BEGAN: u32 = 0;
pub const TOUCH_MOVED: u32 = 1;
pub const TOUCH_ENDED: u32 = 2;
pub const TOUCH_CANCELLED: u32 = 3;
const fn touch_phase(phase: u32) -> Option<TouchPhase> {
match phase {
TOUCH_BEGAN => Some(TouchPhase::Began),
TOUCH_MOVED => Some(TouchPhase::Moved),
TOUCH_ENDED => Some(TouchPhase::Ended),
TOUCH_CANCELLED => Some(TouchPhase::Cancelled),
_ => None,
}
}
fn modifiers(state: u32) -> Modifiers {
let mut modifiers = Modifiers::empty();
modifiers.set(Modifiers::CTRL, state & STATE_CTRL != 0);
modifiers.set(Modifiers::SHIFT, state & STATE_SHIFT != 0);
modifiers.set(Modifiers::ALT, state & STATE_ALT != 0);
modifiers.set(Modifiers::SUPER, state & STATE_SUPER != 0);
modifiers
}
unsafe fn borrow_str<'a>(ptr: *const u8, len: u32) -> Option<&'a str> {
if ptr.is_null() || len == 0 {
return None;
}
let bytes = unsafe { core::slice::from_raw_parts(ptr, len as usize) };
core::str::from_utf8(bytes).ok()
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_canvas(canvas: u32) {
CANVAS.with(|slot| slot.set(canvas));
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn __crcbl_web_key_scratch_ptr() -> *mut u8 {
KEY_SCRATCH.with(|slot| slot.as_ptr().cast::<u8>())
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn __crcbl_web_key_scratch_capacity() -> u32 {
u32::try_from(KEY_SCRATCH_CAPACITY).unwrap_or(u32::MAX)
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_frame(now_ms: f64) {
with_bridge(|bridge| bridge.frame_ms = now_ms);
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_resize(canvas: u32, width: u32, height: u32, scale: f64) {
let scale_factor = if scale.is_finite() && scale > 0.0 {
scale
} else {
1.0
};
queue_for(canvas, |window, _| ShellEvent::Resized {
window,
size: PhysicalSize::new(width, height),
scale_factor,
});
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_focus(canvas: u32, state: u32) {
queue_for(canvas, |window, _| ShellEvent::Focus {
window,
focused: state & STATE_EDGE != 0,
});
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_fullscreen(canvas: u32, state: u32) {
let entered = state & STATE_EDGE != 0;
with_bridge(|bridge| {
if bridge.canvas == canvas {
bridge.fullscreen = entered;
}
});
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_close(canvas: u32) {
queue_for(canvas, |window, _| ShellEvent::CloseRequested { window });
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_key(
canvas: u32,
code_ptr: *const u8,
code_len: u32,
key_ptr: *const u8,
key_len: u32,
time_ms: f64,
state: u32,
) {
let code = unsafe { borrow_str(code_ptr, code_len) }.unwrap_or_default();
let key = unsafe { borrow_str(key_ptr, key_len) }.unwrap_or_default();
queue_for(canvas, |window, bridge| ShellEvent::Key {
window,
device: DeviceId::UNKNOWN,
time: event_time(bridge, time_ms),
scancode: scancode_of(code),
key_code: key_code_of(code),
keysym: keysym_of(key),
state: edge(state),
repeat: state & STATE_REPEAT != 0,
modifiers: modifiers(state),
});
let shortcut =
state & STATE_SUPER != 0 || (state & STATE_CTRL != 0 && state & STATE_ALT_GRAPH == 0);
let typing = state & STATE_EDGE != 0 && !shortcut;
if let Some(text) = typing.then(|| text_of(key)).flatten() {
queue_for(canvas, |window, bridge| ShellEvent::TextCommit {
window,
time: event_time(bridge, time_ms),
text,
});
}
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_pointer_motion(
canvas: u32,
time_ms: f64,
x: f64,
y: f64,
dx: f64,
dy: f64,
) {
queue_for(canvas, |window, bridge| {
let locked = bridge.pointer_locked;
ShellEvent::PointerMotion {
window,
device: DeviceId::UNKNOWN,
time: event_time(bridge, time_ms),
abs: (!locked).then(|| PhysicalPoint::new(x, y)),
raw_delta: locked.then_some((dx, dy)),
}
});
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn __crcbl_web_pointer_lock_wanted(canvas: u32) -> u32 {
let mut wanted = 0;
with_bridge(|bridge| {
if bridge.canvas == canvas && bridge.pointer_lock_wanted {
wanted = 1;
}
});
wanted
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn __crcbl_web_cursor_ptr(canvas: u32) -> u32 {
let mut ptr = 0;
with_bridge(|bridge| {
if bridge.canvas == canvas && !bridge.cursor_css.is_empty() {
ptr = bridge.cursor_css.as_ptr() as usize as u32;
}
});
ptr
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub extern "C" fn __crcbl_web_cursor_len(canvas: u32) -> u32 {
let mut len = 0;
with_bridge(|bridge| {
if bridge.canvas == canvas {
len = bridge.cursor_css.len() as u32;
}
});
len
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_pointer_lock(canvas: u32, state: u32) {
let locked = state & STATE_EDGE != 0;
let mut taken_back = false;
with_bridge(|bridge| {
if bridge.canvas == canvas {
taken_back = bridge.pointer_locked && !locked && bridge.pointer_lock_wanted;
bridge.pointer_locked = locked;
}
});
if taken_back {
queue_for(canvas, |window, _| ShellEvent::Focus {
window,
focused: false,
});
}
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_pointer_button(
canvas: u32,
time_ms: f64,
x: f64,
y: f64,
button: u32,
state: u32,
) {
let button = pointer_button(button);
queue_for(canvas, |window, bridge| ShellEvent::Button {
window,
device: DeviceId::UNKNOWN,
time: event_time(bridge, time_ms),
button,
state: edge(state),
position: (!bridge.pointer_locked).then(|| PhysicalPoint::new(x, y)),
modifiers: modifiers(state),
});
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_pointer_wheel(
canvas: u32,
time_ms: f64,
x: f64,
y: f64,
delta_x: f64,
delta_y: f64,
state: u32,
) {
queue_for(canvas, |window, bridge| ShellEvent::Wheel {
window,
device: DeviceId::UNKNOWN,
time: event_time(bridge, time_ms),
delta: ScrollDelta::Pixels {
x: delta_x,
y: delta_y,
},
position: (!bridge.pointer_locked).then(|| PhysicalPoint::new(x, y)),
modifiers: modifiers(state),
});
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_touch(
canvas: u32,
time_ms: f64,
x: f64,
y: f64,
contact: u32,
phase: u32,
) {
let Some(phase) = touch_phase(phase) else {
crcbl_core::log::debug!("web shim touch with unknown phase {phase} dropped");
return;
};
queue_for(canvas, |window, bridge| ShellEvent::Touch {
window,
device: DeviceId::UNKNOWN,
time: event_time(bridge, time_ms),
contact: ContactId(contact),
phase,
position: PhysicalPoint::new(x, y),
});
}
#[cfg_attr(target_arch = "wasm32", unsafe(no_mangle))]
pub unsafe extern "C" fn __crcbl_web_pointer_focus(
canvas: u32,
time_ms: f64,
x: f64,
y: f64,
state: u32,
) {
let entered = state & STATE_EDGE != 0;
queue_for(canvas, |window, bridge| ShellEvent::PointerFocus {
window,
device: DeviceId::UNKNOWN,
time: event_time(bridge, time_ms),
entered,
position: entered.then(|| PhysicalPoint::new(x, y)),
});
}
fn edge(state: u32) -> ButtonState {
if state & STATE_EDGE != 0 {
ButtonState::Pressed
} else {
ButtonState::Released
}
}
fn pointer_button(button: u32) -> PointerButton {
match button {
0 => PointerButton::Left,
1 => PointerButton::Middle,
2 => PointerButton::Right,
3 => PointerButton::Back,
4 => PointerButton::Forward,
other => PointerButton::Other(u16::try_from(other).unwrap_or(u16::MAX)),
}
}
}
#[derive(Debug)]
pub struct WebShell {
canvas_id: u32,
shared: Shared,
window: Option<WindowId>,
next_generation: u32,
title: String,
visible: bool,
requested_mode: DisplayMode,
requested_constraints: SizeConstraints,
monitors: Vec<MonitorInfo>,
configured: Option<WindowConfiguration>,
focused: bool,
fullscreen: bool,
pointer_mode: PointerMode,
cursor: Option<CursorIcon>,
close_pending: bool,
}
impl WebShell {
#[must_use]
pub fn new(canvas_id: u32) -> Self {
let monitors = vec![MonitorInfo {
id: MonitorId(1),
name: "canvas".to_string(),
bounds: PhysicalRect::new(0, 0, 1280, 720),
work_area: PhysicalRect::new(0, 0, 1280, 720),
scale_factor: 1.0,
refresh_millihertz: 60_000,
is_primary: true,
}];
let shared: Shared = Rc::new(RefCell::new(Bridge {
canvas: canvas_id,
..Bridge::default()
}));
BRIDGE.with(|slot| *slot.borrow_mut() = Some(Rc::clone(&shared)));
Self {
canvas_id,
shared,
window: None,
next_generation: 1,
title: String::new(),
visible: false,
requested_mode: DisplayMode::Windowed,
requested_constraints: SizeConstraints::NONE,
monitors,
configured: None,
focused: false,
fullscreen: false,
pointer_mode: PointerMode::Free,
cursor: Some(CursorIcon::Default),
close_pending: false,
}
}
fn check(&self, window: WindowId) -> Result<(), ShellError> {
if self.window == Some(window) {
Ok(())
} else {
Err(ShellError::invalid_window(window))
}
}
fn unsupported(what: &'static str) -> ShellError {
ShellError::Unsupported {
backend: ShellBackend::Web,
what,
}
}
const fn effective_mode(&self) -> DisplayMode {
if self.fullscreen {
DisplayMode::Borderless { monitor: None }
} else {
DisplayMode::Windowed
}
}
fn configure(&mut self, size: PhysicalSize, scale_factor: f64) {
self.configured = Some(WindowConfiguration {
size,
scale_factor,
mode: self.effective_mode(),
});
if let Some(monitor) = self.monitors.first_mut() {
monitor.bounds = PhysicalRect::new(0, 0, size.width, size.height);
monitor.work_area = PhysicalRect::new(0, 0, size.width, size.height);
monitor.scale_factor = scale_factor;
}
}
}
impl Drop for WebShell {
fn drop(&mut self) {
BRIDGE.with(|slot| {
let mut slot = slot.borrow_mut();
if slot
.as_ref()
.is_some_and(|current| Rc::ptr_eq(current, &self.shared))
{
*slot = None;
}
});
}
}
impl Shell for WebShell {
fn backend(&self) -> ShellBackend {
ShellBackend::Web
}
fn caps(&self) -> ShellCaps {
ShellCaps::HW_UPSCALE
| ShellCaps::FRACTIONAL_SCALE
| ShellCaps::TOUCH
| ShellCaps::POINTER_LOCK
| ShellCaps::RAW_POINTER_MOTION
}
fn create_window(&mut self, desc: &WindowDesc<'_>) -> Result<WindowId, ShellError> {
crate::role::check(desc, self.caps(), self.backend(), |monitor| {
self.monitor(monitor).is_some()
})?;
if self.window.is_some() {
return Err(Self::unsupported("a second window"));
}
let generation = self.next_generation;
self.next_generation = self.next_generation.saturating_add(1);
let window = WindowId::from_bits((u64::from(generation) << 32) | u64::from(self.canvas_id))
.expect("the generation is never zero");
self.window = Some(window);
self.title = desc.title.to_string();
self.visible = desc.visible;
self.requested_mode = desc.mode;
self.requested_constraints = desc.constraints;
self.configured = None;
self.focused = false;
self.close_pending = false;
self.pointer_mode = PointerMode::Free;
self.cursor = Some(CursorIcon::Default);
self.shared.borrow_mut().cursor_css = CursorIcon::Default.as_css_name();
self.shared.borrow_mut().window = Some(window);
Ok(window)
}
fn destroy_window(&mut self, window: WindowId) -> Result<(), ShellError> {
self.check(window)?;
self.window = None;
self.configured = None;
self.focused = false;
self.close_pending = false;
self.pointer_mode = PointerMode::Free;
{
let mut bridge = self.shared.borrow_mut();
bridge.window = None;
bridge.pointer_lock_wanted = false;
bridge.cursor_css = "";
bridge.queue.retain(|event| event.window() != Some(window));
bridge
.queue
.push_back(ShellEvent::WindowDestroyed { window });
}
Ok(())
}
fn window_state(&self, window: WindowId) -> Result<WindowState, ShellError> {
self.check(window)?;
Ok(WindowState {
configuration: self.configured,
requested_mode: self.requested_mode,
requested_constraints: self.requested_constraints,
focused: self.focused,
visible: self.visible,
pointer_mode: self.pointer_mode,
close_pending: self.close_pending,
})
}
fn set_title(&mut self, window: WindowId, title: &str) -> Result<(), ShellError> {
self.check(window)?;
self.title = title.to_string();
Ok(())
}
fn set_visible(&mut self, window: WindowId, visible: bool) -> Result<(), ShellError> {
self.check(window)?;
self.visible = visible;
Ok(())
}
fn set_mode(&mut self, window: WindowId, mode: DisplayMode) -> Result<(), ShellError> {
self.check(window)?;
if let DisplayMode::Borderless {
monitor: Some(monitor),
} = mode
&& self.monitor(monitor).is_none()
{
return Err(ShellError::NoSuchMonitor(monitor.0));
}
self.requested_mode = mode;
Ok(())
}
fn set_constraints(
&mut self,
window: WindowId,
constraints: SizeConstraints,
) -> Result<(), ShellError> {
self.check(window)?;
self.requested_constraints = constraints;
Ok(())
}
fn monitors(&self) -> &[MonitorInfo] {
&self.monitors
}
fn pump(&mut self, sink: &mut dyn FnMut(ShellEvent)) {
let (events, fullscreen): (Vec<ShellEvent>, bool) = {
let mut bridge = self.shared.borrow_mut();
(bridge.queue.drain(..).collect(), bridge.fullscreen)
};
if fullscreen != self.fullscreen {
self.fullscreen = fullscreen;
let mode = self.effective_mode();
if let Some(config) = &mut self.configured {
config.mode = mode;
}
}
for event in events {
match &event {
ShellEvent::Resized {
size, scale_factor, ..
} => self.configure(*size, *scale_factor),
ShellEvent::Focus { focused, .. } => self.focused = *focused,
ShellEvent::CloseRequested { .. } => self.close_pending = true,
_ => {}
}
sink(event);
}
}
fn keep_alive(&mut self) {}
fn wait_events(&mut self, _timeout: Option<Duration>) {}
fn align_event_clock(&mut self, elapsed: Duration) {
let mut bridge = self.shared.borrow_mut();
bridge.clock_offset_ms = elapsed.as_secs_f64() * 1000.0 - bridge.frame_ms;
}
fn surface_target(&self, window: WindowId) -> Result<SurfaceTarget, ShellError> {
self.check(window)?;
Ok(SurfaceTarget::Web {
canvas_id: self.canvas_id,
})
}
fn set_pointer_mode(&mut self, window: WindowId, mode: PointerMode) -> Result<(), ShellError> {
self.check(window)?;
if !self.caps().contains(mode.required_cap()) {
return Err(Self::unsupported(mode.as_str()));
}
self.pointer_mode = mode;
self.shared.borrow_mut().pointer_lock_wanted = mode == PointerMode::Locked;
Ok(())
}
fn set_cursor(
&mut self,
window: WindowId,
cursor: Option<CursorIcon>,
) -> Result<(), ShellError> {
self.check(window)?;
self.cursor = cursor;
self.shared.borrow_mut().cursor_css =
cursor.map_or(HIDDEN_CURSOR_CSS, CursorIcon::as_css_name);
Ok(())
}
fn warp_pointer(
&mut self,
window: WindowId,
_position: PhysicalPoint,
) -> Result<(), ShellError> {
self.check(window)?;
Err(Self::unsupported("pointer warp"))
}
fn reply_close_request(
&mut self,
window: WindowId,
reply: CloseReply,
) -> Result<(), ShellError> {
self.check(window)?;
if !self.close_pending {
return Err(ShellError::NoPendingCloseRequest { window });
}
self.close_pending = false;
match reply {
CloseReply::Close => self.destroy_window(window),
CloseReply::Keep => Ok(()),
}
}
fn clipboard_offer(
&mut self,
_window: WindowId,
_offers: &[ClipboardOffer<'_>],
) -> Result<(), ShellError> {
Err(Self::unsupported("clipboard"))
}
fn clipboard_request(
&mut self,
_window: WindowId,
_mime: crate::MimeType,
) -> Result<ClipboardRequestId, ShellError> {
Err(Self::unsupported("clipboard"))
}
fn clipboard_readable(&self, _window: WindowId) -> bool {
false
}
}
pub fn open(canvas_id: u32) -> Result<WebShell, ShellError> {
Ok(WebShell::new(canvas_id))
}
#[cfg(test)]
mod tests {
use super::shim::{
STATE_ALT, STATE_ALT_GRAPH, STATE_CTRL, STATE_EDGE, STATE_REPEAT, STATE_SHIFT, STATE_SUPER,
};
use super::*;
use crcbl_core::input::{ButtonState, ContactId, Modifiers, PointerButton, TouchPhase};
#[test]
fn the_shims_state_bits_are_the_engines() {
let js = include_str!("../../../../web/engine/shell.js");
let engine = [
("STATE_CTRL", STATE_CTRL),
("STATE_SHIFT", STATE_SHIFT),
("STATE_ALT", STATE_ALT),
("STATE_SUPER", STATE_SUPER),
("STATE_EDGE", STATE_EDGE),
("STATE_REPEAT", STATE_REPEAT),
("STATE_ALT_GRAPH", STATE_ALT_GRAPH),
];
for (name, value) in engine {
let line = format!("export const {name} = 1 << {};", value.trailing_zeros());
assert!(
js.contains(&line),
"shell.js does not export `{line}`, which the engine reads"
);
}
assert_eq!(
js.matches("export const STATE_").count(),
engine.len(),
"shell.js exports a STATE_ bit the engine does not read"
);
}
fn shell_with_window(canvas: u32) -> (WebShell, WindowId) {
let mut shell = WebShell::new(canvas);
let window = shell
.create_window(&WindowDesc::default())
.expect("the first window");
(shell, window)
}
fn drain(shell: &mut WebShell) -> Vec<ShellEvent> {
let mut events = Vec::new();
shell.pump(&mut |event| events.push(event));
events
}
fn positions(events: &[ShellEvent]) -> Vec<Option<PhysicalPoint>> {
events
.iter()
.map(|event| match event {
ShellEvent::PointerMotion { abs, .. } => *abs,
ShellEvent::Button { position, .. } | ShellEvent::Wheel { position, .. } => {
*position
}
other => panic!("not a pointer event: {other:?}"),
})
.collect()
}
fn raw_deltas(events: &[ShellEvent]) -> Vec<Option<(f64, f64)>> {
events
.iter()
.filter_map(|event| match event {
ShellEvent::PointerMotion { raw_delta, .. } => Some(*raw_delta),
_ => None,
})
.collect()
}
#[test]
fn a_window_has_no_size_until_the_shim_reports_one() {
let (shell, window) = shell_with_window(7);
assert_eq!(shell.backend(), ShellBackend::Web);
let state = shell.window_state(window).expect("valid window");
assert!(!state.is_configured());
assert_eq!(state.size(), None);
}
#[test]
fn a_shim_resize_reaches_pump_and_configures_the_window() {
let (mut shell, window) = shell_with_window(3);
unsafe { shim::__crcbl_web_resize(3, 1920, 1080, 2.0) };
let events = drain(&mut shell);
assert_eq!(
events,
vec![ShellEvent::Resized {
window,
size: PhysicalSize::new(1920, 1080),
scale_factor: 2.0,
}]
);
let state = shell.window_state(window).expect("valid window");
assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
assert_eq!(state.scale_factor(), Some(2.0));
assert_eq!(
shell.monitors()[0].bounds,
PhysicalRect::new(0, 0, 1920, 1080)
);
}
#[test]
fn shim_events_before_a_window_exists_are_dropped_not_accumulated() {
let mut shell = WebShell::new(0);
unsafe { shim::__crcbl_web_resize(0, 800, 600, 1.0) };
unsafe { shim::__crcbl_web_close(0) };
assert_eq!(drain(&mut shell), Vec::new());
let window = shell
.create_window(&WindowDesc::default())
.expect("the first window");
unsafe { shim::__crcbl_web_resize(9, 800, 600, 1.0) };
assert_eq!(drain(&mut shell), Vec::new());
assert_eq!(shell.window_state(window).expect("state").size(), None);
}
#[test]
fn the_key_scratch_is_how_a_browser_gets_a_string_into_wasm() {
let (mut shell, window) = shell_with_window(2);
let code = "KeyA";
let key = "a";
let capacity = shim::__crcbl_web_key_scratch_capacity() as usize;
assert!(capacity >= code.len() + key.len());
let base = shim::__crcbl_web_key_scratch_ptr();
assert!(!base.is_null());
assert_eq!(base, shim::__crcbl_web_key_scratch_ptr());
unsafe {
core::ptr::copy_nonoverlapping(code.as_ptr(), base, code.len());
core::ptr::copy_nonoverlapping(key.as_ptr(), base.add(code.len()), key.len());
}
unsafe {
shim::__crcbl_web_key(
2,
base,
code.len() as u32,
base.add(code.len()),
key.len() as u32,
1234.5,
STATE_EDGE | STATE_SHIFT,
);
}
let events = drain(&mut shell);
let [
ShellEvent::Key {
window: got_window,
key_code,
keysym,
state,
repeat,
modifiers,
..
},
ShellEvent::TextCommit {
window: text_window,
text,
..
},
] = events.as_slice()
else {
panic!("expected a key event and the text it committed, got {events:?}");
};
assert_eq!(*got_window, window);
assert_eq!(*key_code, KeyCode::from_name("KeyA"));
assert_eq!(*keysym, Keysym::from_char('a'));
assert_eq!(*state, ButtonState::Pressed);
assert!(!*repeat);
assert_eq!(*modifiers, Modifiers::SHIFT);
assert_eq!(*text_window, window);
assert_eq!(text, "a");
}
#[test]
fn a_press_commits_the_character_it_produced_and_a_shortcut_commits_none() {
let (mut shell, window) = shell_with_window(0);
fn typed(shell: &mut WebShell, key: &str, state: u32) -> Vec<String> {
let code = b"KeyA";
unsafe {
shim::__crcbl_web_key(
0,
code.as_ptr(),
code.len() as u32,
key.as_ptr(),
key.len() as u32,
1.0,
state,
);
}
drain(shell)
.into_iter()
.filter_map(|event| match event {
ShellEvent::TextCommit { text, .. } => Some(text),
_ => None,
})
.collect()
}
assert_eq!(typed(&mut shell, "a", STATE_EDGE), vec!["a".to_string()]);
assert_eq!(
typed(&mut shell, "A", STATE_EDGE | STATE_SHIFT),
vec!["A".to_string()],
"the browser composed the layout already; Shift is not a shortcut"
);
assert_eq!(
typed(&mut shell, "é", STATE_EDGE | STATE_ALT),
vec!["é".to_string()],
"Alt alone is the third level of a European layout, not a shortcut"
);
assert_eq!(
typed(&mut shell, "a", STATE_EDGE | STATE_REPEAT),
vec!["a".to_string()],
"a held key types over and over, which is what the console's own \
toggle swallows repeats for"
);
assert!(
typed(&mut shell, "a", 0).is_empty(),
"a release commits nothing on any backend"
);
assert!(
typed(&mut shell, "a", STATE_EDGE | STATE_CTRL).is_empty(),
"Ctrl+A selects all; it does not type an a"
);
assert!(
typed(&mut shell, "a", STATE_EDGE | STATE_SUPER).is_empty(),
"and neither does Meta+A"
);
assert_eq!(
typed(
&mut shell,
"@",
STATE_EDGE | STATE_CTRL | STATE_ALT | STATE_ALT_GRAPH
),
vec!["@".to_string()],
"AltGr+Q on a German layout arrives as Ctrl+Alt with AltGraph, and types"
);
assert!(
typed(&mut shell, "q", STATE_EDGE | STATE_CTRL | STATE_ALT).is_empty(),
"Ctrl+Alt without AltGraph is a shortcut on a layout that has no AltGr"
);
assert!(
typed(&mut shell, "Enter", STATE_EDGE).is_empty(),
"a key that produced a name rather than a character produced no text"
);
assert!(
typed(&mut shell, "\u{1b}", STATE_EDGE).is_empty(),
"and a lone control character is not text either"
);
assert!(shell.window_state(window).is_ok());
}
#[test]
fn focus_and_close_come_from_the_shim_rather_than_being_assumed() {
let (mut shell, window) = shell_with_window(0);
assert!(
!shell.window_state(window).expect("state").focused,
"a fresh canvas is not focused until the browser says so"
);
assert!(matches!(
shell.reply_close_request(window, CloseReply::Keep),
Err(ShellError::NoPendingCloseRequest { .. })
));
unsafe { shim::__crcbl_web_focus(0, STATE_EDGE) };
unsafe { shim::__crcbl_web_close(0) };
let events = drain(&mut shell);
assert_eq!(events.len(), 2);
let state = shell.window_state(window).expect("state");
assert!(state.focused);
assert!(state.close_pending);
shell
.reply_close_request(window, CloseReply::Keep)
.expect("answered");
assert!(!shell.window_state(window).expect("state").close_pending);
assert!(shell.window_state(window).is_ok(), "still open");
}
#[test]
fn agreeing_to_close_destroys_the_window() {
let (mut shell, window) = shell_with_window(0);
unsafe { shim::__crcbl_web_close(0) };
drain(&mut shell);
shell
.reply_close_request(window, CloseReply::Close)
.expect("answered");
assert_eq!(
drain(&mut shell),
vec![ShellEvent::WindowDestroyed { window }]
);
assert!(shell.window_state(window).is_err());
}
#[test]
fn a_destroyed_window_stops_answering_every_method() {
let (mut shell, window) = shell_with_window(4);
shell.destroy_window(window).expect("destroy");
assert_eq!(
drain(&mut shell),
vec![ShellEvent::WindowDestroyed { window }]
);
assert!(shell.window_state(window).is_err());
assert!(shell.surface_target(window).is_err());
assert!(shell.set_title(window, "gone").is_err());
assert!(shell.set_visible(window, true).is_err());
assert!(shell.set_mode(window, DisplayMode::Windowed).is_err());
assert!(
shell
.set_constraints(window, SizeConstraints::NONE)
.is_err()
);
assert!(shell.set_cursor(window, None).is_err());
assert!(shell.set_pointer_mode(window, PointerMode::Free).is_err());
assert!(shell.destroy_window(window).is_err());
assert!(shell.reply_close_request(window, CloseReply::Keep).is_err());
let next = shell
.create_window(&WindowDesc::default())
.expect("the canvas is still there");
assert_ne!(next, window);
assert!(shell.window_state(window).is_err());
assert!(shell.window_state(next).is_ok());
}
#[test]
fn a_second_window_is_unsupported_because_caps_say_so() {
let (mut shell, _) = shell_with_window(0);
assert!(!shell.caps().contains(ShellCaps::MULTI_WINDOW));
assert!(matches!(
shell.create_window(&WindowDesc::default()),
Err(ShellError::Unsupported { .. })
));
}
#[test]
fn the_descriptor_is_recorded_rather_than_dropped() {
let mut shell = WebShell::new(0);
let constraints = SizeConstraints::min(crate::LogicalSize::new(320.0, 240.0));
let window = shell
.create_window(&WindowDesc {
title: "sandbox",
visible: false,
mode: DisplayMode::Borderless { monitor: None },
constraints,
..WindowDesc::default()
})
.expect("window");
let state = shell.window_state(window).expect("state");
assert!(!state.visible);
assert_eq!(
state.requested_mode,
DisplayMode::Borderless { monitor: None }
);
assert_eq!(state.requested_constraints, constraints);
assert_eq!(shell.title, "sandbox");
}
#[test]
fn fullscreen_is_the_pages_answer_not_the_engines_request() {
let (mut shell, window) = shell_with_window(9);
unsafe { shim::__crcbl_web_resize(9, 800, 600, 1.0) };
drain(&mut shell);
shell
.set_mode(window, DisplayMode::Borderless { monitor: None })
.expect("the window is live");
let state = shell.window_state(window).expect("state");
assert_eq!(
state.requested_mode,
DisplayMode::Borderless { monitor: None }
);
assert_eq!(
state.effective_mode(),
Some(DisplayMode::Windowed),
"a request the page has not answered is not a fact",
);
assert!(!state.mode_request_honoured());
unsafe { shim::__crcbl_web_fullscreen(9, shim::STATE_EDGE) };
unsafe { shim::__crcbl_web_resize(9, 1920, 1080, 1.0) };
drain(&mut shell);
let state = shell.window_state(window).expect("state");
assert_eq!(
state.effective_mode(),
Some(DisplayMode::Borderless { monitor: None })
);
assert!(state.mode_request_honoured());
assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
unsafe { shim::__crcbl_web_fullscreen(9, 0) };
drain(&mut shell);
let state = shell.window_state(window).expect("state");
assert_eq!(state.effective_mode(), Some(DisplayMode::Windowed));
assert_eq!(
state.requested_mode,
DisplayMode::Borderless { monitor: None }
);
assert!(
!state.mode_request_honoured(),
"a toggle reading the request back would still be showing fullscreen",
);
}
#[test]
fn the_mode_is_right_whichever_order_the_resize_and_the_change_arrive_in() {
for resize_first in [true, false] {
let (mut shell, window) = shell_with_window(11);
unsafe { shim::__crcbl_web_resize(11, 800, 600, 1.0) };
drain(&mut shell);
if resize_first {
unsafe { shim::__crcbl_web_resize(11, 1920, 1080, 1.0) };
unsafe { shim::__crcbl_web_fullscreen(11, shim::STATE_EDGE) };
} else {
unsafe { shim::__crcbl_web_fullscreen(11, shim::STATE_EDGE) };
unsafe { shim::__crcbl_web_resize(11, 1920, 1080, 1.0) };
}
drain(&mut shell);
let state = shell.window_state(window).expect("state");
assert_eq!(
state.effective_mode(),
Some(DisplayMode::Borderless { monitor: None }),
"resize_first = {resize_first}",
);
assert_eq!(state.size(), Some(PhysicalSize::new(1920, 1080)));
}
}
#[test]
fn caps_and_behaviour_agree() {
let (mut shell, window) = shell_with_window(0);
let caps = shell.caps();
assert_eq!(
caps,
ShellCaps::HW_UPSCALE
| ShellCaps::FRACTIONAL_SCALE
| ShellCaps::TOUCH
| ShellCaps::POINTER_LOCK
| ShellCaps::RAW_POINTER_MOTION,
"every other bit names something this backend does not do"
);
assert!(!caps.contains(ShellCaps::TEXT_IME));
assert!(!caps.contains(ShellCaps::WINDOW_POSITION));
assert!(!caps.contains(ShellCaps::SERVER_DECORATIONS));
assert!(caps.has_mouselook());
assert_eq!(shim::__crcbl_web_pointer_lock_wanted(0), 0);
shell
.set_pointer_mode(window, PointerMode::Locked)
.expect("the lock is asked for, not refused");
assert_eq!(
shim::__crcbl_web_pointer_lock_wanted(0),
1,
"the request has to reach the shim, or no gesture can complete it"
);
unsafe { shim::__crcbl_web_pointer_lock(0, STATE_EDGE) };
unsafe { shim::__crcbl_web_pointer_motion(0, 1.0, 640.0, 360.0, 3.0, -4.0) };
let events = drain(&mut shell);
let [ShellEvent::PointerMotion { abs, raw_delta, .. }] = events.as_slice() else {
panic!("expected one PointerMotion, got {events:?}");
};
assert_eq!(*raw_delta, Some((3.0, -4.0)));
assert_eq!(*abs, None, "there is no position under a lock");
assert!(!caps.contains(ShellCaps::POINTER_CONFINE));
assert!(matches!(
shell.set_pointer_mode(window, PointerMode::Confined),
Err(ShellError::Unsupported { .. })
));
}
#[test]
fn a_lock_nobody_granted_is_not_a_lock() {
let (mut shell, window) = shell_with_window(0);
shell
.set_pointer_mode(window, PointerMode::Locked)
.expect("recorded");
unsafe { shim::__crcbl_web_pointer_motion(0, 1.0, 10.0, 20.0, 5.0, 5.0) };
unsafe { shim::__crcbl_web_pointer_button(0, 2.0, 10.0, 20.0, 0, STATE_EDGE) };
unsafe { shim::__crcbl_web_pointer_wheel(0, 3.0, 10.0, 20.0, 0.0, -1.0, 0) };
let events = drain(&mut shell);
let expected = Some(PhysicalPoint::new(10.0, 20.0));
assert_eq!(
positions(&events),
vec![expected, expected, expected],
"the browser has not locked anything yet"
);
assert_eq!(raw_deltas(&events), vec![None]);
unsafe { shim::__crcbl_web_pointer_lock(0, STATE_EDGE) };
unsafe { shim::__crcbl_web_pointer_motion(0, 4.0, 10.0, 20.0, 5.0, 5.0) };
unsafe { shim::__crcbl_web_pointer_button(0, 5.0, 10.0, 20.0, 0, STATE_EDGE) };
unsafe { shim::__crcbl_web_pointer_wheel(0, 6.0, 10.0, 20.0, 0.0, -1.0, 0) };
let events = drain(&mut shell);
assert_eq!(positions(&events), vec![None, None, None]);
assert_eq!(raw_deltas(&events), vec![Some((5.0, 5.0))]);
shell
.set_pointer_mode(window, PointerMode::Free)
.expect("recorded");
assert_eq!(shim::__crcbl_web_pointer_lock_wanted(0), 0);
unsafe { shim::__crcbl_web_pointer_lock(0, 0) };
unsafe { shim::__crcbl_web_pointer_motion(0, 7.0, 11.0, 21.0, 5.0, 5.0) };
let events = drain(&mut shell);
assert_eq!(
positions(&events),
vec![Some(PhysicalPoint::new(11.0, 21.0))]
);
assert_eq!(raw_deltas(&events), vec![None]);
}
fn published_cursor(shell: &WebShell) -> &'static str {
shell.shared.borrow().cursor_css
}
#[test]
fn the_cursor_is_published_as_a_css_keyword() {
let (mut shell, window) = shell_with_window(0);
assert_eq!(
published_cursor(&shell),
"default",
"a fresh window starts on the platform's arrow",
);
shell
.set_cursor(window, Some(CursorIcon::Crosshair))
.expect("recorded");
assert_eq!(published_cursor(&shell), "crosshair");
assert_eq!(
shim::__crcbl_web_cursor_len(0) as usize,
"crosshair".len(),
"the length export stopped agreeing with the string",
);
assert_eq!(
shim::__crcbl_web_cursor_ptr(0),
CursorIcon::Crosshair.as_css_name().as_ptr() as usize as u32,
"the pointer export names a string that is not the request",
);
shell.set_cursor(window, None).expect("recorded");
assert_eq!(
published_cursor(&shell),
"none",
"hiding is a CSS keyword like any other, and this is how it is spelled",
);
assert_eq!(shim::__crcbl_web_cursor_len(0) as usize, "none".len());
assert_eq!(shim::__crcbl_web_cursor_ptr(9), 0);
assert_eq!(shim::__crcbl_web_cursor_len(9), 0);
}
#[test]
fn destroying_the_window_withdraws_the_cursor_request() {
let (mut shell, window) = shell_with_window(0);
shell.set_cursor(window, None).expect("recorded");
assert_eq!(shim::__crcbl_web_cursor_len(0) as usize, "none".len());
shell.destroy_window(window).expect("destroy");
assert_eq!(published_cursor(&shell), "");
assert_eq!(shim::__crcbl_web_cursor_len(0), 0);
assert_eq!(shim::__crcbl_web_cursor_ptr(0), 0);
shell
.create_window(&WindowDesc::default())
.expect("the canvas is still there");
assert_eq!(published_cursor(&shell), "default");
}
#[test]
fn destroying_the_window_withdraws_the_lock_request() {
let (mut shell, window) = shell_with_window(0);
shell
.set_pointer_mode(window, PointerMode::Locked)
.expect("recorded");
assert_eq!(shim::__crcbl_web_pointer_lock_wanted(0), 1);
shell.destroy_window(window).expect("destroy");
assert_eq!(shim::__crcbl_web_pointer_lock_wanted(0), 0);
shell
.create_window(&WindowDesc::default())
.expect("the canvas is still there");
assert_eq!(shim::__crcbl_web_pointer_lock_wanted(9), 0);
}
#[test]
fn a_lock_the_browser_takes_back_arrives_as_a_focus_loss() {
let (mut shell, window) = shell_with_window(0);
unsafe { shim::__crcbl_web_focus(0, STATE_EDGE) };
drain(&mut shell);
assert!(shell.window_state(window).expect("state").focused);
shell
.set_pointer_mode(window, PointerMode::Locked)
.expect("recorded");
unsafe { shim::__crcbl_web_pointer_lock(0, STATE_EDGE) };
assert!(
drain(&mut shell).is_empty(),
"taking the lock is not an event"
);
unsafe { shim::__crcbl_web_pointer_lock(0, 0) };
let events = drain(&mut shell);
assert!(
matches!(
events.as_slice(),
[ShellEvent::Focus { focused: false, .. }]
),
"an unasked exit is the focus loss a loop pauses on, got {events:?}",
);
assert!(
!shell.window_state(window).expect("state").focused,
"and the shell reports what it just said",
);
unsafe { shim::__crcbl_web_pointer_lock(0, 0) };
assert!(drain(&mut shell).is_empty(), "one exit, one event");
unsafe { shim::__crcbl_web_pointer_lock(0, STATE_EDGE) };
drain(&mut shell);
shell
.set_pointer_mode(window, PointerMode::Free)
.expect("recorded");
unsafe { shim::__crcbl_web_pointer_lock(0, 0) };
assert!(
drain(&mut shell).is_empty(),
"a lock this shell gave up is not a lock the browser took back",
);
}
#[test]
fn keys_come_from_the_physical_code_and_carry_their_own_timestamp() {
let (mut shell, window) = shell_with_window(0);
unsafe { shim::__crcbl_web_frame(1000.0) };
shell.align_event_clock(Duration::from_millis(40));
let code = b"KeyW";
let key = "w".as_bytes();
unsafe {
shim::__crcbl_web_key(
0,
code.as_ptr(),
code.len() as u32,
key.as_ptr(),
key.len() as u32,
1002.5,
STATE_EDGE | STATE_SHIFT | STATE_CTRL | STATE_REPEAT,
);
}
let events = drain(&mut shell);
let [
ShellEvent::Key {
window: got,
time,
scancode,
key_code,
keysym,
state,
repeat,
modifiers,
..
},
] = events.as_slice()
else {
panic!("expected one Key event and no commit, got {events:?}");
};
assert_eq!(*got, window);
assert_eq!(*key_code, Some(KeyCode::KeyW));
assert_eq!(*keysym, Keysym::from_char('w'));
assert_eq!(*state, ButtonState::Pressed);
assert!(*repeat);
assert_eq!(*modifiers, Modifiers::CTRL | Modifiers::SHIFT);
assert_ne!(*scancode, Scancode(0), "an unnamed key is still bindable");
assert_eq!(*time, EventTime::from_micros(42_500));
}
#[test]
fn two_events_in_one_frame_have_different_timestamps() {
let (mut shell, _) = shell_with_window(0);
unsafe { shim::__crcbl_web_frame(0.0) };
unsafe { shim::__crcbl_web_pointer_motion(0, 4.0, 10.0, 20.0, 1.0, 0.0) };
unsafe { shim::__crcbl_web_pointer_motion(0, 12.0, 11.0, 21.0, 1.0, 1.0) };
let events = drain(&mut shell);
let times: Vec<Option<EventTime>> = events.iter().map(ShellEvent::time).collect();
assert_eq!(
times,
vec![
Some(EventTime::from_millis(4)),
Some(EventTime::from_millis(12))
],
"a frame must not quantize the events inside it"
);
}
#[test]
fn pointer_buttons_and_wheels_arrive_with_modifiers() {
let (mut shell, _) = shell_with_window(0);
unsafe {
shim::__crcbl_web_pointer_button(
0,
1.0,
5.0,
6.0,
2,
STATE_EDGE | STATE_ALT | STATE_SUPER,
);
}
unsafe { shim::__crcbl_web_pointer_wheel(0, 2.0, 5.0, 6.0, 0.0, -120.0, 0) };
unsafe { shim::__crcbl_web_pointer_focus(0, 3.0, 5.0, 6.0, 0) };
let events = drain(&mut shell);
assert_eq!(
events.iter().map(ShellEvent::name).collect::<Vec<&str>>(),
["Button", "Wheel", "PointerFocus"]
);
let ShellEvent::Button {
button,
position,
modifiers,
..
} = &events[0]
else {
panic!("expected a Button");
};
assert_eq!(*button, PointerButton::Right);
assert_eq!(*position, Some(PhysicalPoint::new(5.0, 6.0)));
assert_eq!(*modifiers, Modifiers::ALT | Modifiers::SUPER);
let ShellEvent::PointerFocus {
entered, position, ..
} = &events[2]
else {
panic!("expected a PointerFocus");
};
assert!(!*entered);
assert_eq!(*position, None, "a leave has no position");
}
#[test]
fn every_contact_reaches_the_queue_with_its_own_id_and_place() {
use shim::{TOUCH_BEGAN, TOUCH_CANCELLED, TOUCH_ENDED, TOUCH_MOVED};
let (mut shell, window) = shell_with_window(0);
unsafe {
shim::__crcbl_web_touch(0, 1.0, 10.0, 20.0, 3, TOUCH_BEGAN);
shim::__crcbl_web_touch(0, 2.0, 300.0, 400.0, 4, TOUCH_BEGAN);
shim::__crcbl_web_touch(0, 3.0, 310.0, 405.0, 4, TOUCH_MOVED);
shim::__crcbl_web_touch(0, 4.0, 10.0, 20.0, 3, TOUCH_ENDED);
shim::__crcbl_web_touch(0, 5.0, 310.0, 405.0, 4, TOUCH_CANCELLED);
shim::__crcbl_web_touch(0, 6.0, 0.0, 0.0, 5, 99);
}
let events = drain(&mut shell);
let contacts: Vec<(WindowId, ContactId, TouchPhase, PhysicalPoint)> = events
.iter()
.map(|event| {
let ShellEvent::Touch {
window,
contact,
phase,
position,
..
} = event
else {
panic!("expected only touch events, got {events:?}");
};
(*window, *contact, *phase, *position)
})
.collect();
assert_eq!(
contacts,
vec![
(
window,
ContactId(3),
TouchPhase::Began,
PhysicalPoint::new(10.0, 20.0)
),
(
window,
ContactId(4),
TouchPhase::Began,
PhysicalPoint::new(300.0, 400.0)
),
(
window,
ContactId(4),
TouchPhase::Moved,
PhysicalPoint::new(310.0, 405.0)
),
(
window,
ContactId(3),
TouchPhase::Ended,
PhysicalPoint::new(10.0, 20.0)
),
(
window,
ContactId(4),
TouchPhase::Cancelled,
PhysicalPoint::new(310.0, 405.0)
),
]
);
assert_eq!(events[2].time(), Some(EventTime::from_millis(3)));
}
#[test]
fn a_sub_unit_device_pixel_ratio_is_reported_verbatim() {
let (mut shell, window) = shell_with_window(0);
unsafe { shim::__crcbl_web_resize(0, 640, 360, 0.5) };
drain(&mut shell);
assert_eq!(
shell.window_state(window).expect("state").scale_factor(),
Some(0.5)
);
unsafe { shim::__crcbl_web_resize(0, 640, 360, f64::NAN) };
drain(&mut shell);
assert_eq!(
shell.window_state(window).expect("state").scale_factor(),
Some(1.0)
);
}
#[test]
fn unsupported_operations_name_the_capability() {
let (mut shell, window) = shell_with_window(0);
assert!(
shell
.warp_pointer(window, PhysicalPoint::new(100.0, 200.0))
.is_err()
);
assert!(
shell
.set_pointer_mode(window, PointerMode::Confined)
.is_err()
);
assert!(shell.clipboard_offer(window, &[]).is_err());
assert!(
shell
.clipboard_request(window, crate::MimeType::TextUtf8)
.is_err()
);
assert!(!shell.clipboard_readable(window));
shell
.set_pointer_mode(window, PointerMode::Free)
.expect("free is always available");
shell
.set_pointer_mode(window, PointerMode::Locked)
.expect("the Pointer Lock API is what POINTER_LOCK names");
}
#[test]
fn surface_target_is_the_canvas() {
let (shell, window) = shell_with_window(3);
assert_eq!(
shell.surface_target(window).expect("valid window"),
SurfaceTarget::Web { canvas_id: 3 }
);
}
#[test]
fn foreign_handles_are_rejected() {
let (shell, _) = shell_with_window(0);
let foreign = WindowId::from_bits(0xDEAD_BEEF_0001_0001).expect("valid bits");
assert!(shell.window_state(foreign).is_err());
assert!(shell.surface_target(foreign).is_err());
}
#[test]
fn key_codes_are_the_w3c_code_names() {
assert_eq!(key_code_of("KeyA"), Some(KeyCode::KeyA));
assert_eq!(key_code_of("Space"), Some(KeyCode::Space));
assert_eq!(key_code_of("ArrowLeft"), Some(KeyCode::ArrowLeft));
assert_eq!(key_code_of("F1"), Some(KeyCode::F1));
assert_eq!(key_code_of(""), None);
assert_eq!(key_code_of("Fn"), None);
assert_ne!(scancode_of("Fn"), scancode_of("Lang1"));
assert_eq!(keysym_of("Shift"), Keysym::NONE);
assert_eq!(keysym_of("é"), Keysym::from_char('é'));
}
#[test]
fn the_canvas_id_comes_from_the_shim() {
unsafe { shim::__crcbl_web_canvas(11) };
assert_eq!(canvas_id(), 11);
let shell = open(canvas_id()).expect("opens");
assert!(
shell
.surface_target(WindowId::from_bits(0x0000_0001_0000_000B).expect("bits"))
.is_err(),
"and it has no window until one is created"
);
}
}