use super::event; use crate::dpi::{LogicalPosition, LogicalSize}; use crate::error::OsError as RootOE; use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; use crate::platform_impl::OsError; use wasm_bindgen::{closure::Closure, JsCast}; use web_sys::{FocusEvent, HtmlCanvasElement, KeyboardEvent, PointerEvent, WheelEvent}; pub struct Canvas { raw: HtmlCanvasElement, on_focus: Option>, on_blur: Option>, on_keyboard_release: Option>, on_keyboard_press: Option>, on_received_character: Option>, on_cursor_leave: Option>, on_cursor_enter: Option>, on_cursor_move: Option>, on_mouse_press: Option>, on_mouse_release: Option>, on_mouse_wheel: Option>, } impl Drop for Canvas { fn drop(&mut self) { self.raw.remove(); } } impl Canvas { pub fn create() -> Result { let window = web_sys::window().ok_or(os_error!(OsError("Failed to obtain window".to_owned())))?; let document = window .document() .ok_or(os_error!(OsError("Failed to obtain document".to_owned())))?; let canvas: HtmlCanvasElement = document .create_element("canvas") .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))? .unchecked_into(); // A tabindex is needed in order to capture local keyboard events. // A "0" value means that the element should be focusable in // sequential keyboard navigation, but its order is defined by the // document's source order. // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex canvas .set_attribute("tabindex", "0") .map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?; Ok(Canvas { raw: canvas, on_blur: None, on_focus: None, on_keyboard_release: None, on_keyboard_press: None, on_received_character: None, on_cursor_leave: None, on_cursor_enter: None, on_cursor_move: None, on_mouse_release: None, on_mouse_press: None, on_mouse_wheel: None, }) } pub fn set_attribute(&self, attribute: &str, value: &str) { self.raw .set_attribute(attribute, value) .expect(&format!("Set attribute: {}", attribute)); } pub fn position(&self) -> (f64, f64) { let bounds = self.raw.get_bounding_client_rect(); (bounds.x(), bounds.y()) } pub fn width(&self) -> f64 { self.raw.width() as f64 } pub fn height(&self) -> f64 { self.raw.height() as f64 } pub fn set_size(&self, size: LogicalSize) { self.raw.set_width(size.width as u32); self.raw.set_height(size.height as u32); } pub fn raw(&self) -> &HtmlCanvasElement { &self.raw } pub fn on_blur(&mut self, mut handler: F) where F: 'static + FnMut(), { self.on_blur = Some(self.add_event("blur", move |_: FocusEvent| { handler(); })); } pub fn on_focus(&mut self, mut handler: F) where F: 'static + FnMut(), { self.on_focus = Some(self.add_event("focus", move |_: FocusEvent| { handler(); })); } pub fn on_keyboard_release(&mut self, mut handler: F) where F: 'static + FnMut(ScanCode, Option, ModifiersState), { self.on_keyboard_release = Some(self.add_event("keyup", move |event: KeyboardEvent| { handler( event::scan_code(&event), event::virtual_key_code(&event), event::keyboard_modifiers(&event), ); })); } pub fn on_keyboard_press(&mut self, mut handler: F) where F: 'static + FnMut(ScanCode, Option, ModifiersState), { self.on_keyboard_press = Some(self.add_event("keydown", move |event: KeyboardEvent| { handler( event::scan_code(&event), event::virtual_key_code(&event), event::keyboard_modifiers(&event), ); })); } pub fn on_received_character(&mut self, mut handler: F) where F: 'static + FnMut(char), { // TODO: Use `beforeinput`. // // The `keypress` event is deprecated, but there does not seem to be a // viable/compatible alternative as of now. `beforeinput` is still widely // unsupported. self.on_received_character = Some(self.add_event("keypress", move |event: KeyboardEvent| { handler(event::codepoint(&event)); })); } pub fn on_cursor_leave(&mut self, mut handler: F) where F: 'static + FnMut(i32), { self.on_cursor_leave = Some(self.add_event("pointerout", move |event: PointerEvent| { handler(event.pointer_id()); })); } pub fn on_cursor_enter(&mut self, mut handler: F) where F: 'static + FnMut(i32), { self.on_cursor_enter = Some(self.add_event("pointerover", move |event: PointerEvent| { handler(event.pointer_id()); })); } pub fn on_mouse_release(&mut self, mut handler: F) where F: 'static + FnMut(i32, MouseButton, ModifiersState), { self.on_mouse_release = Some(self.add_event("pointerup", move |event: PointerEvent| { handler( event.pointer_id(), event::mouse_button(&event), event::mouse_modifiers(&event), ); })); } pub fn on_mouse_press(&mut self, mut handler: F) where F: 'static + FnMut(i32, MouseButton, ModifiersState), { self.on_mouse_press = Some(self.add_event("pointerdown", move |event: PointerEvent| { handler( event.pointer_id(), event::mouse_button(&event), event::mouse_modifiers(&event), ); })); } pub fn on_cursor_move(&mut self, mut handler: F) where F: 'static + FnMut(i32, LogicalPosition, ModifiersState), { self.on_cursor_move = Some(self.add_event("pointermove", move |event: PointerEvent| { handler( event.pointer_id(), event::mouse_position(&event), event::mouse_modifiers(&event), ); })); } pub fn on_mouse_wheel(&mut self, mut handler: F) where F: 'static + FnMut(i32, MouseScrollDelta, ModifiersState), { self.on_mouse_wheel = Some(self.add_event("wheel", move |event: WheelEvent| { if let Some(delta) = event::mouse_scroll_delta(&event) { handler(0, delta, event::mouse_modifiers(&event)); } })); } fn add_event(&self, event_name: &str, mut handler: F) -> Closure where E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, F: 'static + FnMut(E), { let closure = Closure::wrap(Box::new(move |event: E| { { let event_ref = event.as_ref(); event_ref.stop_propagation(); event_ref.cancel_bubble(); } handler(event); }) as Box); self.raw .add_event_listener_with_callback(event_name, &closure.as_ref().unchecked_ref()) .expect("Failed to add event listener with callback"); closure } }