Web: improve custom cursor handling and add animated cursors (#3384)

This commit is contained in:
daxpedda 2024-01-12 11:51:19 +01:00 committed by GitHub
parent bdeb2574dc
commit 169cd39f93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 1086 additions and 420 deletions

View file

@ -17,6 +17,7 @@ Unreleased` header.
- Add `CustomCursor` - Add `CustomCursor`
- Add `CustomCursor::from_rgba` to allow creating cursor images from RGBA data. - Add `CustomCursor::from_rgba` to allow creating cursor images from RGBA data.
- Add `CustomCursorExtWebSys::from_url` to allow loading cursor images from URLs. - Add `CustomCursorExtWebSys::from_url` to allow loading cursor images from URLs.
- Add `CustomCursorExtWebSys::from_animation` to allow creating animated cursors from other `CustomCursor`s.
- On macOS, add services menu. - On macOS, add services menu.
- **Breaking:** On Web, remove queuing fullscreen request in absence of transient activation. - **Breaking:** On Web, remove queuing fullscreen request in absence of transient activation.
- On Web, fix setting cursor icon overriding cursor visibility. - On Web, fix setting cursor icon overriding cursor visibility.

View file

@ -205,6 +205,7 @@ features = [
'console', 'console',
'CssStyleDeclaration', 'CssStyleDeclaration',
'Document', 'Document',
'DomException',
'DomRect', 'DomRect',
'DomRectReadOnly', 'DomRectReadOnly',
'Element', 'Element',
@ -240,12 +241,16 @@ features = [
] ]
[target.'cfg(target_family = "wasm")'.dependencies] [target.'cfg(target_family = "wasm")'.dependencies]
atomic-waker = "1"
js-sys = "0.3.64" js-sys = "0.3.64"
pin-project = "1"
wasm-bindgen = "0.2" wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4" wasm-bindgen-futures = "0.4"
web-time = "0.2" web-time = "0.2"
[target.'cfg(all(target_family = "wasm", target_feature = "atomics"))'.dependencies]
atomic-waker = "1"
concurrent-queue = { version = "2", default-features = false }
[target.'cfg(target_family = "wasm")'.dev-dependencies] [target.'cfg(target_family = "wasm")'.dev-dependencies]
console_log = "1" console_log = "1"
web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] } web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] }

View file

@ -8,6 +8,15 @@ use winit::{
keyboard::Key, keyboard::Key,
window::{CursorIcon, CustomCursor, WindowBuilder}, window::{CursorIcon, CustomCursor, WindowBuilder},
}; };
#[cfg(wasm_platform)]
use {
std::sync::atomic::{AtomicU64, Ordering},
std::time::Duration,
winit::platform::web::CustomCursorExtWebSys,
};
#[cfg(wasm_platform)]
static COUNTER: AtomicU64 = AtomicU64::new(0);
fn decode_cursor<T>(bytes: &[u8], window_target: &EventLoopWindowTarget<T>) -> CustomCursor { fn decode_cursor<T>(bytes: &[u8], window_target: &EventLoopWindowTarget<T>) -> CustomCursor {
let img = image::load_from_memory(bytes).unwrap().to_rgba8(); let img = image::load_from_memory(bytes).unwrap().to_rgba8();
@ -74,6 +83,45 @@ fn main() -> Result<(), impl std::error::Error> {
log::debug!("Setting cursor visibility to {:?}", cursor_visible); log::debug!("Setting cursor visibility to {:?}", cursor_visible);
window.set_cursor_visible(cursor_visible); window.set_cursor_visible(cursor_visible);
} }
#[cfg(wasm_platform)]
Key::Character("4") => {
log::debug!("Setting cursor to a random image from an URL");
window.set_cursor(
CustomCursor::from_url(
format!(
"https://picsum.photos/128?random={}",
COUNTER.fetch_add(1, Ordering::Relaxed)
),
64,
64,
)
.build(_elwt),
);
}
#[cfg(wasm_platform)]
Key::Character("5") => {
log::debug!("Setting cursor to an animation");
window.set_cursor(
CustomCursor::from_animation(
Duration::from_secs(3),
vec![
custom_cursors[0].clone(),
custom_cursors[1].clone(),
CustomCursor::from_url(
format!(
"https://picsum.photos/128?random={}",
COUNTER.fetch_add(1, Ordering::Relaxed)
),
64,
64,
)
.build(_elwt),
],
)
.unwrap()
.build(_elwt),
);
}
_ => {} _ => {}
}, },
WindowEvent::RedrawRequested => { WindowEvent::RedrawRequested => {

View file

@ -27,17 +27,24 @@
//! [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border //! [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border
//! [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding //! [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding
use crate::cursor::CustomCursorBuilder; use std::error::Error;
use crate::event::Event; use std::fmt::{self, Display, Formatter};
use crate::event_loop::EventLoop; use std::future::Future;
use crate::event_loop::EventLoopWindowTarget; use std::pin::Pin;
use crate::platform_impl::PlatformCustomCursorBuilder; use std::task::{Context, Poll};
use crate::window::CustomCursor; use std::time::Duration;
use crate::window::{Window, WindowBuilder};
#[cfg(wasm_platform)] #[cfg(wasm_platform)]
use web_sys::HtmlCanvasElement; use web_sys::HtmlCanvasElement;
use crate::cursor::CustomCursorBuilder;
use crate::event::Event;
use crate::event_loop::{EventLoop, EventLoopWindowTarget};
#[cfg(wasm_platform)]
use crate::platform_impl::CustomCursorFuture as PlatformCustomCursorFuture;
use crate::platform_impl::{PlatformCustomCursor, PlatformCustomCursorBuilder};
use crate::window::{CustomCursor, Window, WindowBuilder};
#[cfg(not(wasm_platform))] #[cfg(not(wasm_platform))]
#[doc(hidden)] #[doc(hidden)]
pub struct HtmlCanvasElement; pub struct HtmlCanvasElement;
@ -234,15 +241,29 @@ pub enum PollStrategy {
} }
pub trait CustomCursorExtWebSys { pub trait CustomCursorExtWebSys {
/// Returns if this cursor is an animation.
fn is_animation(&self) -> bool;
/// Creates a new cursor from a URL pointing to an image. /// Creates a new cursor from a URL pointing to an image.
/// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url), /// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url),
/// but browser support for image formats is inconsistent. Using [PNG] is recommended. /// but browser support for image formats is inconsistent. Using [PNG] is recommended.
/// ///
/// [PNG]: https://en.wikipedia.org/wiki/PNG /// [PNG]: https://en.wikipedia.org/wiki/PNG
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder; fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder;
/// Crates a new animated cursor from multiple [`CustomCursor`]s.
/// Supplied `cursors` can't be empty or other animations.
fn from_animation(
duration: Duration,
cursors: Vec<CustomCursor>,
) -> Result<CustomCursorBuilder, BadAnimation>;
} }
impl CustomCursorExtWebSys for CustomCursor { impl CustomCursorExtWebSys for CustomCursor {
fn is_animation(&self) -> bool {
self.inner.animation
}
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder { fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder {
CustomCursorBuilder { CustomCursorBuilder {
inner: PlatformCustomCursorBuilder::Url { inner: PlatformCustomCursorBuilder::Url {
@ -252,4 +273,92 @@ impl CustomCursorExtWebSys for CustomCursor {
}, },
} }
} }
fn from_animation(
duration: Duration,
cursors: Vec<CustomCursor>,
) -> Result<CustomCursorBuilder, BadAnimation> {
if cursors.is_empty() {
return Err(BadAnimation::Empty);
}
if cursors.iter().any(CustomCursor::is_animation) {
return Err(BadAnimation::Animation);
}
Ok(CustomCursorBuilder {
inner: PlatformCustomCursorBuilder::Animation { duration, cursors },
})
}
}
/// An error produced when using [`CustomCursor::from_animation`] with invalid arguments.
#[derive(Debug, Clone)]
pub enum BadAnimation {
/// Produced when no cursors were supplied.
Empty,
/// Produced when a supplied cursor is an animation.
Animation,
}
impl fmt::Display for BadAnimation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Empty => write!(f, "No cursors supplied"),
Self::Animation => write!(f, "A supplied cursor is an animtion"),
}
}
}
impl Error for BadAnimation {}
pub trait CustomCursorBuilderExtWebSys {
/// Async version of [`CustomCursorBuilder::build()`] which waits until the
/// cursor has completely finished loading.
fn build_async<T>(self, window_target: &EventLoopWindowTarget<T>) -> CustomCursorFuture;
}
impl CustomCursorBuilderExtWebSys for CustomCursorBuilder {
fn build_async<T>(self, window_target: &EventLoopWindowTarget<T>) -> CustomCursorFuture {
CustomCursorFuture(PlatformCustomCursor::build_async(
self.inner,
&window_target.p,
))
}
}
#[cfg(not(wasm_platform))]
struct PlatformCustomCursorFuture;
#[derive(Debug)]
pub struct CustomCursorFuture(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 { inner: cursor })
}
}
#[derive(Clone, Debug)]
pub enum CustomCursorError {
Blob,
Decode(String),
Animation,
}
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}"),
Self::Animation => write!(
f,
"found `CustomCursor` that is an animation when building an animation"
),
}
}
} }

View file

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

View file

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

View file

@ -1,23 +1,24 @@
use atomic_waker::AtomicWaker;
use std::future; use std::future;
use std::rc::Rc; use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::{self, Receiver, RecvError, SendError, Sender, TryRecvError}; use std::sync::mpsc::{self, RecvError, SendError, TryRecvError};
use std::sync::{Arc, Mutex}; use std::sync::{Arc, Mutex};
use std::task::Poll; use std::task::Poll;
pub fn channel<T>() -> (AsyncSender<T>, AsyncReceiver<T>) { use super::AtomicWaker;
pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
let (sender, receiver) = mpsc::channel(); let (sender, receiver) = mpsc::channel();
let shared = Arc::new(Shared { let shared = Arc::new(Shared {
closed: AtomicBool::new(false), closed: AtomicBool::new(false),
waker: AtomicWaker::new(), waker: AtomicWaker::new(),
}); });
let sender = AsyncSender(Arc::new(SenderInner { let sender = Sender(Arc::new(SenderInner {
sender: Mutex::new(sender), sender: Mutex::new(sender),
shared: Arc::clone(&shared), shared: Arc::clone(&shared),
})); }));
let receiver = AsyncReceiver { let receiver = Receiver {
receiver: Rc::new(receiver), receiver: Rc::new(receiver),
shared, shared,
}; };
@ -25,18 +26,18 @@ pub fn channel<T>() -> (AsyncSender<T>, AsyncReceiver<T>) {
(sender, receiver) (sender, receiver)
} }
pub struct AsyncSender<T>(Arc<SenderInner<T>>); pub struct Sender<T>(Arc<SenderInner<T>>);
struct SenderInner<T> { struct SenderInner<T> {
// We need to wrap it into a `Mutex` to make it `Sync`. So the sender can't // We need to wrap it into a `Mutex` to make it `Sync`. So the sender can't
// be accessed on the main thread, as it could block. Additionally we need // be accessed on the main thread, as it could block. Additionally we need
// to wrap `Sender` in an `Arc` to make it clonable on the main thread without // to wrap `Sender` in an `Arc` to make it clonable on the main thread without
// having to block. // having to block.
sender: Mutex<Sender<T>>, sender: Mutex<mpsc::Sender<T>>,
shared: Arc<Shared>, shared: Arc<Shared>,
} }
impl<T> AsyncSender<T> { impl<T> Sender<T> {
pub fn send(&self, event: T) -> Result<(), SendError<T>> { pub fn send(&self, event: T) -> Result<(), SendError<T>> {
self.0.sender.lock().unwrap().send(event)?; self.0.sender.lock().unwrap().send(event)?;
self.0.shared.waker.wake(); self.0.shared.waker.wake();
@ -52,7 +53,7 @@ impl<T> SenderInner<T> {
} }
} }
impl<T> Clone for AsyncSender<T> { impl<T> Clone for Sender<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self(Arc::clone(&self.0)) Self(Arc::clone(&self.0))
} }
@ -64,12 +65,12 @@ impl<T> Drop for SenderInner<T> {
} }
} }
pub struct AsyncReceiver<T> { pub struct Receiver<T> {
receiver: Rc<Receiver<T>>, receiver: Rc<mpsc::Receiver<T>>,
shared: Arc<Shared>, shared: Arc<Shared>,
} }
impl<T> AsyncReceiver<T> { impl<T> Receiver<T> {
pub async fn next(&self) -> Result<T, RecvError> { pub async fn next(&self) -> Result<T, RecvError> {
future::poll_fn(|cx| match self.receiver.try_recv() { future::poll_fn(|cx| match self.receiver.try_recv() {
Ok(event) => Poll::Ready(Ok(event)), Ok(event) => Poll::Ready(Ok(event)),
@ -102,7 +103,7 @@ impl<T> AsyncReceiver<T> {
} }
} }
impl<T> Clone for AsyncReceiver<T> { impl<T> Clone for Receiver<T> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Self { Self {
receiver: Rc::clone(&self.receiver), receiver: Rc::clone(&self.receiver),
@ -111,7 +112,7 @@ impl<T> Clone for AsyncReceiver<T> {
} }
} }
impl<T> Drop for AsyncReceiver<T> { impl<T> Drop for Receiver<T> {
fn drop(&mut self) { fn drop(&mut self) {
self.shared.closed.store(true, Ordering::Relaxed); self.shared.closed.store(true, Ordering::Relaxed);
} }

View file

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

View file

@ -1,11 +1,11 @@
use super::super::main_thread::MainThreadMarker; use super::super::main_thread::MainThreadMarker;
use super::{channel, AsyncReceiver, AsyncSender, Wrapper}; use super::{channel, Receiver, Sender, Wrapper};
use std::{ use std::{
cell::Ref, cell::Ref,
sync::{Arc, Condvar, Mutex}, sync::{Arc, Condvar, Mutex},
}; };
pub struct Dispatcher<T: 'static>(Wrapper<true, T, AsyncSender<Closure<T>>, Closure<T>>); pub struct Dispatcher<T: 'static>(Wrapper<true, T, Sender<Closure<T>>, Closure<T>>);
struct Closure<T>(Box<dyn FnOnce(&T) + Send>); struct Closure<T>(Box<dyn FnOnce(&T) + Send>);
@ -85,8 +85,8 @@ impl<T> Dispatcher<T> {
} }
pub struct DispatchRunner<T: 'static> { pub struct DispatchRunner<T: 'static> {
wrapper: Wrapper<true, T, AsyncSender<Closure<T>>, Closure<T>>, wrapper: Wrapper<true, T, Sender<Closure<T>>, Closure<T>>,
receiver: AsyncReceiver<Closure<T>>, receiver: Receiver<Closure<T>>,
} }
impl<T> DispatchRunner<T> { impl<T> DispatchRunner<T> {

View file

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

View file

@ -0,0 +1,78 @@
use std::future::Future;
use std::pin::Pin;
use std::sync::Arc;
use std::sync::OnceLock;
use std::task::Context;
use std::task::Poll;
use std::task::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")
}
self.0.queue.close();
while let Ok(waker) = self.0.queue.pop() {
waker.wake()
}
}
pub fn notified(&self) -> Notified<T> {
Notified(Some(Arc::clone(&self.0)))
}
}
#[derive(Clone, Debug)]
pub struct Notified<T: Clone>(Option<Arc<Inner<T>>>);
impl<T: Clone> Future for Notified<T> {
type Output = 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")
}
}
}
let (Ok(Some(value)) | Err(Some(value))) = Arc::try_unwrap(this)
.map(|mut inner| inner.value.take())
.map_err(|this| this.value.get().cloned())
else {
unreachable!("found no value despite being ready")
};
Poll::Ready(value)
}
}
#[derive(Debug)]
struct Inner<T> {
queue: ConcurrentQueue<Waker>,
value: OnceLock<T>,
}

View file

@ -1,11 +1,11 @@
use super::super::main_thread::MainThreadMarker;
use super::Wrapper;
use atomic_waker::AtomicWaker;
use std::future; use std::future;
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
use std::sync::Arc; use std::sync::Arc;
use std::task::Poll; use std::task::Poll;
use super::super::main_thread::MainThreadMarker;
use super::{AtomicWaker, Wrapper};
pub struct WakerSpawner<T: 'static>(Wrapper<false, Handler<T>, Sender, usize>); pub struct WakerSpawner<T: 'static>(Wrapper<false, Handler<T>, Sender, usize>);
pub struct Waker<T: 'static>(Wrapper<false, Handler<T>, Sender, usize>); pub struct Waker<T: 'static>(Wrapper<false, Handler<T>, Sender, usize>);

View file

@ -1,28 +1,31 @@
use super::backend::Style; use std::cell::RefCell;
use super::event_loop::runner::{EventWrapper, WeakShared}; use std::future::{self, Future};
use super::main_thread::{MainThreadMarker, MainThreadSafe}; use std::hash::{Hash, Hasher};
use super::EventLoopWindowTarget; use std::mem;
use crate::cursor::{BadImage, Cursor, CursorImage}; 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 cursor_icon::CursorIcon;
use std::ops::Deref; use js_sys::{Array, Object};
use std::sync::Weak; use wasm_bindgen::prelude::wasm_bindgen;
use std::{
cell::RefCell,
future,
hash::{Hash, Hasher},
mem,
ops::DerefMut,
rc::Rc,
sync::Arc,
task::{Poll, Waker},
};
use wasm_bindgen::{closure::Closure, JsCast}; use wasm_bindgen::{closure::Closure, JsCast};
use wasm_bindgen_futures::JsFuture; use wasm_bindgen_futures::JsFuture;
use web_sys::{ use web_sys::{
Blob, Document, HtmlCanvasElement, HtmlImageElement, ImageBitmap, ImageBitmapOptions, Blob, Document, DomException, HtmlCanvasElement, HtmlImageElement, ImageBitmap,
ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window, ImageBitmapOptions, ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window,
}; };
use super::backend::Style;
use super::main_thread::{MainThreadMarker, MainThreadSafe};
use super::r#async::{AbortHandle, Abortable, DropAbortHandle, Notified, Notifier};
use super::EventLoopWindowTarget;
use crate::cursor::{BadImage, Cursor, CursorImage, CustomCursor as RootCustomCursor};
use crate::platform::web::CustomCursorError;
#[derive(Debug)] #[derive(Debug)]
pub(crate) enum CustomCursorBuilder { pub(crate) enum CustomCursorBuilder {
Image(CursorImage), Image(CursorImage),
@ -31,6 +34,10 @@ pub(crate) enum CustomCursorBuilder {
hotspot_x: u16, hotspot_x: u16,
hotspot_y: u16, hotspot_y: u16,
}, },
Animation {
duration: Duration,
cursors: Vec<RootCustomCursor>,
},
} }
impl CustomCursorBuilder { impl CustomCursorBuilder {
@ -48,196 +55,392 @@ impl CustomCursorBuilder {
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct CustomCursor(Arc<MainThreadSafe<RefCell<ImageState>>>); pub struct CustomCursor {
pub(crate) animation: bool,
state: Arc<MainThreadSafe<RefCell<ImageState>>>,
}
impl Hash for CustomCursor { impl Hash for CustomCursor {
fn hash<H: Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
Arc::as_ptr(&self.0).hash(state); Arc::as_ptr(&self.state).hash(state);
} }
} }
impl PartialEq for CustomCursor { impl PartialEq for CustomCursor {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0) Arc::ptr_eq(&self.state, &other.state)
} }
} }
impl Eq for CustomCursor {} impl Eq for CustomCursor {}
impl CustomCursor { impl CustomCursor {
fn new(main_thread: MainThreadMarker) -> Self {
Self(Arc::new(MainThreadSafe::new(
main_thread,
RefCell::new(ImageState::Loading(None)),
)))
}
pub(crate) fn build<T>( pub(crate) fn build<T>(
builder: CustomCursorBuilder, builder: CustomCursorBuilder,
window_target: &EventLoopWindowTarget<T>, window_target: &EventLoopWindowTarget<T>,
) -> Self { ) -> Self {
let main_thread = window_target.runner.main_thread();
match builder { match builder {
CustomCursorBuilder::Image(image) => ImageState::from_rgba( CustomCursorBuilder::Image(image) => Self::build_spawn(
main_thread, window_target,
from_rgba(
window_target.runner.window(), window_target.runner.window(),
window_target.runner.document().clone(), window_target.runner.document().clone(),
&image, &image,
), ),
false,
),
CustomCursorBuilder::Url { CustomCursorBuilder::Url {
url, url,
hotspot_x, hotspot_x,
hotspot_y, hotspot_y,
} => ImageState::from_url(main_thread, url, hotspot_x, hotspot_y), } => Self::build_spawn(
window_target,
from_url(UrlType::Plain(url), hotspot_x, hotspot_y),
false,
),
CustomCursorBuilder::Animation { duration, cursors } => Self::build_spawn(
window_target,
from_animation(
window_target.runner.main_thread(),
duration,
cursors.into_iter().map(|cursor| cursor.inner),
),
true,
),
}
}
fn build_spawn<T, F, S>(
window_target: &EventLoopWindowTarget<T>,
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 build_async<T>(
builder: CustomCursorBuilder,
window_target: &EventLoopWindowTarget<T>,
) -> CustomCursorFuture {
let CustomCursor { animation, state } = Self::build(builder, window_target);
let binding = state.get(window_target.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),
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub struct CursorHandler { 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));
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, main_thread: MainThreadMarker,
runner: WeakShared, canvas: HtmlCanvasElement,
style: Style, style: Style,
visible: bool, visible: bool,
cursor: SelectedCursor, cursor: SelectedCursor,
} }
impl CursorHandler { impl CursorHandler {
pub(crate) fn new(main_thread: MainThreadMarker, runner: WeakShared, style: Style) -> Self { pub(crate) fn new(
Self { main_thread: MainThreadMarker,
canvas: HtmlCanvasElement,
style: Style,
) -> Self {
Self(Rc::new(RefCell::new(Inner {
main_thread, main_thread,
runner, canvas,
style, style,
visible: true, visible: true,
cursor: SelectedCursor::default(), cursor: SelectedCursor::default(),
} })))
} }
pub fn set_cursor(&mut self, cursor: Cursor) { pub fn set_cursor(&self, cursor: Cursor) {
let mut this = self.0.borrow_mut();
match cursor { match cursor {
Cursor::Icon(icon) => { Cursor::Icon(icon) => {
if let SelectedCursor::Icon(old_icon) if let SelectedCursor::Icon(old_icon)
| SelectedCursor::ImageLoading { | SelectedCursor::Loading {
previous: Previous::Icon(old_icon), previous: Previous::Icon(old_icon),
.. ..
} = &self.cursor } = &this.cursor
{ {
if *old_icon == icon { if *old_icon == icon {
return; return;
} }
} }
self.cursor = SelectedCursor::Icon(icon); this.cursor = SelectedCursor::Icon(icon);
self.set_style(); this.set_style();
} }
Cursor::Custom(cursor) => { Cursor::Custom(cursor) => {
let cursor = cursor.inner; let cursor = cursor.inner;
if let SelectedCursor::ImageLoading { if let SelectedCursor::Loading {
cursor: old_cursor, .. cursor: old_cursor, ..
} }
| SelectedCursor::ImageReady(old_cursor) = &self.cursor | SelectedCursor::Image(old_cursor)
| SelectedCursor::Animation {
cursor: old_cursor, ..
} = &this.cursor
{ {
if *old_cursor == cursor { if *old_cursor == cursor {
return; return;
} }
} }
let mut image = cursor.0.get(self.main_thread).borrow_mut(); let state = cursor.state.get(this.main_thread).borrow();
match image.deref_mut() {
ImageState::Loading(state) => { match state.deref() {
*state = Some(self.runner.clone()); ImageState::Loading { notifier, .. } => {
drop(image); let notified = notifier.notified();
self.cursor = SelectedCursor::ImageLoading { 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,
previous: mem::take(&mut self.cursor).into(), previous: mem::take(&mut this.cursor).into(),
_handle: handle,
}; };
} }
ImageState::Failed => log::error!("tried to load invalid cursor"), ImageState::Failed(error) => {
ImageState::Ready { .. } => { log::error!("trying to load custom cursor that has failed to load: {error}")
drop(image); }
self.cursor = SelectedCursor::ImageReady(cursor); ImageState::Image(_) => {
self.set_style(); drop(state);
this.cursor = SelectedCursor::Image(cursor);
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,
};
this.set_style();
} }
}; };
} }
} }
} }
pub fn set_cursor_visible(&mut self, visible: bool) { pub fn set_cursor_visible(&self, visible: bool) {
if !visible && self.visible { let mut this = self.0.borrow_mut();
self.visible = false;
self.style.set("cursor", "none"); if !visible && this.visible {
} else if visible && !self.visible { this.visible = false;
self.visible = true; this.style.set("cursor", "none");
self.set_style();
if let SelectedCursor::Animation { animation, .. } = &this.cursor {
animation.0.cancel();
} }
} } else if visible && !this.visible {
this.visible = true;
pub fn handle_cursor_ready(&mut self, result: Result<CustomCursorHandle, CustomCursorHandle>) { this.set_style();
if let SelectedCursor::ImageLoading {
cursor: current_cursor,
..
} = &self.cursor
{
let current_cursor = Arc::downgrade(&current_cursor.0);
let (Ok(new_cursor) | Err(new_cursor)) = &result;
if !new_cursor.0.ptr_eq(&current_cursor) {
return;
}
let SelectedCursor::ImageLoading { cursor, previous } = mem::take(&mut self.cursor)
else {
unreachable!("found wrong state")
};
match result {
Ok(_) => {
self.cursor = SelectedCursor::ImageReady(cursor);
self.set_style();
}
Err(_) => self.cursor = previous.into(),
} }
} }
} }
impl Inner {
fn set_style(&self) { fn set_style(&self) {
if self.visible { if self.visible {
match &self.cursor { match &self.cursor {
SelectedCursor::Icon(icon) SelectedCursor::Icon(icon)
| SelectedCursor::ImageLoading { | SelectedCursor::Loading {
previous: Previous::Icon(icon), previous: Previous::Icon(icon),
.. ..
} => self.style.set("cursor", icon.name()), } => self.style.set("cursor", icon.name()),
SelectedCursor::ImageLoading { SelectedCursor::Loading {
previous: Previous::Image(cursor), previous: Previous::Image(cursor),
.. ..
} }
| SelectedCursor::ImageReady(cursor) => { | SelectedCursor::Image(cursor) => {
if let ImageState::Ready { style, .. } = match cursor.state.get(self.main_thread).borrow().deref() {
cursor.0.get(self.main_thread).borrow().deref() ImageState::Image(Image { style, .. }) => self.style.set("cursor", style),
{ _ => unreachable!("found invalid saved state"),
self.style.set("cursor", style) }
} else { }
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) => {
log::error!("custom cursor failed to load: {error}");
self.cursor = previous.into()
}
ImageState::Loading { .. } => unreachable!("notified without being ready"),
}
} }
} }
#[derive(Debug)] #[derive(Debug)]
enum SelectedCursor { enum SelectedCursor {
Icon(CursorIcon), Icon(CursorIcon),
ImageLoading { Loading {
cursor: CustomCursor, cursor: CustomCursor,
previous: Previous, previous: Previous,
_handle: DropAbortHandle,
},
Image(CustomCursor),
Animation {
cursor: CustomCursor,
animation: AnimationDropper,
}, },
ImageReady(CustomCursor),
} }
impl Default for SelectedCursor { impl Default for SelectedCursor {
@ -250,52 +453,116 @@ impl From<Previous> for SelectedCursor {
fn from(previous: Previous) -> Self { fn from(previous: Previous) -> Self {
match previous { match previous {
Previous::Icon(icon) => Self::Icon(icon), Previous::Icon(icon) => Self::Icon(icon),
Previous::Image(cursor) => Self::ImageReady(cursor), Previous::Image(cursor) => Self::Image(cursor),
Previous::Animation { cursor, animation } => Self::Animation { cursor, animation },
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
pub enum Previous { enum Previous {
Icon(CursorIcon), Icon(CursorIcon),
Image(CustomCursor), Image(CustomCursor),
Animation {
cursor: CustomCursor,
animation: AnimationDropper,
},
} }
impl From<SelectedCursor> for Previous { impl From<SelectedCursor> for Previous {
fn from(value: SelectedCursor) -> Self { fn from(value: SelectedCursor) -> Self {
match value { match value {
SelectedCursor::Icon(icon) => Self::Icon(icon), SelectedCursor::Icon(icon) => Self::Icon(icon),
SelectedCursor::ImageLoading { previous, .. } => previous, SelectedCursor::Loading { previous, .. } => previous,
SelectedCursor::ImageReady(image) => Self::Image(image), SelectedCursor::Image(image) => Self::Image(image),
SelectedCursor::Animation { cursor, animation } => {
Self::Animation { cursor, animation }
}
} }
} }
} }
#[derive(Debug)] #[derive(Debug)]
enum ImageState { enum ImageState {
Loading(Option<WeakShared>), Loading {
Failed, notifier: Notifier<Result<(), CustomCursorError>>,
Ready { _handle: DropAbortHandle,
},
Failed(CustomCursorError),
Image(Image),
Animation(Animation),
}
#[derive(Debug)]
struct Image {
style: String, style: String,
_object_url: Option<ObjectUrl>, _object_url: Option<ObjectUrl>,
_image: HtmlImageElement, _image: HtmlImageElement,
},
} }
impl ImageState { 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( fn from_rgba(
main_thread: MainThreadMarker,
window: &Window, window: &Window,
document: Document, document: Document,
image: &CursorImage, image: &CursorImage,
) -> CustomCursor { ) -> impl Future<Output = Result<Image, CustomCursorError>> {
// 1. Create an `ImageData` from the RGBA data. // 1. Create an `ImageData` from the RGBA data.
// 2. Create an `ImageBitmap` from the `ImageData`. // 2. Create an `ImageBitmap` from the `ImageData`.
// 3. Draw `ImageBitmap` on an `HTMLCanvasElement`. // 3. Draw `ImageBitmap` on an `HTMLCanvasElement`.
// 4. Create a `Blob` from the `HTMLCanvasElement`. // 4. Create a `Blob` from the `HTMLCanvasElement`.
// 5. Create an object URL from the `Blob`. // 5. Create an object URL from the `Blob`.
// 6. Decode the image on an `HTMLImageElement` from the URL. // 6. Decode the image on an `HTMLImageElement` from the URL.
// 7. Notify event loop if one is registered.
// 1. Create an `ImageData` from the RGBA data. // 1. Create an `ImageData` from the RGBA data.
// Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223 // Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223
@ -303,7 +570,6 @@ impl ImageState {
// Can't share `SharedArrayBuffer` with `ImageData`. // Can't share `SharedArrayBuffer` with `ImageData`.
let result = { let result = {
use js_sys::{Uint8Array, Uint8ClampedArray}; use js_sys::{Uint8Array, Uint8ClampedArray};
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue; use wasm_bindgen::JsValue;
#[wasm_bindgen] #[wasm_bindgen]
@ -340,10 +606,6 @@ impl ImageState {
.expect("unexpected exception in `createImageBitmap()`"), .expect("unexpected exception in `createImageBitmap()`"),
); );
let this = CustomCursor::new(main_thread);
wasm_bindgen_futures::spawn_local({
let weak = Arc::downgrade(&this.0);
let CursorImage { let CursorImage {
width, width,
height, height,
@ -352,20 +614,11 @@ impl ImageState {
.. ..
} = *image; } = *image;
async move { async move {
// Keep checking if all references are dropped between every `await` call.
if weak.strong_count() == 0 {
return;
}
let bitmap: ImageBitmap = bitmap let bitmap: ImageBitmap = bitmap
.await .await
.expect("found invalid state in `ImageData`") .expect("found invalid state in `ImageData`")
.unchecked_into(); .unchecked_into();
if weak.strong_count() == 0 {
return;
}
let canvas: HtmlCanvasElement = document let canvas: HtmlCanvasElement = document
.create_element("canvas") .create_element("canvas")
.expect("invalid tag name") .expect("invalid tag name")
@ -417,27 +670,8 @@ impl ImageState {
.await; .await;
drop(canvas); drop(canvas);
if weak.strong_count() == 0 {
return;
}
let Some(blob) = blob else { let Some(blob) = blob else {
log::error!("creating object URL from custom cursor failed"); return Err(CustomCursorError::Blob);
let Some(this) = weak.upgrade() else {
return;
};
let mut this = this.get(main_thread).borrow_mut();
let ImageState::Loading(runner) = this.deref_mut() else {
unreachable!("found invalid state");
};
let runner = runner.take();
*this = ImageState::Failed;
if let Some(runner) = runner.and_then(|weak| weak.upgrade()) {
runner.send_event(EventWrapper::CursorReady(Err(CustomCursorHandle(weak))));
}
return;
}; };
// 5. Create an object URL from the `Blob`. // 5. Create an object URL from the `Blob`.
@ -445,107 +679,122 @@ impl ImageState {
.expect("unexpected exception in `URL.createObjectURL()`"); .expect("unexpected exception in `URL.createObjectURL()`");
let url = UrlType::Object(ObjectUrl(url)); let url = UrlType::Object(ObjectUrl(url));
Self::decode(main_thread, weak, url, hotspot_x, hotspot_y).await; from_url(url, hotspot_x, hotspot_y).await
} }
});
this
} }
fn from_url( async fn from_url(
main_thread: MainThreadMarker,
url: String,
hotspot_x: u16,
hotspot_y: u16,
) -> CustomCursor {
let this = CustomCursor::new(main_thread);
wasm_bindgen_futures::spawn_local(Self::decode(
main_thread,
Arc::downgrade(&this.0),
UrlType::Plain(url),
hotspot_x,
hotspot_y,
));
this
}
async fn decode(
main_thread: MainThreadMarker,
weak: Weak<MainThreadSafe<RefCell<ImageState>>>,
url: UrlType, url: UrlType,
hotspot_x: u16, hotspot_x: u16,
hotspot_y: u16, hotspot_y: u16,
) { ) -> Result<Image, CustomCursorError> {
if weak.strong_count() == 0 {
return;
}
// 6. Decode the image on an `HTMLImageElement` from the URL. // 6. Decode the image on an `HTMLImageElement` from the URL.
let image = let image = HtmlImageElement::new().expect("unexpected exception in `new HtmlImageElement`");
HtmlImageElement::new().expect("unexpected exception in `new HtmlImageElement`");
image.set_src(url.url()); image.set_src(url.url());
let result = JsFuture::from(image.decode()).await; let result = JsFuture::from(image.decode()).await;
let Some(this) = weak.upgrade() else {
return;
};
let mut this = this.get(main_thread).borrow_mut();
let ImageState::Loading(runner) = this.deref_mut() else {
unreachable!("found invalid state");
};
let runner = runner.take();
if let Err(error) = result { if let Err(error) = result {
log::error!("decoding custom cursor failed: {error:?}"); debug_assert!(error.has_type::<DomException>());
*this = ImageState::Failed; let error: DomException = error.unchecked_into();
debug_assert_eq!(error.name(), "EncodingError");
let error = error.message();
if let Some(runner) = runner.and_then(|weak| weak.upgrade()) { return Err(CustomCursorError::Decode(error));
runner.send_event(EventWrapper::CursorReady(Err(CustomCursorHandle(weak))));
} }
return; Ok(Image {
}
*this = ImageState::Ready {
style: format!("url({}) {hotspot_x} {hotspot_y}, auto", url.url()), style: format!("url({}) {hotspot_x} {hotspot_y}, auto", url.url()),
_object_url: match url { _object_url: match url {
UrlType::Plain(_) => None, UrlType::Plain(_) => None,
UrlType::Object(object_url) => Some(object_url), UrlType::Object(object_url) => Some(object_url),
}, },
_image: image, _image: image,
})
}
#[allow(clippy::await_holding_refcell_ref)] // false-positive
async fn from_animation(
main_thread: MainThreadMarker,
duration: Duration,
cursors: impl Iterator<Item = CustomCursor> + ExactSizeIterator,
) -> Result<Animation, CustomCursorError> {
let keyframes = Array::new();
let mut images = Vec::with_capacity(cursors.len());
for cursor in cursors {
let state = cursor.state.get(main_thread).borrow();
match state.deref() {
ImageState::Loading { notifier, .. } => {
let notified = notifier.notified();
drop(state);
notified.await?;
}
ImageState::Failed(error) => return Err(error.clone()),
ImageState::Image(_) => drop(state),
ImageState::Animation(_) => unreachable!("check in `CustomCursorBuilder` failed"),
}
let state = cursor.state.get(main_thread).borrow();
let style = match state.deref() {
ImageState::Image(Image { style, .. }) => style,
_ => unreachable!("found invalid state"),
}; };
// 7. Notify event loop if one is registered. let keyframe: Keyframe = Object::new().unchecked_into();
if let Some(runner) = runner.and_then(|weak| weak.upgrade()) { keyframe.set_cursor(style);
runner.send_event(EventWrapper::CursorReady(Ok(CustomCursorHandle(weak)))); keyframes.push(&keyframe);
} drop(state);
}
images.push(cursor);
} }
#[derive(Clone)] keyframes.push(&keyframes.get(0));
pub struct CustomCursorHandle(Weak<MainThreadSafe<RefCell<ImageState>>>);
enum UrlType { let options: KeyframeAnimationOptions = Object::new().unchecked_into();
Plain(String), options.set_duration(duration.as_millis() as f64);
Object(ObjectUrl), options.set_iterations(f64::INFINITY);
Ok(Animation {
keyframes,
options,
_images: images,
})
} }
impl UrlType { #[wasm_bindgen]
fn url(&self) -> &str { extern "C" {
match &self { type CanvasAnimateExt;
UrlType::Plain(url) => url,
UrlType::Object(object_url) => &object_url.0, #[wasm_bindgen(method, js_name = animate)]
} fn animate_with_keyframe_animation_options(
} this: &CanvasAnimateExt,
} keyframes: Option<&Object>,
options: &KeyframeAnimationOptions,
) -> WebAnimation;
#[derive(Debug)] #[derive(Debug)]
struct ObjectUrl(String); type WebAnimation;
impl Drop for ObjectUrl { #[wasm_bindgen(method)]
fn drop(&mut self) { fn cancel(this: &WebAnimation);
Url::revoke_object_url(&self.0).expect("unexpected exception in `URL.revokeObjectURL()`");
} #[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);
} }

View file

@ -1,4 +1,3 @@
use super::super::cursor::CustomCursorHandle;
use super::super::main_thread::MainThreadMarker; use super::super::main_thread::MainThreadMarker;
use super::super::DeviceId; use super::super::DeviceId;
use super::{backend, state::State}; use super::{backend, state::State};
@ -140,16 +139,6 @@ impl Runner {
) )
} }
} }
EventWrapper::CursorReady(result) => {
for (_, canvas, _) in runner.0.all_canvases.borrow().deref() {
if let Some(canvas) = canvas.upgrade() {
canvas
.borrow_mut()
.cursor
.handle_cursor_ready(result.clone())
}
}
}
} }
} }
} }
@ -822,19 +811,6 @@ impl Shared {
pub(crate) fn waker(&self) -> Waker<Weak<Execution>> { pub(crate) fn waker(&self) -> Waker<Weak<Execution>> {
self.0.proxy_spawner.waker() self.0.proxy_spawner.waker()
} }
pub(crate) fn weak(&self) -> WeakShared {
WeakShared(Rc::downgrade(&self.0))
}
}
#[derive(Clone, Debug)]
pub(crate) struct WeakShared(Weak<Execution>);
impl WeakShared {
pub(crate) fn upgrade(&self) -> Option<Shared> {
self.0.upgrade().map(Shared)
}
} }
pub(crate) enum EventWrapper { pub(crate) enum EventWrapper {
@ -844,7 +820,6 @@ pub(crate) enum EventWrapper {
size: PhysicalSize<u32>, size: PhysicalSize<u32>,
scale: f64, scale: f64,
}, },
CursorReady(Result<CustomCursorHandle, CustomCursorHandle>),
} }
impl From<Event<()>> for EventWrapper { impl From<Event<()>> for EventWrapper {

View file

@ -6,7 +6,7 @@ use std::sync::OnceLock;
use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::{JsCast, JsValue}; use wasm_bindgen::{JsCast, JsValue};
use super::r#async::{self, AsyncSender}; use super::r#async::{self, Sender};
thread_local! { thread_local! {
static MAIN_THREAD: bool = { static MAIN_THREAD: bool = {
@ -85,7 +85,7 @@ impl<T> Drop for MainThreadSafe<T> {
unsafe impl<T> Send for MainThreadSafe<T> {} unsafe impl<T> Send for MainThreadSafe<T> {}
unsafe impl<T> Sync for MainThreadSafe<T> {} unsafe impl<T> Sync for MainThreadSafe<T> {}
static DROP_HANDLER: OnceLock<AsyncSender<DropBox>> = OnceLock::new(); static DROP_HANDLER: OnceLock<Sender<DropBox>> = OnceLock::new();
struct DropBox(#[allow(dead_code)] Box<dyn Any>); struct DropBox(#[allow(dead_code)] Box<dyn Any>);

View file

@ -43,3 +43,4 @@ pub(crate) use crate::icon::NoIcon as PlatformIcon;
pub(crate) use crate::platform_impl::Fullscreen; pub(crate) use crate::platform_impl::Fullscreen;
pub(crate) use cursor::CustomCursor as PlatformCustomCursor; pub(crate) use cursor::CustomCursor as PlatformCustomCursor;
pub(crate) use cursor::CustomCursorBuilder as PlatformCustomCursorBuilder; pub(crate) use cursor::CustomCursorBuilder as PlatformCustomCursorBuilder;
pub(crate) use cursor::CustomCursorFuture;

View file

@ -18,7 +18,6 @@ use crate::platform_impl::{OsError, PlatformSpecificWindowBuilderAttributes};
use crate::window::{WindowAttributes, WindowId as RootWindowId}; use crate::window::{WindowAttributes, WindowId as RootWindowId};
use super::super::cursor::CursorHandler; use super::super::cursor::CursorHandler;
use super::super::event_loop::runner::WeakShared;
use super::super::main_thread::MainThreadMarker; use super::super::main_thread::MainThreadMarker;
use super::super::WindowId; use super::super::WindowId;
use super::animation_frame::AnimationFrameHandler; use super::animation_frame::AnimationFrameHandler;
@ -71,7 +70,6 @@ pub struct Style {
impl Canvas { impl Canvas {
pub(crate) fn create( pub(crate) fn create(
main_thread: MainThreadMarker, main_thread: MainThreadMarker,
runner: WeakShared,
id: WindowId, id: WindowId,
window: web_sys::Window, window: web_sys::Window,
document: Document, document: Document,
@ -111,7 +109,7 @@ impl Canvas {
let style = Style::new(&window, &canvas); let style = Style::new(&window, &canvas);
let cursor = CursorHandler::new(main_thread, runner, style.clone()); let cursor = CursorHandler::new(main_thread, canvas.clone(), style.clone());
let common = Common { let common = Common {
window: window.clone(), window: window.clone(),

View file

@ -39,7 +39,6 @@ impl Window {
let document = target.runner.document(); let document = target.runner.document();
let canvas = backend::Canvas::create( let canvas = backend::Canvas::create(
target.runner.main_thread(), target.runner.main_thread(),
target.runner.weak(),
id, id,
window.clone(), window.clone(),
document.clone(), document.clone(),