From e4d8e22846aff4330abba1aaa50a95c35836a948 Mon Sep 17 00:00:00 2001 From: Ben Merritt Date: Mon, 3 Jun 2019 22:51:01 -0700 Subject: [PATCH] Start implementing web-sys backend --- Cargo.toml | 27 ++ examples/window.rs | 8 +- src/lib.rs | 4 + src/platform/mod.rs | 1 + src/platform/web_sys.rs | 7 + src/platform_impl/mod.rs | 44 ++- src/platform_impl/web_sys/event_loop.rs | 374 ++++++++++++++++++++++++ src/platform_impl/web_sys/events.rs | 1 + src/platform_impl/web_sys/mod.rs | 24 ++ src/platform_impl/web_sys/util.js | 3 + src/platform_impl/web_sys/window.rs | 1 + 11 files changed, 479 insertions(+), 15 deletions(-) create mode 100644 src/platform/web_sys.rs create mode 100644 src/platform_impl/web_sys/event_loop.rs create mode 100644 src/platform_impl/web_sys/events.rs create mode 100644 src/platform_impl/web_sys/mod.rs create mode 100644 src/platform_impl/web_sys/util.js create mode 100644 src/platform_impl/web_sys/window.rs diff --git a/Cargo.toml b/Cargo.toml index 4ee84239..17e759b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,9 @@ libc = "0.2" log = "0.4" serde = { version = "1", optional = true, features = ["serde_derive"] } +[features] +web-sys-support = ["web-sys", "wasm-bindgen"] + [dev-dependencies] image = "0.21" env_logger = "0.5" @@ -74,6 +77,30 @@ percent-encoding = "1.0" [target.'cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "openbsd", target_os = "netbsd", target_os = "windows"))'.dependencies.parking_lot] version = "0.8" +[target.'cfg(target_arch = "wasm32")'.dependencies.web-sys] +version = "0.3.22" +optional = true +features = [ + 'Document', + 'Event', + 'EventTarget', + 'FocusEvent', + 'HtmlCanvasElement', + 'KeyboardEvent', + 'MouseEvent', + 'PointerEvent', + 'Window', + 'WheelEvent' +] + +[target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen] +version = "0.2.45" +optional = true + +[target.'cfg(target_arch = "wasm32")'.dependencies.stdweb] +version = "0.4.17" +optional = true + [target.'cfg(target_arch = "wasm32")'.dependencies] stdweb = { path = "../stdweb", optional = true } instant = { version = "0.1", features = ["stdweb"] } diff --git a/examples/window.rs b/examples/window.rs index b33b5984..70e812d6 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -1,4 +1,8 @@ extern crate winit; +#[cfg(feature = "stdweb")] +#[macro_use] +extern crate stdweb; +#[cfg(feature = "wasm-bindgen")] #[macro_use] extern crate stdweb; @@ -13,10 +17,10 @@ fn main() { .with_title("A fantastic window!") .build(&event_loop) .unwrap(); - console!(log, "Built window!"); + //console!(log, "Built window!"); event_loop.run(|event, _, control_flow| { - console!(log, format!("{:?}", event)); + //console!(log, format!("{:?}", event)); match event { Event::WindowEvent { diff --git a/src/lib.rs b/src/lib.rs index a15664c2..f48dd32c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -115,6 +115,10 @@ extern crate smithay_client_toolkit as sctk; extern crate stdweb; #[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] extern crate calloop; +#[cfg(feature = "web-sys")] +extern crate wasm_bindgen; +#[cfg(feature = "web-sys")] +extern crate web_sys; pub mod dpi; #[macro_use] diff --git a/src/platform/mod.rs b/src/platform/mod.rs index da780f4b..94f0bf08 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -21,5 +21,6 @@ pub mod unix; pub mod windows; pub mod stdweb; +pub mod web_sys; pub mod desktop; diff --git a/src/platform/web_sys.rs b/src/platform/web_sys.rs new file mode 100644 index 00000000..c4539f78 --- /dev/null +++ b/src/platform/web_sys.rs @@ -0,0 +1,7 @@ +#![cfg(feature = "web-sys")] + +use web_sys::HtmlCanvasElement; + +pub trait WindowExtWebSys { + fn canvas(&self) -> HtmlCanvasElement; +} diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index 2a8ccd62..e0ea2b96 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -1,30 +1,48 @@ pub use self::platform::*; #[cfg(target_os = "windows")] -#[path="windows/mod.rs"] +#[path = "windows/mod.rs"] mod platform; -#[cfg(any(target_os = "linux", target_os = "dragonfly", target_os = "freebsd", target_os = "netbsd", target_os = "openbsd"))] -#[path="linux/mod.rs"] +#[cfg(any( + target_os = "linux", + target_os = "dragonfly", + target_os = "freebsd", + target_os = "netbsd", + target_os = "openbsd" +))] +#[path = "linux/mod.rs"] mod platform; #[cfg(target_os = "macos")] -#[path="macos/mod.rs"] +#[path = "macos/mod.rs"] mod platform; #[cfg(target_os = "android")] -#[path="android/mod.rs"] +#[path = "android/mod.rs"] mod platform; #[cfg(target_os = "ios")] -#[path="ios/mod.rs"] +#[path = "ios/mod.rs"] mod platform; #[cfg(target_os = "emscripten")] -#[path="emscripten/mod.rs"] +#[path = "emscripten/mod.rs"] mod platform; #[cfg(feature = "stdweb")] -#[path="stdweb/mod.rs"] +#[path = "stdweb/mod.rs"] +mod platform; +#[cfg(feature = "web-sys")] +#[path = "web_sys/mod.rs"] mod platform; -#[cfg(all(not(target_os = "ios"), not(target_os = "windows"), not(target_os = "linux"), - not(target_os = "macos"), not(target_os = "android"), not(target_os = "dragonfly"), - not(target_os = "freebsd"), not(target_os = "netbsd"), not(target_os = "openbsd"), - not(target_os = "emscripten"), - not(feature = "stdweb")))] +#[cfg(all( + not(target_os = "ios"), + not(target_os = "windows"), + not(target_os = "linux"), + not(target_os = "macos"), + not(target_os = "android"), + not(target_os = "dragonfly"), + not(target_os = "freebsd"), + not(target_os = "netbsd"), + not(target_os = "openbsd"), + not(target_os = "emscripten"), + not(feature = "stdweb"), + not(feature = "web-sys") +))] compile_error!("The platform you're compiling for is not supported by winit"); diff --git a/src/platform_impl/web_sys/event_loop.rs b/src/platform_impl/web_sys/event_loop.rs new file mode 100644 index 00000000..023ae2ce --- /dev/null +++ b/src/platform_impl/web_sys/event_loop.rs @@ -0,0 +1,374 @@ +use super::*; + +use dpi::LogicalPosition; +use event::{ + DeviceId as RootDI, ElementState, Event, KeyboardInput, MouseScrollDelta, StartCause, + TouchPhase, WindowEvent, +}; +use event_loop::{ControlFlow, EventLoopClosed, EventLoopWindowTarget as RootELW}; +use std::cell::RefCell; +use std::collections::vec_deque::IntoIter as VecDequeIter; +use std::collections::VecDeque; +use std::marker::PhantomData; +use std::rc::Rc; +use wasm_bindgen::{prelude::*, JsCast}; +use web_sys::{Document, EventTarget, FocusEvent, HtmlCanvasElement, KeyboardEvent, PointerEvent, WheelEvent}; +use window::WindowId as RootWI; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct DeviceId(i32); + +impl DeviceId { + pub unsafe fn dummy() -> Self { + DeviceId(0) + } +} + +pub struct EventLoop { + elw: RootELW, +} + +pub struct EventLoopWindowTarget { + pub(crate) runner: EventLoopRunnerShared, +} + +impl EventLoopWindowTarget { + fn new() -> Self { + EventLoopWindowTarget { + runner: Rc::new(ELRShared { + runner: RefCell::new(None), + events: RefCell::new(VecDeque::new()), + }), + } + } +} + +#[derive(Clone)] +pub struct EventLoopProxy { + runner: EventLoopRunnerShared, +} + +impl EventLoopProxy { + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.runner.send_event(Event::UserEvent(event)); + Ok(()) + } +} + +pub type EventLoopRunnerShared = Rc>; + +pub struct ELRShared { + runner: RefCell>>, + events: RefCell>>, // TODO: this may not be necessary? +} + +struct EventLoopRunner { + control: ControlFlow, + is_busy: bool, + event_handler: Box, &mut ControlFlow)>, +} + +#[wasm_bindgen] +extern "C" { + #[wasm_bindgen(module = "util.js", js_name = "throwToEscapeEventLoop")] + fn throw_to_escape_event_loop(); +} + +impl EventLoop { + pub fn new() -> Self { + EventLoop { + elw: RootELW { + p: EventLoopWindowTarget::new(), + _marker: PhantomData, + }, + } + } + + pub fn available_monitors(&self) -> VecDequeIter { + VecDeque::new().into_iter() + } + + pub fn primary_monitor(&self) -> MonitorHandle { + MonitorHandle + } + + pub fn run(self, mut event_handler: F) -> ! + where + F: 'static + FnMut(Event, &RootELW, &mut ControlFlow), + { + let runner = self.elw.p.runner; + + let relw = RootELW { + p: EventLoopWindowTarget::new(), + _marker: PhantomData, + }; + runner.set_listener(Box::new(move |evt, ctrl| event_handler(evt, &relw, ctrl))); + + let document = &document(); + add_event(&runner, document, "blur", |elrs, _: FocusEvent| { + elrs.send_event(Event::WindowEvent { + window_id: RootWI(WindowId), + event: WindowEvent::Focused(false), + }); + }); + add_event(&runner, document, "focus", |elrs, _: FocusEvent| { + elrs.send_event(Event::WindowEvent { + window_id: RootWI(WindowId), + event: WindowEvent::Focused(true), + }); + }); + add_event(&runner, document, "keydown", |elrs, event: KeyboardEvent| { + let key = event.key(); + let mut characters = key.chars(); + let first = characters.next(); + let second = characters.next(); + if let (Some(key), None) = (first, second) { + elrs.send_event(Event::WindowEvent { + window_id: RootWI(WindowId), + event: WindowEvent::ReceivedCharacter(key), + }); + } + elrs.send_event(Event::WindowEvent { + window_id: RootWI(WindowId), + event: WindowEvent::KeyboardInput { + device_id: RootDI(unsafe { DeviceId::dummy() }), + input: KeyboardInput { + scancode: scancode(&event), + state: ElementState::Pressed, + virtual_keycode: button_mapping(&event), + modifiers: keyboard_modifiers_state(&event), + }, + }, + }); + }); + add_event(&runner, document, "keyup", |elrs, event: KeyboardEvent| { + elrs.send_event(Event::WindowEvent { + window_id: RootWI(WindowId), + event: WindowEvent::KeyboardInput { + device_id: RootDI(unsafe { DeviceId::dummy() }), + input: KeyboardInput { + scancode: scancode(&event), + state: ElementState::Released, + virtual_keycode: button_mapping(&event), + modifiers: keyboard_modifiers_state(&event), + }, + }, + }); + }); + + runner.send_event(Event::NewEvents(StartCause::Init)); + + // Throw an exception to break out of Rust exceution and use unreachable to tell the + // compiler this function won't return, giving it a return type of '!' + throw_to_escape_event_loop(); + unreachable!(); + } + + pub fn create_proxy(&self) -> EventLoopProxy { + EventLoopProxy { + runner: self.elw.p.runner.clone(), + } + } + + pub fn window_target(&self) -> &RootELW { + &self.elw + } +} + +pub fn register(elrs: &EventLoopRunnerShared, canvas: &HtmlCanvasElement) { + add_event(elrs, canvas, "pointerout", |elrs, event: PointerEvent| { + elrs.send_event(Event::WindowEvent { + window_id: RootWI(WindowId), + event: WindowEvent::CursorLeft { + device_id: RootDI(DeviceId(event.pointer_id())), + }, + }); + }); + add_event(elrs, canvas, "pointerover", |elrs, event: PointerEvent| { + elrs.send_event(Event::WindowEvent { + window_id: RootWI(WindowId), + event: WindowEvent::CursorEntered { + device_id: RootDI(DeviceId(event.pointer_id())), + }, + }); + }); + add_event(elrs, canvas, "pointermove", |elrs, event: PointerEvent| { + elrs.send_event(Event::WindowEvent { + window_id: RootWI(WindowId), + event: WindowEvent::CursorMoved { + device_id: RootDI(DeviceId(event.pointer_id())), + position: LogicalPosition { + x: event.offset_x().into(), + y: event.offset_y().into(), + }, + modifiers: mouse_modifiers_state(&event), + }, + }); + }); + add_event(elrs, canvas, "pointerup", |elrs, event: PointerEvent| { + elrs.send_event(Event::WindowEvent { + window_id: RootWI(WindowId), + event: WindowEvent::MouseInput { + device_id: RootDI(DeviceId(event.pointer_id())), + state: ElementState::Pressed, + button: mouse_button(&event), + modifiers: mouse_modifiers_state(&event), + }, + }); + }); + add_event(elrs, canvas, "pointerdown", |elrs, event: PointerEvent| { + elrs.send_event(Event::WindowEvent { + window_id: RootWI(WindowId), + event: WindowEvent::MouseInput { + device_id: RootDI(DeviceId(event.pointer_id())), + state: ElementState::Released, + button: mouse_button(&event), + modifiers: mouse_modifiers_state(&event), + }, + }); + }); + add_event(elrs, canvas, "wheel", |elrs, event: WheelEvent| { + let x = event.delta_x(); + let y = event.delta_y(); + let delta = match event.delta_mode() { + WheelEvent::DOM_DELTA_LINE => MouseScrollDelta::LineDelta(x as f32, y as f32), + WheelEvent::DOM_DELTA_PIXEL => MouseScrollDelta::PixelDelta(LogicalPosition { x, y }), + WheelEvent::DOM_DELTA_PAGE => return, + }; + elrs.send_event(Event::WindowEvent { + window_id: RootWI(WindowId), + event: WindowEvent::MouseWheel { + device_id: RootDI(DeviceId(0)), + delta, + phase: TouchPhase::Moved, + modifiers: mouse_modifiers_state(&event), + }, + }); + }); +} + +fn add_event( + elrs: &EventLoopRunnerShared, + target: &EventTarget, + event: &str, + mut handler: F, +) where + E: AsRef + wasm_bindgen::convert::FromWasmAbi + 'static, + F: FnMut(&EventLoopRunnerShared, E) + 'static, +{ + let elrs = elrs.clone(); + + target.add_event_listener_with_callback(event, Closure::wrap(Box::new(move |event: E| { + // Don't capture the event if the events loop has been destroyed + match &*elrs.runner.borrow() { + Some(ref runner) if runner.control == ControlFlow::Exit => return, + _ => (), + } + + let event_ref = event.as_ref(); + event_ref.prevent_default(); + event_ref.stop_propagation(); + event_ref.cancel_bubble(); + + handler(&elrs, event); + }) as Box).as_ref().unchecked_ref()); +} + +impl ELRShared { + // Set the event callback to use for the event loop runner + // This the event callback is a fairly thin layer over the user-provided callback that closes + // over a RootEventLoopWindowTarget reference + fn set_listener(&self, event_handler: Box, &mut ControlFlow)>) { + *self.runner.borrow_mut() = Some(EventLoopRunner { + control: ControlFlow::Poll, + is_busy: false, + event_handler, + }); + } + + // Add an event to the event loop runner + // + // It will determine if the event should be immediately sent to the user or buffered for later + pub fn send_event(&self, event: Event) { + // If the event loop is closed, it should discard any new events + if self.closed() { + return; + } + + let start_cause = StartCause::Poll; // TODO: determine start cause + + // Determine if event handling is in process, and then release the borrow on the runner + let is_busy = if let Some(ref runner) = *self.runner.borrow() { + runner.is_busy + } else { + true // If there is no event runner yet, then there's no point in processing events + }; + + if is_busy { + self.events.borrow_mut().push_back(event); + } else { + // Handle starting a new batch of events + // + // The user is informed via Event::NewEvents that there is a batch of events to process + // However, there is only one of these per batch of events + self.handle_event(Event::NewEvents(start_cause)); + self.handle_event(event); + self.handle_event(Event::EventsCleared); + + // If the event loop is closed, it has been closed this iteration and now the closing + // event should be emitted + if self.closed() { + self.handle_event(Event::LoopDestroyed); + } + } + } + + // Check if the event loop is currntly closed + fn closed(&self) -> bool { + match *self.runner.borrow() { + Some(ref runner) => runner.control == ControlFlow::Exit, + None => false, // If the event loop is None, it has not been intialised yet, so it cannot be closed + } + } + + // handle_event takes in events and either queues them or applies a callback + // + // It should only ever be called from send_event + fn handle_event(&self, event: Event) { + let closed = self.closed(); + + match *self.runner.borrow_mut() { + Some(ref mut runner) => { + // An event is being processed, so the runner should be marked busy + runner.is_busy = true; + + // TODO: bracket this in control flow events? + (runner.event_handler)(event, &mut runner.control); + + // Maintain closed state, even if the callback changes it + if closed { + runner.control = ControlFlow::Exit; + } + + // An event is no longer being processed + runner.is_busy = false; + } + // If an event is being handled without a runner somehow, add it to the event queue so + // it will eventually be processed + _ => self.events.borrow_mut().push_back(event), + } + + // Don't take events out of the queue if the loop is closed or the runner doesn't exist + // If the runner doesn't exist and this method recurses, it will recurse infinitely + if !closed && self.runner.borrow().is_some() { + // Take an event out of the queue and handle it + if let Some(event) = self.events.borrow_mut().pop_front() { + self.handle_event(event); + } + } + } +} + +fn document() -> Document { + web_sys::window().unwrap().document().unwrap() +} diff --git a/src/platform_impl/web_sys/events.rs b/src/platform_impl/web_sys/events.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/platform_impl/web_sys/events.rs @@ -0,0 +1 @@ + diff --git a/src/platform_impl/web_sys/mod.rs b/src/platform_impl/web_sys/mod.rs new file mode 100644 index 00000000..a28ac36e --- /dev/null +++ b/src/platform_impl/web_sys/mod.rs @@ -0,0 +1,24 @@ +use std::fmt; + +mod event_loop; +mod events; +mod window; + +pub use self::event_loop::{ + register, DeviceId, EventLoop, EventLoopProxy, EventLoopRunnerShared, EventLoopWindowTarget, +}; +pub use self::events::{ + button_mapping, keyboard_modifiers_state, mouse_button, mouse_modifiers_state, scancode, +}; +pub use self::window::{MonitorHandle, PlatformSpecificWindowBuilderAttributes, Window, WindowId}; + +// TODO: unify with stdweb impl. + +#[derive(Debug)] +pub struct OsError(String); + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/src/platform_impl/web_sys/util.js b/src/platform_impl/web_sys/util.js new file mode 100644 index 00000000..a4b4c4f1 --- /dev/null +++ b/src/platform_impl/web_sys/util.js @@ -0,0 +1,3 @@ +function throwToEscapeEventLoop() { + throw "Using exceptions for control flow, don't mind me. This isn't actually an error!"; +} \ No newline at end of file diff --git a/src/platform_impl/web_sys/window.rs b/src/platform_impl/web_sys/window.rs new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/platform_impl/web_sys/window.rs @@ -0,0 +1 @@ +