Move Web backend to winit-web

This commit is contained in:
Mads Marquart 2025-05-16 01:23:35 +02:00 committed by Kirill Chibisov
parent 2d4b9938f0
commit e542a78deb
50 changed files with 259 additions and 273 deletions

View file

@ -0,0 +1,96 @@
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::future::Future;
use std::pin::Pin;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::task::{Context, Poll};
use pin_project::pin_project;
use super::AtomicWaker;
#[pin_project]
pub struct Abortable<F: Future> {
#[pin]
future: F,
shared: Arc<Shared>,
}
impl<F: Future> Abortable<F> {
pub fn new(handle: AbortHandle, future: F) -> Self {
Self { future, shared: handle.0 }
}
}
impl<F: Future> Future for Abortable<F> {
type Output = Result<F::Output, Aborted>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.shared.aborted.load(Ordering::Relaxed) {
return Poll::Ready(Err(Aborted));
}
if let Poll::Ready(value) = self.as_mut().project().future.poll(cx) {
return Poll::Ready(Ok(value));
}
self.shared.waker.register(cx.waker());
if self.shared.aborted.load(Ordering::Relaxed) {
return Poll::Ready(Err(Aborted));
}
Poll::Pending
}
}
#[derive(Debug)]
struct Shared {
waker: AtomicWaker,
aborted: AtomicBool,
}
#[derive(Clone, Debug)]
pub struct AbortHandle(Arc<Shared>);
impl AbortHandle {
pub fn new() -> Self {
Self(Arc::new(Shared { waker: AtomicWaker::new(), aborted: AtomicBool::new(false) }))
}
pub fn abort(&self) {
self.0.aborted.store(true, Ordering::Relaxed);
self.0.waker.wake()
}
}
#[derive(Debug)]
pub struct DropAbortHandle(AbortHandle);
impl DropAbortHandle {
pub fn new(handle: AbortHandle) -> Self {
Self(handle)
}
pub fn handle(&self) -> AbortHandle {
self.0.clone()
}
}
impl Drop for DropAbortHandle {
fn drop(&mut self) {
self.0.abort()
}
}
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
pub struct Aborted;
impl Display for Aborted {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(f, "`Abortable` future has been aborted")
}
}
impl Error for Aborted {}

View file

@ -0,0 +1,35 @@
use std::cell::RefCell;
use std::ops::Deref;
use std::task::Waker;
#[derive(Debug)]
pub struct AtomicWaker(RefCell<Option<Waker>>);
impl AtomicWaker {
pub const fn new() -> Self {
Self(RefCell::new(None))
}
pub fn register(&self, waker: &Waker) {
let mut this = self.0.borrow_mut();
if let Some(old_waker) = this.deref() {
if old_waker.will_wake(waker) {
return;
}
}
*this = Some(waker.clone());
}
pub fn wake(&self) {
if let Some(waker) = self.0.borrow_mut().take() {
waker.wake();
}
}
}
// SAFETY: Wasm without the `atomics` target feature is single-threaded.
unsafe impl Send for AtomicWaker {}
// SAFETY: Wasm without the `atomics` target feature is single-threaded.
unsafe impl Sync for AtomicWaker {}

View file

@ -0,0 +1,81 @@
use std::future;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, RecvError, SendError, TryRecvError};
use std::sync::Arc;
use std::task::Poll;
use super::AtomicWaker;
pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
let (sender, receiver) = mpsc::channel();
let shared = Arc::new(Shared { closed: AtomicBool::new(false), waker: AtomicWaker::new() });
let sender = Sender { sender, shared: Arc::clone(&shared) };
let receiver = Receiver { receiver, shared };
(sender, receiver)
}
pub struct Sender<T> {
sender: mpsc::Sender<T>,
shared: Arc<Shared>,
}
impl<T> Sender<T> {
pub fn send(&self, event: T) -> Result<(), SendError<T>> {
self.sender.send(event)?;
self.shared.waker.wake();
Ok(())
}
}
impl<T> Drop for Sender<T> {
fn drop(&mut self) {
self.shared.closed.store(true, Ordering::Relaxed);
self.shared.waker.wake();
}
}
pub struct Receiver<T> {
receiver: mpsc::Receiver<T>,
shared: Arc<Shared>,
}
impl<T> Receiver<T> {
pub async fn next(&self) -> Result<T, RecvError> {
future::poll_fn(|cx| match self.receiver.try_recv() {
Ok(event) => Poll::Ready(Ok(event)),
Err(TryRecvError::Empty) => {
self.shared.waker.register(cx.waker());
match self.receiver.try_recv() {
Ok(event) => Poll::Ready(Ok(event)),
Err(TryRecvError::Empty) => {
if self.shared.closed.load(Ordering::Relaxed) {
Poll::Ready(Err(RecvError))
} else {
Poll::Pending
}
},
Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)),
}
},
Err(TryRecvError::Disconnected) => Poll::Ready(Err(RecvError)),
})
.await
}
pub fn try_recv(&self) -> Result<Option<T>, RecvError> {
match self.receiver.try_recv() {
Ok(value) => Ok(Some(value)),
Err(TryRecvError::Empty) => Ok(None),
Err(TryRecvError::Disconnected) => Err(RecvError),
}
}
}
struct Shared {
closed: AtomicBool,
waker: AtomicWaker,
}

View file

@ -0,0 +1,52 @@
use std::cell::{Cell, RefCell};
#[derive(Debug)]
pub struct ConcurrentQueue<T> {
queue: RefCell<Vec<T>>,
closed: Cell<bool>,
}
pub enum PushError<T> {
#[allow(dead_code)]
Full(T),
Closed(T),
}
pub enum PopError {
Empty,
Closed,
}
impl<T> ConcurrentQueue<T> {
pub fn unbounded() -> Self {
Self { queue: RefCell::new(Vec::new()), closed: Cell::new(false) }
}
pub fn push(&self, value: T) -> Result<(), PushError<T>> {
if self.closed.get() {
return Err(PushError::Closed(value));
}
self.queue.borrow_mut().push(value);
Ok(())
}
pub fn pop(&self) -> Result<T, PopError> {
self.queue.borrow_mut().pop().ok_or_else(|| {
if self.closed.get() {
PopError::Closed
} else {
PopError::Empty
}
})
}
pub fn close(&self) -> bool {
!self.closed.replace(true)
}
}
// SAFETY: Wasm without the `atomics` target feature is single-threaded.
unsafe impl<T> Send for ConcurrentQueue<T> {}
// SAFETY: Wasm without the `atomics` target feature is single-threaded.
unsafe impl<T> Sync for ConcurrentQueue<T> {}

View file

@ -0,0 +1,149 @@
use std::cell::Ref;
use std::cmp::Ordering;
use std::fmt::{self, Debug, Formatter};
use std::hash::{Hash, Hasher};
use std::rc::Rc;
use std::sync::{Arc, Condvar, Mutex};
use super::super::main_thread::MainThreadMarker;
use super::{channel, Receiver, Sender, Wrapper};
pub struct Dispatcher<T: 'static>(Wrapper<T, Arc<Sender<Closure<T>>>, Closure<T>>);
struct Closure<T>(Box<dyn FnOnce(&T) + Send>);
impl<T> Clone for Dispatcher<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T> Debug for Dispatcher<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.debug_struct("Dispatcher").finish_non_exhaustive()
}
}
impl<T> Eq for Dispatcher<T> {}
impl<T> Hash for Dispatcher<T> {
fn hash<H: Hasher>(&self, state: &mut H) {
self.0.hash(state);
}
}
impl<T> Ord for Dispatcher<T> {
fn cmp(&self, other: &Self) -> Ordering {
self.0.cmp(&other.0)
}
}
impl<T> PartialEq for Dispatcher<T> {
fn eq(&self, other: &Self) -> bool {
self.0.eq(&other.0)
}
}
impl<T> PartialOrd for Dispatcher<T> {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl<T> Dispatcher<T> {
pub fn new(main_thread: MainThreadMarker, value: T) -> (Self, DispatchRunner<T>) {
let (sender, receiver) = channel::<Closure<T>>();
let sender = Arc::new(sender);
let receiver = Rc::new(receiver);
let wrapper = Wrapper::new(
main_thread,
value,
|value, Closure(closure)| {
// SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do
// anything funny with it here. See `Self::queue()`.
closure(value.borrow().as_ref().unwrap())
},
{
let receiver = Rc::clone(&receiver);
move |value| async move {
while let Ok(Closure(closure)) = receiver.next().await {
// SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't
// do anything funny with it here. See `Self::queue()`.
closure(value.borrow().as_ref().unwrap())
}
}
},
sender,
|sender, closure| {
// SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do
// anything funny with it here. See `Self::queue()`.
sender.send(closure).unwrap()
},
);
(Self(wrapper.clone()), DispatchRunner { wrapper, receiver })
}
pub fn value(&self, main_thread: MainThreadMarker) -> Ref<'_, T> {
self.0.value(main_thread)
}
pub fn dispatch(&self, f: impl 'static + FnOnce(&T) + Send) {
if let Some(main_thread) = MainThreadMarker::new() {
f(&self.0.value(main_thread))
} else {
self.0.send(Closure(Box::new(f)))
}
}
pub fn queue<R: Send>(&self, f: impl FnOnce(&T) -> R + Send) -> R {
if let Some(main_thread) = MainThreadMarker::new() {
f(&self.0.value(main_thread))
} else {
let pair = Arc::new((Mutex::new(None), Condvar::new()));
let closure = Box::new({
let pair = pair.clone();
move |value: &T| {
*pair.0.lock().unwrap() = Some(f(value));
pair.1.notify_one();
}
}) as Box<dyn FnOnce(&T) + Send>;
// SAFETY: The `transmute` is necessary because `Closure` requires `'static`. This is
// safe because this function won't return until `f` has finished executing. See
// `Self::new()`.
let closure = Closure(unsafe {
std::mem::transmute::<
Box<dyn FnOnce(&T) + Send>,
Box<dyn FnOnce(&T) + Send + 'static>,
>(closure)
});
self.0.send(closure);
let mut started = pair.0.lock().unwrap();
while started.is_none() {
started = pair.1.wait(started).unwrap();
}
started.take().unwrap()
}
}
}
pub struct DispatchRunner<T: 'static> {
wrapper: Wrapper<T, Arc<Sender<Closure<T>>>, Closure<T>>,
receiver: Rc<Receiver<Closure<T>>>,
}
impl<T> DispatchRunner<T> {
pub fn run(&self, main_thread: MainThreadMarker) {
while let Some(Closure(closure)) =
self.receiver.try_recv().expect("should only be closed when `Dispatcher` is dropped")
{
// SAFETY: The given `Closure` here isn't really `'static`, so we shouldn't do anything
// funny with it here. See `Self::queue()`.
closure(&self.wrapper.value(main_thread))
}
}
}

View file

@ -0,0 +1,18 @@
mod abortable;
#[cfg(not(target_feature = "atomics"))]
mod atomic_waker;
mod channel;
#[cfg(not(target_feature = "atomics"))]
mod concurrent_queue;
mod dispatcher;
mod notifier;
mod wrapper;
pub(crate) use atomic_waker::AtomicWaker;
use concurrent_queue::{ConcurrentQueue, PushError};
pub use self::abortable::{AbortHandle, Abortable, DropAbortHandle};
pub use self::channel::{channel, Receiver, Sender};
pub use self::dispatcher::{DispatchRunner, Dispatcher};
pub use self::notifier::{Notified, Notifier};
pub(crate) use self::wrapper::Wrapper;

View file

@ -0,0 +1,75 @@
use std::future::Future;
use std::pin::Pin;
use std::sync::{Arc, OnceLock};
use std::task::{Context, Poll, Waker};
use super::{ConcurrentQueue, PushError};
#[derive(Debug)]
pub struct Notifier<T: Clone>(Arc<Inner<T>>);
impl<T: Clone> Notifier<T> {
pub fn new() -> Self {
Self(Arc::new(Inner { queue: ConcurrentQueue::unbounded(), value: OnceLock::new() }))
}
pub fn notify(self, value: T) {
if self.0.value.set(value).is_err() {
unreachable!("value set before")
}
}
pub fn notified(&self) -> Notified<T> {
Notified(Some(Arc::clone(&self.0)))
}
}
impl<T: Clone> Drop for Notifier<T> {
fn drop(&mut self) {
self.0.queue.close();
while let Ok(waker) = self.0.queue.pop() {
waker.wake()
}
}
}
#[derive(Clone, Debug)]
pub struct Notified<T: Clone>(Option<Arc<Inner<T>>>);
impl<T: Clone> Future for Notified<T> {
type Output = Option<T>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.0.take().expect("`Receiver` polled after completion");
if this.value.get().is_none() {
match this.queue.push(cx.waker().clone()) {
Ok(()) => {
if this.value.get().is_none() {
self.0 = Some(this);
return Poll::Pending;
}
},
Err(PushError::Closed(_)) => (),
Err(PushError::Full(_)) => {
unreachable!("found full queue despite using unbounded queue")
},
}
}
match Arc::try_unwrap(this)
.map(|mut inner| inner.value.take())
.map_err(|this| this.value.get().cloned())
{
Ok(Some(value)) | Err(Some(value)) => Poll::Ready(Some(value)),
_ => Poll::Ready(None),
}
}
}
#[derive(Debug)]
struct Inner<T> {
queue: ConcurrentQueue<Waker>,
value: OnceLock<T>,
}

View file

@ -0,0 +1,113 @@
use std::cell::{Ref, RefCell};
use std::cmp;
use std::future::Future;
use std::hash::{Hash, Hasher};
use std::marker::PhantomData;
use std::sync::Arc;
use super::super::main_thread::MainThreadMarker;
// Unsafe wrapper type that allows us to use `T` when it's not `Send` from other threads.
// `value` **must** only be accessed on the main thread.
#[derive(Debug)]
pub struct Wrapper<V: 'static, S: Clone + Send, E> {
value: Value<V>,
handler: fn(&RefCell<Option<V>>, E),
sender_data: S,
sender_handler: fn(&S, E),
}
#[derive(Debug)]
struct Value<V> {
// SAFETY:
// This value must not be accessed if not on the main thread.
//
// - We wrap this in an `Arc` to allow it to be safely cloned without accessing the value.
// - The `RefCell` lets us mutably access in the main thread but is safe to drop in any thread
// because it has no `Drop` behavior.
// - The `Option` lets us safely drop `T` only in the main thread.
value: Arc<RefCell<Option<V>>>,
// Prevent's `Send` or `Sync` to be automatically implemented.
local: PhantomData<*const ()>,
}
// SAFETY: See `Self::value`.
unsafe impl<V> Send for Value<V> {}
// SAFETY: See `Self::value`.
unsafe impl<V> Sync for Value<V> {}
impl<V, S: Clone + Send, E> Wrapper<V, S, E> {
pub fn new<R: Future<Output = ()>>(
_: MainThreadMarker,
value: V,
handler: fn(&RefCell<Option<V>>, E),
receiver: impl 'static + FnOnce(Arc<RefCell<Option<V>>>) -> R,
sender_data: S,
sender_handler: fn(&S, E),
) -> Self {
let value = Arc::new(RefCell::new(Some(value)));
wasm_bindgen_futures::spawn_local({
let value = Arc::clone(&value);
async move {
receiver(Arc::clone(&value)).await;
drop(value.borrow_mut().take().unwrap());
}
});
Self { value: Value { value, local: PhantomData }, handler, sender_data, sender_handler }
}
pub fn send(&self, event: E) {
if MainThreadMarker::new().is_some() {
(self.handler)(&self.value.value, event)
} else {
(self.sender_handler)(&self.sender_data, event)
}
}
pub fn value(&self, _: MainThreadMarker) -> Ref<'_, V> {
Ref::map(self.value.value.borrow(), |value| value.as_ref().unwrap())
}
pub fn with_sender_data<T>(&self, f: impl FnOnce(&S) -> T) -> T {
f(&self.sender_data)
}
}
impl<V, S: Clone + Send, E> Clone for Wrapper<V, S, E> {
fn clone(&self) -> Self {
Self {
value: Value { value: self.value.value.clone(), local: PhantomData },
handler: self.handler,
sender_data: self.sender_data.clone(),
sender_handler: self.sender_handler,
}
}
}
impl<V, S: Clone + Send, E> Eq for Wrapper<V, S, E> {}
impl<V, S: Clone + Send, E> Hash for Wrapper<V, S, E> {
fn hash<H: Hasher>(&self, state: &mut H) {
Arc::as_ptr(&self.value.value).hash(state)
}
}
impl<V, S: Clone + Send, E> Ord for Wrapper<V, S, E> {
fn cmp(&self, other: &Self) -> cmp::Ordering {
Arc::as_ptr(&self.value.value).cmp(&Arc::as_ptr(&other.value.value))
}
}
impl<V, S: Clone + Send, E> PartialOrd for Wrapper<V, S, E> {
fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
Some(self.cmp(other))
}
}
impl<V, S: Clone + Send, E> PartialEq for Wrapper<V, S, E> {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.value.value, &other.value.value)
}
}

721
winit-web/src/cursor.rs Normal file
View file

@ -0,0 +1,721 @@
use std::cell::RefCell;
use std::future::{self, Future};
use std::hash::{Hash, Hasher};
use std::mem;
use std::ops::{Deref, DerefMut};
use std::pin::Pin;
use std::rc::Rc;
use std::sync::Arc;
use std::task::{ready, Context, Poll, Waker};
use std::time::Duration;
use cursor_icon::CursorIcon;
use js_sys::{Array, Object};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::JsFuture;
use web_sys::{
Blob, Document, DomException, HtmlCanvasElement, HtmlImageElement, ImageBitmap,
ImageBitmapOptions, ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window,
};
use winit_core::cursor::{Cursor, CursorImage, CustomCursorProvider, CustomCursorSource};
use crate::backend::Style;
use crate::event_loop::ActiveEventLoop;
use crate::main_thread::{MainThreadMarker, MainThreadSafe};
use crate::r#async::{AbortHandle, Abortable, DropAbortHandle, Notified, Notifier};
use crate::CustomCursorError;
#[derive(Clone, Debug)]
pub struct CustomCursor {
pub(crate) animation: bool,
state: Arc<MainThreadSafe<RefCell<ImageState>>>,
}
impl CustomCursor {
pub(crate) fn new(event_loop: &ActiveEventLoop, source: CustomCursorSource) -> Self {
match source {
CustomCursorSource::Image(image) => Self::build_spawn(
event_loop,
from_rgba(event_loop.runner.window(), event_loop.runner.document().clone(), &image),
false,
),
CustomCursorSource::Url { url, hotspot_x, hotspot_y } => Self::build_spawn(
event_loop,
from_url(UrlType::Plain(url), hotspot_x, hotspot_y),
false,
),
CustomCursorSource::Animation(animation) => {
let (duration, cursors) = animation.into_raw();
Self::build_spawn(
event_loop,
from_animation(event_loop.runner.main_thread(), duration, cursors.into_iter()),
true,
)
},
}
}
fn build_spawn<F, S>(window_target: &ActiveEventLoop, task: F, animation: bool) -> CustomCursor
where
F: 'static + Future<Output = Result<S, CustomCursorError>>,
S: Into<ImageState>,
{
let handle = AbortHandle::new();
let this = CustomCursor {
animation,
state: Arc::new(MainThreadSafe::new(
window_target.runner.main_thread(),
RefCell::new(ImageState::Loading {
notifier: Notifier::new(),
_handle: DropAbortHandle::new(handle.clone()),
}),
)),
};
let weak = Arc::downgrade(&this.state);
let main_thread = window_target.runner.main_thread();
let task = Abortable::new(handle, {
async move {
let result = task.await;
let this = weak.upgrade().expect("`CursorHandler` invalidated without aborting");
let mut this = this.get(main_thread).borrow_mut();
match result {
Ok(new_state) => {
let ImageState::Loading { notifier, .. } =
mem::replace(this.deref_mut(), new_state.into())
else {
unreachable!("found invalid state");
};
notifier.notify(Ok(()));
},
Err(error) => {
let ImageState::Loading { notifier, .. } =
mem::replace(this.deref_mut(), ImageState::Failed(error.clone()))
else {
unreachable!("found invalid state");
};
notifier.notify(Err(error));
},
}
}
});
wasm_bindgen_futures::spawn_local(async move {
let _ = task.await;
});
this
}
pub(crate) fn new_async(
event_loop: &ActiveEventLoop,
source: CustomCursorSource,
) -> CustomCursorFuture {
let CustomCursor { animation, state } = Self::new(event_loop, source);
let binding = state.get(event_loop.runner.main_thread()).borrow();
let ImageState::Loading { notifier, .. } = binding.deref() else {
unreachable!("found invalid state")
};
let notified = notifier.notified();
drop(binding);
CustomCursorFuture { notified, animation, state: Some(state) }
}
}
impl CustomCursorProvider for CustomCursor {
fn is_animated(&self) -> bool {
self.animation
}
}
impl Hash for CustomCursor {
fn hash<H: Hasher>(&self, state: &mut H) {
Arc::as_ptr(&self.state).hash(state);
}
}
impl PartialEq for CustomCursor {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.state, &other.state)
}
}
impl Eq for CustomCursor {}
#[derive(Debug)]
pub struct CustomCursorFuture {
notified: Notified<Result<(), CustomCursorError>>,
animation: bool,
state: Option<Arc<MainThreadSafe<RefCell<ImageState>>>>,
}
impl Future for CustomCursorFuture {
type Output = Result<CustomCursor, CustomCursorError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
if self.state.is_none() {
panic!("`CustomCursorFuture` polled after completion")
}
let result = ready!(Pin::new(&mut self.notified).poll(cx)).unwrap();
let state = self.state.take().expect("`CustomCursorFuture` polled after completion");
Poll::Ready(result.map(|_| CustomCursor { animation: self.animation, state }))
}
}
#[derive(Debug)]
pub struct CursorHandler(Rc<RefCell<Inner>>);
#[derive(Debug)]
struct Inner {
main_thread: MainThreadMarker,
canvas: HtmlCanvasElement,
style: Style,
visible: bool,
cursor: SelectedCursor,
}
impl CursorHandler {
pub(crate) fn new(
main_thread: MainThreadMarker,
canvas: HtmlCanvasElement,
style: Style,
) -> Self {
Self(Rc::new(RefCell::new(Inner {
main_thread,
canvas,
style,
visible: true,
cursor: SelectedCursor::default(),
})))
}
pub fn set_cursor(&self, cursor: Cursor) {
let mut this = self.0.borrow_mut();
match cursor {
Cursor::Icon(icon) => {
if let SelectedCursor::Icon(old_icon)
| SelectedCursor::Loading { previous: Previous::Icon(old_icon), .. } =
&this.cursor
{
if *old_icon == icon {
return;
}
}
this.cursor = SelectedCursor::Icon(icon);
this.set_style();
},
Cursor::Custom(cursor) => {
let cursor = match cursor.cast_ref::<CustomCursor>() {
Some(cursor) => cursor,
None => todo!(),
};
if let SelectedCursor::Loading { cursor: old_cursor, .. }
| SelectedCursor::Image(old_cursor)
| SelectedCursor::Animation { cursor: old_cursor, .. } = &this.cursor
{
if old_cursor == cursor {
return;
}
}
let state = cursor.state.get(this.main_thread).borrow();
match state.deref() {
ImageState::Loading { notifier, .. } => {
let notified = notifier.notified();
let handle = DropAbortHandle::new(AbortHandle::new());
let task = Abortable::new(handle.handle(), {
let weak = Rc::downgrade(&self.0);
async move {
let _ = notified.await;
let handler = weak
.upgrade()
.expect("`CursorHandler` invalidated without aborting");
handler.borrow_mut().notify();
}
});
wasm_bindgen_futures::spawn_local(async move {
let _ = task.await;
});
drop(state);
this.cursor = SelectedCursor::Loading {
cursor: cursor.clone(),
previous: mem::take(&mut this.cursor).into(),
_handle: handle,
};
},
ImageState::Failed(error) => {
tracing::error!(
"trying to load custom cursor that has failed to load: {error}"
)
},
ImageState::Image(_) => {
drop(state);
this.cursor = SelectedCursor::Image(cursor.clone());
this.set_style();
},
ImageState::Animation(animation) => {
let canvas: &CanvasAnimateExt = this.canvas.unchecked_ref();
let animation = canvas.animate_with_keyframe_animation_options(
Some(&animation.keyframes),
&animation.options,
);
drop(state);
if !this.visible {
animation.cancel();
}
this.cursor = SelectedCursor::Animation {
animation: AnimationDropper(animation),
cursor: cursor.clone(),
};
this.set_style();
},
};
},
}
}
pub fn set_cursor_visible(&self, visible: bool) {
let mut this = self.0.borrow_mut();
if !visible && this.visible {
this.visible = false;
this.style.set("cursor", "none");
if let SelectedCursor::Animation { animation, .. } = &this.cursor {
animation.0.cancel();
}
} else if visible && !this.visible {
this.visible = true;
this.set_style();
}
}
}
impl Inner {
fn set_style(&self) {
if self.visible {
match &self.cursor {
SelectedCursor::Icon(icon)
| SelectedCursor::Loading { previous: Previous::Icon(icon), .. } => {
if let CursorIcon::Default = icon {
self.style.remove("cursor")
} else {
self.style.set("cursor", icon.name())
}
},
SelectedCursor::Loading { previous: Previous::Image(cursor), .. }
| SelectedCursor::Image(cursor) => {
match cursor.state.get(self.main_thread).borrow().deref() {
ImageState::Image(Image { style, .. }) => self.style.set("cursor", style),
_ => unreachable!("found invalid saved state"),
}
},
SelectedCursor::Loading {
previous: Previous::Animation { animation, .. }, ..
}
| SelectedCursor::Animation { animation, .. } => {
self.style.remove("cursor");
animation.0.play()
},
}
}
}
fn notify(&mut self) {
let SelectedCursor::Loading { cursor, previous, .. } = mem::take(&mut self.cursor) else {
unreachable!("found wrong state")
};
let state = cursor.state.get(self.main_thread).borrow();
match state.deref() {
ImageState::Image(_) => {
drop(state);
self.cursor = SelectedCursor::Image(cursor);
self.set_style();
},
ImageState::Animation(animation) => {
let canvas: &CanvasAnimateExt = self.canvas.unchecked_ref();
let animation = canvas.animate_with_keyframe_animation_options(
Some(&animation.keyframes),
&animation.options,
);
drop(state);
if !self.visible {
animation.cancel();
}
self.cursor =
SelectedCursor::Animation { animation: AnimationDropper(animation), cursor };
self.set_style();
},
ImageState::Failed(error) => {
tracing::error!("custom cursor failed to load: {error}");
self.cursor = previous.into()
},
ImageState::Loading { .. } => unreachable!("notified without being ready"),
}
}
}
#[derive(Debug)]
enum SelectedCursor {
Icon(CursorIcon),
Loading { cursor: CustomCursor, previous: Previous, _handle: DropAbortHandle },
Image(CustomCursor),
Animation { cursor: CustomCursor, animation: AnimationDropper },
}
impl Default for SelectedCursor {
fn default() -> Self {
Self::Icon(Default::default())
}
}
impl From<Previous> for SelectedCursor {
fn from(previous: Previous) -> Self {
match previous {
Previous::Icon(icon) => Self::Icon(icon),
Previous::Image(cursor) => Self::Image(cursor),
Previous::Animation { cursor, animation } => Self::Animation { cursor, animation },
}
}
}
#[derive(Debug)]
enum Previous {
Icon(CursorIcon),
Image(CustomCursor),
Animation { cursor: CustomCursor, animation: AnimationDropper },
}
impl From<SelectedCursor> for Previous {
fn from(value: SelectedCursor) -> Self {
match value {
SelectedCursor::Icon(icon) => Self::Icon(icon),
SelectedCursor::Loading { previous, .. } => previous,
SelectedCursor::Image(image) => Self::Image(image),
SelectedCursor::Animation { cursor, animation } => {
Self::Animation { cursor, animation }
},
}
}
}
#[derive(Debug)]
enum ImageState {
Loading { notifier: Notifier<Result<(), CustomCursorError>>, _handle: DropAbortHandle },
Failed(CustomCursorError),
Image(Image),
Animation(Animation),
}
#[derive(Debug)]
struct Image {
style: String,
_object_url: Option<ObjectUrl>,
_image: HtmlImageElement,
}
impl From<Image> for ImageState {
fn from(image: Image) -> Self {
Self::Image(image)
}
}
#[derive(Debug)]
struct Animation {
keyframes: Array,
options: KeyframeAnimationOptions,
_images: Vec<CustomCursor>,
}
impl From<Animation> for ImageState {
fn from(animation: Animation) -> Self {
Self::Animation(animation)
}
}
#[derive(Debug)]
enum UrlType {
Plain(String),
Object(ObjectUrl),
}
impl UrlType {
fn url(&self) -> &str {
match &self {
UrlType::Plain(url) => url,
UrlType::Object(object_url) => &object_url.0,
}
}
}
#[derive(Debug)]
struct ObjectUrl(String);
impl Drop for ObjectUrl {
fn drop(&mut self) {
Url::revoke_object_url(&self.0).expect("unexpected exception in `URL.revokeObjectURL()`");
}
}
#[derive(Debug)]
struct AnimationDropper(WebAnimation);
impl Drop for AnimationDropper {
fn drop(&mut self) {
self.0.cancel()
}
}
fn from_rgba(
window: &Window,
document: Document,
image: &CursorImage,
) -> impl Future<Output = Result<Image, CustomCursorError>> {
// 1. Create an `ImageData` from the RGBA data.
// 2. Create an `ImageBitmap` from the `ImageData`.
// 3. Draw `ImageBitmap` on an `HTMLCanvasElement`.
// 4. Create a `Blob` from the `HTMLCanvasElement`.
// 5. Create an object URL from the `Blob`.
// 6. Decode the image on an `HTMLImageElement` from the URL.
// 1. Create an `ImageData` from the RGBA data.
// Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223
#[cfg(target_feature = "atomics")]
// Can't share `SharedArrayBuffer` with `ImageData`.
let result = {
use js_sys::{Uint8Array, Uint8ClampedArray};
use wasm_bindgen::JsValue;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ImageData)]
type ImageDataExt;
#[wasm_bindgen(catch, constructor, js_class = ImageData)]
fn new(array: Uint8ClampedArray, sw: u32) -> Result<ImageDataExt, JsValue>;
}
let array = Uint8Array::new_with_length(image.buffer().len() as u32);
array.copy_from(image.buffer());
let array = Uint8ClampedArray::new(&array);
ImageDataExt::new(array, image.width() as u32)
.map(JsValue::from)
.map(ImageData::unchecked_from_js)
};
#[cfg(not(target_feature = "atomics"))]
let result = ImageData::new_with_u8_clamped_array(
wasm_bindgen::Clamped(image.buffer()),
image.width() as u32,
);
let image_data = result.expect("found wrong image size");
// 2. Create an `ImageBitmap` from the `ImageData`.
//
// We call `createImageBitmap()` before spawning the future,
// to not have to clone the image buffer.
let options = ImageBitmapOptions::new();
options.set_premultiply_alpha(PremultiplyAlpha::None);
let bitmap = JsFuture::from(
window
.create_image_bitmap_with_image_data_and_image_bitmap_options(&image_data, &options)
.expect("unexpected exception in `createImageBitmap()`"),
);
let width = image.width();
let height = image.height();
let hotspot_x = image.hotspot_x();
let hotspot_y = image.hotspot_y();
async move {
let bitmap: ImageBitmap =
bitmap.await.expect("found invalid state in `ImageData`").unchecked_into();
let canvas: HtmlCanvasElement =
document.create_element("canvas").expect("invalid tag name").unchecked_into();
#[allow(clippy::disallowed_methods)]
canvas.set_width(width as u32);
#[allow(clippy::disallowed_methods)]
canvas.set_height(height as u32);
// 3. Draw `ImageBitmap` on an `HTMLCanvasElement`.
let context: ImageBitmapRenderingContext = canvas
.get_context("bitmaprenderer")
.expect("unexpected exception in `HTMLCanvasElement.getContext()`")
.expect("`bitmaprenderer` context unsupported")
.unchecked_into();
context.transfer_from_image_bitmap(&bitmap);
drop(bitmap);
drop(context);
// 4. Create a `Blob` from the `HTMLCanvasElement`.
//
// To keep the `Closure` alive until `HTMLCanvasElement.toBlob()` is done,
// we do the whole `Waker` strategy. Commonly on `Drop` the callback is aborted,
// but it would increase complexity and isn't possible in this case.
// Keep in mind that `HTMLCanvasElement.toBlob()` can call the callback immediately.
let value = Rc::new(RefCell::new(None));
let waker = Rc::new(RefCell::<Option<Waker>>::new(None));
let callback = Closure::once({
let value = value.clone();
let waker = waker.clone();
move |blob: Option<Blob>| {
*value.borrow_mut() = Some(blob);
if let Some(waker) = waker.borrow_mut().take() {
waker.wake();
}
}
});
canvas
.to_blob(callback.as_ref().unchecked_ref())
.expect("failed with `SecurityError` despite only source coming from memory");
let blob = future::poll_fn(|cx| {
if let Some(blob) = value.borrow_mut().take() {
Poll::Ready(blob)
} else {
*waker.borrow_mut() = Some(cx.waker().clone());
Poll::Pending
}
})
.await;
drop(canvas);
let Some(blob) = blob else {
return Err(CustomCursorError::Blob);
};
// 5. Create an object URL from the `Blob`.
let url = Url::create_object_url_with_blob(&blob)
.expect("unexpected exception in `URL.createObjectURL()`");
let url = UrlType::Object(ObjectUrl(url));
from_url(url, hotspot_x, hotspot_y).await
}
}
async fn from_url(
url: UrlType,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<Image, CustomCursorError> {
// 6. Decode the image on an `HTMLImageElement` from the URL.
let image = HtmlImageElement::new().expect("unexpected exception in `new HtmlImageElement`");
image.set_src(url.url());
let result = JsFuture::from(image.decode()).await;
if let Err(error) = result {
debug_assert!(error.has_type::<DomException>());
let error: DomException = error.unchecked_into();
debug_assert_eq!(error.name(), "EncodingError");
let error = error.message();
return Err(CustomCursorError::Decode(error));
}
Ok(Image {
style: format!("url({}) {hotspot_x} {hotspot_y}, auto", url.url()),
_object_url: match url {
UrlType::Plain(_) => None,
UrlType::Object(object_url) => Some(object_url),
},
_image: image,
})
}
#[allow(clippy::await_holding_refcell_ref)] // false-positive
async fn from_animation(
main_thread: MainThreadMarker,
duration: Duration,
cursors: impl ExactSizeIterator<Item = winit_core::cursor::CustomCursor>,
) -> Result<Animation, CustomCursorError> {
let keyframes = Array::new();
let mut images = Vec::with_capacity(cursors.len());
for cursor in cursors {
let cursor = cursor.cast_ref::<CustomCursor>().unwrap();
let state = cursor.state.get(main_thread).borrow();
match state.deref() {
ImageState::Loading { notifier, .. } => {
let notified = notifier.notified();
drop(state);
notified.await.unwrap()?;
},
ImageState::Failed(error) => return Err(error.clone()),
ImageState::Image(_) => drop(state),
ImageState::Animation(_) => unreachable!("check in `CustomCursorSource` failed"),
}
let state = cursor.state.get(main_thread).borrow();
let style = match state.deref() {
ImageState::Image(Image { style, .. }) => style,
_ => unreachable!("found invalid state"),
};
let keyframe: Keyframe = Object::new().unchecked_into();
keyframe.set_cursor(style);
keyframes.push(&keyframe);
drop(state);
images.push(cursor.clone());
}
keyframes.push(&keyframes.get(0));
let options: KeyframeAnimationOptions = Object::new().unchecked_into();
options.set_duration(duration.as_millis() as f64);
options.set_iterations(f64::INFINITY);
Ok(Animation { keyframes, options, _images: images })
}
#[wasm_bindgen]
extern "C" {
type CanvasAnimateExt;
#[wasm_bindgen(method, js_name = animate)]
fn animate_with_keyframe_animation_options(
this: &CanvasAnimateExt,
keyframes: Option<&Object>,
options: &KeyframeAnimationOptions,
) -> WebAnimation;
#[derive(Debug)]
type WebAnimation;
#[wasm_bindgen(method)]
fn cancel(this: &WebAnimation);
#[wasm_bindgen(method)]
fn play(this: &WebAnimation);
#[wasm_bindgen(extends = Object)]
type Keyframe;
#[wasm_bindgen(method, setter, js_name = cursor)]
fn set_cursor(this: &Keyframe, value: &str);
#[derive(Debug)]
#[wasm_bindgen(extends = Object)]
type KeyframeAnimationOptions;
#[wasm_bindgen(method, setter, js_name = duration)]
fn set_duration(this: &KeyframeAnimationOptions, value: f64);
#[wasm_bindgen(method, setter, js_name = iterations)]
fn set_iterations(this: &KeyframeAnimationOptions, value: f64);
}

10
winit-web/src/error.rs Normal file
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)
}
}

12
winit-web/src/event.rs Normal file
View file

@ -0,0 +1,12 @@
use winit_core::event::DeviceId;
pub(crate) fn mkdid(pointer_id: i32) -> Option<DeviceId> {
if let Ok(pointer_id) = u32::try_from(pointer_id) {
Some(DeviceId::from_raw(pointer_id as i64))
} else if pointer_id == -1 {
None
} else {
tracing::error!("found unexpected negative `PointerEvent.pointerId`: {pointer_id}");
None
}
}

View file

@ -0,0 +1,103 @@
use std::sync::atomic::{AtomicBool, Ordering};
use winit_core::application::ApplicationHandler;
use winit_core::error::{EventLoopError, NotSupportedError};
use winit_core::event_loop::ActiveEventLoop as RootActiveEventLoop;
use crate::{
backend, HasMonitorPermissionFuture, MonitorPermissionFuture, PollStrategy, WaitUntilStrategy,
};
mod proxy;
pub(crate) mod runner;
mod state;
mod window_target;
pub(crate) use window_target::ActiveEventLoop;
#[derive(Debug)]
pub struct EventLoop {
elw: ActiveEventLoop,
}
#[derive(Default, Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub struct PlatformSpecificEventLoopAttributes {}
static EVENT_LOOP_CREATED: AtomicBool = AtomicBool::new(false);
impl EventLoop {
pub fn new(_: &PlatformSpecificEventLoopAttributes) -> Result<Self, EventLoopError> {
if EVENT_LOOP_CREATED.swap(true, Ordering::Relaxed) {
// For better cross-platformness.
return Err(EventLoopError::RecreationAttempt);
}
Ok(EventLoop { elw: ActiveEventLoop::new() })
}
fn allow_event_loop_recreation() {
EVENT_LOOP_CREATED.store(false, Ordering::Relaxed);
}
pub fn run_app<A: ApplicationHandler>(self, app: A) -> ! {
let app = Box::new(app);
// SAFETY: The `transmute` is necessary because `run()` requires `'static`. This is safe
// because this function will never return and all resources not cleaned up by the point we
// `throw` will leak, making this actually `'static`.
let app = unsafe {
std::mem::transmute::<
Box<dyn ApplicationHandler + '_>,
Box<dyn ApplicationHandler + 'static>,
>(app)
};
self.elw.run(app, false);
// Throw an exception to break out of Rust execution 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 spawn_app<A: ApplicationHandler + 'static>(self, app: A) {
self.elw.run(Box::new(app), true);
}
pub fn window_target(&self) -> &dyn RootActiveEventLoop {
&self.elw
}
pub fn set_poll_strategy(&self, strategy: PollStrategy) {
self.elw.set_poll_strategy(strategy);
}
pub fn poll_strategy(&self) -> PollStrategy {
self.elw.poll_strategy()
}
pub fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
self.elw.set_wait_until_strategy(strategy);
}
pub fn wait_until_strategy(&self) -> WaitUntilStrategy {
self.elw.wait_until_strategy()
}
pub fn has_multiple_screens(&self) -> Result<bool, NotSupportedError> {
self.elw.has_multiple_screens()
}
pub fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture {
MonitorPermissionFuture(self.elw.request_detailed_monitor_permission())
}
pub fn has_detailed_monitor_permission(&self) -> HasMonitorPermissionFuture {
HasMonitorPermissionFuture(
self.elw.runner.monitor().has_detailed_monitor_permission_async(),
)
}
}

View file

@ -0,0 +1,104 @@
use std::future;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::Arc;
use std::task::Poll;
use winit_core::event_loop::EventLoopProxyProvider;
use super::super::main_thread::MainThreadMarker;
use crate::event_loop::runner::WeakShared;
use crate::r#async::{AtomicWaker, Wrapper};
#[derive(Debug)]
pub struct EventLoopProxy(Wrapper<WeakShared, Arc<State>, ()>);
#[derive(Debug)]
struct State {
awoken: AtomicBool,
waker: AtomicWaker,
closed: AtomicBool,
}
impl EventLoopProxy {
pub fn new(main_thread: MainThreadMarker, runner: WeakShared) -> Self {
let state = Arc::new(State {
awoken: AtomicBool::new(false),
waker: AtomicWaker::new(),
closed: AtomicBool::new(false),
});
Self(Wrapper::new(
main_thread,
runner,
|runner, _| {
let runner = runner.borrow();
let runner = runner.as_ref().unwrap();
if let Some(runner) = runner.upgrade() {
runner.send_proxy_wake_up(true);
}
},
{
let state = Arc::clone(&state);
move |runner| async move {
while future::poll_fn(|cx| {
if state.awoken.swap(false, Ordering::Relaxed) {
Poll::Ready(true)
} else {
state.waker.register(cx.waker());
if state.awoken.swap(false, Ordering::Relaxed) {
Poll::Ready(true)
} else {
if state.closed.load(Ordering::Relaxed) {
return Poll::Ready(false);
}
Poll::Pending
}
}
})
.await
{
let runner = runner.borrow();
let runner = runner.as_ref().unwrap();
if let Some(runner) = runner.upgrade() {
runner.send_proxy_wake_up(false);
}
}
}
},
state,
|state, _| {
state.awoken.store(true, Ordering::Relaxed);
state.waker.wake();
},
))
}
pub fn take(&self) -> bool {
debug_assert!(
MainThreadMarker::new().is_some(),
"this should only be called from the main thread"
);
self.0.with_sender_data(|state| state.awoken.swap(false, Ordering::Relaxed))
}
}
impl Drop for EventLoopProxy {
fn drop(&mut self) {
self.0.with_sender_data(|state| {
state.closed.store(true, Ordering::Relaxed);
state.waker.wake();
});
}
}
impl EventLoopProxyProvider for EventLoopProxy {
fn wake_up(&self) {
self.0.send(())
}
}

View file

@ -0,0 +1,888 @@
use std::cell::{Cell, RefCell};
use std::collections::{HashSet, VecDeque};
use std::ops::Deref;
use std::rc::{Rc, Weak};
use std::sync::Arc;
use std::{fmt, iter};
use dpi::PhysicalSize;
use wasm_bindgen::prelude::Closure;
use wasm_bindgen::JsCast;
use web_sys::{Document, KeyboardEvent, Navigator, PageTransitionEvent, PointerEvent, WheelEvent};
use web_time::{Duration, Instant};
use winit_core::application::ApplicationHandler;
use winit_core::event::{
DeviceEvent, DeviceId, ElementState, RawKeyEvent, StartCause, WindowEvent,
};
use winit_core::event_loop::{ControlFlow, DeviceEvents};
use winit_core::window::WindowId;
use super::proxy::EventLoopProxy;
use super::state::State;
use crate::backend::{EventListenerHandle, SafeAreaHandle};
use crate::event_loop::ActiveEventLoop;
use crate::main_thread::MainThreadMarker;
use crate::monitor::MonitorHandler;
use crate::r#async::DispatchRunner;
use crate::web_sys::event::mouse_button_to_id;
use crate::window::Inner;
use crate::{backend, event, EventLoop, PollStrategy, WaitUntilStrategy};
#[derive(Debug)]
pub struct Shared(Rc<Execution>);
impl Clone for Shared {
fn clone(&self) -> Self {
Shared(self.0.clone())
}
}
type OnEventHandle<T> = RefCell<Option<EventListenerHandle<dyn FnMut(T)>>>;
struct Execution {
main_thread: MainThreadMarker,
event_loop_proxy: Arc<EventLoopProxy>,
control_flow: Cell<ControlFlow>,
poll_strategy: Cell<PollStrategy>,
wait_until_strategy: Cell<WaitUntilStrategy>,
exit: Cell<bool>,
runner: RefCell<RunnerEnum>,
suspended: Cell<bool>,
event_loop_recreation: Cell<bool>,
events: RefCell<VecDeque<Event>>,
id: Cell<usize>,
window: web_sys::Window,
navigator: Navigator,
document: Document,
#[allow(clippy::type_complexity)]
all_canvases: RefCell<Vec<(WindowId, Weak<backend::Canvas>, DispatchRunner<Inner>)>>,
redraw_pending: RefCell<HashSet<WindowId>>,
destroy_pending: RefCell<VecDeque<WindowId>>,
pub(crate) monitor: Rc<MonitorHandler>,
safe_area: Rc<SafeAreaHandle>,
page_transition_event_handle: RefCell<Option<backend::PageTransitionEventHandle>>,
device_events: Cell<DeviceEvents>,
on_mouse_move: OnEventHandle<PointerEvent>,
on_wheel: OnEventHandle<WheelEvent>,
on_mouse_press: OnEventHandle<PointerEvent>,
on_mouse_release: OnEventHandle<PointerEvent>,
on_key_press: OnEventHandle<KeyboardEvent>,
on_key_release: OnEventHandle<KeyboardEvent>,
on_visibility_change: OnEventHandle<web_sys::Event>,
}
impl fmt::Debug for Execution {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Execution").finish_non_exhaustive()
}
}
enum RunnerEnum {
/// The `EventLoop` is created but not being run.
Pending,
/// The `EventLoop` is running some async initialization and is waiting to be started.
Initializing(Runner),
/// The `EventLoop` is being run.
Running(Runner),
/// The `EventLoop` is exited after being started with `EventLoop::run_app`. Since
/// `EventLoop::run_app` takes ownership of the `EventLoop`, we can be certain
/// that this event loop will never be run again.
Destroyed,
}
impl RunnerEnum {
fn maybe_runner(&self) -> Option<&Runner> {
match self {
RunnerEnum::Running(runner) => Some(runner),
_ => None,
}
}
}
struct Runner {
state: State,
app: Box<dyn ApplicationHandler>,
event_loop: ActiveEventLoop,
}
impl fmt::Debug for Runner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Runner")
.field("state", &self.state)
.field("app", &"<ApplicationHandler>")
.field("event_loop", &self.event_loop)
.finish()
}
}
impl Runner {
pub fn new(app: Box<dyn ApplicationHandler>, event_loop: ActiveEventLoop) -> Self {
Runner { state: State::Init, app, event_loop }
}
/// Returns the corresponding `StartCause` for the current `state`, or `None`
/// when in `Exit` state.
fn maybe_start_cause(&self) -> Option<StartCause> {
Some(match self.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 None,
})
}
fn handle_single_event(&mut self, runner: &Shared, event: Event) {
match event {
Event::NewEvents(cause) => self.app.new_events(&self.event_loop, cause),
Event::WindowEvent { window_id, event } => {
self.app.window_event(&self.event_loop, window_id, event)
},
Event::ScaleChange { canvas, size, scale } => {
if let Some(canvas) = canvas.upgrade() {
canvas.handle_scale_change(
runner,
|window_id, event| {
self.app.window_event(&self.event_loop, window_id, event);
},
size,
scale,
)
}
},
Event::DeviceEvent { device_id, event } => {
self.app.device_event(&self.event_loop, device_id, event)
},
Event::UserWakeUp => self.app.proxy_wake_up(&self.event_loop),
Event::Suspended => self.app.suspended(&self.event_loop),
Event::Resumed => self.app.resumed(&self.event_loop),
Event::CreateSurfaces => self.app.can_create_surfaces(&self.event_loop),
Event::AboutToWait => self.app.about_to_wait(&self.event_loop),
}
}
}
impl Shared {
pub fn new() -> Self {
let main_thread = MainThreadMarker::new().expect("only callable from inside the `Window`");
#[allow(clippy::disallowed_methods)]
let window = web_sys::window().expect("only callable from inside the `Window`");
#[allow(clippy::disallowed_methods)]
let navigator = window.navigator();
#[allow(clippy::disallowed_methods)]
let document = window.document().expect("Failed to obtain document");
Shared(Rc::<Execution>::new_cyclic(|weak| {
let proxy_spawner = EventLoopProxy::new(main_thread, WeakShared(weak.clone()));
let monitor = MonitorHandler::new(
main_thread,
window.clone(),
&navigator,
WeakShared(weak.clone()),
);
let safe_area = SafeAreaHandle::new(&window, &document);
Execution {
main_thread,
event_loop_proxy: Arc::new(proxy_spawner),
control_flow: Cell::new(ControlFlow::default()),
poll_strategy: Cell::new(PollStrategy::default()),
wait_until_strategy: Cell::new(WaitUntilStrategy::default()),
exit: Cell::new(false),
runner: RefCell::new(RunnerEnum::Pending),
suspended: Cell::new(false),
event_loop_recreation: Cell::new(false),
events: RefCell::new(VecDeque::new()),
window,
navigator,
document,
id: Cell::new(0),
all_canvases: RefCell::new(Vec::new()),
redraw_pending: RefCell::new(HashSet::new()),
destroy_pending: RefCell::new(VecDeque::new()),
monitor: Rc::new(monitor),
safe_area: Rc::new(safe_area),
page_transition_event_handle: RefCell::new(None),
device_events: Cell::default(),
on_mouse_move: RefCell::new(None),
on_wheel: RefCell::new(None),
on_mouse_press: RefCell::new(None),
on_mouse_release: RefCell::new(None),
on_key_press: RefCell::new(None),
on_key_release: RefCell::new(None),
on_visibility_change: RefCell::new(None),
}
}))
}
pub fn main_thread(&self) -> MainThreadMarker {
self.0.main_thread
}
pub fn window(&self) -> &web_sys::Window {
&self.0.window
}
pub fn navigator(&self) -> &Navigator {
&self.0.navigator
}
pub fn document(&self) -> &Document {
&self.0.document
}
pub fn add_canvas(
&self,
id: WindowId,
canvas: Weak<backend::Canvas>,
runner: DispatchRunner<Inner>,
) {
self.0.all_canvases.borrow_mut().push((id, canvas, runner));
}
pub fn notify_destroy_window(&self, id: WindowId) {
self.0.destroy_pending.borrow_mut().push_back(id);
}
pub(crate) fn start(&self, app: Box<dyn ApplicationHandler>, event_loop: ActiveEventLoop) {
let mut runner = self.0.runner.borrow_mut();
assert!(matches!(*runner, RunnerEnum::Pending));
if self.0.monitor.is_initializing() {
*runner = RunnerEnum::Initializing(Runner::new(app, event_loop));
} else {
*runner = RunnerEnum::Running(Runner::new(app, event_loop));
drop(runner);
self.init();
self.set_listener();
}
}
pub(crate) fn start_delayed(&self) {
let event_handler = match self.0.runner.replace(RunnerEnum::Pending) {
RunnerEnum::Initializing(event_handler) => event_handler,
// The event loop wasn't started yet.
RunnerEnum::Pending => return,
_ => unreachable!("event loop already started before waiting for initialization"),
};
*self.0.runner.borrow_mut() = RunnerEnum::Running(event_handler);
self.init();
self.set_listener();
}
// 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 RootActiveEventLoop reference
fn set_listener(&self) {
*self.0.page_transition_event_handle.borrow_mut() = Some(backend::on_page_transition(
self.window().clone(),
{
let runner = self.clone();
move |event: PageTransitionEvent| {
if event.persisted() {
runner.0.suspended.set(false);
runner.send_event(Event::Resumed);
}
}
},
{
let runner = self.clone();
move |event: PageTransitionEvent| {
runner.0.suspended.set(true);
if event.persisted() {
runner.send_event(Event::Suspended);
} else {
runner.handle_unload();
}
}
},
));
let runner = self.clone();
let window = self.window().clone();
let navigator = self.navigator().clone();
*self.0.on_mouse_move.borrow_mut() = Some(EventListenerHandle::new(
self.window().clone(),
"pointermove",
Closure::new(move |event: PointerEvent| {
if !runner.device_events() {
return;
}
// chorded button event
let device_id = event::mkdid(event.pointer_id());
if let Some(button) = backend::event::mouse_button(&event) {
let state = if backend::event::mouse_buttons(&event).contains(button.into()) {
ElementState::Pressed
} else {
ElementState::Released
};
runner.send_event(Event::DeviceEvent {
device_id,
event: DeviceEvent::Button {
button: mouse_button_to_id(button).into(),
state,
},
});
return;
}
// pointer move event
let mut delta = backend::event::MouseDelta::init(&navigator, &event);
runner.send_events(backend::event::pointer_move_event(event).map(|event| {
let delta = delta.delta(&event).to_physical(backend::scale_factor(&window));
Event::DeviceEvent {
device_id,
event: DeviceEvent::PointerMotion { delta: (delta.x, delta.y) },
}
}));
}),
));
let runner = self.clone();
let window = self.window().clone();
*self.0.on_wheel.borrow_mut() = Some(EventListenerHandle::new(
self.window().clone(),
"wheel",
Closure::new(move |event: WheelEvent| {
if !runner.device_events() {
return;
}
if let Some(delta) = backend::event::mouse_scroll_delta(&window, &event) {
runner.send_event(Event::DeviceEvent {
device_id: None,
event: DeviceEvent::MouseWheel { delta },
});
}
}),
));
let runner = self.clone();
*self.0.on_mouse_press.borrow_mut() = Some(EventListenerHandle::new(
self.window().clone(),
"pointerdown",
Closure::new(move |event: PointerEvent| {
if !runner.device_events() {
return;
}
let button = backend::event::mouse_button(&event).expect("no mouse button pressed");
runner.send_event(Event::DeviceEvent {
device_id: event::mkdid(event.pointer_id()),
event: DeviceEvent::Button {
button: mouse_button_to_id(button).into(),
state: ElementState::Pressed,
},
});
}),
));
let runner = self.clone();
*self.0.on_mouse_release.borrow_mut() = Some(EventListenerHandle::new(
self.window().clone(),
"pointerup",
Closure::new(move |event: PointerEvent| {
if !runner.device_events() {
return;
}
let button = backend::event::mouse_button(&event).expect("no mouse button pressed");
runner.send_event(Event::DeviceEvent {
device_id: event::mkdid(event.pointer_id()),
event: DeviceEvent::Button {
button: mouse_button_to_id(button).into(),
state: ElementState::Released,
},
});
}),
));
let runner = self.clone();
*self.0.on_key_press.borrow_mut() = Some(EventListenerHandle::new(
self.window().clone(),
"keydown",
Closure::new(move |event: KeyboardEvent| {
if !runner.device_events() {
return;
}
runner.send_event(Event::DeviceEvent {
device_id: None,
event: DeviceEvent::Key(RawKeyEvent {
physical_key: backend::event::key_code(&event),
state: ElementState::Pressed,
}),
});
}),
));
let runner = self.clone();
*self.0.on_key_release.borrow_mut() = Some(EventListenerHandle::new(
self.window().clone(),
"keyup",
Closure::new(move |event: KeyboardEvent| {
if !runner.device_events() {
return;
}
runner.send_event(Event::DeviceEvent {
device_id: None,
event: DeviceEvent::Key(RawKeyEvent {
physical_key: backend::event::key_code(&event),
state: ElementState::Released,
}),
});
}),
));
let runner = self.clone();
*self.0.on_visibility_change.borrow_mut() = Some(EventListenerHandle::new(
// Safari <14 doesn't support the `visibilitychange` event on `Window`.
self.document().clone(),
"visibilitychange",
Closure::new(move |_| {
if !runner.0.suspended.get() {
for (id, canvas, _) in &*runner.0.all_canvases.borrow() {
if let Some(canvas) = canvas.upgrade() {
let is_visible = backend::is_visible(runner.document());
// only fire if:
// - not visible and intersects
// - not visible and we don't know if it intersects yet
// - visible and intersects
if let (false, Some(true) | None) | (true, Some(true)) =
(is_visible, canvas.is_intersecting.get())
{
runner.send_event(Event::WindowEvent {
window_id: *id,
event: WindowEvent::Occluded(!is_visible),
});
}
}
}
}
}),
));
}
// Generate a strictly increasing ID
// This is used to differentiate windows when handling events
pub fn generate_id(&self) -> usize {
let id = self.0.id.get();
self.0.id.set(id.checked_add(1).expect("exhausted `WindowId`"));
id
}
pub fn request_redraw(&self, id: WindowId) {
self.0.redraw_pending.borrow_mut().insert(id);
self.send_events([]);
}
fn init(&self) {
// NB: For consistency all platforms must call `can_create_surfaces` even though Web
// applications don't themselves have a formal surface destroy/create lifecycle.
self.run_until_cleared(
[Event::NewEvents(StartCause::Init), Event::CreateSurfaces].into_iter(),
);
}
// Run the polling logic for the Poll ControlFlow, which involves clearing the queue
pub fn poll(&self) {
let start_cause = Event::NewEvents(StartCause::Poll);
self.run_until_cleared(iter::once(start_cause));
}
// Run the logic for waking from a WaitUntil, which involves clearing the queue
// Generally there shouldn't be events built up when this is called
pub fn resume_time_reached(&self, start: Instant, requested_resume: Instant) {
let start_cause =
Event::NewEvents(StartCause::ResumeTimeReached { start, requested_resume });
self.run_until_cleared(iter::once(start_cause));
}
// Add an event to the event loop runner, from the user or an event handler
//
// It will determine if the event should be immediately sent to the user or buffered for later
pub(crate) fn send_event(&self, event: Event) {
self.send_events(iter::once(event));
}
// Add a user event to the event loop runner.
//
// This will schedule the event loop to wake up instead of waking it up immediately if its not
// running.
pub(crate) fn send_proxy_wake_up(&self, local: bool) {
// If the event loop is closed, it should discard any new events
if self.is_closed() {
return;
}
if local {
// If the loop is not running and triggered locally, queue on next microtick.
if let Ok(RunnerEnum::Running(_)) =
self.0.runner.try_borrow().as_ref().map(Deref::deref)
{
self.window().queue_microtask(
&Closure::once_into_js({
let this = Rc::downgrade(&self.0);
move || {
if let Some(shared) = this.upgrade() {
Shared(shared).send_event(Event::UserWakeUp)
}
}
})
.unchecked_into(),
);
return;
}
}
self.send_event(Event::UserWakeUp);
}
// Add a series of events to the event loop runner
//
// It will determine if the event should be immediately sent to the user or buffered for later
pub(crate) fn send_events(&self, events: impl IntoIterator<Item = Event>) {
// If the event loop is closed, it should discard any new events
if self.is_closed() {
return;
}
// If we can run the event processing right now, or need to queue this and wait for later
let mut process_immediately = true;
match self.0.runner.try_borrow().as_ref().map(Deref::deref) {
// If the runner is attached but not running, we always wake it up.
Ok(RunnerEnum::Running(_)) => (),
// The runner still hasn't been attached: queue this event and wait for it to be
Ok(RunnerEnum::Pending | RunnerEnum::Initializing(_)) => {
process_immediately = false;
},
// Some other code is mutating the runner, which most likely means
// the event loop is running and busy. So we queue this event for
// it to be processed later.
Err(_) => {
process_immediately = false;
},
// This is unreachable since `self.is_closed() == true`.
Ok(RunnerEnum::Destroyed) => unreachable!(),
}
if !process_immediately {
// Queue these events to look at later
self.0.events.borrow_mut().extend(events);
return;
}
// At this point, we know this is a fresh set of events
// Now we determine why new events are incoming, and handle the events
let start_cause = match (self.0.runner.borrow().maybe_runner())
.unwrap_or_else(|| {
unreachable!("The runner cannot process events when it is not attached")
})
.maybe_start_cause()
{
Some(c) => c,
// If we're in the exit state, don't do event processing
None => return,
};
// Take the start event, then the events provided to this function, and run an iteration of
// the event loop
let start_event = Event::NewEvents(start_cause);
let events = iter::once(start_event).chain(events);
self.run_until_cleared(events);
}
// Process the destroy-pending windows. This should only be called from
// `run_until_cleared`, somewhere between emitting `NewEvents` and `AboutToWait`.
fn process_destroy_pending_windows(&self) {
while let Some(id) = self.0.destroy_pending.borrow_mut().pop_front() {
self.0.all_canvases.borrow_mut().retain(|&(item_id, ..)| item_id != id);
self.handle_event(Event::WindowEvent {
window_id: id,
event: winit_core::event::WindowEvent::Destroyed,
});
self.0.redraw_pending.borrow_mut().remove(&id);
}
}
// Given the set of new events, run the event loop until the main events and redraw events are
// cleared
//
// This will also process any events that have been queued or that are queued during processing
fn run_until_cleared(&self, events: impl Iterator<Item = Event>) {
for event in events {
self.handle_event(event);
}
self.process_destroy_pending_windows();
// 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,
});
}
self.handle_event(Event::AboutToWait);
self.apply_control_flow();
// 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_loop_destroyed();
}
}
fn handle_unload(&self) {
self.exit();
self.apply_control_flow();
// We don't call `handle_loop_destroyed` here because we don't need to
// perform cleanup when the Web browser is going to destroy the page.
//
// We do want to run the application handler's `Drop` impl.
*self.0.runner.borrow_mut() = RunnerEnum::Destroyed;
}
// handle_event takes in events and either queues them or applies a callback
//
// It should only ever be called from `run_until_cleared`.
fn handle_event(&self, event: Event) {
if self.is_closed() {
self.exit();
}
match *self.0.runner.borrow_mut() {
RunnerEnum::Running(ref mut runner) => {
runner.handle_single_event(self, event);
},
// If an event is being handled without a runner somehow, add it to the event queue so
// it will eventually be processed
RunnerEnum::Pending => self.0.events.borrow_mut().push_back(event),
// If the Runner has been destroyed, there is nothing to do.
RunnerEnum::Destroyed => return,
// This function should never be called if we are still waiting for something.
RunnerEnum::Initializing(_) => unreachable!(),
}
let is_closed = self.exiting();
// 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().maybe_runner().is_some() {
// Pre-fetch window commands to avoid having to wait until the next event loop cycle
// and potentially block other threads in the meantime.
for (_, window, runner) in self.0.all_canvases.borrow().iter() {
if let Some(window) = window.upgrade() {
runner.run(self.main_thread());
drop(window)
}
}
// Take an event out of the queue and handle it
// Make sure not to let the borrow_mut live during the next handle_event
let event = {
let mut events = self.0.events.borrow_mut();
// Pre-fetch `UserEvent`s to avoid having to wait until the next event loop cycle.
events.extend(self.0.event_loop_proxy.take().then_some(Event::UserWakeUp));
events.pop_front()
};
if let Some(event) = event {
self.handle_event(event);
}
}
}
// Apply the new ControlFlow that has been selected by the user
// Start any necessary timeouts etc
fn apply_control_flow(&self) {
let new_state = if self.exiting() {
State::Exit
} else {
match self.control_flow() {
ControlFlow::Poll => {
let cloned = self.clone();
State::Poll {
_request: backend::Schedule::new(
self.poll_strategy(),
self.window(),
move || cloned.poll(),
),
}
},
ControlFlow::Wait => State::Wait { start: Instant::now() },
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::Schedule::new_with_duration(
self.wait_until_strategy(),
self.window(),
move || cloned.resume_time_reached(start, end),
delay,
),
}
},
}
};
if let RunnerEnum::Running(ref mut runner) = *self.0.runner.borrow_mut() {
runner.state = new_state;
}
}
fn handle_loop_destroyed(&self) {
let all_canvases = std::mem::take(&mut *self.0.all_canvases.borrow_mut());
*self.0.page_transition_event_handle.borrow_mut() = None;
*self.0.on_mouse_move.borrow_mut() = None;
*self.0.on_wheel.borrow_mut() = None;
*self.0.on_mouse_press.borrow_mut() = None;
*self.0.on_mouse_release.borrow_mut() = None;
*self.0.on_key_press.borrow_mut() = None;
*self.0.on_key_release.borrow_mut() = None;
*self.0.on_visibility_change.borrow_mut() = None;
// Dropping the `Runner` drops the event handler closure, which will in
// turn drop all `Window`s moved into the closure.
*self.0.runner.borrow_mut() = RunnerEnum::Destroyed;
for (_, canvas, _) in all_canvases {
// In case any remaining `Window`s are still not dropped, we will need
// to explicitly remove the event handlers associated with their canvases.
if let Some(canvas) = canvas.upgrade() {
canvas.remove_listeners();
}
}
// At this point, the `self.0` `Rc` should only be strongly referenced
// by the following:
// * `self`, i.e. the item which triggered this event loop wakeup, which is usually a
// `wasm-bindgen` `Closure`, which will be dropped after returning to the JS glue code.
// * The `ActiveEventLoop` leaked inside `EventLoop::run_app` due to the JS exception thrown
// at the end.
// * For each undropped `Window`:
// * The `register_redraw_request` closure.
// * The `destroy_fn` closure.
if self.0.event_loop_recreation.get() {
EventLoop::allow_event_loop_recreation();
}
}
// Check if the event loop is currently closed
fn is_closed(&self) -> bool {
match self.0.runner.try_borrow().as_ref().map(Deref::deref) {
Ok(RunnerEnum::Running(runner)) => runner.state.exiting(),
// The event loop is not closed since it is not initialized.
Ok(RunnerEnum::Pending) => false,
// The event loop is closed since it has been destroyed.
Ok(RunnerEnum::Destroyed) => true,
// The event loop is not closed since its still waiting to be started.
Ok(RunnerEnum::Initializing(_)) => false,
// Some other code is mutating the runner, which most likely means
// the event loop is running and busy.
Err(_) => false,
}
}
pub fn listen_device_events(&self, allowed: DeviceEvents) {
self.0.device_events.set(allowed)
}
fn device_events(&self) -> bool {
match self.0.device_events.get() {
DeviceEvents::Always => true,
DeviceEvents::WhenFocused => {
self.0.all_canvases.borrow().iter().any(|(_, canvas, _)| {
if let Some(canvas) = canvas.upgrade() {
canvas.has_focus.get()
} else {
false
}
})
},
DeviceEvents::Never => false,
}
}
pub fn event_loop_recreation(&self, allow: bool) {
self.0.event_loop_recreation.set(allow)
}
pub(crate) fn control_flow(&self) -> ControlFlow {
self.0.control_flow.get()
}
pub(crate) fn set_control_flow(&self, control_flow: ControlFlow) {
self.0.control_flow.set(control_flow)
}
pub(crate) fn exit(&self) {
self.0.exit.set(true)
}
pub(crate) fn exiting(&self) -> bool {
self.0.exit.get()
}
pub(crate) fn set_poll_strategy(&self, strategy: PollStrategy) {
self.0.poll_strategy.set(strategy)
}
pub(crate) fn poll_strategy(&self) -> PollStrategy {
self.0.poll_strategy.get()
}
pub(crate) fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
self.0.wait_until_strategy.set(strategy)
}
pub(crate) fn wait_until_strategy(&self) -> WaitUntilStrategy {
self.0.wait_until_strategy.get()
}
pub(crate) fn event_loop_proxy(&self) -> &Arc<EventLoopProxy> {
&self.0.event_loop_proxy
}
pub(crate) fn weak(&self) -> WeakShared {
WeakShared(Rc::downgrade(&self.0))
}
pub(crate) fn monitor(&self) -> &Rc<MonitorHandler> {
&self.0.monitor
}
pub(crate) fn safe_area(&self) -> &Rc<SafeAreaHandle> {
&self.0.safe_area
}
}
#[derive(Clone, Debug)]
pub struct WeakShared(Weak<Execution>);
impl WeakShared {
pub fn upgrade(&self) -> Option<Shared> {
self.0.upgrade().map(Shared)
}
}
#[allow(clippy::enum_variant_names)]
pub(crate) enum Event {
NewEvents(StartCause),
WindowEvent { window_id: WindowId, event: WindowEvent },
ScaleChange { canvas: Weak<backend::Canvas>, size: PhysicalSize<u32>, scale: f64 },
DeviceEvent { device_id: Option<DeviceId>, event: DeviceEvent },
Suspended,
CreateSurfaces,
Resumed,
AboutToWait,
UserWakeUp,
}

