Move Web backend to winit-web
This commit is contained in:
parent
2d4b9938f0
commit
e542a78deb
50 changed files with 259 additions and 273 deletions
95
winit-web/Cargo.toml
Normal file
95
winit-web/Cargo.toml
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
[package]
|
||||
description = "Winit's Web (WebAssembly) backend"
|
||||
documentation = "https://docs.rs/winit-web"
|
||||
edition.workspace = true
|
||||
include = [
|
||||
"/src",
|
||||
"!/src/platform_impl/web/script",
|
||||
"/src/platform_impl/web/script/**/*.min.js",
|
||||
"README.md",
|
||||
]
|
||||
license.workspace = true
|
||||
name = "winit-web"
|
||||
repository.workspace = true
|
||||
rust-version.workspace = true
|
||||
version = "0.0.0"
|
||||
|
||||
[features]
|
||||
serde = ["dep:serde", "bitflags/serde", "smol_str/serde", "dpi/serde"]
|
||||
|
||||
[dependencies]
|
||||
bitflags.workspace = true
|
||||
cursor-icon.workspace = true
|
||||
dpi.workspace = true
|
||||
rwh_06.workspace = true
|
||||
serde = { workspace = true, optional = true }
|
||||
smol_str.workspace = true
|
||||
tracing.workspace = true
|
||||
winit-core.workspace = true
|
||||
|
||||
# Platform-specific
|
||||
js-sys.workspace = true
|
||||
pin-project.workspace = true
|
||||
wasm-bindgen.workspace = true
|
||||
wasm-bindgen-futures.workspace = true
|
||||
web-time.workspace = true
|
||||
web_sys = { workspace = true, features = [
|
||||
"AbortController",
|
||||
"AbortSignal",
|
||||
"Blob",
|
||||
"BlobPropertyBag",
|
||||
"console",
|
||||
"CssStyleDeclaration",
|
||||
"Document",
|
||||
"DomException",
|
||||
"DomRect",
|
||||
"DomRectReadOnly",
|
||||
"Element",
|
||||
"Event",
|
||||
"EventTarget",
|
||||
"FocusEvent",
|
||||
"HtmlCanvasElement",
|
||||
"HtmlElement",
|
||||
"HtmlHtmlElement",
|
||||
"HtmlImageElement",
|
||||
"ImageBitmap",
|
||||
"ImageBitmapOptions",
|
||||
"ImageBitmapRenderingContext",
|
||||
"ImageData",
|
||||
"IntersectionObserver",
|
||||
"IntersectionObserverEntry",
|
||||
"KeyboardEvent",
|
||||
"MediaQueryList",
|
||||
"MessageChannel",
|
||||
"MessagePort",
|
||||
"Navigator",
|
||||
"Node",
|
||||
"OrientationLockType",
|
||||
"OrientationType",
|
||||
"PageTransitionEvent",
|
||||
"Permissions",
|
||||
"PermissionState",
|
||||
"PermissionStatus",
|
||||
"PointerEvent",
|
||||
"PremultiplyAlpha",
|
||||
"ResizeObserver",
|
||||
"ResizeObserverBoxOptions",
|
||||
"ResizeObserverEntry",
|
||||
"ResizeObserverOptions",
|
||||
"ResizeObserverSize",
|
||||
"Screen",
|
||||
"ScreenOrientation",
|
||||
"Url",
|
||||
"VisibilityState",
|
||||
"WheelEvent",
|
||||
"Window",
|
||||
"Worker",
|
||||
] }
|
||||
|
||||
[target.'cfg(target_feature = "atomics")'.dependencies]
|
||||
atomic-waker.workspace = true
|
||||
concurrent-queue.workspace = true
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
targets = ["wasm32-unknown-unknown"]
|
||||
1
winit-web/README.md
Symbolic link
1
winit-web/README.md
Symbolic link
|
|
@ -0,0 +1 @@
|
|||
../README.md
|
||||
96
winit-web/src/async/abortable.rs
Normal file
96
winit-web/src/async/abortable.rs
Normal 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 {}
|
||||
35
winit-web/src/async/atomic_waker.rs
Normal file
35
winit-web/src/async/atomic_waker.rs
Normal 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 {}
|
||||
81
winit-web/src/async/channel.rs
Normal file
81
winit-web/src/async/channel.rs
Normal 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,
|
||||
}
|
||||
52
winit-web/src/async/concurrent_queue.rs
Normal file
52
winit-web/src/async/concurrent_queue.rs
Normal 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> {}
|
||||
149
winit-web/src/async/dispatcher.rs
Normal file
149
winit-web/src/async/dispatcher.rs
Normal 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))
|
||||
}
|
||||
}
|
||||
}
|
||||
18
winit-web/src/async/mod.rs
Normal file
18
winit-web/src/async/mod.rs
Normal 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;
|
||||
75
winit-web/src/async/notifier.rs
Normal file
75
winit-web/src/async/notifier.rs
Normal 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>,
|
||||
}
|
||||
113
winit-web/src/async/wrapper.rs
Normal file
113
winit-web/src/async/wrapper.rs
Normal 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
721
winit-web/src/cursor.rs
Normal 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
10
winit-web/src/error.rs
Normal 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
12
winit-web/src/event.rs
Normal 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
|
||||
}
|
||||
}
|
||||
103
winit-web/src/event_loop/mod.rs
Normal file
103
winit-web/src/event_loop/mod.rs
Normal 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(),
|
||||
)
|
||||
}
|
||||
}
|
||||
104
winit-web/src/event_loop/proxy.rs
Normal file
104
winit-web/src/event_loop/proxy.rs
Normal 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(())
|
||||
}
|
||||
}
|
||||
888
winit-web/src/event_loop/runner.rs
Normal file
888
winit-web/src/event_loop/runner.rs
Normal 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,
|
||||
}
|
||||
18
winit-web/src/event_loop/state.rs
Normal file
18
winit-web/src/event_loop/state.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
574
winit-web/src/event_loop/window_target.rs
Normal file
574
winit-web/src/event_loop/window_target.rs
Normal 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
530
winit-web/src/keyboard.rs
Normal 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
710
winit-web/src/lib.rs
Normal 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
82
winit-web/src/lock.rs
Normal 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);
|
||||
}
|
||||
96
winit-web/src/main_thread.rs
Normal file
96
winit-web/src/main_thread.rs
Normal 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
938
winit-web/src/monitor.rs
Normal 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;
|
||||
}
|
||||
19
winit-web/src/script/.swcrc
Normal file
19
winit-web/src/script/.swcrc
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"module": {
|
||||
"type": "es6"
|
||||
},
|
||||
"isModule": true,
|
||||
"minify": true,
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript"
|
||||
},
|
||||
"target": "es2022",
|
||||
"minify": {
|
||||
"compress": {
|
||||
"unused": true
|
||||
},
|
||||
"mangle": true
|
||||
}
|
||||
}
|
||||
}
|
||||
32
winit-web/src/script/eslint.config.mjs
Normal file
32
winit-web/src/script/eslint.config.mjs
Normal 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,
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
)
|
||||
8
winit-web/src/script/package.json
Normal file
8
winit-web/src/script/package.json
Normal 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
12
winit-web/src/script/scheduler.d.ts
vendored
Normal 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
|
||||
}
|
||||
14
winit-web/src/script/tsconfig.json
Normal file
14
winit-web/src/script/tsconfig.json
Normal 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
1
winit-web/src/script/worker.min.js
vendored
Normal 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)};
|
||||
10
winit-web/src/script/worker.ts
Normal file
10
winit-web/src/script/worker.ts
Normal 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)
|
||||
}
|
||||
}
|
||||
61
winit-web/src/web_sys/animation_frame.rs
Normal file
61
winit-web/src/web_sys/animation_frame.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
||||
594
winit-web/src/web_sys/canvas.rs
Normal file
594
winit-web/src/web_sys/canvas.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
276
winit-web/src/web_sys/event.rs
Normal file
276
winit-web/src/web_sys/event.rs
Normal 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()
|
||||
})
|
||||
})
|
||||
}
|
||||
38
winit-web/src/web_sys/event_handle.rs
Normal file
38
winit-web/src/web_sys/event_handle.rs
Normal 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,
|
||||
)
|
||||
});
|
||||
}
|
||||
}
|
||||
157
winit-web/src/web_sys/fullscreen.rs
Normal file
157
winit-web/src/web_sys/fullscreen.rs
Normal 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()
|
||||
})
|
||||
})
|
||||
}
|
||||
33
winit-web/src/web_sys/intersection_handle.rs
Normal file
33
winit-web/src/web_sys/intersection_handle.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
48
winit-web/src/web_sys/media_query_handle.rs
Normal file
48
winit-web/src/web_sys/media_query_handle.rs
Normal 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),
|
||||
);
|
||||
}
|
||||
262
winit-web/src/web_sys/mod.rs
Normal file
262
winit-web/src/web_sys/mod.rs
Normal 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 }
|
||||
}
|
||||
}
|
||||
268
winit-web/src/web_sys/pointer.rs
Normal file
268
winit-web/src/web_sys/pointer.rs
Normal 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;
|
||||
}
|
||||
}
|
||||
301
winit-web/src/web_sys/resize_scaling.rs
Normal file
301
winit-web/src/web_sys/resize_scaling.rs
Normal 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)
|
||||
}
|
||||
56
winit-web/src/web_sys/safe_area.rs
Normal file
56
winit-web/src/web_sys/safe_area.rs
Normal 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");
|
||||
}
|
||||
}
|
||||
319
winit-web/src/web_sys/schedule.rs
Normal file
319
winit-web/src/web_sys/schedule.rs
Normal 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
467
winit-web/src/window.rs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue