#![cfg(target_arch = "wasm32")]
use alloc::collections::VecDeque;
use alloc::rc::Rc;
use alloc::string::String;
use alloc::vec::Vec;
use core::cell::RefCell;
use wasm_bindgen::JsCast;
use wasm_bindgen::prelude::*;
use web_sys::{
CanvasRenderingContext2d, EventTarget, HtmlCanvasElement, KeyboardEvent, PointerEvent,
TouchEvent, WheelEvent,
};
use super::{BackbufferPersistence, DisplayInfo, InputEvent, Surface};
use crate::cache::InspectCaches;
use crate::draw::texture::ColorFormat;
use crate::event::input::{
KEY_BACKSPACE, KEY_DELETE, KEY_END, KEY_ESCAPE, KEY_HOME, KEY_LEFT, KEY_RETURN, KEY_RIGHT,
};
use crate::types::{Fixed, Rect};
type EventQueue = Rc<RefCell<VecDeque<InputEvent>>>;
struct Listener {
target: EventTarget,
event: String,
closure: Closure<dyn FnMut(JsValue)>,
}
pub struct WebCanvasSurface {
canvas: HtmlCanvasElement,
ctx: CanvasRenderingContext2d,
event_queue: EventQueue,
_listeners: Vec<Listener>,
}
impl WebCanvasSurface {
pub fn new(canvas: HtmlCanvasElement) -> Self {
sync_canvas_size(&canvas);
let ctx = canvas
.get_context("2d")
.expect("canvas.getContext failed")
.expect("canvas has no 2d context")
.dyn_into::<CanvasRenderingContext2d>()
.expect("getContext('2d') returned a non-2d context");
let event_queue: EventQueue = Rc::new(RefCell::new(VecDeque::new()));
let listeners = attach_listeners(&canvas, &event_queue);
Self {
canvas,
ctx,
event_queue,
_listeners: listeners,
}
}
pub fn ctx(&self) -> &CanvasRenderingContext2d {
&self.ctx
}
pub fn canvas(&self) -> &HtmlCanvasElement {
&self.canvas
}
}
impl Drop for WebCanvasSurface {
fn drop(&mut self) {
for listener in self._listeners.drain(..) {
let _ = listener.target.remove_event_listener_with_callback(
&listener.event,
listener.closure.as_ref().unchecked_ref(),
);
}
}
}
impl InspectCaches for WebCanvasSurface {}
impl Surface for WebCanvasSurface {
fn display_info(&self) -> DisplayInfo {
let (css_w, css_h, scale) = sync_canvas_size(&self.canvas);
DisplayInfo {
width: css_w,
height: css_h,
scale,
format: ColorFormat::RGBA8888,
}
}
fn flush(&mut self, _area: &Rect) {}
fn poll_event(&mut self) -> Option<InputEvent> {
self.event_queue.borrow_mut().pop_front()
}
fn persistence(&self) -> BackbufferPersistence {
BackbufferPersistence::Transient
}
}
fn sync_canvas_size(canvas: &HtmlCanvasElement) -> (u16, u16, Fixed) {
let window = web_sys::window().expect("no global `window`");
let dpr = window.device_pixel_ratio().max(1.0);
let css_w = canvas.client_width().max(1) as u16;
let css_h = canvas.client_height().max(1) as u16;
let phys_w = (css_w as f64 * dpr).round() as u32;
let phys_h = (css_h as f64 * dpr).round() as u32;
if canvas.width() != phys_w {
canvas.set_width(phys_w);
}
if canvas.height() != phys_h {
canvas.set_height(phys_h);
}
let scale = Fixed::from_f32(dpr as f32);
(css_w, css_h, scale)
}
fn attach_listeners(canvas: &HtmlCanvasElement, queue: &EventQueue) -> Vec<Listener> {
let mut listeners = Vec::with_capacity(12);
listeners.push(pointer_listener(
canvas,
queue,
"pointerdown",
|id, x, y| InputEvent::PointerDown { id, x, y },
));
listeners.push(pointer_listener(
canvas,
queue,
"pointermove",
|id, x, y| InputEvent::PointerMove { id, x, y },
));
listeners.push(pointer_listener(canvas, queue, "pointerup", |id, x, y| {
InputEvent::PointerUp { id, x, y }
}));
listeners.push(pointer_listener(
canvas,
queue,
"pointercancel",
|id, x, y| InputEvent::PointerUp { id, x, y },
));
listeners.push(leave_listener(canvas, queue));
listeners.push(wheel_listener(canvas, queue));
listeners.push(touch_listener(
canvas,
queue,
"touchstart",
TouchKind::Start,
));
listeners.push(touch_listener(canvas, queue, "touchmove", TouchKind::Move));
listeners.push(touch_listener(canvas, queue, "touchend", TouchKind::End));
listeners.push(touch_listener(canvas, queue, "touchcancel", TouchKind::End));
listeners.push(keyboard_listener(queue, "keydown", true));
listeners.push(keyboard_listener(queue, "keyup", false));
listeners
}
fn pointer_listener(
canvas: &HtmlCanvasElement,
queue: &EventQueue,
name: &str,
map: fn(u8, Fixed, Fixed) -> InputEvent,
) -> Listener {
let q = queue.clone();
let capture_on_down = name == "pointerdown";
let canvas_for_capture = canvas.clone();
let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw: JsValue| {
let evt: PointerEvent = raw.unchecked_into();
evt.prevent_default();
if capture_on_down {
let _ = canvas_for_capture.set_pointer_capture(evt.pointer_id());
}
let id = (evt.pointer_id().rem_euclid(0xff)) as u8;
let x = Fixed::from_int(evt.offset_x());
let y = Fixed::from_int(evt.offset_y());
q.borrow_mut().push_back(map(id, x, y));
});
register_listener(canvas.clone().into(), name, closure)
}
fn register_listener(
target: EventTarget,
name: &str,
closure: Closure<dyn FnMut(JsValue)>,
) -> Listener {
target
.add_event_listener_with_callback(name, closure.as_ref().unchecked_ref())
.expect("addEventListener");
Listener {
target,
event: name.into(),
closure,
}
}
fn leave_listener(canvas: &HtmlCanvasElement, queue: &EventQueue) -> Listener {
let q = queue.clone();
let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw: JsValue| {
let evt: PointerEvent = raw.unchecked_into();
if evt.buttons() != 0 {
return;
}
const OFF: i32 = i16::MIN as i32;
q.borrow_mut().push_back(InputEvent::PointerMove {
id: 0,
x: Fixed::from_int(OFF),
y: Fixed::from_int(OFF),
});
});
register_listener(canvas.clone().into(), "pointerleave", closure)
}
fn wheel_listener(canvas: &HtmlCanvasElement, queue: &EventQueue) -> Listener {
let q = queue.clone();
let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw: JsValue| {
let evt: WheelEvent = raw.unchecked_into();
evt.prevent_default();
let x = Fixed::from_int(evt.offset_x());
let y = Fixed::from_int(evt.offset_y());
let dx_units = evt.delta_x() / 4.0;
let dy_units = evt.delta_y() / 4.0;
let dx = Fixed::from_f32(dx_units as f32);
let dy = Fixed::from_f32(-dy_units as f32);
q.borrow_mut().push_back(InputEvent::Wheel { dx, dy, x, y });
});
register_listener(canvas.clone().into(), "wheel", closure)
}
#[derive(Clone, Copy)]
enum TouchKind {
Start,
Move,
End,
}
fn touch_listener(
canvas: &HtmlCanvasElement,
queue: &EventQueue,
name: &str,
kind: TouchKind,
) -> Listener {
let q = queue.clone();
let canvas_for_rect = canvas.clone();
let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw: JsValue| {
let evt: TouchEvent = raw.unchecked_into();
evt.prevent_default();
let rect = canvas_for_rect.get_bounding_client_rect();
let touches = match kind {
TouchKind::End => evt.changed_touches(),
_ => evt.target_touches(),
};
for i in 0..touches.length() {
let Some(touch) = touches.item(i) else {
continue;
};
let x = Fixed::from_int((touch.client_x() as f64 - rect.left()).round() as i32);
let y = Fixed::from_int((touch.client_y() as f64 - rect.top()).round() as i32);
let id = (touch.identifier().rem_euclid(0xff)) as u8;
let event = match kind {
TouchKind::Start => InputEvent::PointerDown { id, x, y },
TouchKind::Move => InputEvent::PointerMove { id, x, y },
TouchKind::End => InputEvent::PointerUp { id, x, y },
};
q.borrow_mut().push_back(event);
}
});
register_listener(canvas.clone().into(), name, closure)
}
fn keyboard_listener(queue: &EventQueue, name: &str, pressed: bool) -> Listener {
let q = queue.clone();
let window = web_sys::window().expect("no global `window`");
let closure = Closure::<dyn FnMut(JsValue)>::new(move |raw: JsValue| {
let evt: KeyboardEvent = raw.unchecked_into();
let key = evt.key();
if let Some(code) = map_key(&key) {
evt.prevent_default();
q.borrow_mut().push_back(InputEvent::Key { code, pressed });
}
if pressed && key.chars().count() == 1 {
if let Some(ch) = key.chars().next() {
q.borrow_mut().push_back(InputEvent::CharInput { ch });
}
}
});
register_listener(window.into(), name, closure)
}
fn map_key(key: &str) -> Option<u32> {
Some(match key {
"Backspace" => KEY_BACKSPACE,
"Delete" => KEY_DELETE,
"ArrowLeft" => KEY_LEFT,
"ArrowRight" => KEY_RIGHT,
"Home" => KEY_HOME,
"End" => KEY_END,
"Enter" => KEY_RETURN,
"Escape" => KEY_ESCAPE,
_ => return None,
})
}