View file

@ -0,0 +1,18 @@
use web_time::Instant;
use super::backend;
#[derive(Debug)]
pub enum State {
Init,
WaitUntil { _timeout: backend::Schedule, start: Instant, end: Instant },
Wait { start: Instant },
Poll { _request: backend::Schedule },
Exit,
}
impl State {
pub fn exiting(&self) -> bool {
matches!(self, State::Exit)
}
}

View file

@ -0,0 +1,574 @@
use std::cell::Cell;
use std::clone::Clone;
use std::iter;
use std::rc::Rc;
use std::sync::Arc;
use web_sys::Element;
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor as CoreCustomCursor, CustomCursorSource};
use winit_core::error::{NotSupportedError, RequestError};
use winit_core::event::{ElementState, KeyEvent, TouchPhase, WindowEvent};
use winit_core::event_loop::{
ActiveEventLoop as RootActiveEventLoop, ControlFlow, DeviceEvents,
EventLoopProxy as RootEventLoopProxy, OwnedDisplayHandle as CoreOwnedDisplayHandle,
};
use winit_core::keyboard::ModifiersState;
use winit_core::monitor::MonitorHandle as CoremMonitorHandle;
use winit_core::window::{Theme, WindowId};
use super::super::lock;
use super::super::monitor::MonitorPermissionFuture;
use super::runner::Event;
use super::{backend, runner};
use crate::cursor::CustomCursor;
use crate::event_loop::proxy::EventLoopProxy;
use crate::window::Window;
use crate::{CustomCursorFuture, PollStrategy, WaitUntilStrategy};
#[derive(Default, Debug)]
struct ModifiersShared(Rc<Cell<ModifiersState>>);
impl ModifiersShared {
fn set(&self, new: ModifiersState) {
self.0.set(new)
}
fn get(&self) -> ModifiersState {
self.0.get()
}
}
impl Clone for ModifiersShared {
fn clone(&self) -> Self {
Self(Rc::clone(&self.0))
}
}
#[derive(Clone, Debug)]
pub struct ActiveEventLoop {
pub(crate) runner: runner::Shared,
modifiers: ModifiersShared,
}
impl ActiveEventLoop {
pub fn new() -> Self {
Self { runner: runner::Shared::new(), modifiers: ModifiersShared::default() }
}
pub(crate) fn run(&self, app: Box<dyn ApplicationHandler>, event_loop_recreation: bool) {
self.runner.event_loop_recreation(event_loop_recreation);
self.runner.start(app, self.clone());
}
pub fn generate_id(&self) -> WindowId {
WindowId::from_raw(self.runner.generate_id())
}
pub fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture {
CustomCursorFuture(CustomCursor::new_async(self, source))
}
pub fn register(&self, canvas: &Rc<backend::Canvas>, window_id: WindowId) {
let canvas_clone = canvas.clone();
canvas.on_touch_start();
let runner = self.runner.clone();
let has_focus = canvas.has_focus.clone();
let modifiers = self.modifiers.clone();
canvas.on_blur(move || {
has_focus.set(false);
let clear_modifiers = (!modifiers.get().is_empty()).then(|| {
modifiers.set(ModifiersState::empty());
Event::WindowEvent {
window_id,
event: WindowEvent::ModifiersChanged(ModifiersState::empty().into()),
}
});
runner.send_events(clear_modifiers.into_iter().chain(iter::once(Event::WindowEvent {
window_id,
event: WindowEvent::Focused(false),
})));
});
let runner = self.runner.clone();
let has_focus = canvas.has_focus.clone();
canvas.on_focus(move || {
if !has_focus.replace(true) {
runner.send_event(Event::WindowEvent {
window_id,
event: WindowEvent::Focused(true),
});
}
});
// It is possible that at this point the canvas has
// been focused before the callback can be called.
let focused = canvas
.document()
.active_element()
.filter(|element| {
let canvas: &Element = canvas.raw();
element == canvas
})
.is_some();
if focused {
canvas.has_focus.set(true);
self.runner
.send_event(Event::WindowEvent { window_id, event: WindowEvent::Focused(true) })
}
let runner = self.runner.clone();
let modifiers = self.modifiers.clone();
canvas.on_keyboard_press(
move |physical_key, logical_key, text, location, repeat, active_modifiers| {
let modifiers_changed = (modifiers.get() != active_modifiers).then(|| {
modifiers.set(active_modifiers);
Event::WindowEvent {
window_id,
event: WindowEvent::ModifiersChanged(active_modifiers.into()),
}
});
runner.send_events(
iter::once(Event::WindowEvent {
window_id,
event: WindowEvent::KeyboardInput {
device_id: None,
event: KeyEvent {
physical_key,
logical_key: logical_key.clone(),
text: text.clone(),
location,
state: ElementState::Pressed,
repeat,
text_with_all_modifiers: text,
key_without_modifiers: logical_key,
},
is_synthetic: false,
},
})
.chain(modifiers_changed),
);
},
);
let runner = self.runner.clone();
let modifiers = self.modifiers.clone();
canvas.on_keyboard_release(
move |physical_key, logical_key, text, location, repeat, active_modifiers| {
let modifiers_changed = (modifiers.get() != active_modifiers).then(|| {
modifiers.set(active_modifiers);
Event::WindowEvent {
window_id,
event: WindowEvent::ModifiersChanged(active_modifiers.into()),
}
});
runner.send_events(
iter::once(Event::WindowEvent {
window_id,
event: WindowEvent::KeyboardInput {
device_id: None,
event: KeyEvent {
physical_key,
logical_key: logical_key.clone(),
text: text.clone(),
location,
state: ElementState::Released,
repeat,
text_with_all_modifiers: text,
key_without_modifiers: logical_key,
},
is_synthetic: false,
},
})
.chain(modifiers_changed),
)
},
);
let has_focus = canvas.has_focus.clone();
canvas.on_pointer_leave({
let runner = self.runner.clone();
let has_focus = has_focus.clone();
let modifiers = self.modifiers.clone();
move |active_modifiers, device_id, primary, position, kind| {
let focus = (has_focus.get() && modifiers.get() != active_modifiers).then(|| {
modifiers.set(active_modifiers);
Event::WindowEvent {
window_id,
event: WindowEvent::ModifiersChanged(active_modifiers.into()),
}
});
runner.send_events(focus.into_iter().chain(iter::once(Event::WindowEvent {
window_id,
event: WindowEvent::PointerLeft {
device_id,
primary,
position: Some(position),
kind,
},
})))
}
});
canvas.on_pointer_enter({
let runner = self.runner.clone();
let has_focus = has_focus.clone();
let modifiers = self.modifiers.clone();
move |active_modifiers, device_id, primary, position, kind| {
let focus = (has_focus.get() && modifiers.get() != active_modifiers).then(|| {
modifiers.set(active_modifiers);
Event::WindowEvent {
window_id,
event: WindowEvent::ModifiersChanged(active_modifiers.into()),
}
});
runner.send_events(focus.into_iter().chain(iter::once(Event::WindowEvent {
window_id,
event: WindowEvent::PointerEntered { device_id, primary, position, kind },
})))
}
});
canvas.on_pointer_move(
{
let runner = self.runner.clone();
let has_focus = has_focus.clone();
let modifiers = self.modifiers.clone();
move |device_id, events| {
runner.send_events(events.flat_map(
|(active_modifiers, primary, position, source)| {
let modifiers = (has_focus.get()
&& modifiers.get() != active_modifiers)
.then(|| {
modifiers.set(active_modifiers);
Event::WindowEvent {
window_id,
event: WindowEvent::ModifiersChanged(
active_modifiers.into(),
),
}
});
modifiers.into_iter().chain(iter::once(Event::WindowEvent {
window_id,
event: WindowEvent::PointerMoved {
device_id,
primary,
position,
source,
},
}))
},
));
}
},
{
let runner = self.runner.clone();
let has_focus = has_focus.clone();
let modifiers = self.modifiers.clone();
move |active_modifiers, device_id, primary, position, state, button| {
let modifiers =
(has_focus.get() && modifiers.get() != active_modifiers).then(|| {
modifiers.set(active_modifiers);
Event::WindowEvent {
window_id,
event: WindowEvent::ModifiersChanged(active_modifiers.into()),
}
});
runner.send_events(modifiers.into_iter().chain([Event::WindowEvent {
window_id,
event: WindowEvent::PointerButton {
device_id,
primary,
state,
position,
button,
},
}]));
}
},
);
canvas.on_pointer_press({
let runner = self.runner.clone();
let modifiers = self.modifiers.clone();
move |active_modifiers, device_id, primary, position, button| {
let modifiers = (modifiers.get() != active_modifiers).then(|| {
modifiers.set(active_modifiers);
Event::WindowEvent {
window_id,
event: WindowEvent::ModifiersChanged(active_modifiers.into()),
}
});
runner.send_events(modifiers.into_iter().chain(iter::once(Event::WindowEvent {
window_id,
event: WindowEvent::PointerButton {
device_id,
primary,
state: ElementState::Pressed,
position,
button,
},
})));
}
});
canvas.on_pointer_release({
let runner = self.runner.clone();
let has_focus = has_focus.clone();
let modifiers = self.modifiers.clone();
move |active_modifiers, device_id, primary, position, button| {
let modifiers =
(has_focus.get() && modifiers.get() != active_modifiers).then(|| {
modifiers.set(active_modifiers);
Event::WindowEvent {
window_id,
event: WindowEvent::ModifiersChanged(active_modifiers.into()),
}
});
runner.send_events(modifiers.into_iter().chain(iter::once(Event::WindowEvent {
window_id,
event: WindowEvent::PointerButton {
device_id,
primary,
state: ElementState::Released,
position,
button,
},
})));
}
});
let runner = self.runner.clone();
let modifiers = self.modifiers.clone();
canvas.on_mouse_wheel(move |delta, active_modifiers| {
let modifiers_changed =
(has_focus.get() && modifiers.get() != active_modifiers).then(|| {
modifiers.set(active_modifiers);
Event::WindowEvent {
window_id,
event: WindowEvent::ModifiersChanged(active_modifiers.into()),
}
});
runner.send_events(modifiers_changed.into_iter().chain(iter::once(
Event::WindowEvent {
window_id,
event: WindowEvent::MouseWheel {
device_id: None,
delta,
phase: TouchPhase::Moved,
},
},
)));
});
let runner = self.runner.clone();
canvas.on_dark_mode(move |is_dark_mode| {
let theme = if is_dark_mode { Theme::Dark } else { Theme::Light };
runner.send_event(Event::WindowEvent {
window_id,
event: WindowEvent::ThemeChanged(theme),
});
});
canvas.on_resize_scale(
{
let runner = self.runner.clone();
let canvas = canvas_clone.clone();
move |size, scale| {
runner.send_event(Event::ScaleChange {
canvas: Rc::downgrade(&canvas),
size,
scale,
})
}
},
{
let runner = self.runner.clone();
let canvas = canvas_clone.clone();
move |new_size| {
canvas.set_current_size(new_size);
if canvas.old_size() != new_size {
canvas.set_old_size(new_size);
runner.send_event(Event::WindowEvent {
window_id,
event: WindowEvent::SurfaceResized(new_size),
});
canvas.request_animation_frame();
}
}
},
);
let runner = self.runner.clone();
canvas.on_intersection(move |is_intersecting| {
// only fire if visible while skipping the first event if it's intersecting
if backend::is_visible(runner.document())
&& !(is_intersecting && canvas_clone.is_intersecting.get().is_none())
{
runner.send_event(Event::WindowEvent {
window_id,
event: WindowEvent::Occluded(!is_intersecting),
});
}
canvas_clone.is_intersecting.set(Some(is_intersecting));
});
let runner = self.runner.clone();
canvas.on_animation_frame(move || runner.request_redraw(window_id));
canvas.on_context_menu();
}
pub(crate) fn set_poll_strategy(&self, strategy: PollStrategy) {
self.runner.set_poll_strategy(strategy)
}
pub(crate) fn poll_strategy(&self) -> PollStrategy {
self.runner.poll_strategy()
}
pub(crate) fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
self.runner.set_wait_until_strategy(strategy)
}
pub(crate) fn wait_until_strategy(&self) -> WaitUntilStrategy {
self.runner.wait_until_strategy()
}
pub(crate) fn is_cursor_lock_raw(&self) -> bool {
lock::is_cursor_lock_raw(self.runner.navigator(), self.runner.document())
}
pub(crate) fn has_multiple_screens(&self) -> Result<bool, NotSupportedError> {
self.runner
.monitor()
.is_extended()
.ok_or(NotSupportedError::new("has_multiple_screens is not supported"))
}
pub(crate) fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture {
self.runner.monitor().request_detailed_monitor_permission()
}
pub(crate) fn has_detailed_monitor_permission(&self) -> bool {
self.runner.monitor().has_detailed_monitor_permission()
}
pub(crate) fn event_loop_proxy(&self) -> Arc<EventLoopProxy> {
self.runner.event_loop_proxy().clone()
}
}
impl RootActiveEventLoop for ActiveEventLoop {
fn create_proxy(&self) -> RootEventLoopProxy {
let event_loop_proxy = self.event_loop_proxy();
RootEventLoopProxy::new(event_loop_proxy)
}
fn create_window(
&self,
window_attributes: winit_core::window::WindowAttributes,
) -> Result<Box<dyn winit_core::window::Window>, RequestError> {
let window = Window::new(self, window_attributes)?;
Ok(Box::new(window))
}
fn create_custom_cursor(
&self,
source: CustomCursorSource,
) -> Result<CoreCustomCursor, RequestError> {
Ok(CoreCustomCursor(Arc::new(CustomCursor::new(self, source))))
}
fn available_monitors(&self) -> Box<dyn Iterator<Item = CoremMonitorHandle>> {
Box::new(
self.runner
.monitor()
.available_monitors()
.into_iter()
.map(|monitor| CoremMonitorHandle(Arc::new(monitor))),
)
}
fn primary_monitor(&self) -> Option<CoremMonitorHandle> {
self.runner.monitor().primary_monitor().map(|monitor| CoremMonitorHandle(Arc::new(monitor)))
}
fn listen_device_events(&self, allowed: DeviceEvents) {
self.runner.listen_device_events(allowed)
}
fn system_theme(&self) -> Option<Theme> {
backend::is_dark_mode(self.runner.window()).map(|is_dark_mode| {
if is_dark_mode {
Theme::Dark
} else {
Theme::Light
}
})
}
fn set_control_flow(&self, control_flow: ControlFlow) {
self.runner.set_control_flow(control_flow)
}
fn control_flow(&self) -> ControlFlow {
self.runner.control_flow()
}
fn exit(&self) {
self.runner.exit()
}
fn exiting(&self) -> bool {
self.runner.exiting()
}
fn owned_display_handle(&self) -> CoreOwnedDisplayHandle {
CoreOwnedDisplayHandle::new(Arc::new(OwnedDisplayHandle))
}
fn rwh_06_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
self
}
}
impl rwh_06::HasDisplayHandle for ActiveEventLoop {
fn display_handle(&self) -> Result<rwh_06::DisplayHandle<'_>, rwh_06::HandleError> {
let raw = rwh_06::RawDisplayHandle::Web(rwh_06::WebDisplayHandle::new());
unsafe { Ok(rwh_06::DisplayHandle::borrow_raw(raw)) }
}
}
#[derive(Clone)]
pub(crate) struct OwnedDisplayHandle;
impl rwh_06::HasDisplayHandle for OwnedDisplayHandle {
fn display_handle(&self) -> Result<rwh_06::DisplayHandle<'_>, rwh_06::HandleError> {
let raw = rwh_06::RawDisplayHandle::Web(rwh_06::WebDisplayHandle::new());
unsafe { Ok(rwh_06::DisplayHandle::borrow_raw(raw)) }
}
}

530
winit-web/src/keyboard.rs Normal file
View file

