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/.travis.yml b/.travis.yml index aaa56d9e..7a73219b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -58,6 +58,21 @@ matrix: os: osx rust: stable + # wasm stdweb + - env: TARGET=wasm32-unknown-unknown WEB=web FEATURES=stdweb + os: linux + rust: stable + - env: TARGET=wasm32-unknown-unknown WEB=web FEATURES=stdweb + os: linux + rust: nightly + # wasm web-sys + - env: TARGET=wasm32-unknown-unknown FEATURES=web-sys + os: linux + rust: stable + - env: TARGET=wasm32-unknown-unknown FEATURES=web-sys + os: linux + rust: nightly + install: - rustup self update - rustup target add $TARGET; true @@ -66,11 +81,19 @@ install: script: - cargo +stable fmt --all -- --check - - cargo build --target $TARGET --verbose - - cargo build --target $TARGET --features serde --verbose + # Install cargo-web to build stdweb + - if [[ $WEB = "web" ]]; then cargo install -f cargo-web; fi + # Build without serde then with serde + - if [[ -z "$FEATURES" ]]; then + cargo $WEB build --target $TARGET --verbose; + else + cargo $WEB build --target $TARGET --features $FEATURES --verbose; + fi + - cargo $WEB build --target $TARGET --features serde,$FEATURES --verbose # Running iOS apps on macOS requires the Simulator so we skip that for now - - if [[ $TARGET != *-apple-ios ]]; then cargo test --target $TARGET --verbose; fi - - if [[ $TARGET != *-apple-ios ]]; then cargo test --target $TARGET --features serde --verbose; fi + # The web targets also don't support running tests + - if [[ $TARGET != *-apple-ios && $TARGET != wasm32-* ]]; then cargo test --target $TARGET --verbose; fi + - if [[ $TARGET != *-apple-ios && $TARGET != wasm32-* ]]; then cargo test --target $TARGET --features serde --verbose; fi after_success: - | diff --git a/CHANGELOG.md b/CHANGELOG.md index ae7eb7ec..cd95c68a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +- Add web support via the 'stdweb' or 'web-sys' features - On Windows, implemented function to get HINSTANCE - On macOS, implement `run_return`. - On iOS, fix inverted parameter in `set_prefers_home_indicator_hidden`. diff --git a/Cargo.toml b/Cargo.toml index 8dc3e2e5..b2a21fa3 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,36 @@ percent-encoding = "2.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.9" + +[target.'cfg(target_arch = "wasm32")'.dependencies.web_sys] +package = "web-sys" +version = "0.3.22" +optional = true +features = [ + 'console', + 'BeforeUnloadEvent', + '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.20" +optional = true +features = ["experimental_features_which_may_break_on_minor_version_bumps"] diff --git a/FEATURES.md b/FEATURES.md index d634eaf5..b83caac0 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -163,39 +163,39 @@ Legend: ### Windowing |Feature |Windows|MacOS |Linux x11 |Linux Wayland |Android|iOS |WASM | |-------------------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | -|Window initialization |✔️ |✔️ |▢[#5] |✔️ |▢[#33]|▢[#33] |❓ | -|Providing pointer to init OpenGL |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |❓ | -|Providing pointer to init Vulkan |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |❓ | -|Window decorations |✔️ |✔️ |✔️ |▢[#306] |**N/A**|**N/A**|❓ | -|Window decorations toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❓ | -|Window resizing |✔️ |▢[#219]|✔️ |▢[#306] |**N/A**|**N/A**|❓ | -|Window resize increments |❌ |❌ |❌ |❌ |❌ |❌ |❓ | -|Window transparency |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❓ | -|Window maximization |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❓ | -|Window maximization toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❓ | -|Fullscreen |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |❓ | -|Fullscreen toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |❓ | -|Exclusive fullscreen |✔️ |✔️ |✔️ |**N/A** |❌ |✔️ |❓ | -|HiDPI support |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |❓ | -|Popup windows |❌ |❌ |❌ |❌ |❌ |❌ |❓ | +|Window initialization |✔️ |✔️ |▢[#5] |✔️ |▢[#33]|▢[#33] |✔️ | +|Providing pointer to init OpenGL |✔️ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A**| +|Providing pointer to init Vulkan |✔️ |✔️ |✔️ |✔️ |✔️ |❓ |**N/A**| +|Window decorations |✔️ |✔️ |✔️ |▢[#306] |**N/A**|**N/A**|**N/A**| +|Window decorations toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**| +|Window resizing |✔️ |▢[#219]|✔️ |▢[#306] |**N/A**|**N/A**|✔️ | +|Window resize increments |❌ |❌ |❌ |❌ |❌ |❌ |**N/A**| +|Window transparency |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|N/A | +|Window maximization |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**| +|Window maximization toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|**N/A**| +|Fullscreen |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |✔️ | +|Fullscreen toggle |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |✔️ | +|Exclusive fullscreen |✔️ |✔️ |✔️ |**N/A** |❌ |✔️ |**N/A**| +|HiDPI support |✔️ |✔️ |✔️ |✔️ |▢[#721]|✔️ |**N/A**| +|Popup windows |❌ |❌ |❌ |❌ |❌ |❌ |**N/A**| ### System information |Feature |Windows|MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | |---------------- | ----- | ---- | ------- | ----------- | ----- | ------- | -------- | -|Monitor list |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |❓ | -|Video mode query |✔️ |✔️ |✔️ |✔️ |❌ |✔️ |❓ | +|Monitor list |✔️ |✔️ |✔️ |✔️ |**N/A**|✔️ |**N/A**| +|Video mode query |✔️ |✔️ |✔️ |✔️ |❌ |✔️ |**N/A**| ### Input handling |Feature |Windows |MacOS |Linux x11|Linux Wayland|Android|iOS |WASM | |----------------------- | ----- | ---- | ------- | ----------- | ----- | ----- | -------- | -|Mouse events |✔️ |▢[#63] |✔️ |✔️ |**N/A**|**N/A**|❓ | -|Mouse set location |✔️ |✔️ |✔️ |❓ |**N/A**|**N/A**|❓ | +|Mouse events |✔️ |▢[#63] |✔️ |✔️ |**N/A**|**N/A**|✔️ | +|Mouse set location |✔️ |✔️ |✔️ |❓ |**N/A**|**N/A**|**N/A**| |Cursor grab |✔️ |▢[#165] |▢[#242] |✔️ |**N/A**|**N/A**|❓ | -|Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❓ | -|Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |❓ | -|Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |❓ | -|Multitouch |✔️ |❌ |✔️ |✔️ |❓ |✔️ |❓ | -|Keyboard events |✔️ |✔️ |✔️ |✔️ |❓ |❌ |❓ | +|Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ | +|Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |✔️ | +|Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |✔️ | +|Multitouch |✔️ |❌ |✔️ |✔️ |❓ |✔️ |✔️ | +|Keyboard events |✔️ |✔️ |✔️ |✔️ |❓ |❌ |✔️ | |Drag & Drop |▢[#720] |▢[#720] |▢[#720] |❌[#306] |**N/A**|**N/A**|❓ | |Raw Device Events |▢[#750] |▢[#750] |▢[#750] |❌ |❌ |❌ |❓ | |Gamepad/Joystick events |❌[#804] |❌ |❌ |❌ |❌ |❌ |❓ | diff --git a/examples/custom_events.rs b/examples/custom_events.rs index c85d4ecd..dba43624 100644 --- a/examples/custom_events.rs +++ b/examples/custom_events.rs @@ -1,15 +1,16 @@ -use winit::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::WindowBuilder, -}; - -#[derive(Debug, Clone, Copy)] -enum CustomEvent { - Timer, -} - +#[cfg(not(target_arch = "wasm32"))] fn main() { + use winit::{ + event::{Event, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::WindowBuilder, + }; + + #[derive(Debug, Clone, Copy)] + enum CustomEvent { + Timer, + } + let event_loop = EventLoop::::with_user_event(); let _window = WindowBuilder::new() @@ -39,3 +40,8 @@ fn main() { _ => *control_flow = ControlFlow::Wait, }); } + +#[cfg(target_arch = "wasm32")] +fn main() { + panic!("This example is not supported on web."); +} diff --git a/examples/multithreaded.rs b/examples/multithreaded.rs index 4f5dbc49..8ce36077 100644 --- a/examples/multithreaded.rs +++ b/examples/multithreaded.rs @@ -1,16 +1,18 @@ -extern crate env_logger; -use std::{collections::HashMap, sync::mpsc, thread, time::Duration}; - -use winit::{ - event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::{CursorIcon, Fullscreen, WindowBuilder}, -}; - -const WINDOW_COUNT: usize = 3; -const WINDOW_SIZE: (u32, u32) = (600, 400); - +#[cfg(not(target_arch = "wasm32"))] fn main() { + extern crate env_logger; + + use std::{collections::HashMap, sync::mpsc, thread, time::Duration}; + + use winit::{ + event::{ElementState, Event, KeyboardInput, VirtualKeyCode, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::{CursorIcon, Fullscreen, WindowBuilder}, + }; + + const WINDOW_COUNT: usize = 3; + const WINDOW_SIZE: (u32, u32) = (600, 400); + env_logger::init(); let event_loop = EventLoop::new(); let mut window_senders = HashMap::with_capacity(WINDOW_COUNT); @@ -59,7 +61,7 @@ fn main() { } => { window.set_title(&format!("{:?}", key)); let state = !modifiers.shift; - use self::VirtualKeyCode::*; + use VirtualKeyCode::*; match key { A => window.set_always_on_top(state), C => window.set_cursor_icon(match state { @@ -167,3 +169,8 @@ fn main() { } }) } + +#[cfg(target_arch = "wasm32")] +fn main() { + panic!("Example not supported on Wasm"); +} diff --git a/examples/request_redraw.rs b/examples/request_redraw.rs index ac9377e7..b55bd970 100644 --- a/examples/request_redraw.rs +++ b/examples/request_redraw.rs @@ -1,4 +1,5 @@ -use std::time::{Duration, Instant}; +use instant::Instant; +use std::time::Duration; use winit::{ event::{Event, WindowEvent}, diff --git a/examples/timer.rs b/examples/timer.rs index 8d3b9bde..e1e3f290 100644 --- a/examples/timer.rs +++ b/examples/timer.rs @@ -1,4 +1,5 @@ -use std::time::{Duration, Instant}; +use instant::Instant; +use std::time::Duration; use winit::{ event::{Event, StartCause, WindowEvent}, event_loop::{ControlFlow, EventLoop}, diff --git a/examples/window_run_return.rs b/examples/window_run_return.rs index 1dd7c5ac..29ea7b3c 100644 --- a/examples/window_run_return.rs +++ b/examples/window_run_return.rs @@ -15,7 +15,6 @@ fn main() { platform::desktop::EventLoopExtDesktop, window::WindowBuilder, }; - let mut event_loop = EventLoop::new(); let window = WindowBuilder::new() @@ -50,7 +49,7 @@ fn main() { println!("Okay we're done now for real."); } -#[cfg(any(target_os = "ios", target_os = "android"))] +#[cfg(any(target_os = "ios", target_os = "android", target_arch = "wasm32"))] fn main() { println!("This platform doesn't support run_return."); } diff --git a/src/dpi.rs b/src/dpi.rs index 9c89c12c..99848ae9 100644 --- a/src/dpi.rs +++ b/src/dpi.rs @@ -55,6 +55,7 @@ //! - **Wayland:** On Wayland, DPI factors are set per-screen by the server, and are always integers (most often 1 or 2). //! - **iOS:** DPI factors are both constant and device-specific on iOS. //! - **Android:** This feature isn't yet implemented on Android, so the DPI factor will always be returned as 1.0. +//! - **Web:** DPI factors are handled by the browser and will always be 1.0 for your application. //! //! The window's logical size is conserved across DPI changes, resulting in the physical size changing instead. This //! may be surprising on X11, but is quite standard elsewhere. Physical size changes always produce a diff --git a/src/event.rs b/src/event.rs index fc894d97..d5548908 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 0b120573..5bb9692b 100644 --- a/src/event_loop.rs +++ b/src/event_loop.rs @@ -9,7 +9,9 @@ //! [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::MonitorHandle, platform_impl}; @@ -66,7 +68,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 ba8e910f..8d5447f7 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(); //! }, @@ -126,6 +126,8 @@ extern crate bitflags; #[cfg(any(target_os = "macos", target_os = "ios"))] #[macro_use] extern crate objc; +#[cfg(feature = "std_web")] +extern crate std_web as stdweb; pub mod dpi; #[macro_use] diff --git a/src/monitor.rs b/src/monitor.rs index a8949020..9ef58be5 100644 --- a/src/monitor.rs +++ b/src/monitor.rs @@ -118,12 +118,20 @@ impl MonitorHandle { /// Returns a human-readable name of the monitor. /// /// Returns `None` if the monitor doesn't exist anymore. + /// + /// ## Platform-specific + /// + /// - **Web:** Always returns None #[inline] pub fn name(&self) -> Option { self.inner.name() } /// Returns the monitor's resolution. + /// + /// ## Platform-specific + /// + /// - **Web:** Always returns (0,0) #[inline] pub fn size(&self) -> PhysicalSize { self.inner.size() @@ -131,6 +139,10 @@ impl MonitorHandle { /// Returns the top-left corner position of the monitor relative to the larger full /// screen area. + /// + /// ## Platform-specific + /// + /// - **Web:** Always returns (0,0) #[inline] pub fn position(&self) -> PhysicalPosition { self.inner.position() @@ -144,12 +156,17 @@ impl MonitorHandle { /// /// - **X11:** Can be overridden using the `WINIT_HIDPI_FACTOR` environment variable. /// - **Android:** Always returns 1.0. + /// - **Web:** Always returns 1.0 #[inline] pub fn hidpi_factor(&self) -> f64 { self.inner.hidpi_factor() } /// Returns all fullscreen video modes supported by this monitor. + /// + /// ## Platform-specific + /// + /// - **Web:** Always returns an empty iterator #[inline] pub fn video_modes(&self) -> impl Iterator { self.inner.video_modes() diff --git a/src/platform/mod.rs b/src/platform/mod.rs index ba494ac6..27ecde18 100644 --- a/src/platform/mod.rs +++ b/src/platform/mod.rs @@ -7,6 +7,7 @@ //! - `macos` //! - `unix` //! - `windows` +//! - `web` //! //! And the following platform-specific module: //! @@ -21,3 +22,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..5fca0c42 --- /dev/null +++ b/src/platform/web.rs @@ -0,0 +1,22 @@ +#![cfg(target_arch = "wasm32")] + +//! The web target does not automatically insert the canvas element object into the web page, to +//! allow end users to determine how the page should be laid out. Use the `WindowExtStdweb` or +//! `WindowExtWebSys` traits (depending on your web backend) to retrieve the canvas from the +//! Window. + +#[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 bc8fc2c4..152065d8 100644 --- a/src/platform_impl/mod.rs +++ b/src/platform_impl/mod.rs @@ -21,6 +21,9 @@ mod platform; #[cfg(target_os = "ios")] #[path = "ios/mod.rs"] mod platform; +#[cfg(target_arch = "wasm32")] +#[path = "web/mod.rs"] +mod platform; #[cfg(all( not(target_os = "ios"), @@ -32,5 +35,6 @@ mod platform; not(target_os = "freebsd"), not(target_os = "netbsd"), not(target_os = "openbsd"), + 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..6995f2bc --- /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..e98585a3 --- /dev/null +++ b/src/platform_impl/web/event_loop/proxy.rs @@ -0,0 +1,26 @@ +use super::runner; +use crate::event::Event; +use crate::event_loop::EventLoopClosed; + +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(()) + } +} + +impl Clone for Proxy { + fn clone(&self) -> Self { + Proxy { + runner: self.runner.clone(), + } + } +} 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..b3889fad --- /dev/null +++ b/src/platform_impl/web/event_loop/runner.rs @@ -0,0 +1,254 @@ +use super::{backend, state::State}; +use crate::event::{Event, StartCause, WindowEvent}; +use crate::event_loop as root; +use crate::window::WindowId; + +use instant::{Duration, Instant}; +use std::{ + cell::RefCell, + clone::Clone, + collections::{HashSet, 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>>, + id: RefCell, + redraw_pending: 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()), + id: RefCell::new(0), + redraw_pending: RefCell::new(HashSet::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)); + + let close_instance = self.clone(); + backend::on_unload(move || close_instance.handle_unload()); + } + + // Generate a strictly increasing ID + // This is used to differentiate windows when handling events + pub fn generate_id(&self) -> u32 { + let mut id = self.0.id.borrow_mut(); + *id += 1; + + *id + } + + pub fn request_redraw(&self, id: WindowId) { + self.0.redraw_pending.borrow_mut().insert(id); + } + + // 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); + } + // Collect all of the redraw events to avoid double-locking the RefCell + let redraw_events: Vec = self.0.redraw_pending.borrow_mut().drain().collect(); + for window_id in redraw_events { + self.handle_event( + Event::WindowEvent { + window_id, + event: WindowEvent::RedrawRequested, + }, + &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); + } + } + + fn handle_unload(&self) { + self.apply_control_flow(root::ControlFlow::Exit); + let mut control = self.current_control_flow(); + 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..d302edb8 --- /dev/null +++ b/src/platform_impl/web/event_loop/window_target.rs @@ -0,0 +1,197 @@ +use super::{backend, device, proxy::Proxy, runner, window}; +use crate::dpi::LogicalSize; +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 generate_id(&self) -> window::Id { + window::Id(self.runner.generate_id()) + } + + pub fn register(&self, canvas: &mut backend::Canvas, id: window::Id) { + let runner = self.runner.clone(); + canvas.set_attribute("data-raw-handle", &id.0.to_string()); + + canvas.on_blur(move || { + runner.send_event(Event::WindowEvent { + window_id: WindowId(id), + event: WindowEvent::Focused(false), + }); + }); + + let runner = self.runner.clone(); + canvas.on_focus(move || { + runner.send_event(Event::WindowEvent { + window_id: WindowId(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(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(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(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(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(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(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(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(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(id), + event: WindowEvent::MouseWheel { + device_id: DeviceId(device::Id(pointer_id)), + delta, + phase: TouchPhase::Moved, + modifiers, + }, + }); + }); + + let runner = self.runner.clone(); + let raw = canvas.raw().clone(); + let mut intended_size = LogicalSize { + width: raw.width() as f64, + height: raw.height() as f64, + }; + canvas.on_fullscreen_change(move || { + // If the canvas is marked as fullscreen, it is moving *into* fullscreen + // If it is not, it is moving *out of* fullscreen + let new_size = if backend::is_fullscreen(&raw) { + intended_size = LogicalSize { + width: raw.width() as f64, + height: raw.height() as f64, + }; + + backend::window_size() + } else { + intended_size + }; + raw.set_width(new_size.width as u32); + raw.set_height(new_size.height as u32); + runner.send_event(Event::WindowEvent { + window_id: WindowId(id), + event: WindowEvent::Resized(new_size), + }); + runner.request_redraw(WindowId(id)); + }); + } +} diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs new file mode 100644 index 00000000..44d4fc19 --- /dev/null +++ b/src/platform_impl/web/mod.rs @@ -0,0 +1,28 @@ +// TODO: close events (port from old stdweb branch) +// TODO: pointer locking (stdweb PR required) +// TODO: fullscreen API (stdweb PR required) + +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, Mode as VideoMode}; +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..4889c88d --- /dev/null +++ b/src/platform_impl/web/monitor.rs @@ -0,0 +1,51 @@ +use crate::dpi::{PhysicalPosition, PhysicalSize}; +use crate::monitor::{MonitorHandle, VideoMode}; + +#[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 { + PhysicalPosition { x: 0.0, y: 0.0 } + } + + pub fn name(&self) -> Option { + None + } + + pub fn size(&self) -> PhysicalSize { + PhysicalSize { + width: 0.0, + height: 0.0, + } + } + + pub fn video_modes(&self) -> impl Iterator { + std::iter::empty() + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Hash)] +pub struct Mode; + +impl Mode { + pub fn size(&self) -> PhysicalSize { + unimplemented!(); + } + + pub fn bit_depth(&self) -> u16 { + unimplemented!(); + } + + pub fn refresh_rate(&self) -> u16 { + 32 + } + + pub fn monitor(&self) -> MonitorHandle { + MonitorHandle { inner: Handle } + } +} diff --git a/src/platform_impl/web/stdweb/canvas.rs b/src/platform_impl/web/stdweb/canvas.rs new file mode 100644 index 00000000..a0ff5af2 --- /dev/null +++ b/src/platform_impl/web/stdweb/canvas.rs @@ -0,0 +1,282 @@ +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::cell::RefCell; +use std::rc::Rc; +use stdweb::traits::IPointerEvent; +use stdweb::unstable::TryInto; +use stdweb::web::event::{ + BlurEvent, ConcreteEvent, FocusEvent, FullscreenChangeEvent, KeyDownEvent, KeyPressEvent, + KeyUpEvent, MouseWheelEvent, PointerDownEvent, PointerMoveEvent, PointerOutEvent, + PointerOverEvent, PointerUpEvent, +}; +use stdweb::web::html_element::CanvasElement; +use stdweb::web::{ + document, EventListenerHandle, IChildNode, IElement, IEventTarget, IHtmlElement, +}; + +pub struct Canvas { + raw: CanvasElement, + 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, + on_fullscreen_change: Option, + wants_fullscreen: Rc>, +} + +impl Drop for Canvas { + fn drop(&mut self) { + self.raw.remove(); + } +} + +impl Canvas { + pub fn create() -> Result { + 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())))?; + + // A tabindex is needed in order to capture local keyboard events. + // A "0" value means that the element should be focusable in + // sequential keyboard navigation, but its order is defined by the + // document's source order. + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex + canvas + .set_attribute("tabindex", "0") + .map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?; + + Ok(Canvas { + raw: canvas, + on_blur: None, + on_focus: None, + on_keyboard_release: None, + on_keyboard_press: None, + on_received_character: None, + on_cursor_leave: None, + on_cursor_enter: None, + on_cursor_move: None, + on_mouse_release: None, + on_mouse_press: None, + on_mouse_wheel: None, + on_fullscreen_change: None, + wants_fullscreen: Rc::new(RefCell::new(false)), + }) + } + + 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 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_user_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_user_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_user_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_user_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_user_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)); + } + })); + } + + pub fn on_fullscreen_change(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_fullscreen_change = Some(self.add_event(move |_: FullscreenChangeEvent| handler())); + } + + 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); + }) + } + + // The difference between add_event and add_user_event is that the latter has a special meaning + // for browser security. A user event is a deliberate action by the user (like a mouse or key + // press) and is the only time things like a fullscreen request may be successfully completed.) + fn add_user_event(&self, mut handler: F) -> EventListenerHandle + where + E: ConcreteEvent, + F: 'static + FnMut(E), + { + let wants_fullscreen = self.wants_fullscreen.clone(); + let canvas = self.raw.clone(); + + self.add_event(move |event: E| { + handler(event); + + if *wants_fullscreen.borrow() { + canvas.request_fullscreen(); + *wants_fullscreen.borrow_mut() = false; + } + }) + } + + pub fn request_fullscreen(&self) { + *self.wants_fullscreen.borrow_mut() = true; + } + + pub fn is_fullscreen(&self) -> bool { + super::is_fullscreen(&self.raw) + } +} diff --git a/src/platform_impl/web/stdweb/event.rs b/src/platform_impl/web/stdweb/event.rs new file mode 100644 index 00000000..14456dc0 --- /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::{js, 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}.keyCode; ); + + 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..d49dbf02 --- /dev/null +++ b/src/platform_impl/web/stdweb/mod.rs @@ -0,0 +1,52 @@ +mod canvas; +mod event; +mod timeout; + +pub use self::canvas::Canvas; +pub use self::timeout::Timeout; + +use crate::dpi::LogicalSize; +use crate::platform::web::WindowExtStdweb; +use crate::window::Window; + +use stdweb::js; +use stdweb::web::event::BeforeUnloadEvent; +use stdweb::web::window; +use stdweb::web::IEventTarget; +use stdweb::web::{document, html_element::CanvasElement, Element}; + +pub fn throw(msg: &str) { + js! { throw @{msg} } +} + +pub fn exit_fullscreen() { + document().exit_fullscreen(); +} + +pub fn on_unload(mut handler: impl FnMut() + 'static) { + window().add_event_listener(move |_: BeforeUnloadEvent| handler()); +} + +impl WindowExtStdweb for Window { + fn canvas(&self) -> CanvasElement { + self.window.canvas().raw().clone() + } +} + +pub fn window_size() -> LogicalSize { + let window = window(); + let width = window.inner_width() as f64; + let height = window.inner_height() as f64; + + LogicalSize { width, height } +} + +pub fn is_fullscreen(canvas: &CanvasElement) -> bool { + match document().fullscreen_element() { + Some(elem) => { + let raw: Element = canvas.clone().into(); + raw == elem + } + None => false, + } +} 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..0543055e --- /dev/null +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -0,0 +1,303 @@ +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::cell::RefCell; +use std::rc::Rc; + +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, PointerEvent, WheelEvent}; + +pub struct Canvas { + raw: HtmlCanvasElement, + on_focus: Option>, + on_blur: Option>, + on_keyboard_release: Option>, + on_keyboard_press: Option>, + on_received_character: Option>, + on_cursor_leave: Option>, + on_cursor_enter: Option>, + on_cursor_move: Option>, + on_mouse_press: Option>, + on_mouse_release: Option>, + on_mouse_wheel: Option>, + on_fullscreen_change: Option>, + wants_fullscreen: Rc>, +} + +impl Drop for Canvas { + fn drop(&mut self) { + self.raw.remove(); + } +} + +impl Canvas { + pub fn create() -> Result { + let window = + web_sys::window().ok_or(os_error!(OsError("Failed to obtain window".to_owned())))?; + + let document = window + .document() + .ok_or(os_error!(OsError("Failed to obtain document".to_owned())))?; + + let canvas: HtmlCanvasElement = document + .create_element("canvas") + .map_err(|_| os_error!(OsError("Failed to create canvas element".to_owned())))? + .unchecked_into(); + + // A tabindex is needed in order to capture local keyboard events. + // A "0" value means that the element should be focusable in + // sequential keyboard navigation, but its order is defined by the + // document's source order. + // https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/tabindex + canvas + .set_attribute("tabindex", "0") + .map_err(|_| os_error!(OsError("Failed to set a tabindex".to_owned())))?; + + Ok(Canvas { + raw: canvas, + on_blur: None, + on_focus: None, + on_keyboard_release: None, + on_keyboard_press: None, + on_received_character: None, + on_cursor_leave: None, + on_cursor_enter: None, + on_cursor_move: None, + on_mouse_release: None, + on_mouse_press: None, + on_mouse_wheel: None, + on_fullscreen_change: None, + wants_fullscreen: Rc::new(RefCell::new(false)), + }) + } + + pub fn set_attribute(&self, attribute: &str, value: &str) { + self.raw + .set_attribute(attribute, value) + .expect(&format!("Set attribute: {}", attribute)); + } + + pub fn position(&self) -> (f64, f64) { + let bounds = self.raw.get_bounding_client_rect(); + + (bounds.x(), bounds.y()) + } + + pub fn width(&self) -> f64 { + self.raw.width() as f64 + } + + pub fn height(&self) -> f64 { + self.raw.height() as f64 + } + + pub fn set_size(&self, size: LogicalSize) { + self.raw.set_width(size.width as u32); + self.raw.set_height(size.height as u32); + } + + pub fn raw(&self) -> &HtmlCanvasElement { + &self.raw + } + + pub fn on_blur(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_blur = Some(self.add_event("blur", move |_: FocusEvent| { + handler(); + })); + } + + pub fn on_focus(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_focus = Some(self.add_event("focus", move |_: FocusEvent| { + handler(); + })); + } + + pub fn on_keyboard_release(&mut self, mut handler: F) + where + F: 'static + FnMut(ScanCode, Option, ModifiersState), + { + self.on_keyboard_release = + Some(self.add_user_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_user_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_user_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_user_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_user_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)); + } + })); + } + + pub fn on_fullscreen_change(&mut self, mut handler: F) + where + F: 'static + FnMut(), + { + self.on_fullscreen_change = + Some(self.add_event("fullscreenchange", move |_: Event| handler())); + } + + 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 + } + + // The difference between add_event and add_user_event is that the latter has a special meaning + // for browser security. A user event is a deliberate action by the user (like a mouse or key + // press) and is the only time things like a fullscreen request may be successfully completed.) + fn add_user_event(&self, event_name: &str, mut handler: F) -> Closure + where + E: 'static + AsRef + wasm_bindgen::convert::FromWasmAbi, + F: 'static + FnMut(E), + { + let wants_fullscreen = self.wants_fullscreen.clone(); + let canvas = self.raw.clone(); + + self.add_event(event_name, move |event: E| { + handler(event); + + if *wants_fullscreen.borrow() { + canvas + .request_fullscreen() + .expect("Failed to enter fullscreen"); + *wants_fullscreen.borrow_mut() = false; + } + }) + } + + pub fn request_fullscreen(&self) { + *self.wants_fullscreen.borrow_mut() = true; + } + + pub fn is_fullscreen(&self) -> bool { + super::is_fullscreen(&self.raw) + } +} 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..205519d1 --- /dev/null +++ b/src/platform_impl/web/web_sys/mod.rs @@ -0,0 +1,70 @@ +mod canvas; +mod event; +mod timeout; + +pub use self::canvas::Canvas; +pub use self::timeout::Timeout; + +use crate::dpi::LogicalSize; +use crate::platform::web::WindowExtWebSys; +use crate::window::Window; +use wasm_bindgen::{closure::Closure, JsCast}; +use web_sys::{window, BeforeUnloadEvent, Element, HtmlCanvasElement}; + +pub fn throw(msg: &str) { + wasm_bindgen::throw_str(msg); +} + +pub fn exit_fullscreen() { + let window = web_sys::window().expect("Failed to obtain window"); + let document = window.document().expect("Failed to obtain document"); + + document.exit_fullscreen(); +} + +pub fn on_unload(mut handler: impl FnMut() + 'static) { + let window = web_sys::window().expect("Failed to obtain window"); + + let closure = Closure::wrap( + Box::new(move |_: BeforeUnloadEvent| handler()) as Box + ); + + window + .add_event_listener_with_callback("beforeunload", &closure.as_ref().unchecked_ref()) + .expect("Failed to add close listener"); +} + +impl WindowExtWebSys for Window { + fn canvas(&self) -> HtmlCanvasElement { + self.window.canvas().raw().clone() + } +} + +pub fn window_size() -> LogicalSize { + let window = web_sys::window().expect("Failed to obtain window"); + let width = window + .inner_width() + .expect("Failed to get width") + .as_f64() + .expect("Failed to get width as f64"); + let height = window + .inner_height() + .expect("Failed to get height") + .as_f64() + .expect("Failed to get height as f64"); + + LogicalSize { width, height } +} + +pub fn is_fullscreen(canvas: &HtmlCanvasElement) -> bool { + let window = window().expect("Failed to obtain window"); + let document = window.document().expect("Failed to obtain document"); + + match document.fullscreen_element() { + Some(elem) => { + let raw: Element = canvas.clone().into(); + raw == elem + } + None => false, + } +} 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..8752d8b6 --- /dev/null +++ b/src/platform_impl/web/window.rs @@ -0,0 +1,288 @@ +use crate::dpi::{LogicalPosition, LogicalSize}; +use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; +use crate::icon::Icon; +use crate::monitor::MonitorHandle as RootMH; +use crate::window::{CursorIcon, Fullscreen, WindowAttributes, WindowId as RootWI}; + +use raw_window_handle::web::WebHandle; + +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, + id: Id, + register_redraw_request: Box, +} + +impl Window { + pub fn new( + target: &EventLoopWindowTarget, + attr: WindowAttributes, + _: PlatformSpecificBuilderAttributes, + ) -> Result { + let runner = target.runner.clone(); + + let id = target.generate_id(); + + let mut canvas = backend::Canvas::create()?; + + let register_redraw_request = Box::new(move || runner.request_redraw(RootWI(id))); + + target.register(&mut canvas, id); + + let window = Window { + canvas, + previous_pointer: RefCell::new("auto"), + position: RefCell::new(LogicalPosition { x: 0.0, y: 0.0 }), + id, + register_redraw_request, + }; + + 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.register_redraw_request)(); + } + + 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> { + // Intentionally a no-op, as the web does not support setting cursor positions + Ok(()) + } + + #[inline] + pub fn set_cursor_grab(&self, _grab: bool) -> Result<(), ExternalError> { + // Intentionally a no-op, as the web does not (properly) support grabbing the cursor + 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) { + // Intentionally a no-op, as canvases cannot be 'maximized' + } + + #[inline] + pub fn fullscreen(&self) -> Option { + if self.canvas.is_fullscreen() { + Some(Fullscreen::Borderless(self.current_monitor())) + } else { + None + } + } + + #[inline] + pub fn set_fullscreen(&self, monitor: Option) { + if monitor.is_some() { + self.canvas.request_fullscreen(); + } else if self.canvas.is_fullscreen() { + backend::exit_fullscreen(); + } + } + + #[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) { + // Currently a no-op as it does not seem there is good support for this on web + } + + #[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 { + return self.id; + } + + #[inline] + pub fn raw_window_handle(&self) -> raw_window_handle::RawWindowHandle { + let handle = WebHandle { + id: self.id.0, + ..WebHandle::empty() + }; + + raw_window_handle::RawWindowHandle::Web(handle) + } +} + +#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct Id(pub(crate) u32); + +impl Id { + pub unsafe fn dummy() -> Id { + Id(0) + } +} + +#[derive(Debug, Default, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct PlatformSpecificBuilderAttributes; diff --git a/src/window.rs b/src/window.rs index 6ff27d50..23c73e91 100644 --- a/src/window.rs +++ b/src/window.rs @@ -322,6 +322,10 @@ impl WindowBuilder { /// Builds the window. /// /// Possible causes of error include denied permission, incompatible system, and lack of memory. + /// + /// Platform-specific behavior: + /// - **Web**: The window is created but not inserted into the web page automatically. Please + /// see the web platform module for more information. #[inline] pub fn build( self, @@ -341,6 +345,10 @@ impl Window { /// Error should be very rare and only occur in case of permission denied, incompatible system, /// out of memory, etc. /// + /// Platform-specific behavior: + /// - **Web**: The window is created but not inserted into the web page automatically. Please + /// see the web platform module for more information. + /// /// [`WindowBuilder::new().build(event_loop)`]: struct.WindowBuilder.html#method.build #[inline] pub fn new(event_loop: &EventLoopWindowTarget) -> Result { @@ -497,6 +505,7 @@ impl Window { /// ## Platform-specific /// /// - **iOS:** Has no effect. + /// - **Web:** Has no effect. #[inline] pub fn set_min_inner_size(&self, dimensions: Option) { self.window.set_min_inner_size(dimensions) @@ -507,6 +516,7 @@ impl Window { /// ## Platform-specific /// /// - **iOS:** Has no effect. + /// - **Web:** Has no effect. #[inline] pub fn set_max_inner_size(&self, dimensions: Option) { self.window.set_max_inner_size(dimensions) @@ -532,6 +542,7 @@ impl Window { /// /// - **Android:** Has no effect. /// - **iOS:** Can only be called on the main thread. + /// - **Web:** Has no effect. #[inline] pub fn set_visible(&self, visible: bool) { self.window.set_visible(visible) @@ -551,6 +562,7 @@ impl Window { /// ## Platform-specific /// /// - **iOS:** Has no effect. + /// - **Web:** Has no effect. #[inline] pub fn set_resizable(&self, resizable: bool) { self.window.set_resizable(resizable) @@ -561,6 +573,7 @@ impl Window { /// ## Platform-specific /// /// - **iOS:** Has no effect. + /// - **Web:** Has no effect. #[inline] pub fn set_maximized(&self, maximized: bool) { self.window.set_maximized(maximized) @@ -606,8 +619,11 @@ impl Window { /// Turn window decorations on or off. /// /// ## Platform-specific + /// - **iOS:** Can only be called on the main thread. Controls whether the status bar is hidden + /// via [`setPrefersStatusBarHidden`]. + /// - **Web:** Has no effect. /// - /// - **iOS:** Has no effect. + /// [`setPrefersStatusBarHidden`]: https://developer.apple.com/documentation/uikit/uiviewcontroller/1621440-prefersstatusbarhidden?language=objc #[inline] pub fn set_decorations(&self, decorations: bool) { self.window.set_decorations(decorations) @@ -618,6 +634,7 @@ impl Window { /// ## Platform-specific /// /// - **iOS:** Has no effect. + /// - **Web:** Has no effect. #[inline] pub fn set_always_on_top(&self, always_on_top: bool) { self.window.set_always_on_top(always_on_top) @@ -645,6 +662,7 @@ impl Window { /// ## Platform-specific /// /// **iOS:** Has no effect. + /// - **Web:** Has no effect. #[inline] pub fn set_ime_position(&self, position: LogicalPosition) { self.window.set_ime_position(position) @@ -669,6 +687,7 @@ impl Window { /// ## Platform-specific /// /// - **iOS:** Always returns an `Err`. + /// - **Web:** Has no effect. #[inline] pub fn set_cursor_position(&self, position: LogicalPosition) -> Result<(), ExternalError> { self.window.set_cursor_position(position) @@ -684,6 +703,7 @@ impl Window { /// awkward. /// - **Android:** Has no effect. /// - **iOS:** Always returns an Err. + /// - **Web:** Has no effect. #[inline] pub fn set_cursor_grab(&self, grab: bool) -> Result<(), ExternalError> { self.window.set_cursor_grab(grab) 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 diff --git a/tests/send_objects.rs b/tests/send_objects.rs index 9462d073..252b271a 100644 --- a/tests/send_objects.rs +++ b/tests/send_objects.rs @@ -1,6 +1,7 @@ #[allow(dead_code)] fn needs_send() {} +#[cfg(not(target_arch = "wasm32"))] #[test] fn event_loop_proxy_send() { #[allow(dead_code)] @@ -10,6 +11,7 @@ fn event_loop_proxy_send() { } } +#[cfg(not(target_arch = "wasm32"))] #[test] fn window_send() { // ensures that `winit::Window` implements `Send` diff --git a/tests/sync_object.rs b/tests/sync_object.rs index dad65202..56524cb2 100644 --- a/tests/sync_object.rs +++ b/tests/sync_object.rs @@ -1,6 +1,7 @@ #[allow(dead_code)] fn needs_sync() {} +#[cfg(not(target_arch = "wasm32"))] #[test] fn window_sync() { // ensures that `winit::Window` implements `Sync`