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 1e22ed44..9885cd23 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,12 @@ categories = ["gui"] [package.metadata.docs.rs] features = ["serde"] +[features] +web-sys = ["web_sys", "wasm-bindgen", "instant/wasm-bindgen"] +stdweb = ["std_web", "instant/stdweb"] + [dependencies] +instant = "0.1" lazy_static = "1" libc = "0.2" log = "0.4" @@ -79,3 +84,37 @@ 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] +package = "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.std_web] +package = "stdweb" +version = "0.4.17" +optional = true + +[patch.crates-io] +stdweb = { git = "https://github.com/koute/stdweb", rev = "b3a29bb9dd9b9405540d711ed02a21cd7058d5c0"} diff --git a/src/event.rs b/src/event.rs index 8e0ac043..7e40fcf9 100644 --- a/src/event.rs +++ b/src/event.rs @@ -4,7 +4,8 @@ //! processed and used to modify the program state. For more details, see the root-level documentation. //! //! [event_loop_run]: ../event_loop/struct.EventLoop.html#method.run -use std::{path::PathBuf, time::Instant}; +use instant::Instant; +use std::path::PathBuf; use crate::{ dpi::{LogicalPosition, LogicalSize}, @@ -61,7 +62,7 @@ impl Event { } /// Describes the reason the event loop is resuming. -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum StartCause { /// Sent if the time specified by `ControlFlow::WaitUntil` has been reached. Contains the /// moment the timeout was requested and the requested resume time. The actual resume time is diff --git a/src/event_loop.rs b/src/event_loop.rs index b8fd4ca4..68c6f93d 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -9,13 +9,13 @@ //! [create_proxy]: ./struct.EventLoop.html#method.create_proxy //! [event_loop_proxy]: ./struct.EventLoopProxy.html //! [send_event]: ./struct.EventLoopProxy.html#method.send_event -use std::{error, fmt, ops::Deref, time::Instant}; +use instant::Instant; +use std::ops::Deref; +use std::{error, fmt}; -use crate::{ - event::Event, - monitor::{AvailableMonitorsIter, MonitorHandle}, - platform_impl, -}; +use event::Event; +use monitor::{AvailableMonitorsIter, MonitorHandle}; +use platform_impl; /// Provides a way to retrieve events from the system and from the windows that were registered to /// the events loop. @@ -69,7 +69,7 @@ impl fmt::Debug for EventLoopWindowTarget { /// the control flow to `Poll`. /// /// [events_cleared]: ../event/enum.Event.html#variant.EventsCleared -#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] +#[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ControlFlow { /// When the current loop iteration finishes, immediately begin a new iteration regardless of /// whether or not new events are available to process. diff --git a/src/lib.rs b/src/lib.rs index 1290bcd1..87d7d2a5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -50,7 +50,7 @@ //! match event { //! Event::EventsCleared => { //! // Application update code. -//! +//! //! // Queue a RedrawRequested event. //! window.request_redraw(); //! }, @@ -129,6 +129,9 @@ extern crate bitflags; #[cfg(any(target_os = "macos", target_os = "ios"))] #[macro_use] extern crate objc; +#[cfg(feature = "std_web")] +#[macro_use] +extern crate std_web as stdweb; pub mod dpi; #[macro_use] diff --git a/src/platform/mod.rs b/src/platform/mod.rs index ba494ac6..01125fbb 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -21,3 +21,4 @@ pub mod unix; pub mod windows; pub mod desktop; +pub mod web; diff --git a/src/platform/web.rs b/src/platform/web.rs new file mode 100644 index 00000000..edb37f8b --- /dev/null +++ b/src/platform/web.rs @@ -0,0 +1,15 @@ +#[cfg(feature = "stdweb")] +use stdweb::web::html_element::CanvasElement; + +#[cfg(feature = "stdweb")] +pub trait WindowExtStdweb { + fn canvas(&self) -> CanvasElement; +} + +#[cfg(feature = "web-sys")] +use web_sys::HtmlCanvasElement; + +#[cfg(feature = "web-sys")] +pub trait WindowExtWebSys { + fn canvas(&self) -> HtmlCanvasElement; +} diff --git a/src/platform_impl/mod.rs b/src/platform_impl/mod.rs index 3b815208..1c474f94 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -24,6 +24,9 @@ mod platform; #[cfg(target_os = "emscripten")] #[path = "emscripten/mod.rs"] mod platform; +#[cfg(target_arch = "wasm32")] +#[path = "web/mod.rs"] +mod platform; #[cfg(all( not(target_os = "ios"), @@ -35,6 +38,7 @@ mod platform; not(target_os = "freebsd"), not(target_os = "netbsd"), not(target_os = "openbsd"), - not(target_os = "emscripten") + not(target_os = "emscripten"), + not(target_arch = "wasm32"), ))] compile_error!("The platform you're compiling for is not supported by winit"); diff --git a/src/platform_impl/web/device.rs b/src/platform_impl/web/device.rs new file mode 100644 index 00000000..a2f00b69 --- /dev/null +++ b/src/platform_impl/web/device.rs @@ -0,0 +1,8 @@ +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(pub i32); + +impl Id { + pub unsafe fn dummy() -> Self { + Id(0) + } +} diff --git a/src/platform_impl/web/error.rs b/src/platform_impl/web/error.rs new file mode 100644 index 00000000..8f85d6cb --- /dev/null +++ b/src/platform_impl/web/error.rs @@ -0,0 +1,10 @@ +use std::fmt; + +#[derive(Debug)] +pub struct OsError(pub 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/event_loop/mod.rs b/src/platform_impl/web/event_loop/mod.rs new file mode 100644 index 00000000..7124cee7 --- /dev/null +++ b/src/platform_impl/web/event_loop/mod.rs @@ -0,0 +1,67 @@ +mod proxy; +mod runner; +mod state; +mod window_target; + +pub use self::proxy::Proxy; +pub use self::window_target::WindowTarget; + +use super::{backend, device, monitor, window}; +use crate::event::Event; +use crate::event_loop as root; + +use std::collections::{vec_deque::IntoIter as VecDequeIter, VecDeque}; +use std::marker::PhantomData; + +pub struct EventLoop { + elw: root::EventLoopWindowTarget, +} + +impl EventLoop { + pub fn new() -> Self { + EventLoop { + elw: root::EventLoopWindowTarget { + p: WindowTarget::new(), + _marker: PhantomData, + }, + } + } + + pub fn available_monitors(&self) -> VecDequeIter { + VecDeque::new().into_iter() + } + + pub fn primary_monitor(&self) -> monitor::Handle { + monitor::Handle + } + + pub fn run(self, mut event_handler: F) -> ! + where + F: 'static + FnMut(Event, &root::EventLoopWindowTarget, &mut root::ControlFlow), + { + let target = root::EventLoopWindowTarget { + p: self.elw.p.clone(), + _marker: PhantomData, + }; + + self.elw.p.run(Box::new(move |event, flow| { + event_handler(event, &target, flow) + })); + + // 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 '!' + backend::throw( + "Using exceptions for control flow, don't mind me. This isn't actually an error!", + ); + + unreachable!(); + } + + pub fn create_proxy(&self) -> Proxy { + self.elw.p.proxy() + } + + pub fn window_target(&self) -> &root::EventLoopWindowTarget { + &self.elw + } +} diff --git a/src/platform_impl/web/event_loop/proxy.rs b/src/platform_impl/web/event_loop/proxy.rs new file mode 100644 index 00000000..cbc4732e --- /dev/null +++ b/src/platform_impl/web/event_loop/proxy.rs @@ -0,0 +1,19 @@ +use super::runner; +use crate::event::Event; +use crate::event_loop::EventLoopClosed; + +#[derive(Clone)] +pub struct Proxy { + runner: runner::Shared, +} + +impl Proxy { + pub fn new(runner: runner::Shared) -> Self { + Proxy { runner } + } + + pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> { + self.runner.send_event(Event::UserEvent(event)); + Ok(()) + } +} diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs new file mode 100644 index 00000000..c5bb274b --- /dev/null +++ b/src/platform_impl/web/event_loop/runner.rs @@ -0,0 +1,211 @@ +use super::{backend, state::State}; +use crate::event::{Event, StartCause}; +use crate::event_loop as root; + +use instant::{Duration, Instant}; +use std::{cell::RefCell, clone::Clone, collections::VecDeque, rc::Rc}; + +pub struct Shared(Rc>); + +impl Clone for Shared { + fn clone(&self) -> Self { + Shared(self.0.clone()) + } +} + +pub struct Execution { + runner: RefCell>>, + events: RefCell>>, +} + +struct Runner { + state: State, + is_busy: bool, + event_handler: Box, &mut root::ControlFlow)>, +} + +impl Runner { + pub fn new(event_handler: Box, &mut root::ControlFlow)>) -> Self { + Runner { + state: State::Init, + is_busy: false, + event_handler, + } + } +} + +impl Shared { + pub fn new() -> Self { + Shared(Rc::new(Execution { + runner: RefCell::new(None), + events: RefCell::new(VecDeque::new()), + })) + } + + // 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 + pub fn set_listener(&self, event_handler: Box, &mut root::ControlFlow)>) { + self.0.runner.replace(Some(Runner::new(event_handler))); + self.send_event(Event::NewEvents(StartCause::Init)); + } + + // 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.is_closed() { + return; + } + + // Determine if event handling is in process, and then release the borrow on the runner + let (start_cause, event_is_start) = match *self.0.runner.borrow() { + Some(ref runner) if !runner.is_busy => { + if let Event::NewEvents(cause) = event { + (cause, true) + } else { + ( + match runner.state { + State::Init => StartCause::Init, + State::Poll { .. } => StartCause::Poll, + State::Wait { start } => StartCause::WaitCancelled { + start, + requested_resume: None, + }, + State::WaitUntil { start, end, .. } => StartCause::WaitCancelled { + start, + requested_resume: Some(end), + }, + State::Exit => { + return; + } + }, + false, + ) + } + } + _ => { + // Events are currently being handled, so queue this one and don't try to + // double-process the event queue + self.0.events.borrow_mut().push_back(event); + return; + } + }; + let mut control = self.current_control_flow(); + // 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), &mut control); + if !event_is_start { + self.handle_event(event, &mut control); + } + self.handle_event(Event::EventsCleared, &mut control); + self.apply_control_flow(control); + // If the event loop is closed, it has been closed this iteration and now the closing + // event should be emitted + if self.is_closed() { + self.handle_event(Event::LoopDestroyed, &mut control); + } + } + + // 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, control: &mut root::ControlFlow) { + let is_closed = self.is_closed(); + + match *self.0.runner.borrow_mut() { + Some(ref mut runner) => { + // An event is being processed, so the runner should be marked busy + runner.is_busy = true; + + (runner.event_handler)(event, control); + + // Maintain closed state, even if the callback changes it + if is_closed { + *control = root::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.0.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 !is_closed && self.0.runner.borrow().is_some() { + // Take an event out of the queue and handle it + if let Some(event) = self.0.events.borrow_mut().pop_front() { + self.handle_event(event, control); + } + } + } + + // Apply the new ControlFlow that has been selected by the user + // Start any necessary timeouts etc + fn apply_control_flow(&self, control_flow: root::ControlFlow) { + let new_state = match control_flow { + root::ControlFlow::Poll => { + let cloned = self.clone(); + State::Poll { + timeout: backend::Timeout::new( + move || cloned.send_event(Event::NewEvents(StartCause::Poll)), + Duration::from_millis(0), + ), + } + } + root::ControlFlow::Wait => State::Wait { + start: Instant::now(), + }, + root::ControlFlow::WaitUntil(end) => { + let start = Instant::now(); + + let delay = if end <= start { + Duration::from_millis(0) + } else { + end - start + }; + + let cloned = self.clone(); + + State::WaitUntil { + start, + end, + timeout: backend::Timeout::new( + move || cloned.send_event(Event::NewEvents(StartCause::Poll)), + delay, + ), + } + } + root::ControlFlow::Exit => State::Exit, + }; + + match *self.0.runner.borrow_mut() { + Some(ref mut runner) => { + runner.state = new_state; + } + None => (), + } + } + + // Check if the event loop is currently closed + fn is_closed(&self) -> bool { + match *self.0.runner.borrow() { + Some(ref runner) => runner.state.is_exit(), + None => false, // If the event loop is None, it has not been intialised yet, so it cannot be closed + } + } + + // Get the current control flow state + fn current_control_flow(&self) -> root::ControlFlow { + match *self.0.runner.borrow() { + Some(ref runner) => runner.state.control_flow(), + None => root::ControlFlow::Poll, + } + } +} diff --git a/src/platform_impl/web/event_loop/state.rs b/src/platform_impl/web/event_loop/state.rs new file mode 100644 index 00000000..23e8045f --- /dev/null +++ b/src/platform_impl/web/event_loop/state.rs @@ -0,0 +1,40 @@ +use super::backend; +use crate::event_loop::ControlFlow; + +use instant::Instant; + +#[derive(Debug)] +pub enum State { + Init, + WaitUntil { + timeout: backend::Timeout, + start: Instant, + end: Instant, + }, + Wait { + start: Instant, + }, + Poll { + timeout: backend::Timeout, + }, + Exit, +} + +impl State { + pub fn is_exit(&self) -> bool { + match self { + State::Exit => true, + _ => false, + } + } + + pub fn control_flow(&self) -> ControlFlow { + match self { + State::Init => ControlFlow::Poll, + State::WaitUntil { end, .. } => ControlFlow::WaitUntil(*end), + State::Wait { .. } => ControlFlow::Wait, + State::Poll { .. } => ControlFlow::Poll, + State::Exit => ControlFlow::Exit, + } + } +} diff --git a/src/platform_impl/web/event_loop/window_target.rs b/src/platform_impl/web/event_loop/window_target.rs new file mode 100644 index 00000000..17ba26ae --- /dev/null +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -0,0 +1,162 @@ +use super::{backend, device, proxy::Proxy, runner, window}; +use crate::event::{DeviceId, ElementState, Event, KeyboardInput, TouchPhase, WindowEvent}; +use crate::event_loop::ControlFlow; +use crate::window::WindowId; +use std::clone::Clone; + +pub struct WindowTarget { + pub(crate) runner: runner::Shared, +} + +impl Clone for WindowTarget { + fn clone(&self) -> Self { + WindowTarget { + runner: self.runner.clone(), + } + } +} + +impl WindowTarget { + pub fn new() -> Self { + WindowTarget { + runner: runner::Shared::new(), + } + } + + pub fn proxy(&self) -> Proxy { + Proxy::new(self.runner.clone()) + } + + pub fn run(&self, event_handler: Box, &mut ControlFlow)>) { + self.runner.set_listener(event_handler); + } + + pub fn register(&self, canvas: &mut backend::Canvas) { + let runner = self.runner.clone(); + canvas.on_blur(move || { + runner.send_event(Event::WindowEvent { + window_id: WindowId(window::Id), + event: WindowEvent::Focused(false), + }); + }); + + let runner = self.runner.clone(); + canvas.on_focus(move || { + runner.send_event(Event::WindowEvent { + window_id: WindowId(window::Id), + event: WindowEvent::Focused(true), + }); + }); + + let runner = self.runner.clone(); + canvas.on_keyboard_press(move |scancode, virtual_keycode, modifiers| { + runner.send_event(Event::WindowEvent { + window_id: WindowId(window::Id), + event: WindowEvent::KeyboardInput { + device_id: DeviceId(unsafe { device::Id::dummy() }), + input: KeyboardInput { + scancode, + state: ElementState::Pressed, + virtual_keycode, + modifiers, + }, + }, + }); + }); + + let runner = self.runner.clone(); + canvas.on_keyboard_release(move |scancode, virtual_keycode, modifiers| { + runner.send_event(Event::WindowEvent { + window_id: WindowId(window::Id), + event: WindowEvent::KeyboardInput { + device_id: DeviceId(unsafe { device::Id::dummy() }), + input: KeyboardInput { + scancode, + state: ElementState::Released, + virtual_keycode, + modifiers, + }, + }, + }); + }); + + let runner = self.runner.clone(); + canvas.on_received_character(move |char_code| { + runner.send_event(Event::WindowEvent { + window_id: WindowId(window::Id), + event: WindowEvent::ReceivedCharacter(char_code), + }); + }); + + let runner = self.runner.clone(); + canvas.on_cursor_leave(move |pointer_id| { + runner.send_event(Event::WindowEvent { + window_id: WindowId(window::Id), + event: WindowEvent::CursorLeft { + device_id: DeviceId(device::Id(pointer_id)), + }, + }); + }); + + let runner = self.runner.clone(); + canvas.on_cursor_enter(move |pointer_id| { + runner.send_event(Event::WindowEvent { + window_id: WindowId(window::Id), + event: WindowEvent::CursorEntered { + device_id: DeviceId(device::Id(pointer_id)), + }, + }); + }); + + let runner = self.runner.clone(); + canvas.on_cursor_move(move |pointer_id, position, modifiers| { + runner.send_event(Event::WindowEvent { + window_id: WindowId(window::Id), + event: WindowEvent::CursorMoved { + device_id: DeviceId(device::Id(pointer_id)), + position, + modifiers, + }, + }); + }); + + let runner = self.runner.clone(); + canvas.on_mouse_press(move |pointer_id, button, modifiers| { + runner.send_event(Event::WindowEvent { + window_id: WindowId(window::Id), + event: WindowEvent::MouseInput { + device_id: DeviceId(device::Id(pointer_id)), + state: ElementState::Pressed, + button, + modifiers, + }, + }); + }); + + let runner = self.runner.clone(); + canvas.on_mouse_release(move |pointer_id, button, modifiers| { + runner.send_event(Event::WindowEvent { + window_id: WindowId(window::Id), + event: WindowEvent::MouseInput { + device_id: DeviceId(device::Id(pointer_id)), + state: ElementState::Released, + button, + modifiers, + }, + }); + }); + + let runner = self.runner.clone(); + canvas.on_mouse_wheel(move |pointer_id, delta, modifiers| { + runner.send_event(Event::WindowEvent { + window_id: WindowId(window::Id), + event: WindowEvent::MouseWheel { + device_id: DeviceId(device::Id(pointer_id)), + delta, + phase: TouchPhase::Moved, + modifiers, + }, + }); + }); + } +} diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs new file mode 100644 index 00000000..ca321b09 --- /dev/null +++ b/src/platform_impl/web/mod.rs @@ -0,0 +1,31 @@ +// TODO: dpi +// TODO: close events (stdweb PR required) +// TODO: pointer locking (stdweb PR required) +// TODO: mouse wheel events (stdweb PR required) +// TODO: key event: .which() (stdweb PR) +// TODO: should there be a maximization / fullscreen API? + +mod device; +mod error; +mod event_loop; +mod monitor; +mod window; + +#[cfg(feature = "web-sys")] +#[path = "web_sys/mod.rs"] +mod backend; + +#[cfg(feature = "stdweb")] +#[path = "stdweb/mod.rs"] +mod backend; + +pub use self::device::Id as DeviceId; +pub use self::error::OsError; +pub use self::event_loop::{ + EventLoop, Proxy as EventLoopProxy, WindowTarget as EventLoopWindowTarget, +}; +pub use self::monitor::Handle as MonitorHandle; +pub use self::window::{ + Id as WindowId, PlatformSpecificBuilderAttributes as PlatformSpecificWindowBuilderAttributes, + Window, +}; diff --git a/src/platform_impl/web/monitor.rs b/src/platform_impl/web/monitor.rs new file mode 100644 index 00000000..8ac60fb2 --- /dev/null +++ b/src/platform_impl/web/monitor.rs @@ -0,0 +1,22 @@ +use crate::dpi::{PhysicalPosition, PhysicalSize}; + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Handle; + +impl Handle { + 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!(); + } +} diff --git a/src/platform_impl/web/stdweb/canvas.rs b/src/platform_impl/web/stdweb/canvas.rs new file mode 100644 index 00000000..14194858 --- /dev/null +++ b/src/platform_impl/web/stdweb/canvas.rs @@ -0,0 +1,251 @@ +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 std::rc::Rc; +use stdweb::traits::IPointerEvent; +use stdweb::unstable::TryInto; +use stdweb::web::event::{ + BlurEvent, ConcreteEvent, FocusEvent, KeyDownEvent, KeyPressEvent, KeyUpEvent, MouseWheelEvent, + PointerDownEvent, PointerMoveEvent, PointerOutEvent, PointerOverEvent, PointerUpEvent, +}; +use stdweb::web::html_element::CanvasElement; +use stdweb::web::{ + document, window, EventListenerHandle, IChildNode, IElement, IEventTarget, IHtmlElement, INode, +}; + +pub struct Canvas { + raw: CanvasElement, + on_redraw: Rc, + 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(on_redraw: F) -> Result + where + F: 'static + Fn(), + { + let canvas: CanvasElement = document() + .create_element("canvas") + .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))? + .try_into() + .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))?; + + document() + .body() + .ok_or_else(|| os_error!(OsError("Failed to find body node".to_owned())))? + .append_child(&canvas); + + // TODO: Set up unique ids + canvas + .set_attribute("tabindex", "0") + .expect("Failed to set a tabindex"); + + Ok(Canvas { + raw: canvas, + on_redraw: Rc::new(on_redraw), + 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.get_x(), bounds.get_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) -> &CanvasElement { + &self.raw + } + + pub fn request_redraw(&self) { + let on_redraw = self.on_redraw.clone(); + window().request_animation_frame(move |_| on_redraw()); + } + + pub fn on_blur(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_blur = Some(self.add_event(move |_: BlurEvent| { + handler(); + })); + } + + pub fn on_focus(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_focus = Some(self.add_event(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(move |event: KeyUpEvent| { + 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(move |event: KeyDownEvent| { + 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(move |event: KeyPressEvent| { + 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(move |event: PointerOutEvent| { + 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(move |event: PointerOverEvent| { + 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(move |event: PointerUpEvent| { + 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(move |event: PointerDownEvent| { + 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(move |event: PointerMoveEvent| { + 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(move |event: MouseWheelEvent| { + if let Some(delta) = event::mouse_scroll_delta(&event) { + handler(0, delta, event::mouse_modifiers(&event)); + } + })); + } + + fn add_event(&self, mut handler: F) -> EventListenerHandle + where + E: ConcreteEvent, + F: 'static + FnMut(E), + { + self.raw.add_event_listener(move |event: E| { + event.stop_propagation(); + event.cancel_bubble(); + + handler(event); + }) + } +} diff --git a/src/platform_impl/web/stdweb/event.rs b/src/platform_impl/web/stdweb/event.rs new file mode 100644 index 00000000..81397056 --- /dev/null +++ b/src/platform_impl/web/stdweb/event.rs @@ -0,0 +1,229 @@ +use crate::dpi::LogicalPosition; +use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; + +use stdweb::web::event::{IKeyboardEvent, IMouseEvent, MouseWheelDeltaMode, MouseWheelEvent}; +use stdweb::{unstable::TryInto, JsSerialize}; + +pub fn mouse_button(event: &impl IMouseEvent) -> MouseButton { + match event.button() { + stdweb::web::event::MouseButton::Left => MouseButton::Left, + stdweb::web::event::MouseButton::Right => MouseButton::Right, + stdweb::web::event::MouseButton::Wheel => MouseButton::Middle, + stdweb::web::event::MouseButton::Button4 => MouseButton::Other(0), + stdweb::web::event::MouseButton::Button5 => MouseButton::Other(1), + } +} + +pub fn mouse_modifiers(event: &impl IMouseEvent) -> ModifiersState { + ModifiersState { + shift: event.shift_key(), + ctrl: event.ctrl_key(), + alt: event.alt_key(), + logo: event.meta_key(), + } +} + +pub fn mouse_position(event: &impl IMouseEvent) -> LogicalPosition { + LogicalPosition { + x: event.offset_x() as f64, + y: event.offset_y() as f64, + } +} + +pub fn mouse_scroll_delta(event: &MouseWheelEvent) -> Option { + let x = event.delta_x(); + let y = event.delta_y(); + + match event.delta_mode() { + MouseWheelDeltaMode::Line => Some(MouseScrollDelta::LineDelta(x as f32, y as f32)), + MouseWheelDeltaMode::Pixel => Some(MouseScrollDelta::PixelDelta(LogicalPosition { x, y })), + MouseWheelDeltaMode::Page => None, + } +} + +pub fn scan_code(event: &T) -> ScanCode { + let key_code = js! ( return @{event}.key_code; ); + + key_code + .try_into() + .expect("The which value should be a number") +} + +pub fn virtual_key_code(event: &impl IKeyboardEvent) -> 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 keyboard_modifiers(event: &impl IKeyboardEvent) -> ModifiersState { + ModifiersState { + shift: event.shift_key(), + ctrl: event.ctrl_key(), + alt: event.alt_key(), + logo: event.meta_key(), + } +} + +pub fn codepoint(event: &impl IKeyboardEvent) -> char { + // `event.key()` always returns a non-empty `String`. Therefore, this should + // never panic. + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key + event.key().chars().next().unwrap() +} diff --git a/src/platform_impl/web/stdweb/mod.rs b/src/platform_impl/web/stdweb/mod.rs new file mode 100644 index 00000000..27998655 --- /dev/null +++ b/src/platform_impl/web/stdweb/mod.rs @@ -0,0 +1,21 @@ +mod canvas; +mod event; +mod timeout; + +pub use self::canvas::Canvas; +pub use self::timeout::Timeout; + +use crate::platform::web::WindowExtStdweb; +use crate::window::Window; + +use stdweb::web::html_element::CanvasElement; + +pub fn throw(msg: &str) { + js! { throw @{msg} } +} + +impl WindowExtStdweb for Window { + fn canvas(&self) -> CanvasElement { + self.window.canvas().raw().clone() + } +} diff --git a/src/platform_impl/web/stdweb/timeout.rs b/src/platform_impl/web/stdweb/timeout.rs new file mode 100644 index 00000000..fceb1113 --- /dev/null +++ b/src/platform_impl/web/stdweb/timeout.rs @@ -0,0 +1,25 @@ +use std::time::Duration; +use stdweb::web::{window, IWindowOrWorker, TimeoutHandle}; + +#[derive(Debug)] +pub struct Timeout { + handle: TimeoutHandle, +} + +impl Timeout { + pub fn new(f: F, duration: Duration) -> Timeout + where + F: 'static + FnMut(), + { + Timeout { + handle: window().set_clearable_timeout(f, duration.as_millis() as u32), + } + } +} + +impl Drop for Timeout { + fn drop(&mut self) { + let handle = std::mem::replace(&mut self.handle, unsafe { std::mem::uninitialized() }); + handle.clear(); + } +} diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs new file mode 100644 index 00000000..4e435aaf --- /dev/null +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -0,0 +1,257 @@ +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_redraw: Closure, + 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(on_redraw: F) -> Result + where + F: 'static + Fn(), + { + let window = web_sys::window().expect("Failed to obtain window"); + let document = window.document().expect("Failed to obtain document"); + + let canvas: HtmlCanvasElement = document + .create_element("canvas") + .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))? + .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())))?; + + // TODO: Set up unique ids + canvas + .set_attribute("tabindex", "0") + .expect("Failed to set a tabindex"); + + Ok(Canvas { + raw: canvas, + on_redraw: Closure::wrap(Box::new(on_redraw) as Box), + 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 request_redraw(&self) { + let window = web_sys::window().expect("Failed to obtain window"); + window + .request_animation_frame(&self.on_redraw.as_ref().unchecked_ref()) + .expect("Failed to request animation frame"); + } + + 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 + } +} diff --git a/src/platform_impl/web/web_sys/event.rs b/src/platform_impl/web/web_sys/event.rs new file mode 100644 index 00000000..af557b99 --- /dev/null +++ b/src/platform_impl/web/web_sys/event.rs @@ -0,0 +1,227 @@ +use crate::dpi::LogicalPosition; +use crate::event::{ModifiersState, MouseButton, MouseScrollDelta, ScanCode, VirtualKeyCode}; + +use std::convert::TryInto; +use web_sys::{KeyboardEvent, MouseEvent, WheelEvent}; + +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 mouse_modifiers(event: &MouseEvent) -> ModifiersState { + ModifiersState { + shift: event.shift_key(), + ctrl: event.ctrl_key(), + alt: event.alt_key(), + logo: event.meta_key(), + } +} + +pub fn mouse_position(event: &MouseEvent) -> LogicalPosition { + LogicalPosition { + x: event.offset_x() as f64, + y: event.offset_y() as f64, + } +} + +pub fn mouse_scroll_delta(event: &WheelEvent) -> Option { + let x = event.delta_x(); + let y = event.delta_y(); + + match event.delta_mode() { + WheelEvent::DOM_DELTA_LINE => Some(MouseScrollDelta::LineDelta(x as f32, y as f32)), + WheelEvent::DOM_DELTA_PIXEL => Some(MouseScrollDelta::PixelDelta(LogicalPosition { x, y })), + _ => None, + } +} + +pub fn scan_code(event: &KeyboardEvent) -> ScanCode { + match event.key_code() { + 0 => event.char_code(), + i => i, + } +} + +pub fn virtual_key_code(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 keyboard_modifiers(event: &KeyboardEvent) -> ModifiersState { + ModifiersState { + shift: event.shift_key(), + ctrl: event.ctrl_key(), + alt: event.alt_key(), + logo: event.meta_key(), + } +} + +pub fn codepoint(event: &KeyboardEvent) -> char { + // `event.key()` always returns a non-empty `String`. Therefore, this should + // never panic. + // https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key + event.key().chars().next().unwrap() +} diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs new file mode 100644 index 00000000..33e217bd --- /dev/null +++ b/src/platform_impl/web/web_sys/mod.rs @@ -0,0 +1,20 @@ +mod canvas; +mod event; +mod timeout; + +pub use self::canvas::Canvas; +pub use self::timeout::Timeout; + +use crate::platform::web::WindowExtWebSys; +use crate::window::Window; +use web_sys::HtmlCanvasElement; + +pub fn throw(msg: &str) { + wasm_bindgen::throw_str(msg); +} + +impl WindowExtWebSys for Window { + fn canvas(&self) -> HtmlCanvasElement { + self.window.canvas().raw().clone() + } +} diff --git a/src/platform_impl/web/web_sys/timeout.rs b/src/platform_impl/web/web_sys/timeout.rs new file mode 100644 index 00000000..e7ce69a0 --- /dev/null +++ b/src/platform_impl/web/web_sys/timeout.rs @@ -0,0 +1,40 @@ +use std::time::Duration; +use wasm_bindgen::closure::Closure; +use wasm_bindgen::JsCast; + +#[derive(Debug)] +pub struct Timeout { + handle: i32, + _closure: Closure, +} + +impl Timeout { + pub fn new(f: F, duration: Duration) -> Timeout + where + F: 'static + FnMut(), + { + let window = web_sys::window().expect("Failed to obtain window"); + + let closure = Closure::wrap(Box::new(f) as Box); + + let handle = window + .set_timeout_with_callback_and_timeout_and_arguments_0( + &closure.as_ref().unchecked_ref(), + duration.as_millis() as i32, + ) + .expect("Failed to set timeout"); + + Timeout { + handle, + _closure: closure, + } + } +} + +impl Drop for Timeout { + fn drop(&mut self) { + let window = web_sys::window().expect("Failed to obtain window"); + + window.clear_timeout_with_handle(self.handle); + } +} diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs new file mode 100644 index 00000000..7a8f740a --- /dev/null +++ b/src/platform_impl/web/window.rs @@ -0,0 +1,268 @@ +use crate::dpi::{LogicalPosition, LogicalSize}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; +use crate::event::{Event, WindowEvent}; +use crate::icon::Icon; +use crate::monitor::MonitorHandle as RootMH; +use crate::window::{CursorIcon, WindowAttributes, WindowId as RootWI}; + +use super::{backend, monitor, EventLoopWindowTarget}; + +use std::cell::RefCell; +use std::collections::vec_deque::IntoIter as VecDequeIter; +use std::collections::VecDeque; + +pub struct Window { + canvas: backend::Canvas, + previous_pointer: RefCell<&'static str>, + position: RefCell, +} + +impl Window { + pub fn new( + target: &EventLoopWindowTarget, + attr: WindowAttributes, + _: PlatformSpecificBuilderAttributes, + ) -> Result { + let runner = target.runner.clone(); + + let mut canvas = backend::Canvas::create(move || { + runner.send_event(Event::WindowEvent { + window_id: RootWI(Id), + event: WindowEvent::RedrawRequested, + }) + })?; + + target.register(&mut canvas); + + let window = Window { + canvas, + previous_pointer: RefCell::new("auto"), + position: RefCell::new(LogicalPosition { x: 0.0, y: 0.0 }), + }; + + window.set_inner_size(attr.inner_size.unwrap_or(LogicalSize { + width: 1024.0, + height: 768.0, + })); + window.set_title(&attr.title); + window.set_maximized(attr.maximized); + window.set_visible(attr.visible); + window.set_window_icon(attr.window_icon); + + Ok(window) + } + + pub fn canvas(&self) -> &backend::Canvas { + &self.canvas + } + + pub fn set_title(&self, title: &str) { + self.canvas.set_attribute("alt", title); + } + + pub fn set_visible(&self, _visible: bool) { + // Intentionally a no-op + } + + pub fn request_redraw(&self) { + self.canvas.request_redraw(); + } + + pub fn outer_position(&self) -> Result { + let (x, y) = self.canvas.position(); + + Ok(LogicalPosition { x, 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"); + self.canvas.set_attribute("left", &position.x.to_string()); + self.canvas.set_attribute("top", &position.y.to_string()); + } + + #[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_size(size); + } + + #[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("style", &format!("cursor: {}", text)); + } + + #[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"); + } else { + self.canvas + .set_attribute("cursor", *self.previous_pointer.borrow()); + } + } + + #[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: monitor::Handle, + } + } + + #[inline] + pub fn available_monitors(&self) -> VecDequeIter { + VecDeque::new().into_iter() + } + + #[inline] + pub fn primary_monitor(&self) -> monitor::Handle { + monitor::Handle + } + + #[inline] + pub fn id(&self) -> Id { + // TODO ? + unsafe { Id::dummy() } + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id; + +impl Id { + pub unsafe fn dummy() -> Id { + Id + } +} + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PlatformSpecificBuilderAttributes; 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