diff --git a/.gitignore b/.gitignore index bf5cea56..7cd4f7f9 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,9 @@ Cargo.lock target/ rls/ .vscode/ +util/ *~ +*.wasm +*.ts +*.js #*# diff --git a/Cargo.toml b/Cargo.toml index 5127f78f..6161bbcd 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 = ["web-sys", "wasm-bindgen"] + [dev-dependencies] image = "0.21" env_logger = "0.5" @@ -74,9 +77,34 @@ 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 = [ + 'console', + 'Document', + 'DomRect', + 'Element', + 'Event', + 'EventTarget', + 'FocusEvent', + 'HtmlCanvasElement', + 'HtmlElement', + 'KeyboardEvent', + 'MouseEvent', + 'Node', + 'PointerEvent', + 'Window', + 'WheelEvent' +] + +[target.'cfg(target_arch = "wasm32")'.dependencies.wasm-bindgen] +version = "0.2.45" +optional = true + [target.'cfg(target_arch = "wasm32")'.dependencies] stdweb = { path = "../stdweb", optional = true } -instant = { version = "0.1", features = ["stdweb"] } +instant = { version = "0.1", features = ["wasm-bindgen"] } [patch.crates-io] stdweb = { path = "../stdweb" } diff --git a/examples/window.rs b/examples/window.rs index eb87fa81..5ae76e29 100644 --- a/examples/window.rs +++ b/examples/window.rs @@ -1,22 +1,31 @@ extern crate winit; +#[cfg(feature = "stdweb")] #[macro_use] extern crate stdweb; +#[cfg(feature = "wasm-bindgen")] +extern crate wasm_bindgen; +#[cfg(feature = "wasm-bindgen")] +extern crate web_sys; use winit::window::WindowBuilder; use winit::event::{Event, WindowEvent}; use winit::event_loop::{EventLoop, ControlFlow}; +use wasm_bindgen::{prelude::*, JsValue}; +use web_sys::console; -fn main() { +#[wasm_bindgen(start)] +pub fn main() { + console::log_1(&JsValue::from_str("main")); let event_loop = EventLoop::new(); let _window = WindowBuilder::new() .with_title("A fantastic window!") .build(&event_loop) .unwrap(); - console!(log, "Built window!"); + console::log_1(&JsValue::from_str("Created window")); event_loop.run(|event, _, control_flow| { - console!(log, format!("{:?}", event)); + console::log_1(&JsValue::from_str(&format!("{:?}", event))); match event { Event::WindowEvent { @@ -26,4 +35,4 @@ fn main() { _ => () } }); -} +} \ No newline at end of file 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..5cb0485a 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..9a6918a9 --- /dev/null +++ b/src/platform_impl/web_sys/event_loop.rs @@ -0,0 +1,378 @@ +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 platform_impl::platform::document; +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::{ + 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)>, +} + +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 '!' + wasm_bindgen::throw_str( + "Using exceptions for control flow, don't mind me. This isn't actually an error!", + ); + } + + 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 }), + _ => 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(); + + let closure = 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); + + target.add_event_listener_with_callback(event, &closure.as_ref().unchecked_ref()); + closure.forget(); // TODO: don't leak this. +} + +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); + } + } + } +} diff --git a/src/platform_impl/web_sys/events.rs b/src/platform_impl/web_sys/events.rs new file mode 100644 index 00000000..4e13ca2a --- /dev/null +++ b/src/platform_impl/web_sys/events.rs @@ -0,0 +1,201 @@ +use std::convert::TryInto; + +use web_sys::{KeyboardEvent, MouseEvent}; +use event::{MouseButton, ModifiersState, ScanCode, VirtualKeyCode}; + +pub fn button_mapping(event: &KeyboardEvent) -> Option { + Some(match &event.code()[..] { + "Digit1" => VirtualKeyCode::Key1, + "Digit2" => VirtualKeyCode::Key2, + "Digit3" => VirtualKeyCode::Key3, + "Digit4" => VirtualKeyCode::Key4, + "Digit5" => VirtualKeyCode::Key5, + "Digit6" => VirtualKeyCode::Key6, + "Digit7" => VirtualKeyCode::Key7, + "Digit8" => VirtualKeyCode::Key8, + "Digit9" => VirtualKeyCode::Key9, + "Digit0" => VirtualKeyCode::Key0, + "KeyA" => VirtualKeyCode::A, + "KeyB" => VirtualKeyCode::B, + "KeyC" => VirtualKeyCode::C, + "KeyD" => VirtualKeyCode::D, + "KeyE" => VirtualKeyCode::E, + "KeyF" => VirtualKeyCode::F, + "KeyG" => VirtualKeyCode::G, + "KeyH" => VirtualKeyCode::H, + "KeyI" => VirtualKeyCode::I, + "KeyJ" => VirtualKeyCode::J, + "KeyK" => VirtualKeyCode::K, + "KeyL" => VirtualKeyCode::L, + "KeyM" => VirtualKeyCode::M, + "KeyN" => VirtualKeyCode::N, + "KeyO" => VirtualKeyCode::O, + "KeyP" => VirtualKeyCode::P, + "KeyQ" => VirtualKeyCode::Q, + "KeyR" => VirtualKeyCode::R, + "KeyS" => VirtualKeyCode::S, + "KeyT" => VirtualKeyCode::T, + "KeyU" => VirtualKeyCode::U, + "KeyV" => VirtualKeyCode::V, + "KeyW" => VirtualKeyCode::W, + "KeyX" => VirtualKeyCode::X, + "KeyY" => VirtualKeyCode::Y, + "KeyZ" => VirtualKeyCode::Z, + "Escape" => VirtualKeyCode::Escape, + "F1" => VirtualKeyCode::F1, + "F2" => VirtualKeyCode::F2, + "F3" => VirtualKeyCode::F3, + "F4" => VirtualKeyCode::F4, + "F5" => VirtualKeyCode::F5, + "F6" => VirtualKeyCode::F6, + "F7" => VirtualKeyCode::F7, + "F8" => VirtualKeyCode::F8, + "F9" => VirtualKeyCode::F9, + "F10" => VirtualKeyCode::F10, + "F11" => VirtualKeyCode::F11, + "F12" => VirtualKeyCode::F12, + "F13" => VirtualKeyCode::F13, + "F14" => VirtualKeyCode::F14, + "F15" => VirtualKeyCode::F15, + "F16" => VirtualKeyCode::F16, + "F17" => VirtualKeyCode::F17, + "F18" => VirtualKeyCode::F18, + "F19" => VirtualKeyCode::F19, + "F20" => VirtualKeyCode::F20, + "F21" => VirtualKeyCode::F21, + "F22" => VirtualKeyCode::F22, + "F23" => VirtualKeyCode::F23, + "F24" => VirtualKeyCode::F24, + "PrintScreen" => VirtualKeyCode::Snapshot, + "ScrollLock" => VirtualKeyCode::Scroll, + "Pause" => VirtualKeyCode::Pause, + "Insert" => VirtualKeyCode::Insert, + "Home" => VirtualKeyCode::Home, + "Delete" => VirtualKeyCode::Delete, + "End" => VirtualKeyCode::End, + "PageDown" => VirtualKeyCode::PageDown, + "PageUp" => VirtualKeyCode::PageUp, + "ArrowLeft" => VirtualKeyCode::Left, + "ArrowUp" => VirtualKeyCode::Up, + "ArrowRight" => VirtualKeyCode::Right, + "ArrowDown" => VirtualKeyCode::Down, + "Backspace" => VirtualKeyCode::Back, + "Enter" => VirtualKeyCode::Return, + "Space" => VirtualKeyCode::Space, + "Compose" => VirtualKeyCode::Compose, + "Caret" => VirtualKeyCode::Caret, + "NumLock" => VirtualKeyCode::Numlock, + "Numpad0" => VirtualKeyCode::Numpad0, + "Numpad1" => VirtualKeyCode::Numpad1, + "Numpad2" => VirtualKeyCode::Numpad2, + "Numpad3" => VirtualKeyCode::Numpad3, + "Numpad4" => VirtualKeyCode::Numpad4, + "Numpad5" => VirtualKeyCode::Numpad5, + "Numpad6" => VirtualKeyCode::Numpad6, + "Numpad7" => VirtualKeyCode::Numpad7, + "Numpad8" => VirtualKeyCode::Numpad8, + "Numpad9" => VirtualKeyCode::Numpad9, + "AbntC1" => VirtualKeyCode::AbntC1, + "AbntC2" => VirtualKeyCode::AbntC2, + "NumpadAdd" => VirtualKeyCode::Add, + "Quote" => VirtualKeyCode::Apostrophe, + "Apps" => VirtualKeyCode::Apps, + "At" => VirtualKeyCode::At, + "Ax" => VirtualKeyCode::Ax, + "Backslash" => VirtualKeyCode::Backslash, + "Calculator" => VirtualKeyCode::Calculator, + "Capital" => VirtualKeyCode::Capital, + "Semicolon" => VirtualKeyCode::Semicolon, + "Comma" => VirtualKeyCode::Comma, + "Convert" => VirtualKeyCode::Convert, + "NumpadDecimal" => VirtualKeyCode::Decimal, + "NumpadDivide" => VirtualKeyCode::Divide, + "Equal" => VirtualKeyCode::Equals, + "Backquote" => VirtualKeyCode::Grave, + "Kana" => VirtualKeyCode::Kana, + "Kanji" => VirtualKeyCode::Kanji, + "AltLeft" => VirtualKeyCode::LAlt, + "BracketLeft" => VirtualKeyCode::LBracket, + "ControlLeft" => VirtualKeyCode::LControl, + "ShiftLeft" => VirtualKeyCode::LShift, + "MetaLeft" => VirtualKeyCode::LWin, + "Mail" => VirtualKeyCode::Mail, + "MediaSelect" => VirtualKeyCode::MediaSelect, + "MediaStop" => VirtualKeyCode::MediaStop, + "Minus" => VirtualKeyCode::Minus, + "NumpadMultiply" => VirtualKeyCode::Multiply, + "Mute" => VirtualKeyCode::Mute, + "LaunchMyComputer" => VirtualKeyCode::MyComputer, + "NavigateForward" => VirtualKeyCode::NavigateForward, + "NavigateBackward" => VirtualKeyCode::NavigateBackward, + "NextTrack" => VirtualKeyCode::NextTrack, + "NoConvert" => VirtualKeyCode::NoConvert, + "NumpadComma" => VirtualKeyCode::NumpadComma, + "NumpadEnter" => VirtualKeyCode::NumpadEnter, + "NumpadEquals" => VirtualKeyCode::NumpadEquals, + "OEM102" => VirtualKeyCode::OEM102, + "Period" => VirtualKeyCode::Period, + "PlayPause" => VirtualKeyCode::PlayPause, + "Power" => VirtualKeyCode::Power, + "PrevTrack" => VirtualKeyCode::PrevTrack, + "AltRight" => VirtualKeyCode::RAlt, + "BracketRight" => VirtualKeyCode::RBracket, + "ControlRight" => VirtualKeyCode::RControl, + "ShiftRight" => VirtualKeyCode::RShift, + "MetaRight" => VirtualKeyCode::RWin, + "Slash" => VirtualKeyCode::Slash, + "Sleep" => VirtualKeyCode::Sleep, + "Stop" => VirtualKeyCode::Stop, + "NumpadSubtract" => VirtualKeyCode::Subtract, + "Sysrq" => VirtualKeyCode::Sysrq, + "Tab" => VirtualKeyCode::Tab, + "Underline" => VirtualKeyCode::Underline, + "Unlabeled" => VirtualKeyCode::Unlabeled, + "AudioVolumeDown" => VirtualKeyCode::VolumeDown, + "AudioVolumeUp" => VirtualKeyCode::VolumeUp, + "Wake" => VirtualKeyCode::Wake, + "WebBack" => VirtualKeyCode::WebBack, + "WebFavorites" => VirtualKeyCode::WebFavorites, + "WebForward" => VirtualKeyCode::WebForward, + "WebHome" => VirtualKeyCode::WebHome, + "WebRefresh" => VirtualKeyCode::WebRefresh, + "WebSearch" => VirtualKeyCode::WebSearch, + "WebStop" => VirtualKeyCode::WebStop, + "Yen" => VirtualKeyCode::Yen, + _ => return None + }) +} + +pub fn mouse_modifiers_state(event: &MouseEvent) -> ModifiersState { + ModifiersState { + shift: event.shift_key(), + ctrl: event.ctrl_key(), + alt: event.alt_key(), + logo: event.meta_key(), + } +} + +pub fn mouse_button(event: &MouseEvent) -> MouseButton { + match event.button() { + 0 => MouseButton::Left, + 1 => MouseButton::Middle, + 2 => MouseButton::Right, + i => MouseButton::Other((i - 3).try_into().expect("very large mouse button value")), + } +} + +pub fn keyboard_modifiers_state(event: &KeyboardEvent) -> ModifiersState { + ModifiersState { + shift: event.shift_key(), + ctrl: event.ctrl_key(), + alt: event.alt_key(), + logo: event.meta_key(), + } +} + +pub fn scancode(event: &KeyboardEvent) -> ScanCode { + match event.key_code() { + 0 => event.char_code(), + i => i, + } +} diff --git a/src/platform_impl/web_sys/mod.rs b/src/platform_impl/web_sys/mod.rs new file mode 100644 index 00000000..1b0591b1 --- /dev/null +++ b/src/platform_impl/web_sys/mod.rs @@ -0,0 +1,30 @@ +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}; + +#[derive(Debug)] +pub struct OsError(String); + +impl fmt::Display for OsError { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{}", self.0) + } +} + +fn window() -> web_sys::Window { + web_sys::window().unwrap() +} + +fn document() -> web_sys::Document { + window().document().unwrap() +} diff --git a/src/platform_impl/web_sys/window.rs b/src/platform_impl/web_sys/window.rs new file mode 100644 index 00000000..bd51c3f3 --- /dev/null +++ b/src/platform_impl/web_sys/window.rs @@ -0,0 +1,330 @@ +use super::{register, EventLoopWindowTarget, OsError}; +use dpi::{LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize}; +use error::{ExternalError, NotSupportedError, OsError as RootOE}; +use event::{Event, WindowEvent}; +use icon::Icon; +use monitor::MonitorHandle as RootMH; +use platform::web_sys::WindowExtWebSys; +use platform_impl::platform::{document, window}; +use std::cell::RefCell; +use std::collections::vec_deque::IntoIter as VecDequeIter; +use std::collections::VecDeque; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::HtmlCanvasElement; +use window::{CursorIcon, Window as RootWindow, WindowAttributes, WindowId as RootWI}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct MonitorHandle; + +impl MonitorHandle { + pub fn hidpi_factor(&self) -> f64 { + 1.0 + } + + pub fn position(&self) -> PhysicalPosition { + unimplemented!(); + } + + pub fn dimensions(&self) -> PhysicalSize { + unimplemented!(); + } + + pub fn name(&self) -> Option { + unimplemented!(); + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct WindowId; + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PlatformSpecificWindowBuilderAttributes; + +impl WindowId { + pub unsafe fn dummy() -> WindowId { + WindowId + } +} + +pub struct Window { + pub(crate) canvas: HtmlCanvasElement, + pub(crate) redraw: Box, + previous_pointer: RefCell<&'static str>, + position: RefCell, +} + +impl Window { + pub fn new( + target: &EventLoopWindowTarget, + attr: WindowAttributes, + _: PlatformSpecificWindowBuilderAttributes, + ) -> Result { + let element = document() + .create_element("canvas") + .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))?; + let canvas: HtmlCanvasElement = element.unchecked_into(); + document() + .body() + .ok_or_else(|| os_error!(OsError("Failed to find body node".to_owned())))? + .append_child(&canvas) + .map_err(|_| os_error!(OsError("Failed to append canvas".to_owned())))?; + + register(&target.runner, &canvas); + + let runner = target.runner.clone(); + let redraw = Box::new(move || { + let runner = runner.clone(); + let closure = Closure::once_into_js(move |_: f64| { + runner.send_event(Event::WindowEvent { + window_id: RootWI(WindowId), + event: WindowEvent::RedrawRequested, + }); + }); + window().request_animation_frame(closure.as_ref().unchecked_ref()); + }); + + let window = Window { + canvas, + redraw, + previous_pointer: RefCell::new("auto"), + position: RefCell::new(LogicalPosition { x: 0.0, y: 0.0 }), + }; + + if let Some(inner_size) = attr.inner_size { + window.set_inner_size(inner_size); + } else { + window.set_inner_size(LogicalSize { + width: 1024.0, + height: 768.0, + }) + } + window.set_min_inner_size(attr.min_inner_size); + window.set_max_inner_size(attr.max_inner_size); + window.set_resizable(attr.resizable); + window.set_title(&attr.title); + window.set_maximized(attr.maximized); + window.set_visible(attr.visible); + //window.set_transparent(attr.transparent); + window.set_decorations(attr.decorations); + window.set_always_on_top(attr.always_on_top); + window.set_window_icon(attr.window_icon); + + Ok(window) + } + + pub fn set_title(&self, title: &str) { + document().set_title(title); + } + + pub fn set_visible(&self, _visible: bool) { + // Intentionally a no-op + } + + pub fn request_redraw(&self) { + (self.redraw)(); + } + + pub fn outer_position(&self) -> Result { + let bounds = self.canvas.get_bounding_client_rect(); + Ok(LogicalPosition { + x: bounds.x(), + y: bounds.y(), + }) + } + + pub fn inner_position(&self) -> Result { + Ok(*self.position.borrow()) + } + + pub fn set_outer_position(&self, position: LogicalPosition) { + *self.position.borrow_mut() = position; + self.canvas + .set_attribute("position", "fixed") + .expect("Setting the position for the canvas"); + self.canvas + .set_attribute("left", &position.x.to_string()) + .expect("Setting the position for the canvas"); + self.canvas + .set_attribute("top", &position.y.to_string()) + .expect("Setting the position for the canvas"); + } + + #[inline] + pub fn inner_size(&self) -> LogicalSize { + LogicalSize { + width: self.canvas.width() as f64, + height: self.canvas.height() as f64, + } + } + + #[inline] + pub fn outer_size(&self) -> LogicalSize { + LogicalSize { + width: self.canvas.width() as f64, + height: self.canvas.height() as f64, + } + } + + #[inline] + pub fn set_inner_size(&self, size: LogicalSize) { + self.canvas.set_width(size.width as u32); + self.canvas.set_height(size.height as u32); + } + + #[inline] + pub fn set_min_inner_size(&self, _dimensions: Option) { + // Intentionally a no-op: users can't resize canvas elements + } + + #[inline] + pub fn set_max_inner_size(&self, _dimensions: Option) { + // Intentionally a no-op: users can't resize canvas elements + } + + #[inline] + pub fn set_resizable(&self, _resizable: bool) { + // Intentionally a no-op: users can't resize canvas elements + } + + #[inline] + pub fn hidpi_factor(&self) -> f64 { + 1.0 + } + + #[inline] + pub fn set_cursor_icon(&self, cursor: CursorIcon) { + let text = match cursor { + CursorIcon::Default => "auto", + CursorIcon::Crosshair => "crosshair", + CursorIcon::Hand => "pointer", + CursorIcon::Arrow => "default", + CursorIcon::Move => "move", + CursorIcon::Text => "text", + CursorIcon::Wait => "wait", + CursorIcon::Help => "help", + CursorIcon::Progress => "progress", + + CursorIcon::NotAllowed => "not-allowed", + CursorIcon::ContextMenu => "context-menu", + CursorIcon::Cell => "cell", + CursorIcon::VerticalText => "vertical-text", + CursorIcon::Alias => "alias", + CursorIcon::Copy => "copy", + CursorIcon::NoDrop => "no-drop", + CursorIcon::Grab => "grab", + CursorIcon::Grabbing => "grabbing", + CursorIcon::AllScroll => "all-scroll", + CursorIcon::ZoomIn => "zoom-in", + CursorIcon::ZoomOut => "zoom-out", + + CursorIcon::EResize => "e-resize", + CursorIcon::NResize => "n-resize", + CursorIcon::NeResize => "ne-resize", + CursorIcon::NwResize => "nw-resize", + CursorIcon::SResize => "s-resize", + CursorIcon::SeResize => "se-resize", + CursorIcon::SwResize => "sw-resize", + CursorIcon::WResize => "w-resize", + CursorIcon::EwResize => "ew-resize", + CursorIcon::NsResize => "ns-resize", + CursorIcon::NeswResize => "nesw-resize", + CursorIcon::NwseResize => "nwse-resize", + CursorIcon::ColResize => "col-resize", + CursorIcon::RowResize => "row-resize", + }; + *self.previous_pointer.borrow_mut() = text; + self.canvas + .set_attribute("cursor", text) + .expect("Setting the cursor on the canvas"); + } + + #[inline] + pub fn set_cursor_position(&self, _position: LogicalPosition) -> Result<(), ExternalError> { + // TODO: pointer capture + Ok(()) + } + + #[inline] + pub fn set_cursor_grab(&self, _grab: bool) -> Result<(), ExternalError> { + // TODO: pointer capture + Ok(()) + } + + #[inline] + pub fn set_cursor_visible(&self, visible: bool) { + if !visible { + self.canvas + .set_attribute("cursor", "none") + .expect("Setting the cursor on the canvas"); + } else { + self.canvas + .set_attribute("cursor", *self.previous_pointer.borrow()) + .expect("Setting the cursor on the canvas"); + } + } + + #[inline] + pub fn set_maximized(&self, _maximized: bool) { + // TODO: should there be a maximization / fullscreen API? + } + + #[inline] + pub fn fullscreen(&self) -> Option { + // TODO: should there be a maximization / fullscreen API? + None + } + + #[inline] + pub fn set_fullscreen(&self, _monitor: Option) { + // TODO: should there be a maximization / fullscreen API? + } + + #[inline] + pub fn set_decorations(&self, _decorations: bool) { + // Intentionally a no-op, no canvas decorations + } + + #[inline] + pub fn set_always_on_top(&self, _always_on_top: bool) { + // Intentionally a no-op, no window ordering + } + + #[inline] + pub fn set_window_icon(&self, _window_icon: Option) { + // Currently an intentional no-op + } + + #[inline] + pub fn set_ime_position(&self, _position: LogicalPosition) { + // TODO: what is this? + } + + #[inline] + pub fn current_monitor(&self) -> RootMH { + RootMH { + inner: MonitorHandle, + } + } + + #[inline] + pub fn available_monitors(&self) -> VecDequeIter { + VecDeque::new().into_iter() + } + + #[inline] + pub fn primary_monitor(&self) -> MonitorHandle { + MonitorHandle + } + + #[inline] + pub fn id(&self) -> WindowId { + // TODO ? + unsafe { WindowId::dummy() } + } +} + +impl WindowExtWebSys for RootWindow { + fn canvas(&self) -> HtmlCanvasElement { + self.window.canvas.clone() + } +} diff --git a/test.html b/test.html new file mode 100644 index 00000000..ee88fd5b --- /dev/null +++ b/test.html @@ -0,0 +1,20 @@ + + + + + + + + + + \ No newline at end of file diff --git a/test.ps1 b/test.ps1 new file mode 100644 index 00000000..81924470 --- /dev/null +++ b/test.ps1 @@ -0,0 +1,2 @@ +cargo build --target wasm32-unknown-unknown --features web_sys --example window +wasm-bindgen .\target\wasm32-unknown-unknown\debug\examples\window.wasm --out-dir . --target web \ No newline at end of file