@ -0,0 +1,530 @@
use smol_str::SmolStr;
use winit_core::keyboard::{Key, KeyCode, NamedKey, NativeKey, NativeKeyCode, PhysicalKey};
pub trait FromAttributeValue {
fn from_attribute_value(kav: &str) -> Self
where
Self: Sized;
}
impl FromAttributeValue for Key {
fn from_attribute_value(kav: &str) -> Self {
Key::Named(match kav {
"Unidentified" => return Key::Unidentified(NativeKey::Web(SmolStr::new(kav))),
"Dead" => return Key::Dead(None),
"Alt" => NamedKey::Alt,
"AltGraph" => NamedKey::AltGraph,
"CapsLock" => NamedKey::CapsLock,
"Control" => NamedKey::Control,
"Fn" => NamedKey::Fn,
"FnLock" => NamedKey::FnLock,
"NumLock" => NamedKey::NumLock,
"ScrollLock" => NamedKey::ScrollLock,
"Shift" => NamedKey::Shift,
"Symbol" => NamedKey::Symbol,
"SymbolLock" => NamedKey::SymbolLock,
#[allow(deprecated)]
"Super" => NamedKey::Super,
#[allow(deprecated)]
"Hyper" => NamedKey::Hyper,
"Meta" => NamedKey::Meta,
"Enter" => NamedKey::Enter,
"Tab" => NamedKey::Tab,
"ArrowDown" => NamedKey::ArrowDown,
"ArrowLeft" => NamedKey::ArrowLeft,
"ArrowRight" => NamedKey::ArrowRight,
"ArrowUp" => NamedKey::ArrowUp,
"End" => NamedKey::End,
"Home" => NamedKey::Home,
"PageDown" => NamedKey::PageDown,
"PageUp" => NamedKey::PageUp,
"Backspace" => NamedKey::Backspace,
"Clear" => NamedKey::Clear,
"Copy" => NamedKey::Copy,
"CrSel" => NamedKey::CrSel,
"Cut" => NamedKey::Cut,
"Delete" => NamedKey::Delete,
"EraseEof" => NamedKey::EraseEof,
"ExSel" => NamedKey::ExSel,
"Insert" => NamedKey::Insert,
"Paste" => NamedKey::Paste,
"Redo" => NamedKey::Redo,
"Undo" => NamedKey::Undo,
"Accept" => NamedKey::Accept,
"Again" => NamedKey::Again,
"Attn" => NamedKey::Attn,
"Cancel" => NamedKey::Cancel,
"ContextMenu" => NamedKey::ContextMenu,
"Escape" => NamedKey::Escape,
"Execute" => NamedKey::Execute,
"Find" => NamedKey::Find,
"Help" => NamedKey::Help,
"Pause" => NamedKey::Pause,
"Play" => NamedKey::Play,
"Props" => NamedKey::Props,
"Select" => NamedKey::Select,
"ZoomIn" => NamedKey::ZoomIn,
"ZoomOut" => NamedKey::ZoomOut,
"BrightnessDown" => NamedKey::BrightnessDown,
"BrightnessUp" => NamedKey::BrightnessUp,
"Eject" => NamedKey::Eject,
"LogOff" => NamedKey::LogOff,
"Power" => NamedKey::Power,
"PowerOff" => NamedKey::PowerOff,
"PrintScreen" => NamedKey::PrintScreen,
"Hibernate" => NamedKey::Hibernate,
"Standby" => NamedKey::Standby,
"WakeUp" => NamedKey::WakeUp,
"AllCandidates" => NamedKey::AllCandidates,
"Alphanumeric" => NamedKey::Alphanumeric,
"CodeInput" => NamedKey::CodeInput,
"Compose" => NamedKey::Compose,
"Convert" => NamedKey::Convert,
"FinalMode" => NamedKey::FinalMode,
"GroupFirst" => NamedKey::GroupFirst,
"GroupLast" => NamedKey::GroupLast,
"GroupNext" => NamedKey::GroupNext,
"GroupPrevious" => NamedKey::GroupPrevious,
"ModeChange" => NamedKey::ModeChange,
"NextCandidate" => NamedKey::NextCandidate,
"NonConvert" => NamedKey::NonConvert,
"PreviousCandidate" => NamedKey::PreviousCandidate,
"Process" => NamedKey::Process,
"SingleCandidate" => NamedKey::SingleCandidate,
"HangulMode" => NamedKey::HangulMode,
"HanjaMode" => NamedKey::HanjaMode,
"JunjaMode" => NamedKey::JunjaMode,
"Eisu" => NamedKey::Eisu,
"Hankaku" => NamedKey::Hankaku,
"Hiragana" => NamedKey::Hiragana,
"HiraganaKatakana" => NamedKey::HiraganaKatakana,
"KanaMode" => NamedKey::KanaMode,
"KanjiMode" => NamedKey::KanjiMode,
"Katakana" => NamedKey::Katakana,
"Romaji" => NamedKey::Romaji,
"Zenkaku" => NamedKey::Zenkaku,
"ZenkakuHankaku" => NamedKey::ZenkakuHankaku,
"Soft1" => NamedKey::Soft1,
"Soft2" => NamedKey::Soft2,
"Soft3" => NamedKey::Soft3,
"Soft4" => NamedKey::Soft4,
"ChannelDown" => NamedKey::ChannelDown,
"ChannelUp" => NamedKey::ChannelUp,
"Close" => NamedKey::Close,
"MailForward" => NamedKey::MailForward,
"MailReply" => NamedKey::MailReply,
"MailSend" => NamedKey::MailSend,
"MediaClose" => NamedKey::MediaClose,
"MediaFastForward" => NamedKey::MediaFastForward,
"MediaPause" => NamedKey::MediaPause,
"MediaPlay" => NamedKey::MediaPlay,
"MediaPlayPause" => NamedKey::MediaPlayPause,
"MediaRecord" => NamedKey::MediaRecord,
"MediaRewind" => NamedKey::MediaRewind,
"MediaStop" => NamedKey::MediaStop,
"MediaTrackNext" => NamedKey::MediaTrackNext,
"MediaTrackPrevious" => NamedKey::MediaTrackPrevious,
"New" => NamedKey::New,
"Open" => NamedKey::Open,
"Print" => NamedKey::Print,
"Save" => NamedKey::Save,
"SpellCheck" => NamedKey::SpellCheck,
"Key11" => NamedKey::Key11,
"Key12" => NamedKey::Key12,
"AudioBalanceLeft" => NamedKey::AudioBalanceLeft,
"AudioBalanceRight" => NamedKey::AudioBalanceRight,
"AudioBassBoostDown" => NamedKey::AudioBassBoostDown,
"AudioBassBoostToggle" => NamedKey::AudioBassBoostToggle,
"AudioBassBoostUp" => NamedKey::AudioBassBoostUp,
"AudioFaderFront" => NamedKey::AudioFaderFront,
"AudioFaderRear" => NamedKey::AudioFaderRear,
"AudioSurroundModeNext" => NamedKey::AudioSurroundModeNext,
"AudioTrebleDown" => NamedKey::AudioTrebleDown,
"AudioTrebleUp" => NamedKey::AudioTrebleUp,
"AudioVolumeDown" => NamedKey::AudioVolumeDown,
"AudioVolumeUp" => NamedKey::AudioVolumeUp,
"AudioVolumeMute" => NamedKey::AudioVolumeMute,
"MicrophoneToggle" => NamedKey::MicrophoneToggle,
"MicrophoneVolumeDown" => NamedKey::MicrophoneVolumeDown,
"MicrophoneVolumeUp" => NamedKey::MicrophoneVolumeUp,
"MicrophoneVolumeMute" => NamedKey::MicrophoneVolumeMute,
"SpeechCorrectionList" => NamedKey::SpeechCorrectionList,
"SpeechInputToggle" => NamedKey::SpeechInputToggle,
"LaunchApplication1" => NamedKey::LaunchApplication1,
"LaunchApplication2" => NamedKey::LaunchApplication2,
"LaunchCalendar" => NamedKey::LaunchCalendar,
"LaunchContacts" => NamedKey::LaunchContacts,
"LaunchMail" => NamedKey::LaunchMail,
"LaunchMediaPlayer" => NamedKey::LaunchMediaPlayer,
"LaunchMusicPlayer" => NamedKey::LaunchMusicPlayer,
"LaunchPhone" => NamedKey::LaunchPhone,
"LaunchScreenSaver" => NamedKey::LaunchScreenSaver,
"LaunchSpreadsheet" => NamedKey::LaunchSpreadsheet,
"LaunchWebBrowser" => NamedKey::LaunchWebBrowser,
"LaunchWebCam" => NamedKey::LaunchWebCam,
"LaunchWordProcessor" => NamedKey::LaunchWordProcessor,
"BrowserBack" => NamedKey::BrowserBack,
"BrowserFavorites" => NamedKey::BrowserFavorites,
"BrowserForward" => NamedKey::BrowserForward,
"BrowserHome" => NamedKey::BrowserHome,
"BrowserRefresh" => NamedKey::BrowserRefresh,
"BrowserSearch" => NamedKey::BrowserSearch,
"BrowserStop" => NamedKey::BrowserStop,
"AppSwitch" => NamedKey::AppSwitch,
"Call" => NamedKey::Call,
"Camera" => NamedKey::Camera,
"CameraFocus" => NamedKey::CameraFocus,
"EndCall" => NamedKey::EndCall,
"GoBack" => NamedKey::GoBack,
"GoHome" => NamedKey::GoHome,
"HeadsetHook" => NamedKey::HeadsetHook,
"LastNumberRedial" => NamedKey::LastNumberRedial,
"Notification" => NamedKey::Notification,
"MannerMode" => NamedKey::MannerMode,
"VoiceDial" => NamedKey::VoiceDial,
"TV" => NamedKey::TV,
"TV3DMode" => NamedKey::TV3DMode,
"TVAntennaCable" => NamedKey::TVAntennaCable,
"TVAudioDescription" => NamedKey::TVAudioDescription,
"TVAudioDescriptionMixDown" => NamedKey::TVAudioDescriptionMixDown,
"TVAudioDescriptionMixUp" => NamedKey::TVAudioDescriptionMixUp,
"TVContentsMenu" => NamedKey::TVContentsMenu,
"TVDataService" => NamedKey::TVDataService,
"TVInput" => NamedKey::TVInput,
"TVInputComponent1" => NamedKey::TVInputComponent1,
"TVInputComponent2" => NamedKey::TVInputComponent2,
"TVInputComposite1" => NamedKey::TVInputComposite1,
"TVInputComposite2" => NamedKey::TVInputComposite2,
"TVInputHDMI1" => NamedKey::TVInputHDMI1,
"TVInputHDMI2" => NamedKey::TVInputHDMI2,
"TVInputHDMI3" => NamedKey::TVInputHDMI3,
"TVInputHDMI4" => NamedKey::TVInputHDMI4,
"TVInputVGA1" => NamedKey::TVInputVGA1,
"TVMediaContext" => NamedKey::TVMediaContext,
"TVNetwork" => NamedKey::TVNetwork,
"TVNumberEntry" => NamedKey::TVNumberEntry,
"TVPower" => NamedKey::TVPower,
"TVRadioService" => NamedKey::TVRadioService,
"TVSatellite" => NamedKey::TVSatellite,
"TVSatelliteBS" => NamedKey::TVSatelliteBS,
"TVSatelliteCS" => NamedKey::TVSatelliteCS,
"TVSatelliteToggle" => NamedKey::TVSatelliteToggle,
"TVTerrestrialAnalog" => NamedKey::TVTerrestrialAnalog,
"TVTerrestrialDigital" => NamedKey::TVTerrestrialDigital,
"TVTimer" => NamedKey::TVTimer,
"AVRInput" => NamedKey::AVRInput,
"AVRPower" => NamedKey::AVRPower,
"ColorF0Red" => NamedKey::ColorF0Red,
"ColorF1Green" => NamedKey::ColorF1Green,
"ColorF2Yellow" => NamedKey::ColorF2Yellow,
"ColorF3Blue" => NamedKey::ColorF3Blue,
"ColorF4Grey" => NamedKey::ColorF4Grey,
"ColorF5Brown" => NamedKey::ColorF5Brown,
"ClosedCaptionToggle" => NamedKey::ClosedCaptionToggle,
"Dimmer" => NamedKey::Dimmer,
"DisplaySwap" => NamedKey::DisplaySwap,
"DVR" => NamedKey::DVR,
"Exit" => NamedKey::Exit,
"FavoriteClear0" => NamedKey::FavoriteClear0,
"FavoriteClear1" => NamedKey::FavoriteClear1,
"FavoriteClear2" => NamedKey::FavoriteClear2,
"FavoriteClear3" => NamedKey::FavoriteClear3,
"FavoriteRecall0" => NamedKey::FavoriteRecall0,
"FavoriteRecall1" => NamedKey::FavoriteRecall1,
"FavoriteRecall2" => NamedKey::FavoriteRecall2,
"FavoriteRecall3" => NamedKey::FavoriteRecall3,
"FavoriteStore0" => NamedKey::FavoriteStore0,
"FavoriteStore1" => NamedKey::FavoriteStore1,
"FavoriteStore2" => NamedKey::FavoriteStore2,
"FavoriteStore3" => NamedKey::FavoriteStore3,
"Guide" => NamedKey::Guide,
"GuideNextDay" => NamedKey::GuideNextDay,
"GuidePreviousDay" => NamedKey::GuidePreviousDay,
"Info" => NamedKey::Info,
"InstantReplay" => NamedKey::InstantReplay,
"Link" => NamedKey::Link,
"ListProgram" => NamedKey::ListProgram,
"LiveContent" => NamedKey::LiveContent,
"Lock" => NamedKey::Lock,
"MediaApps" => NamedKey::MediaApps,
"MediaAudioTrack" => NamedKey::MediaAudioTrack,
"MediaLast" => NamedKey::MediaLast,
"MediaSkipBackward" => NamedKey::MediaSkipBackward,
"MediaSkipForward" => NamedKey::MediaSkipForward,
"MediaStepBackward" => NamedKey::MediaStepBackward,
"MediaStepForward" => NamedKey::MediaStepForward,
"MediaTopMenu" => NamedKey::MediaTopMenu,
"NavigateIn" => NamedKey::NavigateIn,
"NavigateNext" => NamedKey::NavigateNext,
"NavigateOut" => NamedKey::NavigateOut,
"NavigatePrevious" => NamedKey::NavigatePrevious,
"NextFavoriteChannel" => NamedKey::NextFavoriteChannel,
"NextUserProfile" => NamedKey::NextUserProfile,
"OnDemand" => NamedKey::OnDemand,
"Pairing" => NamedKey::Pairing,
"PinPDown" => NamedKey::PinPDown,
"PinPMove" => NamedKey::PinPMove,
"PinPToggle" => NamedKey::PinPToggle,
"PinPUp" => NamedKey::PinPUp,
"PlaySpeedDown" => NamedKey::PlaySpeedDown,
"PlaySpeedReset" => NamedKey::PlaySpeedReset,
"PlaySpeedUp" => NamedKey::PlaySpeedUp,
"RandomToggle" => NamedKey::RandomToggle,
"RcLowBattery" => NamedKey::RcLowBattery,
"RecordSpeedNext" => NamedKey::RecordSpeedNext,
"RfBypass" => NamedKey::RfBypass,
"ScanChannelsToggle" => NamedKey::ScanChannelsToggle,
"ScreenModeNext" => NamedKey::ScreenModeNext,
"Settings" => NamedKey::Settings,
"SplitScreenToggle" => NamedKey::SplitScreenToggle,
"STBInput" => NamedKey::STBInput,
"STBPower" => NamedKey::STBPower,
"Subtitle" => NamedKey::Subtitle,
"Teletext" => NamedKey::Teletext,
"VideoModeNext" => NamedKey::VideoModeNext,
"Wink" => NamedKey::Wink,
"ZoomToggle" => NamedKey::ZoomToggle,
"F1" => NamedKey::F1,
"F2" => NamedKey::F2,
"F3" => NamedKey::F3,
"F4" => NamedKey::F4,
"F5" => NamedKey::F5,
"F6" => NamedKey::F6,
"F7" => NamedKey::F7,
"F8" => NamedKey::F8,
"F9" => NamedKey::F9,
"F10" => NamedKey::F10,
"F11" => NamedKey::F11,
"F12" => NamedKey::F12,
"F13" => NamedKey::F13,
"F14" => NamedKey::F14,
"F15" => NamedKey::F15,
"F16" => NamedKey::F16,
"F17" => NamedKey::F17,
"F18" => NamedKey::F18,
"F19" => NamedKey::F19,
"F20" => NamedKey::F20,
"F21" => NamedKey::F21,
"F22" => NamedKey::F22,
"F23" => NamedKey::F23,
"F24" => NamedKey::F24,
"F25" => NamedKey::F25,
"F26" => NamedKey::F26,
"F27" => NamedKey::F27,
"F28" => NamedKey::F28,
"F29" => NamedKey::F29,
"F30" => NamedKey::F30,
"F31" => NamedKey::F31,
"F32" => NamedKey::F32,
"F33" => NamedKey::F33,
"F34" => NamedKey::F34,
"F35" => NamedKey::F35,
string => return Key::Character(SmolStr::new(string)),
})
}
}
impl FromAttributeValue for PhysicalKey {
fn from_attribute_value(kcav: &str) -> Self {
PhysicalKey::Code(match kcav {
"Backquote" => KeyCode::Backquote,
"Backslash" => KeyCode::Backslash,
"BracketLeft" => KeyCode::BracketLeft,
"BracketRight" => KeyCode::BracketRight,
"Comma" => KeyCode::Comma,
"Digit0" => KeyCode::Digit0,
"Digit1" => KeyCode::Digit1,
"Digit2" => KeyCode::Digit2,
"Digit3" => KeyCode::Digit3,
"Digit4" => KeyCode::Digit4,
"Digit5" => KeyCode::Digit5,
"Digit6" => KeyCode::Digit6,
"Digit7" => KeyCode::Digit7,
"Digit8" => KeyCode::Digit8,
"Digit9" => KeyCode::Digit9,
"Equal" => KeyCode::Equal,
"IntlBackslash" => KeyCode::IntlBackslash,
"IntlRo" => KeyCode::IntlRo,
"IntlYen" => KeyCode::IntlYen,
"KeyA" => KeyCode::KeyA,
"KeyB" => KeyCode::KeyB,
"KeyC" => KeyCode::KeyC,
"KeyD" => KeyCode::KeyD,
"KeyE" => KeyCode::KeyE,
"KeyF" => KeyCode::KeyF,
"KeyG" => KeyCode::KeyG,
"KeyH" => KeyCode::KeyH,
"KeyI" => KeyCode::KeyI,
"KeyJ" => KeyCode::KeyJ,
"KeyK" => KeyCode::KeyK,
"KeyL" => KeyCode::KeyL,
"KeyM" => KeyCode::KeyM,
"KeyN" => KeyCode::KeyN,
"KeyO" => KeyCode::KeyO,
"KeyP" => KeyCode::KeyP,
"KeyQ" => KeyCode::KeyQ,
"KeyR" => KeyCode::KeyR,
"KeyS" => KeyCode::KeyS,
"KeyT" => KeyCode::KeyT,
"KeyU" => KeyCode::KeyU,
"KeyV" => KeyCode::KeyV,
"KeyW" => KeyCode::KeyW,
"KeyX" => KeyCode::KeyX,
"KeyY" => KeyCode::KeyY,
"KeyZ" => KeyCode::KeyZ,
"Minus" => KeyCode::Minus,
"Period" => KeyCode::Period,
"Quote" => KeyCode::Quote,
"Semicolon" => KeyCode::Semicolon,
"Slash" => KeyCode::Slash,
"AltLeft" => KeyCode::AltLeft,
"AltRight" => KeyCode::AltRight,
"Backspace" => KeyCode::Backspace,
"CapsLock" => KeyCode::CapsLock,
"ContextMenu" => KeyCode::ContextMenu,
"ControlLeft" => KeyCode::ControlLeft,
"ControlRight" => KeyCode::ControlRight,
"Enter" => KeyCode::Enter,
"MetaLeft" => KeyCode::MetaLeft,
"MetaRight" => KeyCode::MetaRight,
"ShiftLeft" => KeyCode::ShiftLeft,
"ShiftRight" => KeyCode::ShiftRight,
"Space" => KeyCode::Space,
"Tab" => KeyCode::Tab,
"Convert" => KeyCode::Convert,
"KanaMode" => KeyCode::KanaMode,
"Lang1" => KeyCode::Lang1,
"Lang2" => KeyCode::Lang2,
"Lang3" => KeyCode::Lang3,
"Lang4" => KeyCode::Lang4,
"Lang5" => KeyCode::Lang5,
"NonConvert" => KeyCode::NonConvert,
"Delete" => KeyCode::Delete,
"End" => KeyCode::End,
"Help" => KeyCode::Help,
"Home" => KeyCode::Home,
"Insert" => KeyCode::Insert,
"PageDown" => KeyCode::PageDown,
"PageUp" => KeyCode::PageUp,
"ArrowDown" => KeyCode::ArrowDown,
"ArrowLeft" => KeyCode::ArrowLeft,
"ArrowRight" => KeyCode::ArrowRight,
"ArrowUp" => KeyCode::ArrowUp,
"NumLock" => KeyCode::NumLock,
"Numpad0" => KeyCode::Numpad0,
"Numpad1" => KeyCode::Numpad1,
"Numpad2" => KeyCode::Numpad2,
"Numpad3" => KeyCode::Numpad3,
"Numpad4" => KeyCode::Numpad4,
"Numpad5" => KeyCode::Numpad5,
"Numpad6" => KeyCode::Numpad6,
"Numpad7" => KeyCode::Numpad7,
"Numpad8" => KeyCode::Numpad8,
"Numpad9" => KeyCode::Numpad9,
"NumpadAdd" => KeyCode::NumpadAdd,
"NumpadBackspace" => KeyCode::NumpadBackspace,
"NumpadClear" => KeyCode::NumpadClear,
"NumpadClearEntry" => KeyCode::NumpadClearEntry,
"NumpadComma" => KeyCode::NumpadComma,
"NumpadDecimal" => KeyCode::NumpadDecimal,
"NumpadDivide" => KeyCode::NumpadDivide,
"NumpadEnter" => KeyCode::NumpadEnter,
"NumpadEqual" => KeyCode::NumpadEqual,
"NumpadHash" => KeyCode::NumpadHash,
"NumpadMemoryAdd" => KeyCode::NumpadMemoryAdd,
"NumpadMemoryClear" => KeyCode::NumpadMemoryClear,
"NumpadMemoryRecall" => KeyCode::NumpadMemoryRecall,
"NumpadMemoryStore" => KeyCode::NumpadMemoryStore,
"NumpadMemorySubtract" => KeyCode::NumpadMemorySubtract,
"NumpadMultiply" => KeyCode::NumpadMultiply,
"NumpadParenLeft" => KeyCode::NumpadParenLeft,
"NumpadParenRight" => KeyCode::NumpadParenRight,
"NumpadStar" => KeyCode::NumpadStar,
"NumpadSubtract" => KeyCode::NumpadSubtract,
"Escape" => KeyCode::Escape,
"Fn" => KeyCode::Fn,
"FnLock" => KeyCode::FnLock,
"PrintScreen" => KeyCode::PrintScreen,
"ScrollLock" => KeyCode::ScrollLock,
"Pause" => KeyCode::Pause,
"BrowserBack" => KeyCode::BrowserBack,
"BrowserFavorites" => KeyCode::BrowserFavorites,
"BrowserForward" => KeyCode::BrowserForward,
"BrowserHome" => KeyCode::BrowserHome,
"BrowserRefresh" => KeyCode::BrowserRefresh,
"BrowserSearch" => KeyCode::BrowserSearch,
"BrowserStop" => KeyCode::BrowserStop,
"Eject" => KeyCode::Eject,
"LaunchApp1" => KeyCode::LaunchApp1,
"LaunchApp2" => KeyCode::LaunchApp2,
"LaunchMail" => KeyCode::LaunchMail,
"MediaPlayPause" => KeyCode::MediaPlayPause,
"MediaSelect" => KeyCode::MediaSelect,
"MediaStop" => KeyCode::MediaStop,
"MediaTrackNext" => KeyCode::MediaTrackNext,
"MediaTrackPrevious" => KeyCode::MediaTrackPrevious,
"Power" => KeyCode::Power,
"Sleep" => KeyCode::Sleep,
"AudioVolumeDown" => KeyCode::AudioVolumeDown,
"AudioVolumeMute" => KeyCode::AudioVolumeMute,
"AudioVolumeUp" => KeyCode::AudioVolumeUp,
"WakeUp" => KeyCode::WakeUp,
#[allow(deprecated)]
"Super" => KeyCode::Super,
#[allow(deprecated)]
"Hyper" => KeyCode::Hyper,
#[allow(deprecated)]
"Turbo" => KeyCode::Turbo,
"Abort" => KeyCode::Abort,
"Resume" => KeyCode::Resume,
"Suspend" => KeyCode::Suspend,
"Again" => KeyCode::Again,
"Copy" => KeyCode::Copy,
"Cut" => KeyCode::Cut,
"Find" => KeyCode::Find,
"Open" => KeyCode::Open,
"Paste" => KeyCode::Paste,
"Props" => KeyCode::Props,
"Select" => KeyCode::Select,
"Undo" => KeyCode::Undo,
"Hiragana" => KeyCode::Hiragana,
"Katakana" => KeyCode::Katakana,
"F1" => KeyCode::F1,
"F2" => KeyCode::F2,
"F3" => KeyCode::F3,
"F4" => KeyCode::F4,
"F5" => KeyCode::F5,
"F6" => KeyCode::F6,
"F7" => KeyCode::F7,
"F8" => KeyCode::F8,
"F9" => KeyCode::F9,
"F10" => KeyCode::F10,
"F11" => KeyCode::F11,
"F12" => KeyCode::F12,
"F13" => KeyCode::F13,
"F14" => KeyCode::F14,
"F15" => KeyCode::F15,
"F16" => KeyCode::F16,
"F17" => KeyCode::F17,
"F18" => KeyCode::F18,
"F19" => KeyCode::F19,
"F20" => KeyCode::F20,
"F21" => KeyCode::F21,
"F22" => KeyCode::F22,
"F23" => KeyCode::F23,
"F24" => KeyCode::F24,
"F25" => KeyCode::F25,
"F26" => KeyCode::F26,
"F27" => KeyCode::F27,
"F28" => KeyCode::F28,
"F29" => KeyCode::F29,
"F30" => KeyCode::F30,
"F31" => KeyCode::F31,
"F32" => KeyCode::F32,
"F33" => KeyCode::F33,
"F34" => KeyCode::F34,
"F35" => KeyCode::F35,
_ => return PhysicalKey::Unidentified(NativeKeyCode::Unidentified),
})
}
}

710
winit-web/src/lib.rs Normal file
View file

@ -0,0 +1,710 @@
//! # Web
//!
//! Winit supports running in Browsers by compiling to WebAssembly with
//! [`wasm-bindgen`][wasm_bindgen]. For information on using Rust on WebAssembly, check out the
//! [Rust and WebAssembly book].
//!
//! The officially supported browsers are Chrome, Firefox and Safari 13.1+, though forks of these
//! should work fine.
//!
//! On the Web platform, a Winit [`Window`] is backed by a [`HTMLCanvasElement`][canvas]. Winit will
//! create that canvas for you or you can [provide your own][with_canvas]. Then you can either let
//! Winit [insert it into the DOM for you][insert], or [retrieve the canvas][get] and insert it
//! yourself.
//!
//! [canvas]: https://developer.mozilla.org/en-US/docs/Web/API/HTMLCanvasElement
//! [with_canvas]: WindowAttributesWeb::with_canvas
//! [get]: WindowExtWeb::canvas
//! [insert]: WindowAttributesWeb::with_append
//! [wasm_bindgen]: https://docs.rs/wasm-bindgen
//! [Rust and WebAssembly book]: https://rustwasm.github.io/book
//!
//! ## CSS properties
//!
//! It is recommended **not** to apply certain CSS properties to the canvas:
//! - [`transform`](https://developer.mozilla.org/en-US/docs/Web/CSS/transform)
//! - [`border`](https://developer.mozilla.org/en-US/docs/Web/CSS/border)
//! - [`padding`](https://developer.mozilla.org/en-US/docs/Web/CSS/padding)
//!
//! The following APIs can't take them into account and will therefore provide inaccurate results:
//! - [`WindowEvent::SurfaceResized`] and [`Window::(set_)surface_size()`]
//! - [`WindowEvent::Occluded`]
//! - [`WindowEvent::PointerMoved`], [`WindowEvent::PointerEntered`] and
//! [`WindowEvent::PointerLeft`].
//! - [`Window::set_outer_position()`]
//!
//! [`WindowEvent::SurfaceResized`]: crate::event::WindowEvent::SurfaceResized
//! [`Window::(set_)surface_size()`]: crate::window::Window::surface_size
//! [`WindowEvent::Occluded`]: crate::event::WindowEvent::Occluded
//! [`WindowEvent::PointerMoved`]: crate::event::WindowEvent::PointerMoved
//! [`WindowEvent::PointerEntered`]: crate::event::WindowEvent::PointerEntered
//! [`WindowEvent::PointerLeft`]: crate::event::WindowEvent::PointerLeft
//! [`Window::set_outer_position()`]: crate::window::Window::set_outer_position
// Brief introduction to the internals of the Web backend:
// The Web backend used to support both wasm-bindgen and stdweb as methods of binding to the
// environment. Because they are both supporting the same underlying APIs, the actual Web bindings
// are cordoned off into backend abstractions, which present the thinnest unifying layer possible.
//
// When adding support for new events or interactions with the browser, first consult trusted
// documentation (such as MDN) to ensure it is well-standardised and supported across many browsers.
// Once you have decided on the relevant Web APIs, add support to both backends.
//
// The backend is used by the rest of the module to implement Winit's business logic, which forms
// the rest of the code. 'device', 'error', 'monitor', and 'window' define Web-specific structures
// for winit's cross-platform structures. They are all relatively simple translations.
//
// The event_loop module handles listening for and processing events. 'Proxy' implements
// EventLoopProxy and 'WindowTarget' implements ActiveEventLoop. WindowTarget also handles
// registering the event handlers. The 'Execution' struct in the 'runner' module handles taking
// incoming events (from the registered handlers) and ensuring they are passed to the user in a
// compliant way.
macro_rules! os_error {
($error:expr) => {{
winit_core::error::OsError::new(line!(), file!(), $error)
}};
}
mod r#async;
mod cursor;
mod error;
mod event;
pub(crate) mod event_loop;
mod keyboard;
mod lock;
pub(crate) mod main_thread;
mod monitor;
pub(crate) mod web_sys;
pub(crate) mod window;
use std::cell::Ref;
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::task::{Context, Poll};
use ::web_sys::HtmlCanvasElement;
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
use winit_core::application::ApplicationHandler;
use winit_core::cursor::{CustomCursor, CustomCursorSource};
use winit_core::error::NotSupportedError;
use winit_core::event_loop::ActiveEventLoop;
use winit_core::monitor::MonitorHandleProvider;
use winit_core::window::{PlatformWindowAttributes, Window};
pub use self::event_loop::{EventLoop, PlatformSpecificEventLoopAttributes};
use self::web_sys as backend;
use self::window::Window as WebWindow;
use crate::cursor::CustomCursorFuture as PlatformCustomCursorFuture;
use crate::event_loop::ActiveEventLoop as WebActiveEventLoop;
use crate::main_thread::{MainThreadMarker, MainThreadSafe};
use crate::monitor::{
HasMonitorPermissionFuture as PlatformHasMonitorPermissionFuture,
MonitorHandle as WebMonitorHandle, MonitorPermissionFuture as PlatformMonitorPermissionFuture,
OrientationLockFuture as PlatformOrientationLockFuture,
};
pub trait WindowExtWeb {
/// Only returns the canvas if called from inside the window context (the
/// main thread).
fn canvas(&self) -> Option<Ref<'_, HtmlCanvasElement>>;
/// Returns [`true`] if calling `event.preventDefault()` is enabled.
///
/// See [`WindowExtWeb::set_prevent_default()`] for more details.
fn prevent_default(&self) -> bool;
/// Sets whether `event.preventDefault()` should be called on events on the
/// canvas that have side effects.
///
/// For example, by default using the mouse wheel would cause the page to scroll, enabling this
/// would prevent that.
///
/// Some events are impossible to prevent. E.g. Firefox allows to access the native browser
/// context menu with Shift+Rightclick.
fn set_prevent_default(&self, prevent_default: bool);
/// Returns whether using [`CursorGrabMode::Locked`] returns raw, un-accelerated mouse input.
///
/// This is the same as [`ActiveEventLoopExtWeb::is_cursor_lock_raw()`], and is provided for
/// convenience.
///
/// [`CursorGrabMode::Locked`]: crate::window::CursorGrabMode::Locked
fn is_cursor_lock_raw(&self) -> bool;
}
impl WindowExtWeb for dyn Window + '_ {
#[inline]
fn canvas(&self) -> Option<Ref<'_, HtmlCanvasElement>> {
self.cast_ref::<WebWindow>().expect("non Web window on Web").canvas()
}
fn prevent_default(&self) -> bool {
self.cast_ref::<WebWindow>().expect("non Web window on Web").prevent_default()
}
fn set_prevent_default(&self, prevent_default: bool) {
self.cast_ref::<WebWindow>()
.expect("non Web window on Web")
.set_prevent_default(prevent_default)
}
fn is_cursor_lock_raw(&self) -> bool {
self.cast_ref::<WebWindow>().expect("non Web window on Web").is_cursor_lock_raw()
}
}
#[derive(Clone, Debug)]
pub struct WindowAttributesWeb {
pub(crate) canvas: Option<Arc<MainThreadSafe<backend::RawCanvasType>>>,
pub(crate) prevent_default: bool,
pub(crate) focusable: bool,
pub(crate) append: bool,
}
impl WindowAttributesWeb {
/// Pass an [`HtmlCanvasElement`] to be used for this [`Window`]. If [`None`],
/// the default one will be created.
///
/// In any case, the canvas won't be automatically inserted into the Web page.
///
/// [`None`] by default.
pub fn with_canvas(mut self, canvas: Option<HtmlCanvasElement>) -> Self {
match canvas {
Some(canvas) => {
let main_thread = MainThreadMarker::new()
.expect("received a `HtmlCanvasElement` outside the window context");
self.canvas = Some(Arc::new(MainThreadSafe::new(main_thread, canvas)));
},
None => self.canvas = None,
}
self
}
/// Sets whether `event.preventDefault()` should be called on events on the
/// canvas that have side effects.
///
/// See [`WindowExtWeb::set_prevent_default()`] for more details.
///
/// Enabled by default.
pub fn with_prevent_default(mut self, prevent_default: bool) -> Self {
self.prevent_default = prevent_default;
self
}
/// Whether the canvas should be focusable using the tab key. This is necessary to capture
/// canvas keyboard events.
///
/// Enabled by default.
pub fn with_focusable(mut self, focusable: bool) -> Self {
self.focusable = focusable;
self
}
/// On window creation, append the canvas element to the Web page if it isn't already.
///
/// Disabled by default.
pub fn with_append(mut self, append: bool) -> Self {
self.append = append;
self
}
}
impl PlatformWindowAttributes for WindowAttributesWeb {
fn box_clone(&self) -> Box<dyn PlatformWindowAttributes> {
Box::from(self.clone())
}
}
impl PartialEq for WindowAttributesWeb {
fn eq(&self, other: &Self) -> bool {
(match (&self.canvas, &other.canvas) {
(Some(this), Some(other)) => Arc::ptr_eq(this, other),
(None, None) => true,
_ => false,
}) && self.prevent_default.eq(&other.prevent_default)
&& self.focusable.eq(&other.focusable)
&& self.append.eq(&other.append)
}
}
impl Default for WindowAttributesWeb {
fn default() -> Self {
Self { canvas: None, prevent_default: true, focusable: true, append: false }
}
}
/// Additional methods on `EventLoop` that are specific to the Web.
pub trait EventLoopExtWeb {
/// Initializes the winit event loop.
///
/// Unlike
#[cfg_attr(target_feature = "exception-handling", doc = "`run_app()`")]
#[cfg_attr(
not(target_feature = "exception-handling"),
doc = "[`run_app()`]"
)]
/// [^1], this returns immediately, and doesn't throw an exception in order to
/// satisfy its [`!`] return type.
///
/// Once the event loop has been destroyed, it's possible to reinitialize another event loop
/// by calling this function again. This can be useful if you want to recreate the event loop
/// while the WebAssembly module is still loaded. For example, this can be used to recreate the
/// event loop when switching between tabs on a single page application.
#[rustfmt::skip]
///
#[cfg_attr(
not(target_feature = "exception-handling"),
doc = "[`run_app()`]: EventLoop::run_app()"
)]
/// [^1]: `run_app()` is _not_ available on Wasm when the target supports `exception-handling`.
fn spawn_app<A: ApplicationHandler + 'static>(self, app: A);
/// Sets the strategy for [`ControlFlow::Poll`].
///
/// See [`PollStrategy`].
///
/// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
fn set_poll_strategy(&self, strategy: PollStrategy);
/// Gets the strategy for [`ControlFlow::Poll`].
///
/// See [`PollStrategy`].
///
/// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
fn poll_strategy(&self) -> PollStrategy;
/// Sets the strategy for [`ControlFlow::WaitUntil`].
///
/// See [`WaitUntilStrategy`].
///
/// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy);
/// Gets the strategy for [`ControlFlow::WaitUntil`].
///
/// See [`WaitUntilStrategy`].
///
/// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
fn wait_until_strategy(&self) -> WaitUntilStrategy;
/// Returns if the users device has multiple screens. Useful to check before prompting the user
/// with [`EventLoopExtWeb::request_detailed_monitor_permission()`].
///
/// Browsers might always return [`false`] to reduce fingerprinting.
fn has_multiple_screens(&self) -> Result<bool, NotSupportedError>;
/// Prompts the user for permission to query detailed information about available monitors. The
/// returned [`MonitorPermissionFuture`] can be dropped without aborting the request.
///
/// Check [`EventLoopExtWeb::has_multiple_screens()`] before unnecessarily prompting the user
/// for such permissions.
///
/// [`MonitorHandle`]s don't automatically make use of this after permission is granted. New
/// [`MonitorHandle`]s have to be created instead.
///
/// [`MonitorHandle`]: crate::monitor::MonitorHandle
fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture;
/// Returns whether the user has given permission to access detailed monitor information.
///
/// [`MonitorHandle`]s don't automatically make use of detailed monitor information after
/// permission is granted. New [`MonitorHandle`]s have to be created instead.
///
/// [`MonitorHandle`]: crate::monitor::MonitorHandle
///
/// [`MonitorHandle`]: crate::monitor::MonitorHandle
fn has_detailed_monitor_permission(&self) -> HasMonitorPermissionFuture;
}
pub trait ActiveEventLoopExtWeb {
/// Sets the strategy for [`ControlFlow::Poll`].
///
/// See [`PollStrategy`].
///
/// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
fn set_poll_strategy(&self, strategy: PollStrategy);
/// Gets the strategy for [`ControlFlow::Poll`].
///
/// See [`PollStrategy`].
///
/// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
fn poll_strategy(&self) -> PollStrategy;
/// Sets the strategy for [`ControlFlow::WaitUntil`].
///
/// See [`WaitUntilStrategy`].
///
/// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy);
/// Gets the strategy for [`ControlFlow::WaitUntil`].
///
/// See [`WaitUntilStrategy`].
///
/// [`ControlFlow::WaitUntil`]: crate::event_loop::ControlFlow::WaitUntil
fn wait_until_strategy(&self) -> WaitUntilStrategy;
/// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the
/// cursor has completely finished loading.
fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture;
/// Returns whether using [`CursorGrabMode::Locked`] returns raw, un-accelerated mouse input.
///
/// [`CursorGrabMode::Locked`]: crate::window::CursorGrabMode::Locked
fn is_cursor_lock_raw(&self) -> bool;
/// Returns if the users device has multiple screens. Useful to check before prompting the user
/// with [`EventLoopExtWeb::request_detailed_monitor_permission()`].
///
/// Browsers might always return [`false`] to reduce fingerprinting.
fn has_multiple_screens(&self) -> Result<bool, NotSupportedError>;
/// Prompts the user for permission to query detailed information about available monitors. The
/// returned [`MonitorPermissionFuture`] can be dropped without aborting the request.
///
/// Check [`EventLoopExtWeb::has_multiple_screens()`] before unnecessarily prompting the user
/// for such permissions.
///
/// [`MonitorHandle`]s don't automatically make use of this after permission is granted. New
/// [`MonitorHandle`]s have to be created instead.
///
/// [`MonitorHandle`]: crate::monitor::MonitorHandle
fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture;
/// Returns whether the user has given permission to access detailed monitor information.
///
/// [`MonitorHandle`]s don't automatically make use of detailed monitor information after
/// permission is granted. New [`MonitorHandle`]s have to be created instead.
///
/// [`MonitorHandle`]: crate::monitor::MonitorHandle
fn has_detailed_monitor_permission(&self) -> bool;
}
impl ActiveEventLoopExtWeb for dyn ActiveEventLoop + '_ {
#[inline]
fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture {
let event_loop = self.cast_ref::<WebActiveEventLoop>().expect("non Web event loop on Web");
event_loop.create_custom_cursor_async(source)
}
#[inline]
fn set_poll_strategy(&self, strategy: PollStrategy) {
let event_loop = self.cast_ref::<WebActiveEventLoop>().expect("non Web event loop on Web");
event_loop.set_poll_strategy(strategy);
}
#[inline]
fn poll_strategy(&self) -> PollStrategy {
let event_loop = self.cast_ref::<WebActiveEventLoop>().expect("non Web event loop on Web");
event_loop.poll_strategy()
}
#[inline]
fn set_wait_until_strategy(&self, strategy: WaitUntilStrategy) {
let event_loop = self.cast_ref::<WebActiveEventLoop>().expect("non Web event loop on Web");
event_loop.set_wait_until_strategy(strategy);
}
#[inline]
fn wait_until_strategy(&self) -> WaitUntilStrategy {
let event_loop = self.cast_ref::<WebActiveEventLoop>().expect("non Web event loop on Web");
event_loop.wait_until_strategy()
}
#[inline]
fn is_cursor_lock_raw(&self) -> bool {
let event_loop = self.cast_ref::<WebActiveEventLoop>().expect("non Web event loop on Web");
event_loop.is_cursor_lock_raw()
}
#[inline]
fn has_multiple_screens(&self) -> Result<bool, NotSupportedError> {
let event_loop = self.cast_ref::<WebActiveEventLoop>().expect("non Web event loop on Web");
event_loop.has_multiple_screens()
}
#[inline]
fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture {
let event_loop = self.cast_ref::<WebActiveEventLoop>().expect("non Web event loop on Web");
MonitorPermissionFuture(event_loop.request_detailed_monitor_permission())
}
#[inline]
fn has_detailed_monitor_permission(&self) -> bool {
let event_loop = self.cast_ref::<WebActiveEventLoop>().expect("non Web event loop on Web");
event_loop.has_detailed_monitor_permission()
}
}
/// Strategy used for [`ControlFlow::Poll`][crate::event_loop::ControlFlow::Poll].
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum PollStrategy {
/// Uses [`Window.requestIdleCallback()`] to queue the next event loop. If not available
/// this will fallback to [`setTimeout()`].
///
/// This strategy will wait for the browser to enter an idle period before running and might
/// be affected by browser throttling.
///
/// [`Window.requestIdleCallback()`]: https://developer.mozilla.org/en-US/docs/Web/API/Window/requestIdleCallback
/// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
IdleCallback,
/// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available
/// this will fallback to [`setTimeout()`].
///
/// This strategy will run as fast as possible without disturbing users from interacting with
/// the page and is not affected by browser throttling.
///
/// This is the default strategy.
///
/// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API
/// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
#[default]
Scheduler,
}
/// Strategy used for [`ControlFlow::WaitUntil`][crate::event_loop::ControlFlow::WaitUntil].
#[derive(Clone, Copy, Debug, Default, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum WaitUntilStrategy {
/// Uses the [Prioritized Task Scheduling API] to queue the next event loop. If not available
/// this will fallback to [`setTimeout()`].
///
/// This strategy is commonly not affected by browser throttling unless the window is not
/// focused.
///
/// This is the default strategy.
///
/// [Prioritized Task Scheduling API]: https://developer.mozilla.org/en-US/docs/Web/API/Prioritized_Task_Scheduling_API
/// [`setTimeout()`]: https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
#[default]
Scheduler,
/// Equal to [`Scheduler`][Self::Scheduler] but wakes up the event loop from a [worker].
///
/// This strategy is commonly not affected by browser throttling regardless of window focus.
///
/// [worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API
Worker,
}
#[derive(Debug)]
pub struct CustomCursorFuture(pub(crate) PlatformCustomCursorFuture);
impl Future for CustomCursorFuture {
type Output = Result<CustomCursor, CustomCursorError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.0).poll(cx).map_ok(|cursor| CustomCursor(Arc::new(cursor)))
}
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum CustomCursorError {
Blob,
Decode(String),
}
impl Display for CustomCursorError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Blob => write!(f, "failed to create `Blob`"),
Self::Decode(error) => write!(f, "failed to decode image: {error}"),
}
}
}
impl Error for CustomCursorError {}
/// Can be dropped without aborting the request for detailed monitor permissions.
#[derive(Debug)]
pub struct MonitorPermissionFuture(pub(crate) PlatformMonitorPermissionFuture);
impl Future for MonitorPermissionFuture {
type Output = Result<(), MonitorPermissionError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.0).poll(cx)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum MonitorPermissionError {
/// User has explicitly denied permission to query detailed monitor information.
Denied,
/// User has not decided to give permission to query detailed monitor information.
Prompt,
/// Browser does not support detailed monitor information.
Unsupported,
}
impl Display for MonitorPermissionError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
MonitorPermissionError::Denied => write!(
f,
"User has explicitly denied permission to query detailed monitor information"
),
MonitorPermissionError::Prompt => write!(
f,
"User has not decided to give permission to query detailed monitor information"
),
MonitorPermissionError::Unsupported => {
write!(f, "Browser does not support detailed monitor information")
},
}
}
}
impl Error for MonitorPermissionError {}
#[derive(Debug)]
pub struct HasMonitorPermissionFuture(PlatformHasMonitorPermissionFuture);
impl Future for HasMonitorPermissionFuture {
type Output = bool;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.0).poll(cx)
}
}
/// Additional methods on [`MonitorHandle`] that are specific to the Web.
///
/// [`MonitorHandle`]: crate::monitor::MonitorHandle
pub trait MonitorHandleExtWeb {
/// Returns whether the screen is internal to the device or external.
///
/// External devices are generally manufactured separately from the device they are attached to
/// and can be connected and disconnected as needed, whereas internal screens are part of
/// the device and not intended to be disconnected.
fn is_internal(&self) -> Option<bool>;
/// Returns screen orientation data for this monitor.
fn orientation(&self) -> OrientationData;
/// Lock the screen orientation. The returned [`OrientationLockFuture`] can be dropped without
/// aborting the request.
///
/// Will fail if another locking call is in progress.
fn request_lock(&self, orientation: OrientationLock) -> OrientationLockFuture;
/// Unlock the screen orientation.
///
/// Will fail if a locking call is in progress.
fn unlock(&self) -> Result<(), OrientationLockError>;
/// Returns whether this [`MonitorHandle`] was created using detailed monitor permissions. If
/// [`false`] will always represent the current monitor the browser window is in instead of a
/// specific monitor.
///
/// See [`ActiveEventLoopExtWeb::request_detailed_monitor_permission()`].
///
/// [`MonitorHandle`]: crate::monitor::MonitorHandle
fn is_detailed(&self) -> bool;
}
impl MonitorHandleExtWeb for dyn MonitorHandleProvider + '_ {
fn is_internal(&self) -> Option<bool> {
self.cast_ref::<WebMonitorHandle>().unwrap().is_internal()
}
fn orientation(&self) -> OrientationData {
self.cast_ref::<WebMonitorHandle>().unwrap().orientation()
}
fn request_lock(&self, orientation_lock: OrientationLock) -> OrientationLockFuture {
let future = self.cast_ref::<WebMonitorHandle>().unwrap().request_lock(orientation_lock);
OrientationLockFuture(future)
}
fn unlock(&self) -> Result<(), OrientationLockError> {
self.cast_ref::<WebMonitorHandle>().unwrap().unlock()
}
fn is_detailed(&self) -> bool {
self.cast_ref::<WebMonitorHandle>().unwrap().is_detailed()
}
}
/// Screen orientation data.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub struct OrientationData {
/// The orientation.
pub orientation: Orientation,
/// [`true`] if the [`orientation`](Self::orientation) is flipped upside down.
pub flipped: bool,
/// The most natural orientation for the screen. Computer monitors are commonly naturally
/// landscape mode, while mobile phones are commonly naturally portrait mode.
pub natural: Orientation,
}
/// Screen orientation.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum Orientation {
/// The screen's aspect ratio has a width greater than the height.
Landscape,
/// The screen's aspect ratio has a height greater than the width.
Portrait,
}
/// Screen orientation lock options. Represents which orientations a user can use.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum OrientationLock {
/// User is free to use any orientation.
Any,
/// User is locked to the most upright natural orientation for the screen. Computer monitors
/// are commonly naturally landscape mode, while mobile phones are commonly
/// naturally portrait mode.
Natural,
/// User is locked to landscape mode.
Landscape {
/// - [`None`]: User is locked to both upright or upside down landscape mode.
/// - [`true`]: User is locked to upright landscape mode.
/// - [`false`]: User is locked to upside down landscape mode.
flipped: Option<bool>,
},
/// User is locked to portrait mode.
Portrait {
/// - [`None`]: User is locked to both upright or upside down portrait mode.
/// - [`true`]: User is locked to upright portrait mode.
/// - [`false`]: User is locked to upside down portrait mode.
flipped: Option<bool>,
},
}
/// Can be dropped without aborting the request to lock the screen.
#[derive(Debug)]
pub struct OrientationLockFuture(PlatformOrientationLockFuture);
impl Future for OrientationLockFuture {
type Output = Result<(), OrientationLockError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
Pin::new(&mut self.0).poll(cx)
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub enum OrientationLockError {
Unsupported,
Busy,
}
impl Display for OrientationLockError {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Unsupported => write!(f, "Locking the screen orientation is not supported"),
Self::Busy => write!(f, "Another locking call is in progress"),
}
}
}
impl Error for OrientationLockError {}

