Merge branch 'web'

This commit is contained in:
Ryan Goldstein 2019-10-16 16:09:39 -04:00
commit 1c6353aa3a
41 changed files with 2435 additions and 62 deletions

4
.gitignore vendored
View file

@ -2,5 +2,9 @@ Cargo.lock
target/
rls/
.vscode/
util/
*~
*.wasm
*.ts
*.js
#*#

View file

@ -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:
- |

View file

@ -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`.

View file

@ -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"]

View file

@ -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] |❌ |❌ |❌ |❌ |❌ |❓ |

View file

@ -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::<CustomEvent>::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.");
}

View file

@ -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");
}

View file

@ -1,4 +1,5 @@
use std::time::{Duration, Instant};
use instant::Instant;
use std::time::Duration;
use winit::{
event::{Event, WindowEvent},

View file

@ -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},

View file

@ -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.");
}

View file

@ -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

View file

@ -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<T> Event<T> {
}
/// 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

View file

@ -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<T> fmt::Debug for EventLoopWindowTarget<T> {
/// 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.

View file

@ -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]

View file

@ -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<String> {
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<Item = VideoMode> {
self.inner.video_modes()

View file

@ -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;

22
src/platform/web.rs Normal file
View file

@ -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;
}

View file

@ -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");

View file

@ -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)
}
}

View file

@ -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)
}
}

View file

@ -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<T: 'static> {
elw: root::EventLoopWindowTarget<T>,
}
impl<T> EventLoop<T> {
pub fn new() -> Self {
EventLoop {
elw: root::EventLoopWindowTarget {
p: WindowTarget::new(),
_marker: PhantomData,
},
}
}
pub fn available_monitors(&self) -> VecDequeIter<monitor::Handle> {
VecDeque::new().into_iter()
}
pub fn primary_monitor(&self) -> monitor::Handle {
monitor::Handle
}
pub fn run<F>(self, mut event_handler: F) -> !
where
F: 'static + FnMut(Event<T>, &root::EventLoopWindowTarget<T>, &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<T> {
self.elw.p.proxy()
}
pub fn window_target(&self) -> &root::EventLoopWindowTarget<T> {
&self.elw
}
}

View file

@ -0,0 +1,26 @@
use super::runner;
use crate::event::Event;
use crate::event_loop::EventLoopClosed;
pub struct Proxy<T: 'static> {
runner: runner::Shared<T>,
}
impl<T: 'static> Proxy<T> {
pub fn new(runner: runner::Shared<T>) -> Self {
Proxy { runner }
}
pub fn send_event(&self, event: T) -> Result<(), EventLoopClosed> {
self.runner.send_event(Event::UserEvent(event));
Ok(())
}
}
impl<T: 'static> Clone for Proxy<T> {
fn clone(&self) -> Self {
Proxy {
runner: self.runner.clone(),
}
}
}

View file

@ -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<T>(Rc<Execution<T>>);
impl<T> Clone for Shared<T> {
fn clone(&self) -> Self {
Shared(self.0.clone())
}
}
pub struct Execution<T> {
runner: RefCell<Option<Runner<T>>>,
events: RefCell<VecDeque<Event<T>>>,
id: RefCell<u32>,
redraw_pending: RefCell<HashSet<WindowId>>,
}
struct Runner<T> {
state: State,
is_busy: bool,
event_handler: Box<dyn FnMut(Event<T>, &mut root::ControlFlow)>,
}
impl<T: 'static> Runner<T> {
pub fn new(event_handler: Box<dyn FnMut(Event<T>, &mut root::ControlFlow)>) -> Self {
Runner {
state: State::Init,
is_busy: false,
event_handler,
}
}
}
impl<T: 'static> Shared<T> {
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<dyn FnMut(Event<T>, &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<T>) {
// 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<WindowId> = 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<T>, 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,
}
}
}

View file

@ -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,
}
}
}

View file

@ -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<T: 'static> {
pub(crate) runner: runner::Shared<T>,
}
impl<T> Clone for WindowTarget<T> {
fn clone(&self) -> Self {
WindowTarget {
runner: self.runner.clone(),
}
}
}
impl<T> WindowTarget<T> {
pub fn new() -> Self {
WindowTarget {
runner: runner::Shared::new(),
}
}
pub fn proxy(&self) -> Proxy<T> {
Proxy::new(self.runner.clone())
}
pub fn run(&self, event_handler: Box<dyn FnMut(Event<T>, &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));
});
}
}

View file

@ -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,
};

View file

@ -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<String> {
None
}
pub fn size(&self) -> PhysicalSize {
PhysicalSize {
width: 0.0,
height: 0.0,
}
}
pub fn video_modes(&self) -> impl Iterator<Item = VideoMode> {
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 }
}
}

View file

@ -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<EventListenerHandle>,
on_blur: Option<EventListenerHandle>,
on_keyboard_release: Option<EventListenerHandle>,
on_keyboard_press: Option<EventListenerHandle>,
on_received_character: Option<EventListenerHandle>,
on_cursor_leave: Option<EventListenerHandle>,
on_cursor_enter: Option<EventListenerHandle>,
on_cursor_move: Option<EventListenerHandle>,
on_mouse_press: Option<EventListenerHandle>,
on_mouse_release: Option<EventListenerHandle>,
on_mouse_wheel: Option<EventListenerHandle>,
on_fullscreen_change: Option<EventListenerHandle>,
wants_fullscreen: Rc<RefCell<bool>>,
}
impl Drop for Canvas {
fn drop(&mut self) {
self.raw.remove();
}
}
impl Canvas {
pub fn create() -> Result<Self, RootOE> {
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<F>(&mut self, mut handler: F)
where
F: 'static + FnMut(),
{
self.on_blur = Some(self.add_event(move |_: BlurEvent| {
handler();
}));
}
pub fn on_focus<F>(&mut self, mut handler: F)
where
F: 'static + FnMut(),
{
self.on_focus = Some(self.add_event(move |_: FocusEvent| {
handler();
}));
}
pub fn on_keyboard_release<F>(&mut self, mut handler: F)
where
F: 'static + FnMut(ScanCode, Option<VirtualKeyCode>, 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<F>(&mut self, mut handler: F)
where
F: 'static + FnMut(ScanCode, Option<VirtualKeyCode>, 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<F>(&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<F>(&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<F>(&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<F>(&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<F>(&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<F>(&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<F>(&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<F>(&mut self, mut handler: F)
where
F: 'static + FnMut(),
{
self.on_fullscreen_change = Some(self.add_event(move |_: FullscreenChangeEvent| handler()));
}
fn add_event<E, F>(&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<E, F>(&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)
}
}

View file

@ -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<MouseScrollDelta> {
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<T: JsSerialize>(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<VirtualKeyCode> {
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()
}

View file

@ -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,
}
}

View file

@ -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: 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();
}
}

View file

@ -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<Closure<dyn FnMut(FocusEvent)>>,
on_blur: Option<Closure<dyn FnMut(FocusEvent)>>,
on_keyboard_release: Option<Closure<dyn FnMut(KeyboardEvent)>>,
on_keyboard_press: Option<Closure<dyn FnMut(KeyboardEvent)>>,
on_received_character: Option<Closure<dyn FnMut(KeyboardEvent)>>,
on_cursor_leave: Option<Closure<dyn FnMut(PointerEvent)>>,
on_cursor_enter: Option<Closure<dyn FnMut(PointerEvent)>>,
on_cursor_move: Option<Closure<dyn FnMut(PointerEvent)>>,
on_mouse_press: Option<Closure<dyn FnMut(PointerEvent)>>,
on_mouse_release: Option<Closure<dyn FnMut(PointerEvent)>>,
on_mouse_wheel: Option<Closure<dyn FnMut(WheelEvent)>>,
on_fullscreen_change: Option<Closure<dyn FnMut(Event)>>,
wants_fullscreen: Rc<RefCell<bool>>,
}
impl Drop for Canvas {
fn drop(&mut self) {
self.raw.remove();
}
}
impl Canvas {
pub fn create() -> Result<Self, RootOE> {
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<F>(&mut self, mut handler: F)
where
F: 'static + FnMut(),
{
self.on_blur = Some(self.add_event("blur", move |_: FocusEvent| {
handler();
}));
}
pub fn on_focus<F>(&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<F>(&mut self, mut handler: F)
where
F: 'static + FnMut(ScanCode, Option<VirtualKeyCode>, 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<F>(&mut self, mut handler: F)
where
F: 'static + FnMut(ScanCode, Option<VirtualKeyCode>, 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<F>(&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<F>(&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<F>(&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<F>(&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<F>(&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<F>(&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<F>(&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<F>(&mut self, mut handler: F)
where
F: 'static + FnMut(),
{
self.on_fullscreen_change =
Some(self.add_event("fullscreenchange", move |_: Event| handler()));
}
fn add_event<E, F>(&self, event_name: &str, mut handler: F) -> Closure<dyn FnMut(E)>
where
E: 'static + AsRef<web_sys::Event> + 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<dyn FnMut(E)>);
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<E, F>(&self, event_name: &str, mut handler: F) -> Closure<dyn FnMut(E)>
where
E: 'static + AsRef<web_sys::Event> + 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)
}
}

View file

@ -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<MouseScrollDelta> {
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<VirtualKeyCode> {
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()
}

View file

@ -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<dyn FnMut(BeforeUnloadEvent)>
);
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,
}
}

View file

@ -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<dyn FnMut()>,
}
impl Timeout {
pub fn new<F>(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<dyn FnMut()>);
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);
}
}

View file

@ -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<LogicalPosition>,
id: Id,
register_redraw_request: Box<dyn Fn()>,
}
impl Window {
pub fn new<T>(
target: &EventLoopWindowTarget<T>,
attr: WindowAttributes,
_: PlatformSpecificBuilderAttributes,
) -> Result<Self, RootOE> {
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<LogicalPosition, NotSupportedError> {
let (x, y) = self.canvas.position();
Ok(LogicalPosition { x, y })
}
pub fn inner_position(&self) -> Result<LogicalPosition, NotSupportedError> {
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<LogicalSize>) {
// Intentionally a no-op: users can't resize canvas elements
}
#[inline]
pub fn set_max_inner_size(&self, _dimensions: Option<LogicalSize>) {
// 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<Fullscreen> {
if self.canvas.is_fullscreen() {
Some(Fullscreen::Borderless(self.current_monitor()))
} else {
None
}
}
#[inline]
pub fn set_fullscreen(&self, monitor: Option<Fullscreen>) {
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<Icon>) {
// 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<monitor::Handle> {
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;

View file

@ -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<T: 'static>(
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<T: 'static>(event_loop: &EventLoopWindowTarget<T>) -> Result<Window, OsError> {
@ -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<LogicalSize>) {
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<LogicalSize>) {
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)

20
test.html Normal file
View file

@ -0,0 +1,20 @@
<!doctype html>
<html>
<head>
<script type="module">
import { default as init } from './window.js';
async function run() {
console.log("Loading");
await init('./window_bg.wasm');
console.log("Loaded");
}
run();
</script>
</head>
<body>
<canvas id="test_canvas"></canvas>
</body>
</html>

2
test.ps1 Normal file
View file

@ -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

View file

@ -1,6 +1,7 @@
#[allow(dead_code)]
fn needs_send<T: 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`

View file

@ -1,6 +1,7 @@
#[allow(dead_code)]
fn needs_sync<T: Sync>() {}
#[cfg(not(target_arch = "wasm32"))]
#[test]
fn window_sync() {
// ensures that `winit::Window` implements `Sync`