82
winit-web/src/lock.rs Normal file
View file

@ -0,0 +1,82 @@
use std::cell::OnceCell;
use js_sys::{Object, Promise};
use tracing::error;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{console, Document, DomException, Element, Navigator};
pub(crate) fn is_cursor_lock_raw(navigator: &Navigator, document: &Document) -> bool {
thread_local! {
static IS_CURSOR_LOCK_RAW: OnceCell<bool> = const { OnceCell::new() };
}
IS_CURSOR_LOCK_RAW.with(|cell| {
*cell.get_or_init(|| {
// TODO: Remove when Chrome can better advertise that they don't support unaccelerated
// movement on Linux.
// See <https://issues.chromium.org/issues/40833850>.
if super::web_sys::chrome_linux(navigator) {
return false;
}
let element: ElementExt = document.create_element("div").unwrap().unchecked_into();
let promise = element.request_pointer_lock();
if promise.is_undefined() {
false
} else {
thread_local! {
static REJECT_HANDLER: Closure<dyn FnMut(JsValue)> = Closure::new(|_| ());
}
let promise: Promise = promise.unchecked_into();
let _ = REJECT_HANDLER.with(|handler| promise.catch(handler));
true
}
})
})
}
pub(crate) fn request_pointer_lock(navigator: &Navigator, document: &Document, element: &Element) {
if is_cursor_lock_raw(navigator, document) {
thread_local! {
static REJECT_HANDLER: Closure<dyn FnMut(JsValue)> = Closure::new(|error: JsValue| {
if let Some(error) = error.dyn_ref::<DomException>() {
error!("Failed to lock pointer. {}: {}", error.name(), error.message());
} else {
console::error_1(&error);
error!("Failed to lock pointer");
}
});
}
let element: &ElementExt = element.unchecked_ref();
let options: PointerLockOptions = Object::new().unchecked_into();
options.set_unadjusted_movement(true);
let _ = REJECT_HANDLER
.with(|handler| element.request_pointer_lock_with_options(&options).catch(handler));
} else {
element.request_pointer_lock();
}
}
#[wasm_bindgen]
extern "C" {
type ElementExt;
#[wasm_bindgen(method, js_name = requestPointerLock)]
fn request_pointer_lock(this: &ElementExt) -> JsValue;
#[wasm_bindgen(method, js_name = requestPointerLock)]
fn request_pointer_lock_with_options(
this: &ElementExt,
options: &PointerLockOptions,
) -> Promise;
type PointerLockOptions;
#[wasm_bindgen(method, setter, js_name = unadjustedMovement)]
fn set_unadjusted_movement(this: &PointerLockOptions, value: bool);
}

View file

@ -0,0 +1,96 @@
use std::fmt::{self, Debug, Formatter};
use std::marker::PhantomData;
use std::mem;
use std::sync::OnceLock;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};
use super::r#async::{self, Sender};
thread_local! {
static MAIN_THREAD: bool = {
#[wasm_bindgen]
extern "C" {
#[derive(Clone)]
type Global;
#[wasm_bindgen(method, getter, js_name = Window)]
fn window(this: &Global) -> JsValue;
}
let global: Global = js_sys::global().unchecked_into();
!global.window().is_undefined()
};
}
#[derive(Clone, Copy, Debug)]
pub struct MainThreadMarker(PhantomData<*const ()>);
impl MainThreadMarker {
pub fn new() -> Option<Self> {
MAIN_THREAD.with(|is| is.then_some(Self(PhantomData)))
}
}
pub struct MainThreadSafe<T: 'static>(Option<T>);
impl<T> MainThreadSafe<T> {
pub fn new(_: MainThreadMarker, value: T) -> Self {
DROP_HANDLER.get_or_init(|| {
let (sender, receiver) = r#async::channel();
wasm_bindgen_futures::spawn_local(
async move { while receiver.next().await.is_ok() {} },
);
sender
});
Self(Some(value))
}
pub fn into_inner(mut self, _: MainThreadMarker) -> T {
self.0.take().expect("already taken or dropped")
}
pub fn get(&self, _: MainThreadMarker) -> &T {
self.0.as_ref().expect("already taken or dropped")
}
}
impl<T: Debug> Debug for MainThreadSafe<T> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if MainThreadMarker::new().is_some() {
f.debug_tuple("MainThreadSafe").field(&self.0).finish()
} else {
f.debug_struct("MainThreadSafe").finish_non_exhaustive()
}
}
}
impl<T> Drop for MainThreadSafe<T> {
fn drop(&mut self) {
if let Some(value) = self.0.take() {
if mem::needs_drop::<T>() && MainThreadMarker::new().is_none() {
DROP_HANDLER
.get()
.expect("drop handler not initialized when setting canvas")
.send(DropBox(Box::new(value)))
.expect("sender dropped in main thread")
}
}
}
}
unsafe impl<T> Send for MainThreadSafe<T> {}
unsafe impl<T> Sync for MainThreadSafe<T> {}
static DROP_HANDLER: OnceLock<Sender<DropBox>> = OnceLock::new();
struct DropBox(#[allow(dead_code)] Box<dyn Any>);
unsafe impl Send for DropBox {}
unsafe impl Sync for DropBox {}
trait Any {}
impl<T> Any for T {}

938
winit-web/src/monitor.rs Normal file
View file

@ -0,0 +1,938 @@
use std::cell::{OnceCell, Ref, RefCell};
use std::cmp::Ordering;
use std::fmt::{self, Debug, Formatter};
use std::future::Future;
use std::hash::{Hash, Hasher};
use std::mem;
use std::num::NonZeroU16;
use std::ops::{Deref, DerefMut};
use std::pin::Pin;
use std::rc::{Rc, Weak};
use std::sync::{Arc, OnceLock};
use std::task::{ready, Context, Poll};
use dpi::{LogicalSize, PhysicalPosition, PhysicalSize};
use js_sys::{Object, Promise};
use tracing::error;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
console, DomException, Navigator, OrientationLockType, OrientationType, PermissionState,
PermissionStatus, ScreenOrientation, Window,
};
use winit_core::monitor::{MonitorHandle as CoreMonitorHandle, MonitorHandleProvider, VideoMode};
use super::event_loop::runner::WeakShared;
use super::main_thread::MainThreadMarker;
use super::r#async::{Dispatcher, Notified, Notifier};
use super::web_sys::{Engine, EventListenerHandle};
use crate::{
MonitorPermissionError, Orientation, OrientationData, OrientationLock, OrientationLockError,
};
#[derive(Clone, Eq)]
pub struct MonitorHandle {
/// [`None`] means [`web_sys::Screen`], which is always the same.
id: Option<u64>,
inner: Dispatcher<Inner>,
}
impl MonitorHandle {
fn new(main_thread: MainThreadMarker, inner: Inner) -> Self {
let id = if let Screen::Detailed { id, .. } = inner.screen { Some(id) } else { None };
Self { id, inner: Dispatcher::new(main_thread, inner).0 }
}
pub fn orientation(&self) -> OrientationData {
self.inner.queue(|inner| inner.orientation())
}
pub fn request_lock(&self, orientation_lock: OrientationLock) -> OrientationLockFuture {
// Short-circuit without blocking.
if let Some(support) = has_previous_lock_support() {
if !support {
return OrientationLockFuture::Ready(Some(Err(OrientationLockError::Unsupported)));
}
}
self.inner.queue(|inner| {
if !inner.has_lock_support() {
return OrientationLockFuture::Ready(Some(Err(OrientationLockError::Unsupported)));
}
let future =
JsFuture::from(inner.orientation_raw().lock(orientation_lock.to_js()).unwrap());
let notifier = Notifier::new();
let notified = notifier.notified();
wasm_bindgen_futures::spawn_local(async move {
notifier.notify(future.await.map(|_| ()).map_err(OrientationLockError::from_js));
});
OrientationLockFuture::Future(notified)
})
}
pub fn unlock(&self) -> Result<(), OrientationLockError> {
// Short-circuit without blocking.
if let Some(support) = has_previous_lock_support() {
if !support {
return Err(OrientationLockError::Unsupported);
}
}
self.inner.queue(|inner| {
if !inner.has_lock_support() {
return Err(OrientationLockError::Unsupported);
}
inner.orientation_raw().unlock().map_err(OrientationLockError::from_js)
})
}
pub fn is_internal(&self) -> Option<bool> {
self.inner.queue(|inner| inner.is_internal())
}
pub fn is_detailed(&self) -> bool {
self.inner.queue(|inner| inner.is_detailed())
}
pub(crate) fn detailed(
&self,
main_thread: MainThreadMarker,
) -> Option<Ref<'_, ScreenDetailed>> {
let inner = self.inner.value(main_thread);
match &inner.screen {
Screen::Screen(_) => None,
Screen::Detailed { .. } => Some(Ref::map(inner, |inner| {
if let Screen::Detailed { screen, .. } = &inner.screen {
screen.deref()
} else {
unreachable!()
}
})),
}
}
}
impl MonitorHandleProvider for MonitorHandle {
fn id(&self) -> u128 {
self.native_id() as _
}
fn native_id(&self) -> u64 {
self.id.unwrap_or_default()
}
fn scale_factor(&self) -> f64 {
self.inner.queue(|inner| inner.scale_factor())
}
fn position(&self) -> Option<PhysicalPosition<i32>> {
self.inner.queue(|inner| inner.position())
}
fn name(&self) -> Option<std::borrow::Cow<'_, str>> {
self.inner.queue(|inner| inner.name().map(Into::into))
}
fn current_video_mode(&self) -> Option<VideoMode> {
Some(VideoMode::new(
self.inner.queue(|inner| inner.size()),
self.inner.queue(|inner| inner.bit_depth()),
None,
))
}
fn video_modes(&self) -> Box<dyn Iterator<Item = VideoMode>> {
Box::new(self.current_video_mode().into_iter())
}
}
impl Debug for MonitorHandle {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let (name, position, scale_factor, orientation, is_internal, is_detailed) =
self.inner.queue(|this| {
(
this.name(),
this.position(),
this.scale_factor(),
this.orientation(),
this.is_internal(),
this.is_detailed(),
)
});
f.debug_struct("MonitorHandle")
.field("name", &name)
.field("position", &position)
.field("scale_factor", &scale_factor)
.field("orientation", &orientation)
.field("is_internal", &is_internal)
.field("is_detailed", &is_detailed)
.finish()
}
}
impl Hash for MonitorHandle {
fn hash<H: Hasher>(&self, state: &mut H) {
self.id.hash(state)
}
}
impl Ord for MonitorHandle {
fn cmp(&self, other: &Self) -> Ordering {
self.id.cmp(&other.id)
}
}
impl PartialEq for MonitorHandle {
fn eq(&self, other: &Self) -> bool {
self.id.eq(&other.id)
}
}
impl PartialOrd for MonitorHandle {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl From<MonitorHandle> for CoreMonitorHandle {
fn from(monitor: MonitorHandle) -> Self {
CoreMonitorHandle(Arc::new(monitor))
}
}
#[derive(Debug)]
pub enum OrientationLockFuture {
Future(Notified<Result<(), OrientationLockError>>),
Ready(Option<Result<(), OrientationLockError>>),
}
impl Future for OrientationLockFuture {
type Output = Result<(), OrientationLockError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.get_mut() {
Self::Future(notified) => Pin::new(notified).poll(cx).map(Option::unwrap),
Self::Ready(result) => {
Poll::Ready(result.take().expect("`OrientationLockFuture` polled after completion"))
},
}
}
}
impl OrientationLock {
fn to_js(self) -> OrientationLockType {
match self {
OrientationLock::Any => OrientationLockType::Any,
OrientationLock::Natural => OrientationLockType::Natural,
OrientationLock::Landscape { flipped: None } => OrientationLockType::Landscape,
OrientationLock::Landscape { flipped: Some(flipped) } => {
if flipped {
OrientationLockType::LandscapeSecondary
} else {
OrientationLockType::LandscapePrimary
}
},
OrientationLock::Portrait { flipped: None } => OrientationLockType::Portrait,
OrientationLock::Portrait { flipped: Some(flipped) } => {
if flipped {
OrientationLockType::PortraitSecondary
} else {
OrientationLockType::PortraitPrimary
}
},
}
}
}
impl OrientationLockError {
fn from_js(error: JsValue) -> Self {
debug_assert!(error.has_type::<DomException>());
let error: DomException = error.unchecked_into();
if let DomException::ABORT_ERR = error.code() {
OrientationLockError::Busy
} else {
OrientationLockError::Unsupported
}
}
}
struct Inner {
window: WindowExt,
engine: Option<Engine>,
screen: Screen,
orientation: OnceCell<ScreenOrientationExt>,
}
impl Inner {
fn new(window: WindowExt, engine: Option<Engine>, screen: Screen) -> Self {
Self { window, engine, screen, orientation: OnceCell::new() }
}
fn scale_factor(&self) -> f64 {
match &self.screen {
Screen::Screen(_) => 0.,
Screen::Detailed { screen, .. } => screen.device_pixel_ratio(),
}
}
fn position(&self) -> Option<PhysicalPosition<i32>> {
if let Screen::Detailed { screen, .. } = &self.screen {
Some(PhysicalPosition::new(screen.left(), screen.top()))
} else {
None
}
}
fn name(&self) -> Option<String> {
if let Screen::Detailed { screen, .. } = &self.screen {
Some(screen.label())
} else {
None
}
}
fn orientation_raw(&self) -> &ScreenOrientationExt {
self.orientation.get_or_init(|| self.screen.orientation().unchecked_into())
}
fn orientation(&self) -> OrientationData {
let orientation = self.orientation_raw();
let angle = orientation.angle().unwrap();
match orientation.type_().unwrap() {
OrientationType::LandscapePrimary => OrientationData {
orientation: Orientation::Landscape,
flipped: false,
natural: if angle == 0 { Orientation::Landscape } else { Orientation::Portrait },
},
OrientationType::LandscapeSecondary => OrientationData {
orientation: Orientation::Landscape,
flipped: true,
natural: if angle == 180 { Orientation::Landscape } else { Orientation::Portrait },
},
OrientationType::PortraitPrimary => OrientationData {
orientation: Orientation::Portrait,
flipped: false,
natural: if angle == 0 { Orientation::Portrait } else { Orientation::Landscape },
},
OrientationType::PortraitSecondary => OrientationData {
orientation: Orientation::Portrait,
flipped: true,
natural: if angle == 180 { Orientation::Portrait } else { Orientation::Landscape },
},
_ => {
unreachable!("found unrecognized orientation: {}", orientation.type_string())
},
}
}
fn is_internal(&self) -> Option<bool> {
if let Screen::Detailed { screen, .. } = &self.screen {
Some(screen.is_internal())
} else {
None
}
}
fn is_detailed(&self) -> bool {
matches!(self.screen, Screen::Detailed { .. })
}
fn size(&self) -> PhysicalSize<u32> {
let width = self.screen.width().unwrap();
let height = self.screen.height().unwrap();
if let Some(Engine::Chromium) = self.engine {
PhysicalSize::new(width, height).cast()
} else {
LogicalSize::new(width, height).to_physical(super::web_sys::scale_factor(&self.window))
}
}
fn bit_depth(&self) -> Option<NonZeroU16> {
NonZeroU16::new(self.screen.color_depth().unwrap().try_into().unwrap())
}
fn has_lock_support(&self) -> bool {
*HAS_LOCK_SUPPORT.get_or_init(|| !self.orientation_raw().has_lock().is_undefined())
}
}
impl Drop for Inner {
fn drop(&mut self) {
if let Screen::Detailed { runner, id, screen } = &self.screen {
// If this is the last screen with its ID, clean it up in the `MonitorHandler`.
if Rc::strong_count(screen) == 1 {
if let Some(runner) = runner.upgrade() {
let mut state = runner.monitor().state.borrow_mut();
let State::Detailed(detailed) = state.deref_mut() else {
unreachable!("found a `ScreenDetailed` without being in `State::Detailed`")
};
detailed.screens.retain(|(id_internal, _)| *id_internal != *id)
}
}
}
}
}
enum Screen {
Screen(ScreenExt),
Detailed { runner: WeakShared, id: u64, screen: Rc<ScreenDetailed> },
}
impl Deref for Screen {
type Target = ScreenExt;
fn deref(&self) -> &Self::Target {
match self {
Screen::Screen(screen) => screen,
Screen::Detailed { screen, .. } => screen,
}
}
}
pub struct MonitorHandler {
runner: WeakShared,
state: RefCell<State>,
main_thread: MainThreadMarker,
window: WindowExt,
engine: Option<Engine>,
screen: ScreenExt,
}
enum State {
Unsupported,
Initialize(Notified<Result<(), MonitorPermissionError>>),
Permission { permission: PermissionStatusExt, _handle: EventListenerHandle<dyn Fn()> },
Upgrade(Notified<Result<(), MonitorPermissionError>>),
Detailed(Detailed),
}
struct Detailed {
details: ScreenDetails,
id_counter: u64,
screens: Vec<(u64, Weak<ScreenDetailed>)>,
}
impl Detailed {
fn handle(
&mut self,
main_thread: MainThreadMarker,
runner: WeakShared,
window: WindowExt,
engine: Option<Engine>,
screen: ScreenDetailed,
) -> MonitorHandle {
// Before creating a new entry, see if we have an ID for this screen already.
let found_screen = self.screens.iter().find_map(|(id, internal_screen)| {
let internal_screen =
internal_screen.upgrade().expect("dropped `MonitorHandle` without cleaning up");
if *internal_screen == screen {
Some((*id, internal_screen))
} else {
None
}
});
let (id, screen) = if let Some((id, screen)) = found_screen {
(id, screen)
} else {
let id = self.id_counter;
self.id_counter += 1;
let screen = Rc::new(screen);
self.screens.push((id, Rc::downgrade(&screen)));
(id, screen)
};
MonitorHandle::new(
main_thread,
Inner::new(window, engine, Screen::Detailed { runner, id, screen }),
)
}
}
impl MonitorHandler {
/// When the [`MonitorHandler`] is created, it first checks if permission has already been
/// granted by the user for this page, in which case it retrieves [`ScreenDetails`].
///
/// If not, it will listen to external changes in the permission and automatically elevate
/// [`MonitorHandler`].
pub fn new(
main_thread: MainThreadMarker,
window: Window,
navigator: &Navigator,
runner: WeakShared,
) -> Self {
let window: WindowExt = window.unchecked_into();
let engine = super::web_sys::engine(navigator);
let screen: ScreenExt = window.screen().unwrap().unchecked_into();
let state = if has_screen_details_support(&window) {
// First try and get permissions.
let permissions = navigator.permissions().expect(
"expected the Permissions API to be implemented if the Window Management API is \
as well",
);
let descriptor: PermissionDescriptor = Object::new().unchecked_into();
descriptor.set_name("window-management");
let future = JsFuture::from(permissions.query(&descriptor).unwrap());
let runner = runner.clone();
let window = window.clone();
let notifier = Notifier::new();
let notified = notifier.notified();
wasm_bindgen_futures::spawn_local(async move {
let permission: PermissionStatusExt = match future.await {
Ok(permission) => permission.unchecked_into(),
Err(error) => unreachable_error(
&error,
"retrieving permission for Window Management API failed even though its \
implemented",
),
};
let details = match permission.state() {
// If we have permission, go ahead and get `ScreenDetails`.
PermissionState::Granted => {
let details = match JsFuture::from(window.screen_details()).await {
Ok(details) => details.unchecked_into(),
Err(error) => unreachable_error(
&error,
"getting screen details failed even though permission was granted",
),
};
notifier.notify(Ok(()));
Some(details)
},
PermissionState::Denied => {
notifier.notify(Err(MonitorPermissionError::Denied));
None
},
PermissionState::Prompt => {
notifier.notify(Err(MonitorPermissionError::Prompt));
None
},
_ => {
error!(
"encountered unknown permission state: {}",
permission.state_string()
);
notifier.notify(Err(MonitorPermissionError::Denied));
None
},
};
// Notifying `Future`s is not dependant on the lifetime of the runner,
// because they can outlive it.
if let Some(runner) = runner.upgrade() {
if let Some(details) = details {
runner.monitor().upgrade(details);
} else {
// If permission is denied we listen for changes so we can catch external
// permission granting.
let handle =
Self::setup_listener(runner.weak(), window, permission.clone());
*runner.monitor().state.borrow_mut() =
State::Permission { permission, _handle: handle };
};
runner.start_delayed();
}
});
State::Initialize(notified)
} else {
State::Unsupported
};
Self { runner, state: RefCell::new(state), main_thread, window, engine, screen }
}
/// Listens to external permission changes and elevates [`MonitorHandle`] automatically.
fn setup_listener(
runner: WeakShared,
window: WindowExt,
permission: PermissionStatus,
) -> EventListenerHandle<dyn Fn()> {
EventListenerHandle::new(
permission.clone(),
"change",
Closure::new(move || {
if let PermissionState::Granted = permission.state() {
let future = JsFuture::from(window.screen_details());
let runner = runner.clone();
wasm_bindgen_futures::spawn_local(async move {
let details = match future.await {
Ok(details) => details.unchecked_into(),
Err(error) => unreachable_error(
&error,
"getting screen details failed even though permission was granted",
),
};
if let Some(runner) = runner.upgrade() {
// We drop the event listener handle here, which
// doesn't drop it during its execution, because
// we are in a `spawn_local()` context.
runner.monitor().upgrade(details);
}
});
}
}),
)
}
/// Elevate [`MonitorHandler`] to [`ScreenDetails`].
fn upgrade(&self, details: ScreenDetails) {
*self.state.borrow_mut() =
State::Detailed(Detailed { details, id_counter: 0, screens: Vec::new() });
}
pub fn is_extended(&self) -> Option<bool> {
self.screen.is_extended()
}
pub fn is_initializing(&self) -> bool {
matches!(self.state.borrow().deref(), State::Initialize(_))
}
fn handle(&self, detailed: &mut Detailed, screen: ScreenDetailed) -> MonitorHandle {
detailed.handle(
self.main_thread,
self.runner.clone(),
self.window.clone(),
self.engine,
screen,
)
}
pub fn current_monitor(&self) -> MonitorHandle {
if let State::Detailed(detailed) = self.state.borrow_mut().deref_mut() {
self.handle(detailed, detailed.details.current_screen())
} else {
MonitorHandle::new(
self.main_thread,
Inner::new(self.window.clone(), self.engine, Screen::Screen(self.screen.clone())),
)
}
}
// Note: We have to return a `Vec` here because the iterator is otherwise not `Send` + `Sync`.
pub fn available_monitors(&self) -> Vec<MonitorHandle> {
let mut state = self.state.borrow_mut();
if let State::Detailed(detailed) = state.deref_mut() {
detailed
.details
.screens()
.into_iter()
.map(move |screen| self.handle(detailed, screen))
.collect()
} else {
drop(state);
vec![self.current_monitor()]
}
}
pub fn primary_monitor(&self) -> Option<MonitorHandle> {
if let State::Detailed(detailed) = self.state.borrow_mut().deref_mut() {
detailed
.details
.screens()
.into_iter()
.find_map(|screen| screen.is_primary().then(|| self.handle(detailed, screen)))
} else {
None
}
}
pub(crate) fn request_detailed_monitor_permission(&self) -> MonitorPermissionFuture {
let state = self.state.borrow();
let permission = match state.deref() {
State::Unsupported => {
return MonitorPermissionFuture::Ready(Some(Err(
MonitorPermissionError::Unsupported,
)))
},
// If we are currently initializing, wait for initialization to finish before we do our
// thing.
State::Initialize(notified) => {
return MonitorPermissionFuture::Initialize {
runner: Dispatcher::new(
self.main_thread,
(self.runner.clone(), self.window.clone()),
)
.0,
notified: notified.clone(),
}
},
// If we finished initialization we at least possess `PermissionStatus`.
State::Permission { permission, .. } => permission,
// A request is already in progress. Use that!
State::Upgrade(notified) => return MonitorPermissionFuture::Upgrade(notified.clone()),
State::Detailed { .. } => return MonitorPermissionFuture::Ready(Some(Ok(()))),
};
match permission.state() {
PermissionState::Granted | PermissionState::Prompt => (),
PermissionState::Denied => {
return MonitorPermissionFuture::Ready(Some(Err(MonitorPermissionError::Denied)))
},
_ => {
error!("encountered unknown permission state: {}", permission.state_string());
return MonitorPermissionFuture::Ready(Some(Err(MonitorPermissionError::Denied)));
},
}
drop(state);
// We are ready to explicitly ask the user for permission, lets go!
let notifier = Notifier::new();
let notified = notifier.notified();
*self.state.borrow_mut() = State::Upgrade(notified.clone());
MonitorPermissionFuture::upgrade_internal(self.runner.clone(), &self.window, notifier);
MonitorPermissionFuture::Upgrade(notified)
}
pub fn has_detailed_monitor_permission_async(&self) -> HasMonitorPermissionFuture {
match self.state.borrow().deref() {
State::Unsupported | State::Permission { .. } | State::Upgrade(_) => {
HasMonitorPermissionFuture::Ready(Some(false))
},
State::Initialize(notified) => HasMonitorPermissionFuture::Future(notified.clone()),
State::Detailed { .. } => HasMonitorPermissionFuture::Ready(Some(true)),
}
}
pub fn has_detailed_monitor_permission(&self) -> bool {
match self.state.borrow().deref() {
State::Unsupported | State::Permission { .. } | State::Upgrade(_) => false,
State::Initialize(_) => {
unreachable!("called `has_detailed_monitor_permission()` while initializing")
},
State::Detailed { .. } => true,
}
}
}
#[derive(Debug)]
pub(crate) enum MonitorPermissionFuture {
Initialize {
runner: Dispatcher<(WeakShared, WindowExt)>,
notified: Notified<Result<(), MonitorPermissionError>>,
},
Upgrade(Notified<Result<(), MonitorPermissionError>>),
Ready(Option<Result<(), MonitorPermissionError>>),
}
impl MonitorPermissionFuture {
fn upgrade(&mut self) {
let notifier = Notifier::new();
let notified = notifier.notified();
let Self::Initialize { runner, .. } = mem::replace(self, Self::Upgrade(notified.clone()))
else {
unreachable!()
};
runner.dispatch(|(runner, window)| {
if let Some(runner) = runner.upgrade() {
*runner.monitor().state.borrow_mut() = State::Upgrade(notified);
}
Self::upgrade_internal(runner.clone(), window, notifier);
});
}
fn upgrade_internal(
runner: WeakShared,
window: &WindowExt,
notifier: Notifier<Result<(), MonitorPermissionError>>,
) {
let future = JsFuture::from(window.screen_details());
wasm_bindgen_futures::spawn_local(async move {
match future.await {
Ok(details) => {
// Notifying `Future`s is not dependant on the lifetime of the runner, because
// they can outlive it.
notifier.notify(Ok(()));
if let Some(runner) = runner.upgrade() {
runner.monitor().upgrade(details.unchecked_into());
}
},
Err(error) => unreachable_error(
&error,
"getting screen details failed even though permission was granted",
),
}
});
}
}
impl Future for MonitorPermissionFuture {
type Output = Result<(), MonitorPermissionError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
match this {
Self::Initialize { notified, .. } => {
if let Err(error) = ready!(Pin::new(notified).poll(cx).map(Option::unwrap)) {
match error {
MonitorPermissionError::Denied | MonitorPermissionError::Unsupported => {
Poll::Ready(Err(error))
},
MonitorPermissionError::Prompt => {
this.upgrade();
Poll::Pending
},
}
} else {
Poll::Ready(Ok(()))
}
},
Self::Upgrade(notified) => Pin::new(notified).poll(cx).map(Option::unwrap),
Self::Ready(result) => Poll::Ready(
result.take().expect("`MonitorPermissionFuture` polled after completion"),
),
}
}
}
#[derive(Debug)]
pub enum HasMonitorPermissionFuture {
Future(Notified<Result<(), MonitorPermissionError>>),
Ready(Option<bool>),
}
impl Future for HasMonitorPermissionFuture {
type Output = bool;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.get_mut() {
Self::Future(notified) => {
Pin::new(notified).poll(cx).map(Option::unwrap).map(|result| result.is_ok())
},
Self::Ready(result) => Poll::Ready(
result.take().expect("`MonitorPermissionFuture` polled after completion"),
),
}
}
}
#[track_caller]
fn unreachable_error(error: &JsValue, message: &str) -> ! {
if let Some(error) = error.dyn_ref::<DomException>() {
unreachable!("{message}. {}: {}", error.name(), error.message());
} else {
console::error_1(error);
unreachable!("{message}");
}
}
static HAS_LOCK_SUPPORT: OnceLock<bool> = OnceLock::new();
fn has_previous_lock_support() -> Option<bool> {
HAS_LOCK_SUPPORT.get().cloned()
}
pub fn has_screen_details_support(window: &Window) -> bool {
thread_local! {
static HAS_SCREEN_DETAILS: OnceCell<bool> = const { OnceCell::new() };
}
HAS_SCREEN_DETAILS.with(|support| {
*support.get_or_init(|| {
let window: &WindowExt = window.unchecked_ref();
!window.has_screen_details().is_undefined()
})
})
}
#[wasm_bindgen]
extern "C" {
#[derive(Clone)]
#[wasm_bindgen(extends = Window)]
pub(crate) type WindowExt;
#[wasm_bindgen(method, getter, js_name = getScreenDetails)]
fn has_screen_details(this: &WindowExt) -> JsValue;
#[wasm_bindgen(method, js_name = getScreenDetails)]
fn screen_details(this: &WindowExt) -> Promise;
type ScreenDetails;
#[wasm_bindgen(method, getter, js_name = currentScreen)]
fn current_screen(this: &ScreenDetails) -> ScreenDetailed;
#[wasm_bindgen(method, getter)]
fn screens(this: &ScreenDetails) -> Vec<ScreenDetailed>;
#[derive(Clone, PartialEq)]
#[wasm_bindgen(extends = web_sys::Screen)]
pub(crate) type ScreenExt;
#[wasm_bindgen(method, getter, js_name = isExtended)]
fn is_extended(this: &ScreenExt) -> Option<bool>;
#[wasm_bindgen(extends = ScreenOrientation)]
type ScreenOrientationExt;
#[wasm_bindgen(method, getter, js_name = type)]
fn type_string(this: &ScreenOrientationExt) -> String;
#[wasm_bindgen(method, getter, js_name = lock)]
fn has_lock(this: &ScreenOrientationExt) -> JsValue;
#[derive(PartialEq)]
#[wasm_bindgen(extends = ScreenExt)]
pub(crate) type ScreenDetailed;
#[wasm_bindgen(method, getter, js_name = devicePixelRatio)]
fn device_pixel_ratio(this: &ScreenDetailed) -> f64;
#[wasm_bindgen(method, getter, js_name = isInternal)]
fn is_internal(this: &ScreenDetailed) -> bool;
#[wasm_bindgen(method, getter, js_name = isPrimary)]
fn is_primary(this: &ScreenDetailed) -> bool;
#[wasm_bindgen(method, getter)]
fn label(this: &ScreenDetailed) -> String;
#[wasm_bindgen(method, getter)]
fn left(this: &ScreenDetailed) -> i32;
#[wasm_bindgen(method, getter)]
fn top(this: &ScreenDetailed) -> i32;
#[wasm_bindgen(extends = Object)]
type PermissionDescriptor;
#[wasm_bindgen(method, setter, js_name = name)]
fn set_name(this: &PermissionDescriptor, name: &str);
#[wasm_bindgen(extends = PermissionStatus)]
type PermissionStatusExt;
#[wasm_bindgen(method, getter, js_name = state)]
fn state_string(this: &PermissionStatusExt) -> String;
}

View file

@ -0,0 +1,19 @@
{
"module": {
"type": "es6"
},
"isModule": true,
"minify": true,
"jsc": {
"parser": {
"syntax": "typescript"
},
"target": "es2022",
"minify": {
"compress": {
"unused": true
},
"mangle": true
}
}
}

View file

@ -0,0 +1,32 @@
import eslint from '@eslint/js'
import tseslint from 'typescript-eslint'
import globals from 'globals'
export default tseslint.config(
{
ignores: ['**/*.min.js', 'eslint.config.mjs'],
},
eslint.configs.recommended,
...tseslint.configs.strictTypeChecked,
...tseslint.configs.stylisticTypeChecked,
{
languageOptions: {
parserOptions: {
ecmaVersion: 'latest',
project: ['tsconfig.json'],
sourceType: 'module',
},
globals: {
...globals.browser,
},
},
rules: {
'@typescript-eslint/no-confusing-void-expression': [
'error',
{
ignoreArrowShorthand: true,
},
],
},
}
)

View file

@ -0,0 +1,8 @@
{
"devDependencies": {
"@eslint/js": "^9",
"eslint": "^9",
"typescript": "^5",
"typescript-eslint": "^8"
}
}

12
winit-web/src/script/scheduler.d.ts vendored Normal file
View file

@ -0,0 +1,12 @@
declare global {
// eslint-disable-next-line no-var
var scheduler: Scheduler
}
export interface Scheduler {
postTask<T>(callback: () => T | PromiseLike<T>, options?: SchedulerPostTaskOptions): Promise<T>
}
export interface SchedulerPostTaskOptions {
delay?: number
}

View file

@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
"noUncheckedIndexedAccess": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"strict": true,
}
}

1
winit-web/src/script/worker.min.js vendored Normal file
View file

@ -0,0 +1 @@
onmessage=e=>{let[s,a]=e.data,l=()=>s.postMessage(void 0);"scheduler"in globalThis?globalThis.scheduler.postTask(l,{delay:a}):setTimeout(l,a)};

View file

@ -0,0 +1,10 @@
onmessage = (event) => {
const [port, timeout] = event.data as [MessagePort, number]
const f = () => port.postMessage(undefined)
if ('scheduler' in globalThis) {
void globalThis.scheduler.postTask(f, { delay: timeout })
} else {
setTimeout(f, timeout)
}
}

View file

@ -0,0 +1,61 @@
use std::cell::Cell;
use std::rc::Rc;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
pub struct AnimationFrameHandler {
window: web_sys::Window,
closure: Closure<dyn FnMut()>,
handle: Rc<Cell<Option<i32>>>,
}
impl AnimationFrameHandler {
pub fn new(window: web_sys::Window) -> Self {
let handle = Rc::new(Cell::new(None));
let closure = Closure::new({
let handle = handle.clone();
move || handle.set(None)
});
Self { window, closure, handle }
}
pub fn on_animation_frame<F>(&mut self, mut f: F)
where
F: 'static + FnMut(),
{
let handle = self.handle.clone();
self.closure = Closure::new(move || {
handle.set(None);
f();
})
}
pub fn request(&self) {
if let Some(handle) = self.handle.take() {
self.window.cancel_animation_frame(handle).expect("Failed to cancel animation frame");
}
let handle = self
.window
.request_animation_frame(self.closure.as_ref().unchecked_ref())
.expect("Failed to request animation frame");
self.handle.set(Some(handle));
}
pub fn cancel(&mut self) {
if let Some(handle) = self.handle.take() {
self.window.cancel_animation_frame(handle).expect("Failed to cancel animation frame");
}
}
}
impl Drop for AnimationFrameHandler {
fn drop(&mut self) {
if let Some(handle) = self.handle.take() {
self.window.cancel_animation_frame(handle).expect("Failed to cancel animation frame");
}
}
}

View file

@ -0,0 +1,594 @@
use std::cell::{Cell, RefCell};
use std::ops::Deref;
use std::rc::Rc;
use std::sync::{Arc, Mutex};
use dpi::{LogicalPosition, PhysicalPosition, PhysicalSize};
use smol_str::SmolStr;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::JsCast;
use web_sys::{
CssStyleDeclaration, Document, Event, FocusEvent, HtmlCanvasElement, KeyboardEvent, Navigator,
PointerEvent, WheelEvent,
};
use winit_core::error::RequestError;
use winit_core::event::{
ButtonSource, DeviceId, ElementState, MouseScrollDelta, PointerKind, PointerSource,
SurfaceSizeWriter, WindowEvent,
};
use winit_core::keyboard::{Key, KeyLocation, ModifiersState, PhysicalKey};
use winit_core::monitor::Fullscreen;
use winit_core::window::{WindowAttributes, WindowId};
use super::super::cursor::CursorHandler;
use super::super::event_loop::runner;
use super::super::main_thread::MainThreadMarker;
use super::animation_frame::AnimationFrameHandler;
use super::event_handle::EventListenerHandle;
use super::intersection_handle::IntersectionObserverHandle;
use super::media_query_handle::MediaQueryListHandle;
use super::pointer::PointerHandler;
use super::{event, fullscreen, ResizeScaleHandle};
use crate::WindowAttributesWeb;
#[allow(dead_code)]
pub struct Canvas {
main_thread: MainThreadMarker,
common: Common,
id: WindowId,
pub has_focus: Rc<Cell<bool>>,
pub prevent_default: Rc<Cell<bool>>,
pub is_intersecting: Cell<Option<bool>>,
pub cursor: CursorHandler,
handlers: RefCell<Handlers>,
}
struct Handlers {
animation_frame_handler: AnimationFrameHandler,
on_touch_start: Option<EventListenerHandle<dyn FnMut(Event)>>,
on_focus: Option<EventListenerHandle<dyn FnMut(FocusEvent)>>,
on_blur: Option<EventListenerHandle<dyn FnMut(FocusEvent)>>,
on_keyboard_release: Option<EventListenerHandle<dyn FnMut(KeyboardEvent)>>,
on_keyboard_press: Option<EventListenerHandle<dyn FnMut(KeyboardEvent)>>,
on_mouse_wheel: Option<EventListenerHandle<dyn FnMut(WheelEvent)>>,
on_dark_mode: Option<MediaQueryListHandle>,
pointer_handler: PointerHandler,
on_resize_scale: Option<ResizeScaleHandle>,
on_intersect: Option<IntersectionObserverHandle>,
on_touch_end: Option<EventListenerHandle<dyn FnMut(Event)>>,
on_context_menu: Option<EventListenerHandle<dyn FnMut(PointerEvent)>>,
}
pub struct Common {
pub window: web_sys::Window,
navigator: Navigator,
pub document: Document,
/// Note: resizing the HTMLCanvasElement should go through `backend::set_canvas_size` to ensure
/// the DPI factor is maintained. Note: this is read-only because we use a pointer to this
/// for [`WindowHandle`][rwh_06::WindowHandle].
raw: Rc<HtmlCanvasElement>,
style: Style,
old_size: Rc<Cell<PhysicalSize<u32>>>,
current_size: Rc<Cell<PhysicalSize<u32>>>,
}
#[derive(Clone, Debug)]
pub struct Style {
pub(super) read: CssStyleDeclaration,
pub(super) write: CssStyleDeclaration,
}
impl Canvas {
pub(crate) fn create(
main_thread: MainThreadMarker,
id: WindowId,
window: web_sys::Window,
navigator: Navigator,
document: Document,
mut attr: WindowAttributes,
) -> Result<Self, RequestError> {
let web_attributes = attr
.platform
.take()
.and_then(|attrs| attrs.cast::<WindowAttributesWeb>().ok())
.unwrap_or_default();
let canvas = match web_attributes.canvas.map(Arc::try_unwrap) {
Some(Ok(canvas)) => canvas.into_inner(main_thread),
Some(Err(canvas)) => canvas.get(main_thread).clone(),
None => document
.create_element("canvas")
.map_err(|_| os_error!("Failed to create canvas element"))?
.unchecked_into(),
};
if web_attributes.append && !document.contains(Some(&canvas)) {
document
.body()
.expect("Failed to get body from document")
.append_child(&canvas)
.expect("Failed to append canvas to body");
}
// 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
if web_attributes.focusable {
canvas
.set_attribute("tabindex", "0")
.map_err(|_| os_error!("Failed to set a tabindex"))?;
}
let style = Style::new(&window, &canvas);
let cursor = CursorHandler::new(main_thread, canvas.clone(), style.clone());
let common = Common {
window: window.clone(),
document: document.clone(),
navigator,
raw: Rc::new(canvas.clone()),
style,
old_size: Rc::default(),
current_size: Rc::default(),
};
if let Some(size) = attr.surface_size {
let size = size.to_logical(super::scale_factor(&common.window));
super::set_canvas_size(&common.document, &common.raw, &common.style, size);
}
if let Some(size) = attr.min_surface_size {
let size = size.to_logical(super::scale_factor(&common.window));
super::set_canvas_min_size(&common.document, &common.raw, &common.style, Some(size));
}
if let Some(size) = attr.max_surface_size {
let size = size.to_logical(super::scale_factor(&common.window));
super::set_canvas_max_size(&common.document, &common.raw, &common.style, Some(size));
}
if let Some(position) = attr.position {
let position = position.to_logical(super::scale_factor(&common.window));
super::set_canvas_position(&common.document, &common.raw, &common.style, position);
}
if let Some(fullscreen) = attr.fullscreen {
fullscreen::request_fullscreen(main_thread, &window, &document, &canvas, fullscreen);
}
if attr.active {
let _ = common.raw.focus();
}
Ok(Canvas {
main_thread,
common,
id,
has_focus: Rc::new(Cell::new(false)),
prevent_default: Rc::new(Cell::new(web_attributes.prevent_default)),
is_intersecting: Cell::new(None),
cursor,
handlers: RefCell::new(Handlers {
animation_frame_handler: AnimationFrameHandler::new(window),
on_touch_start: None,
on_blur: None,
on_focus: None,
on_keyboard_release: None,
on_keyboard_press: None,
on_mouse_wheel: None,
on_dark_mode: None,
pointer_handler: PointerHandler::new(),
on_resize_scale: None,
on_intersect: None,
on_touch_end: None,
on_context_menu: None,
}),
})
}
pub fn set_attribute(&self, attribute: &str, value: &str) {
self.common
.raw
.set_attribute(attribute, value)
.unwrap_or_else(|err| panic!("error: {err:?}\nSet attribute: {attribute}"))
}
pub fn position(&self) -> LogicalPosition<f64> {
let bounds = self.common.raw.get_bounding_client_rect();
let mut position = LogicalPosition { x: bounds.x(), y: bounds.y() };
if self.document().contains(Some(self.raw())) && self.style().get("display") != "none" {
position.x += super::style_size_property(self.style(), "border-left-width")
+ super::style_size_property(self.style(), "padding-left");
position.y += super::style_size_property(self.style(), "border-top-width")
+ super::style_size_property(self.style(), "padding-top");
}
position
}
#[inline]
pub fn old_size(&self) -> PhysicalSize<u32> {
self.common.old_size.get()
}
#[inline]
pub fn surface_size(&self) -> PhysicalSize<u32> {
self.common.current_size.get()
}
#[inline]
pub fn set_old_size(&self, size: PhysicalSize<u32>) {
self.common.old_size.set(size)
}
#[inline]
pub fn set_current_size(&self, size: PhysicalSize<u32>) {
self.common.current_size.set(size)
}
#[inline]
pub fn window(&self) -> &web_sys::Window {
&self.common.window
}
#[inline]
pub fn navigator(&self) -> &Navigator {
&self.common.navigator
}
#[inline]
pub fn document(&self) -> &Document {
&self.common.document
}
#[inline]
pub fn raw(&self) -> &HtmlCanvasElement {
&self.common.raw
}
#[inline]
pub fn style(&self) -> &Style {
&self.common.style
}
pub fn on_touch_start(&self) {
let prevent_default = Rc::clone(&self.prevent_default);
self.handlers.borrow_mut().on_touch_start =
Some(self.common.add_event("touchstart", move |event: Event| {
if prevent_default.get() {
event.prevent_default();
}
}));
}
pub fn on_blur<F>(&self, mut handler: F)
where
F: 'static + FnMut(),
{
self.handlers.borrow_mut().on_blur =
Some(self.common.add_event("blur", move |_: FocusEvent| {
handler();
}));
}
pub fn on_focus<F>(&self, mut handler: F)
where
F: 'static + FnMut(),
{
self.handlers.borrow_mut().on_focus =
Some(self.common.add_event("focus", move |_: FocusEvent| {
handler();
}));
}
pub fn on_keyboard_release<F>(&self, mut handler: F)
where
F: 'static + FnMut(PhysicalKey, Key, Option<SmolStr>, KeyLocation, bool, ModifiersState),
{
let prevent_default = Rc::clone(&self.prevent_default);
self.handlers.borrow_mut().on_keyboard_release =
Some(self.common.add_event("keyup", move |event: KeyboardEvent| {
if prevent_default.get() {
event.prevent_default();
}
let key = event::key(&event);
let modifiers = event::keyboard_modifiers(&event);
handler(
event::key_code(&event),
key,
event::key_text(&event),
event::key_location(&event),
event.repeat(),
modifiers,
);
}));
}
pub fn on_keyboard_press<F>(&self, mut handler: F)
where
F: 'static + FnMut(PhysicalKey, Key, Option<SmolStr>, KeyLocation, bool, ModifiersState),
{
let prevent_default = Rc::clone(&self.prevent_default);
self.handlers.borrow_mut().on_keyboard_press =
Some(self.common.add_event("keydown", move |event: KeyboardEvent| {
if prevent_default.get() {
event.prevent_default();
}
let key = event::key(&event);
let modifiers = event::keyboard_modifiers(&event);
handler(
event::key_code(&event),
key,
event::key_text(&event),
event::key_location(&event),
event.repeat(),
modifiers,
);
}));
}
pub fn on_pointer_leave<F>(&self, handler: F)
where
F: 'static
+ FnMut(ModifiersState, Option<DeviceId>, bool, PhysicalPosition<f64>, PointerKind),
{
self.handlers.borrow_mut().pointer_handler.on_pointer_leave(&self.common, handler)
}
pub fn on_pointer_enter<F>(&self, handler: F)
where
F: 'static
+ FnMut(ModifiersState, Option<DeviceId>, bool, PhysicalPosition<f64>, PointerKind),
{
self.handlers.borrow_mut().pointer_handler.on_pointer_enter(&self.common, handler)
}
pub fn on_pointer_release<C>(&self, handler: C)
where
C: 'static
+ FnMut(ModifiersState, Option<DeviceId>, bool, PhysicalPosition<f64>, ButtonSource),
{
self.handlers.borrow_mut().pointer_handler.on_pointer_release(&self.common, handler)
}
pub fn on_pointer_press<C>(&self, handler: C)
where
C: 'static
+ FnMut(ModifiersState, Option<DeviceId>, bool, PhysicalPosition<f64>, ButtonSource),
{
self.handlers.borrow_mut().pointer_handler.on_pointer_press(
&self.common,
handler,
Rc::clone(&self.prevent_default),
)
}
pub fn on_pointer_move<C, B>(&self, cursor_handler: C, button_handler: B)
where
C: 'static
+ FnMut(
Option<DeviceId>,
&mut dyn Iterator<
Item = (ModifiersState, bool, PhysicalPosition<f64>, PointerSource),
>,
),
B: 'static
+ FnMut(
ModifiersState,
Option<DeviceId>,
bool,
PhysicalPosition<f64>,
ElementState,
ButtonSource,
),
{
self.handlers.borrow_mut().pointer_handler.on_pointer_move(
&self.common,
cursor_handler,
button_handler,
Rc::clone(&self.prevent_default),
)
}
pub fn on_mouse_wheel<F>(&self, mut handler: F)
where
F: 'static + FnMut(MouseScrollDelta, ModifiersState),
{
let window = self.common.window.clone();
let prevent_default = Rc::clone(&self.prevent_default);
self.handlers.borrow_mut().on_mouse_wheel =
Some(self.common.add_event("wheel", move |event: WheelEvent| {
if prevent_default.get() {
event.prevent_default();
}
if let Some(delta) = event::mouse_scroll_delta(&window, &event) {
let modifiers = event::mouse_modifiers(&event);
handler(delta, modifiers);
}
}));
}
pub fn on_dark_mode<F>(&self, mut handler: F)
where
F: 'static + FnMut(bool),
{
self.handlers.borrow_mut().on_dark_mode = Some(MediaQueryListHandle::new(
&self.common.window,
"(prefers-color-scheme: dark)",
move |mql| handler(mql.matches()),
));
}
pub(crate) fn on_resize_scale<S, R>(&self, scale_handler: S, size_handler: R)
where
S: 'static + Fn(PhysicalSize<u32>, f64),
R: 'static + Fn(PhysicalSize<u32>),
{
self.handlers.borrow_mut().on_resize_scale = Some(ResizeScaleHandle::new(
self.window().clone(),
self.document().clone(),
self.raw().clone(),
self.style().clone(),
scale_handler,
size_handler,
));
}
pub(crate) fn on_intersection<F>(&self, handler: F)
where
F: 'static + FnMut(bool),
{
self.handlers.borrow_mut().on_intersect =
Some(IntersectionObserverHandle::new(self.raw(), handler));
}
pub(crate) fn on_animation_frame<F>(&self, f: F)
where
F: 'static + FnMut(),
{
self.handlers.borrow_mut().animation_frame_handler.on_animation_frame(f)
}
pub(crate) fn on_context_menu(&self) {
let prevent_default = Rc::clone(&self.prevent_default);
self.handlers.borrow_mut().on_context_menu =
Some(self.common.add_event("contextmenu", move |event: PointerEvent| {
if prevent_default.get() {
event.prevent_default();
}
}));
}
pub(crate) fn request_fullscreen(&self, fullscreen: Fullscreen) {
fullscreen::request_fullscreen(
self.main_thread,
self.window(),
self.document(),
self.raw(),
fullscreen,
);
}
pub fn exit_fullscreen(&self) {
fullscreen::exit_fullscreen(self.document(), self.raw());
}
pub fn is_fullscreen(&self) -> bool {
fullscreen::is_fullscreen(self.document(), self.raw())
}
pub fn request_animation_frame(&self) {
self.handlers.borrow().animation_frame_handler.request();
}
pub(crate) fn handle_scale_change(
&self,
runner: &super::super::event_loop::runner::Shared,
event_handler: impl FnOnce(WindowId, WindowEvent),
current_size: PhysicalSize<u32>,
scale: f64,
) {
// First, we send the `ScaleFactorChanged` event:
self.set_current_size(current_size);
let new_size = {
let new_size = Arc::new(Mutex::new(current_size));
event_handler(self.id, WindowEvent::ScaleFactorChanged {
scale_factor: scale,
surface_size_writer: SurfaceSizeWriter::new(Arc::downgrade(&new_size)),
});
let new_size = *new_size.lock().unwrap();
new_size
};
if current_size != new_size {
// Then we resize the canvas to the new size, a new `SurfaceResized` event will be sent
// by the `ResizeObserver`:
let new_size = new_size.to_logical(scale);
super::set_canvas_size(self.document(), self.raw(), self.style(), new_size);
// Set the size might not trigger the event because the calculation is inaccurate.
self.handlers
.borrow()
.on_resize_scale
.as_ref()
.expect("expected Window to still be active")
.notify_resize();
} else if self.old_size() != new_size {
// Then we at least send a resized event.
self.set_old_size(new_size);
runner.send_event(runner::Event::WindowEvent {
window_id: self.id,
event: WindowEvent::SurfaceResized(new_size),
})
}
}
pub fn remove_listeners(&self) {
let mut handlers = self.handlers.borrow_mut();
handlers.on_touch_start.take();
handlers.on_focus.take();
handlers.on_blur.take();
handlers.on_keyboard_release.take();
handlers.on_keyboard_press.take();
handlers.on_mouse_wheel.take();
handlers.on_dark_mode.take();
handlers.pointer_handler.remove_listeners();
handlers.on_resize_scale = None;
handlers.on_intersect = None;
handlers.animation_frame_handler.cancel();
handlers.on_touch_end = None;
handlers.on_context_menu = None;
}
}
impl Common {
pub fn add_event<E, F>(
&self,
event_name: &'static str,
handler: F,
) -> EventListenerHandle<dyn FnMut(E)>
where
E: 'static + AsRef<web_sys::Event> + wasm_bindgen::convert::FromWasmAbi,
F: 'static + FnMut(E),
{
EventListenerHandle::new(self.raw.deref().clone(), event_name, Closure::new(handler))
}
pub fn raw(&self) -> &HtmlCanvasElement {
&self.raw
}
}
impl Style {
fn new(window: &web_sys::Window, canvas: &HtmlCanvasElement) -> Self {
#[allow(clippy::disallowed_methods)]
let read = window
.get_computed_style(canvas)
.expect("Failed to obtain computed style")
// this can't fail: we aren't using a pseudo-element
.expect("Invalid pseudo-element");
#[allow(clippy::disallowed_methods)]
let write = canvas.style();
Self { read, write }
}
pub(crate) fn get(&self, property: &str) -> String {
self.read.get_property_value(property).expect("Invalid property")
}
pub(crate) fn remove(&self, property: &str) {
self.write.remove_property(property).expect("Property is read only");
}
pub(crate) fn set(&self, property: &str, value: &str) {
self.write.set_property(property, value).expect("Property is read only");
}
}

View file

@ -0,0 +1,276 @@
use std::cell::OnceCell;
use dpi::{LogicalPosition, PhysicalPosition, Position};
use smol_str::SmolStr;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{KeyboardEvent, MouseEvent, Navigator, PointerEvent, WheelEvent};
use winit_core::event::{FingerId, MouseButton, MouseScrollDelta, PointerKind};
use winit_core::keyboard::{Key, KeyLocation, ModifiersState, NamedKey, PhysicalKey};
use super::Engine;
use crate::keyboard::FromAttributeValue;
bitflags::bitflags! {
// https://www.w3.org/TR/pointerevents3/#the-buttons-property
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ButtonsState: u16 {
const LEFT = 0b00001;
const RIGHT = 0b00010;
const MIDDLE = 0b00100;
const BACK = 0b01000;
const FORWARD = 0b10000;
}
}
impl From<ButtonsState> for MouseButton {
fn from(value: ButtonsState) -> Self {
match value {
ButtonsState::LEFT => MouseButton::Left,
ButtonsState::RIGHT => MouseButton::Right,
ButtonsState::MIDDLE => MouseButton::Middle,
ButtonsState::BACK => MouseButton::Back,
ButtonsState::FORWARD => MouseButton::Forward,
_ => MouseButton::Other(value.bits()),
}
}
}
impl From<MouseButton> for ButtonsState {
fn from(value: MouseButton) -> Self {
match value {
MouseButton::Left => ButtonsState::LEFT,
MouseButton::Right => ButtonsState::RIGHT,
MouseButton::Middle => ButtonsState::MIDDLE,
MouseButton::Back => ButtonsState::BACK,
MouseButton::Forward => ButtonsState::FORWARD,
MouseButton::Other(value) => ButtonsState::from_bits_retain(value),
}
}
}
pub fn mouse_buttons(event: &MouseEvent) -> ButtonsState {
ButtonsState::from_bits_retain(event.buttons())
}
pub fn mouse_button(event: &MouseEvent) -> Option<MouseButton> {
// https://www.w3.org/TR/pointerevents3/#the-button-property
match event.button() {
-1 => None,
0 => Some(MouseButton::Left),
1 => Some(MouseButton::Middle),
2 => Some(MouseButton::Right),
3 => Some(MouseButton::Back),
4 => Some(MouseButton::Forward),
i => {
Some(MouseButton::Other(i.try_into().expect("unexpected negative mouse button value")))
},
}
}
pub fn mouse_button_to_id(button: MouseButton) -> u16 {
match button {
MouseButton::Left => 0,
MouseButton::Right => 1,
MouseButton::Middle => 2,
MouseButton::Back => 3,
MouseButton::Forward => 4,
MouseButton::Other(value) => value,
}
}
pub fn mouse_position(event: &MouseEvent) -> LogicalPosition<f64> {
#[wasm_bindgen]
extern "C" {
type MouseEventExt;
#[wasm_bindgen(method, getter, js_name = offsetX)]
fn offset_x(this: &MouseEventExt) -> f64;
#[wasm_bindgen(method, getter, js_name = offsetY)]
fn offset_y(this: &MouseEventExt) -> f64;
}
let event: &MouseEventExt = event.unchecked_ref();
LogicalPosition { x: event.offset_x(), y: event.offset_y() }
}
// TODO: Remove this when Firefox supports correct movement values in coalesced events and browsers
// have agreed on what coordinate space `movementX/Y` is using.
// See <https://bugzilla.mozilla.org/show_bug.cgi?id=1753724>.
// See <https://github.com/w3c/pointerlock/issues/42>.
pub enum MouseDelta {
Chromium,
Gecko { old_position: LogicalPosition<f64>, old_delta: LogicalPosition<f64> },
Other,
}
impl MouseDelta {
pub fn init(navigator: &Navigator, event: &PointerEvent) -> Self {
match super::engine(navigator) {
Some(Engine::Chromium) => Self::Chromium,
// Firefox has wrong movement values in coalesced events.
Some(Engine::Gecko) if has_coalesced_events_support(event) => Self::Gecko {
old_position: mouse_position(event),
old_delta: LogicalPosition::new(
event.movement_x() as f64,
event.movement_y() as f64,
),
},
_ => Self::Other,
}
}
pub fn delta(&mut self, event: &MouseEvent) -> Position {
match self {
MouseDelta::Chromium => {
PhysicalPosition::new(event.movement_x(), event.movement_y()).into()
},
MouseDelta::Gecko { old_position, old_delta } => {
let new_position = mouse_position(event);
let x = new_position.x - old_position.x + old_delta.x;
let y = new_position.y - old_position.y + old_delta.y;
*old_position = new_position;
*old_delta = LogicalPosition::new(0., 0.);
LogicalPosition::new(x, y).into()
},
MouseDelta::Other => {
LogicalPosition::new(event.movement_x(), event.movement_y()).into()
},
}
}
}
pub fn mouse_scroll_delta(
window: &web_sys::Window,
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 => {
let delta = LogicalPosition::new(x, y).to_physical(super::scale_factor(window));
Some(MouseScrollDelta::PixelDelta(delta))
},
_ => None,
}
}
pub fn pointer_type(event: &PointerEvent, pointer_id: i32) -> PointerKind {
match event.pointer_type().as_str() {
"mouse" => PointerKind::Mouse,
"touch" => PointerKind::Touch(FingerId::from_raw(pointer_id as usize)),
_ => PointerKind::Unknown,
}
}
pub fn key_code(event: &KeyboardEvent) -> PhysicalKey {
let code = event.code();
PhysicalKey::from_attribute_value(&code)
}
pub fn key(event: &KeyboardEvent) -> Key {
Key::from_attribute_value(&event.key())
}
pub fn key_text(event: &KeyboardEvent) -> Option<SmolStr> {
let key = event.key();
let key = Key::from_attribute_value(&key);
match &key {
Key::Character(text) => Some(text.clone()),
Key::Named(NamedKey::Tab) => Some(SmolStr::new("\t")),
Key::Named(NamedKey::Enter) => Some(SmolStr::new("\r")),
_ => None,
}
.map(SmolStr::new)
}
pub fn key_location(event: &KeyboardEvent) -> KeyLocation {
match event.location() {
KeyboardEvent::DOM_KEY_LOCATION_LEFT => KeyLocation::Left,
KeyboardEvent::DOM_KEY_LOCATION_RIGHT => KeyLocation::Right,
KeyboardEvent::DOM_KEY_LOCATION_NUMPAD => KeyLocation::Numpad,
KeyboardEvent::DOM_KEY_LOCATION_STANDARD => KeyLocation::Standard,
location => {
tracing::warn!("Unexpected key location: {location}");
KeyLocation::Standard
},
}
}
pub fn keyboard_modifiers(event: &KeyboardEvent) -> ModifiersState {
let mut state = ModifiersState::empty();
if event.shift_key() {
state |= ModifiersState::SHIFT;
}
if event.ctrl_key() {
state |= ModifiersState::CONTROL;
}
if event.alt_key() {
state |= ModifiersState::ALT;
}
if event.meta_key() {
state |= ModifiersState::META;
}
state
}
pub fn mouse_modifiers(event: &MouseEvent) -> ModifiersState {
let mut state = ModifiersState::empty();
if event.shift_key() {
state |= ModifiersState::SHIFT;
}
if event.ctrl_key() {
state |= ModifiersState::CONTROL;
}
if event.alt_key() {
state |= ModifiersState::ALT;
}
if event.meta_key() {
state |= ModifiersState::META;
}
state
}
pub fn pointer_move_event(event: PointerEvent) -> impl Iterator<Item = PointerEvent> {
// make a single iterator depending on the availability of coalesced events
if has_coalesced_events_support(&event) {
None.into_iter().chain(
Some(event.get_coalesced_events().into_iter().map(PointerEvent::unchecked_from_js))
.into_iter()
.flatten(),
)
} else {
Some(event).into_iter().chain(None.into_iter().flatten())
}
}
// TODO: Remove when Safari supports `getCoalescedEvents`.
// See <https://bugs.webkit.org/show_bug.cgi?id=210454>.
pub fn has_coalesced_events_support(event: &PointerEvent) -> bool {
thread_local! {
static COALESCED_EVENTS_SUPPORT: OnceCell<bool> = const { OnceCell::new() };
}
COALESCED_EVENTS_SUPPORT.with(|support| {
*support.get_or_init(|| {
#[wasm_bindgen]
extern "C" {
type PointerCoalescedEventsSupport;
#[wasm_bindgen(method, getter, js_name = getCoalescedEvents)]
fn has_get_coalesced_events(this: &PointerCoalescedEventsSupport) -> JsValue;
}
let support: &PointerCoalescedEventsSupport = event.unchecked_ref();
!support.has_get_coalesced_events().is_undefined()
})
})
}

View file

@ -0,0 +1,38 @@
use wasm_bindgen::prelude::Closure;
use wasm_bindgen::JsCast;
use web_sys::EventTarget;
pub struct EventListenerHandle<T: ?Sized> {
target: EventTarget,
event_type: &'static str,
listener: Closure<T>,
}
impl<T: ?Sized> EventListenerHandle<T> {
pub fn new<U>(target: U, event_type: &'static str, listener: Closure<T>) -> Self
where
U: Into<EventTarget>,
{
let target = target.into();
target
.add_event_listener_with_callback(event_type, listener.as_ref().unchecked_ref())
.expect("Failed to add event listener");
EventListenerHandle { target, event_type, listener }
}
}
impl<T: ?Sized> Drop for EventListenerHandle<T> {
fn drop(&mut self) {
self.target
.remove_event_listener_with_callback(
self.event_type,
self.listener.as_ref().unchecked_ref(),
)
.unwrap_or_else(|e| {
web_sys::console::error_2(
&format!("Error removing event listener {}", self.event_type).into(),
&e,
)
});
}
}

View file

@ -0,0 +1,157 @@
use std::cell::OnceCell;
use js_sys::{Object, Promise};
use tracing::error;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{console, Document, Element, HtmlCanvasElement, Window};
use winit_core::monitor::Fullscreen;
use crate::main_thread::MainThreadMarker;
use crate::monitor::{self, MonitorHandle, ScreenDetailed};
pub(crate) fn request_fullscreen(
main_thread: MainThreadMarker,
window: &Window,
document: &Document,
canvas: &HtmlCanvasElement,
fullscreen: Fullscreen,
) {
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends = HtmlCanvasElement)]
type RequestFullscreen;
#[wasm_bindgen(method, js_name = requestFullscreen)]
fn request_fullscreen(this: &RequestFullscreen) -> Promise;
#[wasm_bindgen(method, js_name = requestFullscreen)]
fn request_fullscreen_with_options(
this: &RequestFullscreen,
options: &FullscreenOptions,
) -> Promise;
#[wasm_bindgen(method, js_name = webkitRequestFullscreen)]
fn webkit_request_fullscreen(this: &RequestFullscreen);
type FullscreenOptions;
#[wasm_bindgen(method, setter, js_name = screen)]
fn set_screen(this: &FullscreenOptions, screen: &ScreenDetailed);
}
thread_local! {
static REJECT_HANDLER: Closure<dyn FnMut(JsValue)> = Closure::new(|error| {
console::error_1(&error);
error!("Failed to transition to full screen mode")
});
}
if is_fullscreen(document, canvas) {
return;
}
let canvas: &RequestFullscreen = canvas.unchecked_ref();
match fullscreen {
Fullscreen::Exclusive(..) => error!("Exclusive full screen mode is not supported"),
Fullscreen::Borderless(Some(monitor)) => {
if !monitor::has_screen_details_support(window) {
error!(
"Fullscreen mode selecting a specific screen is not supported by this browser"
);
return;
}
let monitor = monitor.cast_ref::<MonitorHandle>().unwrap();
if let Some(monitor) = monitor.detailed(main_thread) {
let options: FullscreenOptions = Object::new().unchecked_into();
options.set_screen(&monitor);
REJECT_HANDLER.with(|handler| {
let _ = canvas.request_fullscreen_with_options(&options).catch(handler);
});
} else {
error!(
"Selecting a specific screen for fullscreen mode requires a detailed screen. \
See `MonitorHandleExtWeb::is_detailed()`."
)
}
},
Fullscreen::Borderless(None) => {
if has_fullscreen_api_support(canvas) {
REJECT_HANDLER.with(|handler| {
let _ = canvas.request_fullscreen().catch(handler);
});
} else {
canvas.webkit_request_fullscreen();
}
},
}
}
pub fn is_fullscreen(document: &Document, canvas: &HtmlCanvasElement) -> bool {
#[wasm_bindgen]
extern "C" {
type FullscreenElement;
#[wasm_bindgen(method, getter, js_name = webkitFullscreenElement)]
fn webkit_fullscreen_element(this: &FullscreenElement) -> Option<Element>;
}
let element = if has_fullscreen_api_support(canvas) {
#[allow(clippy::disallowed_methods)]
document.fullscreen_element()
} else {
let document: &FullscreenElement = document.unchecked_ref();
document.webkit_fullscreen_element()
};
match element {
Some(element) => {
let canvas: &Element = canvas;
canvas == &element
},
None => false,
}
}
pub fn exit_fullscreen(document: &Document, canvas: &HtmlCanvasElement) {
#[wasm_bindgen]
extern "C" {
type ExitFullscreen;
#[wasm_bindgen(method, js_name = webkitExitFullscreen)]
fn webkit_exit_fullscreen(this: &ExitFullscreen);
}
if has_fullscreen_api_support(canvas) {
#[allow(clippy::disallowed_methods)]
document.exit_fullscreen()
} else {
let document: &ExitFullscreen = document.unchecked_ref();
document.webkit_exit_fullscreen()
}
}
fn has_fullscreen_api_support(canvas: &HtmlCanvasElement) -> bool {
thread_local! {
static FULLSCREEN_API_SUPPORT: OnceCell<bool> = const { OnceCell::new() };
}
FULLSCREEN_API_SUPPORT.with(|support| {
*support.get_or_init(|| {
#[wasm_bindgen]
extern "C" {
type CanvasFullScreenApiSupport;
#[wasm_bindgen(method, getter, js_name = requestFullscreen)]
fn has_request_fullscreen(this: &CanvasFullScreenApiSupport) -> JsValue;
}
let support: &CanvasFullScreenApiSupport = canvas.unchecked_ref();
!support.has_request_fullscreen().is_undefined()
})
})
}

View file

@ -0,0 +1,33 @@
use js_sys::Array;
use wasm_bindgen::prelude::Closure;
use wasm_bindgen::JsCast;
use web_sys::{Element, IntersectionObserver, IntersectionObserverEntry};
pub(super) struct IntersectionObserverHandle {
observer: IntersectionObserver,
_closure: Closure<dyn FnMut(Array)>,
}
impl IntersectionObserverHandle {
pub fn new<F>(element: &Element, mut callback: F) -> Self
where
F: 'static + FnMut(bool),
{
let closure = Closure::new(move |entries: Array| {
let entry: IntersectionObserverEntry = entries.get(0).unchecked_into();
callback(entry.is_intersecting());
});
let observer = IntersectionObserver::new(closure.as_ref().unchecked_ref())
// we don't provide any `options`
.expect("Invalid `options`");
observer.observe(element);
Self { observer, _closure: closure }
}
}
impl Drop for IntersectionObserverHandle {
fn drop(&mut self) {
self.observer.disconnect()
}
}

View file

@ -0,0 +1,48 @@
use wasm_bindgen::prelude::Closure;
use wasm_bindgen::JsCast;
use web_sys::MediaQueryList;
pub(super) struct MediaQueryListHandle {
mql: MediaQueryList,
closure: Closure<dyn FnMut()>,
}
impl MediaQueryListHandle {
pub fn new<F>(window: &web_sys::Window, media_query: &str, mut listener: F) -> Self
where
F: 'static + FnMut(&MediaQueryList),
{
let mql = window
.match_media(media_query)
.expect("Failed to parse media query")
.expect("Found empty media query");
let closure = Closure::new({
let mql = mql.clone();
move || listener(&mql)
});
// TODO: Replace obsolete `addListener()` with `addEventListener()` and use
// `MediaQueryListEvent` instead of cloning the `MediaQueryList`.
// Requires Safari v14.
mql.add_listener_with_opt_callback(Some(closure.as_ref().unchecked_ref()))
.expect("Invalid listener");
Self { mql, closure }
}
pub fn mql(&self) -> &MediaQueryList {
&self.mql
}
}
impl Drop for MediaQueryListHandle {
fn drop(&mut self) {
remove_listener(&self.mql, &self.closure);
}
}
fn remove_listener(mql: &MediaQueryList, listener: &Closure<dyn FnMut()>) {
mql.remove_listener_with_opt_callback(Some(listener.as_ref().unchecked_ref())).unwrap_or_else(
|e| web_sys::console::error_2(&"Error removing media query listener".into(), &e),
);
}

View file

@ -0,0 +1,262 @@
mod animation_frame;
mod canvas;
pub mod event;
mod event_handle;
mod fullscreen;
mod intersection_handle;
mod media_query_handle;
mod pointer;
mod resize_scaling;
mod safe_area;
mod schedule;
use std::cell::OnceCell;
use dpi::{LogicalPosition, LogicalSize};
use js_sys::Array;
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsCast;
use web_sys::{Document, HtmlCanvasElement, Navigator, PageTransitionEvent, VisibilityState};
pub use self::canvas::{Canvas, Style};
pub use self::event_handle::EventListenerHandle;
pub use self::resize_scaling::ResizeScaleHandle;
pub use self::safe_area::SafeAreaHandle;
pub use self::schedule::Schedule;
pub fn throw(msg: &str) {
wasm_bindgen::throw_str(msg);
}
pub struct PageTransitionEventHandle {
_show_listener: event_handle::EventListenerHandle<dyn FnMut(PageTransitionEvent)>,
_hide_listener: event_handle::EventListenerHandle<dyn FnMut(PageTransitionEvent)>,
}
pub fn on_page_transition(
window: web_sys::Window,
show_handler: impl FnMut(PageTransitionEvent) + 'static,
hide_handler: impl FnMut(PageTransitionEvent) + 'static,
) -> PageTransitionEventHandle {
let show_closure = Closure::new(show_handler);
let hide_closure = Closure::new(hide_handler);
let show_listener =
event_handle::EventListenerHandle::new(window.clone(), "pageshow", show_closure);
let hide_listener = event_handle::EventListenerHandle::new(window, "pagehide", hide_closure);
PageTransitionEventHandle { _show_listener: show_listener, _hide_listener: hide_listener }
}
pub fn scale_factor(window: &web_sys::Window) -> f64 {
window.device_pixel_ratio()
}
fn fix_canvas_size(style: &Style, mut size: LogicalSize<f64>) -> LogicalSize<f64> {
if style.get("box-sizing") == "border-box" {
size.width += style_size_property(style, "border-left-width")
+ style_size_property(style, "border-right-width")
+ style_size_property(style, "padding-left")
+ style_size_property(style, "padding-right");
size.height += style_size_property(style, "border-top-width")
+ style_size_property(style, "border-bottom-width")
+ style_size_property(style, "padding-top")
+ style_size_property(style, "padding-bottom");
}
size
}
pub fn set_canvas_size(
document: &Document,
raw: &HtmlCanvasElement,
style: &Style,
new_size: LogicalSize<f64>,
) {
if !document.contains(Some(raw)) || style.get("display") == "none" {
return;
}
let new_size = fix_canvas_size(style, new_size);
style.set("width", &format!("{}px", new_size.width));
style.set("height", &format!("{}px", new_size.height));
}
pub fn set_canvas_min_size(
document: &Document,
raw: &HtmlCanvasElement,
style: &Style,
dimensions: Option<LogicalSize<f64>>,
) {
if let Some(dimensions) = dimensions {
if !document.contains(Some(raw)) || style.get("display") == "none" {
return;
}
let new_size = fix_canvas_size(style, dimensions);
style.set("min-width", &format!("{}px", new_size.width));
style.set("min-height", &format!("{}px", new_size.height));
} else {
style.remove("min-width");
style.remove("min-height");
}
}
pub fn set_canvas_max_size(
document: &Document,
raw: &HtmlCanvasElement,
style: &Style,
dimensions: Option<LogicalSize<f64>>,
) {
if let Some(dimensions) = dimensions {
if !document.contains(Some(raw)) || style.get("display") == "none" {
return;
}
let new_size = fix_canvas_size(style, dimensions);
style.set("max-width", &format!("{}px", new_size.width));
style.set("max-height", &format!("{}px", new_size.height));
} else {
style.remove("max-width");
style.remove("max-height");
}
}
pub fn set_canvas_position(
document: &Document,
raw: &HtmlCanvasElement,
style: &Style,
mut position: LogicalPosition<f64>,
) {
if document.contains(Some(raw)) && style.get("display") != "none" {
position.x -= style_size_property(style, "margin-left")
+ style_size_property(style, "border-left-width")
+ style_size_property(style, "padding-left");
position.y -= style_size_property(style, "margin-top")
+ style_size_property(style, "border-top-width")
+ style_size_property(style, "padding-top");
}
style.set("position", "fixed");
style.set("left", &format!("{}px", position.x));
style.set("top", &format!("{}px", position.y));
}
/// This function will panic if the element is not inserted in the DOM
/// or is not a CSS property that represents a size in pixel.
pub fn style_size_property(style: &Style, property: &str) -> f64 {
let prop = style.get(property);
prop.strip_suffix("px")
.expect("Element was not inserted into the DOM or is not a size in pixel")
.parse()
.expect("CSS property is not a size in pixel")
}
pub fn is_dark_mode(window: &web_sys::Window) -> Option<bool> {
window.match_media("(prefers-color-scheme: dark)").ok().flatten().map(|media| media.matches())
}
pub fn is_visible(document: &Document) -> bool {
document.visibility_state() == VisibilityState::Visible
}
pub type RawCanvasType = HtmlCanvasElement;
#[derive(Clone, Copy)]
pub enum Engine {
Chromium,
Gecko,
WebKit,
}
struct UserAgentData {
engine: Option<Engine>,
chrome_linux: bool,
}
thread_local! {
static USER_AGENT_DATA: OnceCell<UserAgentData> = const { OnceCell::new() };
}
pub fn chrome_linux(navigator: &Navigator) -> bool {
USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(navigator)).chrome_linux)
}
pub fn engine(navigator: &Navigator) -> Option<Engine> {
USER_AGENT_DATA.with(|data| data.get_or_init(|| user_agent(navigator)).engine)
}
fn user_agent(navigator: &Navigator) -> UserAgentData {
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(extends = Navigator)]
type NavigatorExt;
#[wasm_bindgen(method, getter, js_name = userAgentData)]
fn user_agent_data(this: &NavigatorExt) -> Option<NavigatorUaData>;
type NavigatorUaData;
#[wasm_bindgen(method, getter)]
fn brands(this: &NavigatorUaData) -> Array;
#[wasm_bindgen(method, getter)]
fn platform(this: &NavigatorUaData) -> String;
type NavigatorUaBrandVersion;
#[wasm_bindgen(method, getter)]
fn brand(this: &NavigatorUaBrandVersion) -> String;
}
let navigator: &NavigatorExt = navigator.unchecked_ref();
if let Some(data) = navigator.user_agent_data() {
let engine = 'engine: {
for brand in data
.brands()
.iter()
.map(NavigatorUaBrandVersion::unchecked_from_js)
.map(|brand| brand.brand())
{
match brand.as_str() {
"Chromium" => break 'engine Some(Engine::Chromium),
// TODO: verify when Firefox actually implements it.
"Gecko" => break 'engine Some(Engine::Gecko),
// TODO: verify when Safari actually implements it.
"WebKit" => break 'engine Some(Engine::WebKit),
_ => (),
}
}
None
};
let chrome_linux = matches!(engine, Some(Engine::Chromium))
.then(|| data.platform() == "Linux")
.unwrap_or(false);
UserAgentData { engine, chrome_linux }
} else {
let engine = 'engine: {
let Ok(data) = navigator.user_agent() else {
break 'engine None;
};
if data.contains("Chrome/") {
Some(Engine::Chromium)
} else if data.contains("Gecko/") {
Some(Engine::Gecko)
} else if data.contains("AppleWebKit/") {
Some(Engine::WebKit)
} else {
None
}
};
UserAgentData { engine, chrome_linux: false }
}
}

View file

@ -0,0 +1,268 @@
use std::cell::Cell;
use std::rc::Rc;
use dpi::PhysicalPosition;
use web_sys::PointerEvent;
use winit_core::event::{ButtonSource, DeviceId, ElementState, Force, PointerKind, PointerSource};
use winit_core::keyboard::ModifiersState;
use super::canvas::Common;
use super::event;
use super::event_handle::EventListenerHandle;
use crate::event::mkdid;
use crate::web_sys::event::mouse_button_to_id;
#[allow(dead_code)]
pub(super) struct PointerHandler {
on_cursor_leave: Option<EventListenerHandle<dyn FnMut(PointerEvent)>>,
on_cursor_enter: Option<EventListenerHandle<dyn FnMut(PointerEvent)>>,
on_cursor_move: Option<EventListenerHandle<dyn FnMut(PointerEvent)>>,
on_pointer_press: Option<EventListenerHandle<dyn FnMut(PointerEvent)>>,
on_pointer_release: Option<EventListenerHandle<dyn FnMut(PointerEvent)>>,
on_touch_cancel: Option<EventListenerHandle<dyn FnMut(PointerEvent)>>,
}
impl PointerHandler {
pub fn new() -> Self {
Self {
on_cursor_leave: None,
on_cursor_enter: None,
on_cursor_move: None,
on_pointer_press: None,
on_pointer_release: None,
on_touch_cancel: None,
}
}
pub fn on_pointer_leave<F>(&mut self, canvas_common: &Common, mut handler: F)
where
F: 'static
+ FnMut(ModifiersState, Option<DeviceId>, bool, PhysicalPosition<f64>, PointerKind),
{
let window = canvas_common.window.clone();
self.on_cursor_leave =
Some(canvas_common.add_event("pointerout", move |event: PointerEvent| {
let modifiers = event::mouse_modifiers(&event);
let pointer_id = event.pointer_id();
let device_id = mkdid(pointer_id);
let position =
event::mouse_position(&event).to_physical(super::scale_factor(&window));
let kind = event::pointer_type(&event, pointer_id);
handler(modifiers, device_id, event.is_primary(), position, kind);
}));
}
pub fn on_pointer_enter<F>(&mut self, canvas_common: &Common, mut handler: F)
where
F: 'static
+ FnMut(ModifiersState, Option<DeviceId>, bool, PhysicalPosition<f64>, PointerKind),
{
let window = canvas_common.window.clone();
self.on_cursor_enter =
Some(canvas_common.add_event("pointerover", move |event: PointerEvent| {
let modifiers = event::mouse_modifiers(&event);
let pointer_id = event.pointer_id();
let device_id = mkdid(pointer_id);
let position =
event::mouse_position(&event).to_physical(super::scale_factor(&window));
let kind = event::pointer_type(&event, pointer_id);
handler(modifiers, device_id, event.is_primary(), position, kind);
}));
}
pub fn on_pointer_release<C>(&mut self, canvas_common: &Common, mut handler: C)
where
C: 'static
+ FnMut(ModifiersState, Option<DeviceId>, bool, PhysicalPosition<f64>, ButtonSource),
{
let window = canvas_common.window.clone();
self.on_pointer_release =
Some(canvas_common.add_event("pointerup", move |event: PointerEvent| {
let modifiers = event::mouse_modifiers(&event);
let pointer_id = event.pointer_id();
let kind = event::pointer_type(&event, pointer_id);
let button = event::mouse_button(&event).expect("no mouse button pressed");
let source = match kind {
PointerKind::Mouse => ButtonSource::Mouse(button),
PointerKind::Touch(finger_id) => ButtonSource::Touch {
finger_id,
force: Some(Force::Normalized(event.pressure().into())),
},
PointerKind::Unknown => ButtonSource::Unknown(mouse_button_to_id(button)),
};
handler(
modifiers,
mkdid(pointer_id),
event.is_primary(),
event::mouse_position(&event).to_physical(super::scale_factor(&window)),
source,
)
}));
}
pub fn on_pointer_press<C>(
&mut self,
canvas_common: &Common,
mut handler: C,
prevent_default: Rc<Cell<bool>>,
) where
C: 'static
+ FnMut(ModifiersState, Option<DeviceId>, bool, PhysicalPosition<f64>, ButtonSource),
{
let window = canvas_common.window.clone();
let canvas = canvas_common.raw().clone();
self.on_pointer_press =
Some(canvas_common.add_event("pointerdown", move |event: PointerEvent| {
if prevent_default.get() {
// prevent text selection
event.prevent_default();
// but still focus element
let _ = canvas.focus();
}
let modifiers = event::mouse_modifiers(&event);
let pointer_id = event.pointer_id();
let kind = event::pointer_type(&event, pointer_id);
let button = event::mouse_button(&event).expect("no mouse button pressed");
let source = match kind {
PointerKind::Mouse => {
// Error is swallowed here since the error would occur every time the
// mouse is clicked when the cursor is
// grabbed, and there is probably not a
// situation where this could fail, that we
// care if it fails.
let _e = canvas.set_pointer_capture(pointer_id);
ButtonSource::Mouse(button)
},
PointerKind::Touch(finger_id) => ButtonSource::Touch {
finger_id,
force: Some(Force::Normalized(event.pressure().into())),
},
PointerKind::Unknown => ButtonSource::Unknown(mouse_button_to_id(button)),
};
handler(
modifiers,
mkdid(pointer_id),
event.is_primary(),
event::mouse_position(&event).to_physical(super::scale_factor(&window)),
source,
)
}));
}
pub fn on_pointer_move<C, B>(
&mut self,
canvas_common: &Common,
mut cursor_handler: C,
mut button_handler: B,
prevent_default: Rc<Cell<bool>>,
) where
C: 'static
+ FnMut(
Option<DeviceId>,
&mut dyn Iterator<
Item = (ModifiersState, bool, PhysicalPosition<f64>, PointerSource),
>,
),
B: 'static
+ FnMut(
ModifiersState,
Option<DeviceId>,
bool,
PhysicalPosition<f64>,
ElementState,
ButtonSource,
),
{
let window = canvas_common.window.clone();
let canvas = canvas_common.raw().clone();
self.on_cursor_move =
Some(canvas_common.add_event("pointermove", move |event: PointerEvent| {
let pointer_id = event.pointer_id();
let device_id = mkdid(pointer_id);
let kind = event::pointer_type(&event, pointer_id);
let primary = event.is_primary();
// chorded button event
if let Some(button) = event::mouse_button(&event) {
if prevent_default.get() {
// prevent text selection
event.prevent_default();
// but still focus element
let _ = canvas.focus();
}
let state = if event::mouse_buttons(&event).contains(button.into()) {
ElementState::Pressed
} else {
ElementState::Released
};
let button = match kind {
PointerKind::Mouse => ButtonSource::Mouse(button),
PointerKind::Touch(finger_id) => {
let button_id = mouse_button_to_id(button);
if button_id != 1 {
tracing::error!("unexpected touch button id: {button_id}");
}
ButtonSource::Touch {
finger_id,
force: Some(Force::Normalized(event.pressure().into())),
}
},
PointerKind::Unknown => todo!(),
};
button_handler(
event::mouse_modifiers(&event),
device_id,
primary,
event::mouse_position(&event).to_physical(super::scale_factor(&window)),
state,
button,
);
return;
}
// pointer move event
let scale = super::scale_factor(&window);
cursor_handler(
device_id,
&mut event::pointer_move_event(event).map(|event| {
(
event::mouse_modifiers(&event),
event.is_primary(),
event::mouse_position(&event).to_physical(scale),
match kind {
PointerKind::Mouse => PointerSource::Mouse,
PointerKind::Touch(finger_id) => PointerSource::Touch {
finger_id,
force: Some(Force::Normalized(event.pressure().into())),
},
PointerKind::Unknown => PointerSource::Unknown,
},
)
}),
);
}));
}
pub fn remove_listeners(&mut self) {
self.on_cursor_leave = None;
self.on_cursor_enter = None;
self.on_cursor_move = None;
self.on_pointer_press = None;
self.on_pointer_release = None;
self.on_touch_cancel = None;
}
}

View file

@ -0,0 +1,301 @@
use std::cell::{Cell, RefCell};
use std::rc::Rc;
use dpi::{LogicalSize, PhysicalSize};
use js_sys::{Array, Object};
use tracing::warn;
use wasm_bindgen::prelude::{wasm_bindgen, Closure};
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
Document, HtmlCanvasElement, MediaQueryList, ResizeObserver, ResizeObserverBoxOptions,
ResizeObserverEntry, ResizeObserverOptions, ResizeObserverSize, Window,
};
use super::super::backend;
use super::canvas::Style;
use super::media_query_handle::MediaQueryListHandle;
pub struct ResizeScaleHandle(Rc<ResizeScaleInternal>);
impl ResizeScaleHandle {
pub(crate) fn new<S, R>(
window: Window,
document: Document,
canvas: HtmlCanvasElement,
style: Style,
scale_handler: S,
resize_handler: R,
) -> Self
where
S: 'static + Fn(PhysicalSize<u32>, f64),
R: 'static + Fn(PhysicalSize<u32>),
{
Self(ResizeScaleInternal::new(
window,
document,
canvas,
style,
scale_handler,
resize_handler,
))
}
pub(crate) fn notify_resize(&self) {
self.0.notify()
}
}
/// This is a helper type to help manage the `MediaQueryList` used for detecting
/// changes of the `devicePixelRatio`.
struct ResizeScaleInternal {
window: Window,
document: Document,
canvas: HtmlCanvasElement,
style: Style,
mql: RefCell<MediaQueryListHandle>,
observer: ResizeObserver,
_observer_closure: Closure<dyn FnMut(Array, ResizeObserver)>,
scale_handler: Box<dyn Fn(PhysicalSize<u32>, f64)>,
resize_handler: Box<dyn Fn(PhysicalSize<u32>)>,
notify_scale: Cell<bool>,
}
impl ResizeScaleInternal {
fn new<S, R>(
window: Window,
document: Document,
canvas: HtmlCanvasElement,
style: Style,
scale_handler: S,
resize_handler: R,
) -> Rc<Self>
where
S: 'static + Fn(PhysicalSize<u32>, f64),
R: 'static + Fn(PhysicalSize<u32>),
{
Rc::<ResizeScaleInternal>::new_cyclic(|weak_self| {
let mql = Self::create_mql(&window, {
let weak_self = weak_self.clone();
move |mql| {
if let Some(rc_self) = weak_self.upgrade() {
Self::handle_scale(rc_self, mql);
}
}
});
let weak_self = weak_self.clone();
let observer_closure = Closure::new(move |entries: Array, _| {
if let Some(this) = weak_self.upgrade() {
let size = this.process_entry(entries);
if this.notify_scale.replace(false) {
let scale = backend::scale_factor(&this.window);
(this.scale_handler)(size, scale)
} else {
(this.resize_handler)(size)
}
}
});
let observer = Self::create_observer(&canvas, observer_closure.as_ref());
Self {
window,
document,
canvas,
style,
mql: RefCell::new(mql),
observer,
_observer_closure: observer_closure,
scale_handler: Box::new(scale_handler),
resize_handler: Box::new(resize_handler),
notify_scale: Cell::new(false),
}
})
}
fn create_mql<F>(window: &Window, closure: F) -> MediaQueryListHandle
where
F: 'static + FnMut(&MediaQueryList),
{
let current_scale = super::scale_factor(window);
// TODO: Remove `-webkit-device-pixel-ratio`. Requires Safari v16.
let media_query = format!(
"(resolution: {current_scale}dppx),
(-webkit-device-pixel-ratio: {current_scale})",
);
let mql = MediaQueryListHandle::new(window, &media_query, closure);
debug_assert!(
mql.mql().matches(),
"created media query doesn't match, {current_scale} != {}",
super::scale_factor(window)
);
mql
}
fn create_observer(canvas: &HtmlCanvasElement, closure: &JsValue) -> ResizeObserver {
let observer = ResizeObserver::new(closure.as_ref().unchecked_ref())
.expect("Failed to create `ResizeObserver`");
// Safari doesn't support `devicePixelContentBoxSize`
if has_device_pixel_support() {
let options = ResizeObserverOptions::new();
options.set_box(ResizeObserverBoxOptions::DevicePixelContentBox);
observer.observe_with_options(canvas, &options);
} else {
observer.observe(canvas);
}
observer
}
fn notify(&self) {
if !self.document.contains(Some(&self.canvas)) || self.style.get("display") == "none" {
let size = PhysicalSize::new(0, 0);
if self.notify_scale.replace(false) {
let scale = backend::scale_factor(&self.window);
(self.scale_handler)(size, scale)
} else {
(self.resize_handler)(size)
}
return;
}
// Safari doesn't support `devicePixelContentBoxSize`
if has_device_pixel_support() {
self.observer.unobserve(&self.canvas);
self.observer.observe(&self.canvas);
return;
}
let mut size = LogicalSize::new(
backend::style_size_property(&self.style, "width"),
backend::style_size_property(&self.style, "height"),
);
if self.style.get("box-sizing") == "border-box" {
size.width -= backend::style_size_property(&self.style, "border-left-width")
+ backend::style_size_property(&self.style, "border-right-width")
+ backend::style_size_property(&self.style, "padding-left")
+ backend::style_size_property(&self.style, "padding-right");
size.height -= backend::style_size_property(&self.style, "border-top-width")
+ backend::style_size_property(&self.style, "border-bottom-width")
+ backend::style_size_property(&self.style, "padding-top")
+ backend::style_size_property(&self.style, "padding-bottom");
}
let size = size.to_physical(backend::scale_factor(&self.window));
if self.notify_scale.replace(false) {
let scale = backend::scale_factor(&self.window);
(self.scale_handler)(size, scale)
} else {
(self.resize_handler)(size)
}
}
fn handle_scale(self: Rc<Self>, mql: &MediaQueryList) {
let weak_self = Rc::downgrade(&self);
let scale = super::scale_factor(&self.window);
// TODO: confirm/reproduce this problem, see:
// <https://github.com/rust-windowing/winit/issues/2597>.
// This should never happen, but if it does then apparently the scale factor didn't change.
if mql.matches() {
warn!(
"media query tracking scale factor was triggered without a change:\nMedia Query: \
{}\nCurrent Scale: {scale}",
mql.media(),
);
return;
}
let new_mql = Self::create_mql(&self.window, move |mql| {
if let Some(rc_self) = weak_self.upgrade() {
Self::handle_scale(rc_self, mql);
}
});
self.mql.replace(new_mql);
self.notify_scale.set(true);
self.notify();
}
fn process_entry(&self, entries: Array) -> PhysicalSize<u32> {
let entry: ResizeObserverEntry = entries.get(0).unchecked_into();
// Safari doesn't support `devicePixelContentBoxSize`
if !has_device_pixel_support() {
let rect = entry.content_rect();
return LogicalSize::new(rect.width(), rect.height())
.to_physical(backend::scale_factor(&self.window));
}
let entry: ResizeObserverSize =
entry.device_pixel_content_box_size().get(0).unchecked_into();
let writing_mode = self.style.get("writing-mode");
// means the canvas is not inserted into the DOM
if writing_mode.is_empty() {
debug_assert_eq!(entry.inline_size(), 0.);
debug_assert_eq!(entry.block_size(), 0.);
return PhysicalSize::new(0, 0);
}
let horizontal = match writing_mode.as_str() {
_ if writing_mode.starts_with("horizontal") => true,
_ if writing_mode.starts_with("vertical") | writing_mode.starts_with("sideways") => {
false
},
// deprecated values
"lr" | "lr-tb" | "rl" => true,
"tb" | "tb-lr" | "tb-rl" => false,
_ => {
warn!("unrecognized `writing-mode`, assuming horizontal");
true
},
};
if horizontal {
PhysicalSize::new(entry.inline_size() as u32, entry.block_size() as u32)
} else {
PhysicalSize::new(entry.block_size() as u32, entry.inline_size() as u32)
}
}
}
impl Drop for ResizeScaleInternal {
fn drop(&mut self) {
self.observer.disconnect();
}
}
// TODO: Remove when Safari supports `devicePixelContentBoxSize`.
// See <https://bugs.webkit.org/show_bug.cgi?id=219005>.
pub fn has_device_pixel_support() -> bool {
thread_local! {
static DEVICE_PIXEL_SUPPORT: bool = {
#[wasm_bindgen]
extern "C" {
type ResizeObserverEntryExt;
#[wasm_bindgen(js_class = ResizeObserverEntry, static_method_of = ResizeObserverEntryExt, getter)]
fn prototype() -> Object;
}
let prototype = ResizeObserverEntryExt::prototype();
let descriptor = Object::get_own_property_descriptor(
&prototype,
&JsValue::from_str("devicePixelContentBoxSize"),
);
!descriptor.is_undefined()
};
}
DEVICE_PIXEL_SUPPORT.with(|support| *support)
}

View file

@ -0,0 +1,56 @@
use dpi::{LogicalPosition, LogicalSize};
use wasm_bindgen::JsCast;
use web_sys::{Document, HtmlHtmlElement, Window};
use super::Style;
pub struct SafeAreaHandle {
style: Style,
}
impl SafeAreaHandle {
pub fn new(window: &Window, document: &Document) -> Self {
let document: HtmlHtmlElement = document.document_element().unwrap().unchecked_into();
#[allow(clippy::disallowed_methods)]
let write = document.style();
write
.set_property(
"--__winit_safe_area",
"env(safe-area-inset-top) env(safe-area-inset-right) env(safe-area-inset-bottom) \
env(safe-area-inset-left)",
)
.expect("unexpected read-only declaration block");
#[allow(clippy::disallowed_methods)]
let read = window
.get_computed_style(&document)
.expect("failed to obtain computed style")
// this can't fail: we aren't using a pseudo-element
.expect("invalid pseudo-element");
SafeAreaHandle { style: Style { read, write } }
}
pub fn get(&self) -> (LogicalPosition<f64>, LogicalSize<f64>) {
let value = self.style.get("--__winit_safe_area");
let mut values = value
.split(' ')
.map(|value| value.strip_suffix("px").expect("unexpected unit other then `px` found"));
let top: f64 = values.next().unwrap().parse().unwrap();
let right: f64 = values.next().unwrap().parse().unwrap();
let bottom: f64 = values.next().unwrap().parse().unwrap();
let left: f64 = values.next().unwrap().parse().unwrap();
assert_eq!(values.next(), None, "unexpected fifth value");
let width = super::style_size_property(&self.style, "width") - left - right;
let height = super::style_size_property(&self.style, "height") - top - bottom;
(LogicalPosition::new(left, top), LogicalSize::new(width, height))
}
}
impl Drop for SafeAreaHandle {
fn drop(&mut self) {
self.style.remove("--__winit_safe_area");
}
}

View file

@ -0,0 +1,319 @@
use std::cell::OnceCell;
use std::time::Duration;
use js_sys::{Array, Function, Object, Promise};
use wasm_bindgen::closure::Closure;
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue};
use web_sys::{
AbortController, AbortSignal, Blob, BlobPropertyBag, MessageChannel, MessagePort, Url, Worker,
};
use crate::{PollStrategy, WaitUntilStrategy};
#[derive(Debug)]
pub struct Schedule {
_closure: Closure<dyn FnMut()>,
inner: Inner,
}
#[derive(Debug)]
enum Inner {
Scheduler {
controller: AbortController,
},
IdleCallback {
window: web_sys::Window,
handle: u32,
},
Timeout {
window: web_sys::Window,
handle: i32,
port: MessagePort,
_timeout_closure: Closure<dyn FnMut()>,
},
Worker(MessagePort),
}
impl Schedule {
pub fn new<F>(strategy: PollStrategy, window: &web_sys::Window, f: F) -> Schedule
where
F: 'static + FnMut(),
{
if strategy == PollStrategy::Scheduler && has_scheduler_support(window) {
Self::new_scheduler(window, f, None)
} else if strategy == PollStrategy::IdleCallback && has_idle_callback_support(window) {
Self::new_idle_callback(window.clone(), f)
} else {
Self::new_timeout(window.clone(), f, None)
}
}
pub fn new_with_duration<F>(
strategy: WaitUntilStrategy,
window: &web_sys::Window,
f: F,
duration: Duration,
) -> Schedule
where
F: 'static + FnMut(),
{
match strategy {
WaitUntilStrategy::Scheduler => {
if has_scheduler_support(window) {
Self::new_scheduler(window, f, Some(duration))
} else {
Self::new_timeout(window.clone(), f, Some(duration))
}
},
WaitUntilStrategy::Worker => Self::new_worker(f, duration),
}
}
fn new_scheduler<F>(window: &web_sys::Window, f: F, duration: Option<Duration>) -> Schedule
where
F: 'static + FnMut(),
{
let window: &WindowSupportExt = window.unchecked_ref();
let scheduler = window.scheduler();
let closure = Closure::new(f);
let options: SchedulerPostTaskOptions = Object::new().unchecked_into();
let controller = AbortController::new().expect("Failed to create `AbortController`");
options.set_signal(&controller.signal());
if let Some(duration) = duration {
// `Duration::as_millis()` always rounds down (because of truncation), we want to round
// up instead. This makes sure that the we never wake up **before** the given time.
let duration = duration
.as_secs()
.checked_mul(1000)
.and_then(|secs| secs.checked_add(duration.subsec_micros().div_ceil(1000).into()))
.unwrap_or(u64::MAX);
options.set_delay(duration as f64);
}
thread_local! {
static REJECT_HANDLER: Closure<dyn FnMut(JsValue)> = Closure::new(|_| ());
}
REJECT_HANDLER.with(|handler| {
let _ = scheduler
.post_task_with_options(closure.as_ref().unchecked_ref(), &options)
.catch(handler);
});
Schedule { _closure: closure, inner: Inner::Scheduler { controller } }
}
fn new_idle_callback<F>(window: web_sys::Window, f: F) -> Schedule
where
F: 'static + FnMut(),
{
let closure = Closure::new(f);
let handle = window
.request_idle_callback(closure.as_ref().unchecked_ref())
.expect("Failed to request idle callback");
Schedule { _closure: closure, inner: Inner::IdleCallback { window, handle } }
}
fn new_timeout<F>(window: web_sys::Window, f: F, duration: Option<Duration>) -> Schedule
where
F: 'static + FnMut(),
{
let channel = MessageChannel::new().unwrap();
let closure = Closure::new(f);
let port_1 = channel.port1();
port_1.set_onmessage(Some(closure.as_ref().unchecked_ref()));
port_1.start();
let port_2 = channel.port2();
let timeout_closure = Closure::new(move || {
port_2.post_message(&JsValue::UNDEFINED).expect("Failed to send message")
});
let handle = if let Some(duration) = duration {
// `Duration::as_millis()` always rounds down (because of truncation), we want to round
// up instead. This makes sure that the we never wake up **before** the given time.
let duration = duration
.as_secs()
.try_into()
.ok()
.and_then(|secs: i32| secs.checked_mul(1000))
.and_then(|secs: i32| {
let millis: i32 = duration
.subsec_micros()
.div_ceil(1000)
.try_into()
.expect("millis are somehow bigger then 1K");
secs.checked_add(millis)
})
.unwrap_or(i32::MAX);
window.set_timeout_with_callback_and_timeout_and_arguments_0(
timeout_closure.as_ref().unchecked_ref(),
duration,
)
} else {
window.set_timeout_with_callback(timeout_closure.as_ref().unchecked_ref())
}
.expect("Failed to set timeout");
Schedule {
_closure: closure,
inner: Inner::Timeout {
window,
handle,
port: port_1,
_timeout_closure: timeout_closure,
},
}
}
fn new_worker<F>(f: F, duration: Duration) -> Schedule
where
F: 'static + FnMut(),
{
thread_local! {
static URL: ScriptUrl = ScriptUrl::new(include_str!("../script/worker.min.js"));
static WORKER: Worker = URL.with(|url| Worker::new(&url.0)).expect("`new Worker()` is not expected to fail with a local script");
}
let channel = MessageChannel::new().unwrap();
let closure = Closure::new(f);
let port_1 = channel.port1();
port_1.set_onmessage(Some(closure.as_ref().unchecked_ref()));
port_1.start();
// `Duration::as_millis()` always rounds down (because of truncation), we want to round
// up instead. This makes sure that the we never wake up **before** the given time.
let duration = duration
.as_secs()
.try_into()
.ok()
.and_then(|secs: u32| secs.checked_mul(1000))
.and_then(|secs| secs.checked_add(duration.subsec_micros().div_ceil(1000)))
.unwrap_or(u32::MAX);
WORKER
.with(|worker| {
let port_2 = channel.port2();
worker.post_message_with_transfer(
&Array::of2(&port_2, &duration.into()),
&Array::of1(&port_2).into(),
)
})
.expect("`Worker.postMessage()` is not expected to fail");
Schedule { _closure: closure, inner: Inner::Worker(port_1) }
}
}
impl Drop for Schedule {
fn drop(&mut self) {
match &self.inner {
Inner::Scheduler { controller, .. } => controller.abort(),
Inner::IdleCallback { window, handle, .. } => window.cancel_idle_callback(*handle),
Inner::Timeout { window, handle, port, .. } => {
window.clear_timeout_with_handle(*handle);
port.close();
port.set_onmessage(None);
},
Inner::Worker(port) => {
port.close();
port.set_onmessage(None);
},
}
}
}
fn has_scheduler_support(window: &web_sys::Window) -> bool {
thread_local! {
static SCHEDULER_SUPPORT: OnceCell<bool> = const { OnceCell::new() };
}
SCHEDULER_SUPPORT.with(|support| {
*support.get_or_init(|| {
#[wasm_bindgen]
extern "C" {
type SchedulerSupport;
#[wasm_bindgen(method, getter, js_name = scheduler)]
fn has_scheduler(this: &SchedulerSupport) -> JsValue;
}
let support: &SchedulerSupport = window.unchecked_ref();
!support.has_scheduler().is_undefined()
})
})
}
fn has_idle_callback_support(window: &web_sys::Window) -> bool {
thread_local! {
static IDLE_CALLBACK_SUPPORT: OnceCell<bool> = const { OnceCell::new() };
}
IDLE_CALLBACK_SUPPORT.with(|support| {
*support.get_or_init(|| {
#[wasm_bindgen]
extern "C" {
type IdleCallbackSupport;
#[wasm_bindgen(method, getter, js_name = requestIdleCallback)]
fn has_request_idle_callback(this: &IdleCallbackSupport) -> JsValue;
}
let support: &IdleCallbackSupport = window.unchecked_ref();
!support.has_request_idle_callback().is_undefined()
})
})
}
struct ScriptUrl(String);
impl ScriptUrl {
fn new(script: &str) -> Self {
let sequence = Array::of1(&script.into());
let property = BlobPropertyBag::new();
property.set_type("text/javascript");
let blob = Blob::new_with_str_sequence_and_options(&sequence, &property)
.expect("`new Blob()` should never throw");
let url = Url::create_object_url_with_blob(&blob)
.expect("`URL.createObjectURL()` should never throw");
Self(url)
}
}
impl Drop for ScriptUrl {
fn drop(&mut self) {
Url::revoke_object_url(&self.0).expect("`URL.revokeObjectURL()` should never throw");
}
}
#[wasm_bindgen]
extern "C" {
type WindowSupportExt;
#[wasm_bindgen(method, getter)]
fn scheduler(this: &WindowSupportExt) -> Scheduler;
type Scheduler;
#[wasm_bindgen(method, js_name = postTask)]
fn post_task_with_options(
this: &Scheduler,
callback: &Function,
options: &SchedulerPostTaskOptions,
) -> Promise;
type SchedulerPostTaskOptions;
#[wasm_bindgen(method, setter, js_name = delay)]
fn set_delay(this: &SchedulerPostTaskOptions, value: f64);
#[wasm_bindgen(method, setter, js_name = signal)]
fn set_signal(this: &SchedulerPostTaskOptions, value: &AbortSignal);
}

467
winit-web/src/window.rs Normal file
View file

@ -0,0 +1,467 @@
use std::cell::Ref;
use std::fmt;
use std::rc::Rc;
use dpi::{
LogicalInsets, LogicalPosition, LogicalSize, PhysicalInsets, PhysicalPosition, PhysicalSize,
Position, Size,
};
use web_sys::HtmlCanvasElement;
use winit_core::cursor::Cursor;
use winit_core::error::{NotSupportedError, RequestError};
use winit_core::icon::Icon;
use winit_core::monitor::{Fullscreen, MonitorHandle as CoremMonitorHandle};
use winit_core::window::{
CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, Window as RootWindow,
WindowAttributes, WindowButtons, WindowId, WindowLevel,
};
use crate::event_loop::ActiveEventLoop;
use crate::main_thread::MainThreadMarker;
use crate::monitor::MonitorHandler;
use crate::r#async::Dispatcher;
use crate::{backend, lock};
pub struct Window {
inner: Dispatcher<Inner>,
}
impl fmt::Debug for Window {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Window").finish_non_exhaustive()
}
}
pub struct Inner {
id: WindowId,
pub window: web_sys::Window,
monitor: Rc<MonitorHandler>,
safe_area: Rc<backend::SafeAreaHandle>,
canvas: Rc<backend::Canvas>,
destroy_fn: Option<Box<dyn FnOnce()>>,
}
impl Window {
pub(crate) fn new(
target: &ActiveEventLoop,
attr: WindowAttributes,
) -> Result<Self, RequestError> {
let id = target.generate_id();
let window = target.runner.window();
let navigator = target.runner.navigator();
let document = target.runner.document();
let canvas = backend::Canvas::create(
target.runner.main_thread(),
id,
window.clone(),
navigator.clone(),
document.clone(),
attr,
)?;
let canvas = Rc::new(canvas);
target.register(&canvas, id);
let runner = target.runner.clone();
let destroy_fn = Box::new(move || runner.notify_destroy_window(id));
let inner = Inner {
id,
window: window.clone(),
monitor: Rc::clone(target.runner.monitor()),
safe_area: Rc::clone(target.runner.safe_area()),
canvas,
destroy_fn: Some(destroy_fn),
};
let canvas = Rc::downgrade(&inner.canvas);
let (dispatcher, runner) = Dispatcher::new(target.runner.main_thread(), inner);
target.runner.add_canvas(id, canvas, runner);
Ok(Window { inner: dispatcher })
}
pub fn canvas(&self) -> Option<Ref<'_, HtmlCanvasElement>> {
MainThreadMarker::new()
.map(|main_thread| Ref::map(self.inner.value(main_thread), |inner| inner.canvas.raw()))
}
pub(crate) fn prevent_default(&self) -> bool {
self.inner.queue(|inner| inner.canvas.prevent_default.get())
}
pub(crate) fn set_prevent_default(&self, prevent_default: bool) {
self.inner.dispatch(move |inner| inner.canvas.prevent_default.set(prevent_default))
}
pub(crate) fn is_cursor_lock_raw(&self) -> bool {
self.inner.queue(move |inner| {
lock::is_cursor_lock_raw(inner.canvas.navigator(), inner.canvas.document())
})
}
}
impl RootWindow for Window {
fn id(&self) -> WindowId {
self.inner.queue(|inner| inner.id)
}
fn scale_factor(&self) -> f64 {
self.inner.queue(Inner::scale_factor)
}
fn request_redraw(&self) {
self.inner.dispatch(|inner| inner.canvas.request_animation_frame())
}
fn pre_present_notify(&self) {}
fn reset_dead_keys(&self) {
// Not supported
}
fn surface_position(&self) -> PhysicalPosition<i32> {
// Note: the canvas element has no window decorations.
(0, 0).into()
}
fn outer_position(&self) -> Result<PhysicalPosition<i32>, RequestError> {
Ok(self.inner.queue(|inner| inner.canvas.position().to_physical(inner.scale_factor())))
}
fn set_outer_position(&self, position: Position) {
self.inner.dispatch(move |inner| {
let position = position.to_logical::<f64>(inner.scale_factor());
backend::set_canvas_position(
inner.canvas.document(),
inner.canvas.raw(),
inner.canvas.style(),
position,
)
})
}
fn surface_size(&self) -> PhysicalSize<u32> {
self.inner.queue(|inner| inner.canvas.surface_size())
}
fn request_surface_size(&self, size: Size) -> Option<PhysicalSize<u32>> {
self.inner.queue(|inner| {
let size = size.to_logical(self.scale_factor());
backend::set_canvas_size(
inner.canvas.document(),
inner.canvas.raw(),
inner.canvas.style(),
size,
);
None
})
}
fn outer_size(&self) -> PhysicalSize<u32> {
// Note: the canvas element has no window decorations, so this is equal to `surface_size`.
self.surface_size()
}
fn safe_area(&self) -> PhysicalInsets<u32> {
self.inner.queue(|inner| {
let (safe_start_pos, safe_size) = inner.safe_area.get();
let safe_end_pos = LogicalPosition::new(
safe_start_pos.x + safe_size.width,
safe_start_pos.y + safe_size.height,
);
let surface_start_pos = inner.canvas.position();
let surface_size = LogicalSize::new(
backend::style_size_property(inner.canvas.style(), "width"),
backend::style_size_property(inner.canvas.style(), "height"),
);
let surface_end_pos = LogicalPosition::new(
surface_start_pos.x + surface_size.width,
surface_start_pos.y + surface_size.height,
);
let top = f64::max(safe_start_pos.y - surface_start_pos.y, 0.);
let left = f64::max(safe_start_pos.x - surface_start_pos.x, 0.);
let bottom = f64::max(surface_end_pos.y - safe_end_pos.y, 0.);
let right = f64::max(surface_end_pos.x - safe_end_pos.x, 0.);
let insets = LogicalInsets::new(top, left, bottom, right);
insets.to_physical(inner.scale_factor())
})
}
fn set_min_surface_size(&self, min_size: Option<Size>) {
self.inner.dispatch(move |inner| {
let dimensions = min_size.map(|min_size| min_size.to_logical(inner.scale_factor()));
backend::set_canvas_min_size(
inner.canvas.document(),
inner.canvas.raw(),
inner.canvas.style(),
dimensions,
)
})
}
fn set_max_surface_size(&self, max_size: Option<Size>) {
self.inner.dispatch(move |inner| {
let dimensions = max_size.map(|dimensions| dimensions.to_logical(inner.scale_factor()));
backend::set_canvas_max_size(
inner.canvas.document(),
inner.canvas.raw(),
inner.canvas.style(),
dimensions,
)
})
}
fn surface_resize_increments(&self) -> Option<PhysicalSize<u32>> {
None
}
fn set_surface_resize_increments(&self, _: Option<Size>) {
// Intentionally a no-op: users can't resize canvas elements
}
fn set_title(&self, title: &str) {
self.inner.queue(|inner| inner.canvas.set_attribute("alt", title))
}
fn set_transparent(&self, _: bool) {}
fn set_blur(&self, _: bool) {}
fn set_visible(&self, _: bool) {
// Intentionally a no-op
}
fn is_visible(&self) -> Option<bool> {
None
}
fn set_resizable(&self, _: bool) {
// Intentionally a no-op: users can't resize canvas elements
}
fn is_resizable(&self) -> bool {
true
}
fn set_enabled_buttons(&self, _: WindowButtons) {}
fn enabled_buttons(&self) -> WindowButtons {
WindowButtons::all()
}
fn set_minimized(&self, _: bool) {
// Intentionally a no-op, as canvases cannot be 'minimized'
}
fn is_minimized(&self) -> Option<bool> {
// Canvas cannot be 'minimized'
Some(false)
}
fn set_maximized(&self, _: bool) {
// Intentionally a no-op, as canvases cannot be 'maximized'
}
fn is_maximized(&self) -> bool {
// Canvas cannot be 'maximized'
false
}
fn set_fullscreen(&self, fullscreen: Option<Fullscreen>) {
self.inner.dispatch(move |inner| {
if let Some(fullscreen) = fullscreen {
inner.canvas.request_fullscreen(fullscreen);
} else {
inner.canvas.exit_fullscreen()
}
})
}
fn fullscreen(&self) -> Option<Fullscreen> {
self.inner.queue(|inner| {
if inner.canvas.is_fullscreen() {
Some(Fullscreen::Borderless(None))
} else {
None
}
})
}
fn set_decorations(&self, _: bool) {
// Intentionally a no-op, no canvas decorations
}
fn is_decorated(&self) -> bool {
true
}
fn set_window_level(&self, _: WindowLevel) {
// Intentionally a no-op, no window ordering
}
fn set_window_icon(&self, _: Option<Icon>) {
// Currently an intentional no-op
}
fn set_ime_cursor_area(&self, _: Position, _: Size) {
// Currently not implemented
}
fn set_ime_allowed(&self, _: bool) {
// Currently not implemented
}
fn set_ime_purpose(&self, _: ImePurpose) {
// Currently not implemented
}
fn focus_window(&self) {
self.inner.dispatch(|inner| {
let _ = inner.canvas.raw().focus();
})
}
fn has_focus(&self) -> bool {
self.inner.queue(|inner| inner.canvas.has_focus.get())
}
fn request_user_attention(&self, _: Option<UserAttentionType>) {
// Currently an intentional no-op
}
fn set_theme(&self, _: Option<Theme>) {}
fn theme(&self) -> Option<Theme> {
self.inner.queue(|inner| {
backend::is_dark_mode(&inner.window).map(|is_dark_mode| {
if is_dark_mode {
Theme::Dark
} else {
Theme::Light
}
})
})
}
fn set_content_protected(&self, _: bool) {}
fn title(&self) -> String {
String::new()
}
fn set_cursor(&self, cursor: Cursor) {
self.inner.dispatch(move |inner| inner.canvas.cursor.set_cursor(cursor))
}
fn set_cursor_position(&self, _: Position) -> Result<(), RequestError> {
Err(NotSupportedError::new("set_cursor_position is not supported").into())
}
fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), RequestError> {
Ok(self.inner.queue(|inner| {
match mode {
CursorGrabMode::None => inner.canvas.document().exit_pointer_lock(),
CursorGrabMode::Locked => lock::request_pointer_lock(
inner.canvas.navigator(),
inner.canvas.document(),
inner.canvas.raw(),
),
CursorGrabMode::Confined => {
return Err(NotSupportedError::new("confined cursor mode is not supported"))
},
}
Ok(())
})?)
}
fn set_cursor_visible(&self, visible: bool) {
self.inner.dispatch(move |inner| inner.canvas.cursor.set_cursor_visible(visible))
}
fn drag_window(&self) -> Result<(), RequestError> {
Err(NotSupportedError::new("drag_window is not supported").into())
}
fn drag_resize_window(&self, _: ResizeDirection) -> Result<(), RequestError> {
Err(NotSupportedError::new("drag_resize_window is not supported").into())
}
fn show_window_menu(&self, _: Position) {}
fn set_cursor_hittest(&self, _: bool) -> Result<(), RequestError> {
Err(NotSupportedError::new("set_cursor_hittest is not supported").into())
}
fn current_monitor(&self) -> Option<CoremMonitorHandle> {
Some(self.inner.queue(|inner| inner.monitor.current_monitor()).into())
}
fn available_monitors(&self) -> Box<dyn Iterator<Item = CoremMonitorHandle>> {
Box::new(
self.inner
.queue(|inner| inner.monitor.available_monitors())
.into_iter()
.map(CoremMonitorHandle::from),
)
}
fn primary_monitor(&self) -> Option<CoremMonitorHandle> {
self.inner.queue(|inner| inner.monitor.primary_monitor()).map(CoremMonitorHandle::from)
}
fn rwh_06_display_handle(&self) -> &dyn rwh_06::HasDisplayHandle {
self
}
fn rwh_06_window_handle(&self) -> &dyn rwh_06::HasWindowHandle {
self
}
}
impl rwh_06::HasWindowHandle for Window {
fn window_handle(&self) -> Result<rwh_06::WindowHandle<'_>, rwh_06::HandleError> {
MainThreadMarker::new()
.map(|main_thread| {
let inner = self.inner.value(main_thread);
// SAFETY: This will only work if the reference to `HtmlCanvasElement` stays valid.
let canvas: &wasm_bindgen::JsValue = inner.canvas.raw();
let window_handle =
rwh_06::WebCanvasWindowHandle::new(std::ptr::NonNull::from(canvas).cast());
// SAFETY: The pointer won't be invalidated as long as `Window` lives, which the
// lifetime is bound to.
unsafe {
rwh_06::WindowHandle::borrow_raw(rwh_06::RawWindowHandle::WebCanvas(
window_handle,
))
}
})
.ok_or(rwh_06::HandleError::Unavailable)
}
}
impl rwh_06::HasDisplayHandle for Window {
fn display_handle(&self) -> Result<rwh_06::DisplayHandle<'_>, rwh_06::HandleError> {
Ok(rwh_06::DisplayHandle::web())
}
}
impl Inner {
#[inline]
pub fn scale_factor(&self) -> f64 {
super::backend::scale_factor(&self.window)
}
}
impl Drop for Inner {
fn drop(&mut self) {
if let Some(destroy_fn) = self.destroy_fn.take() {
destroy_fn();
}
}
}