From 27eeabcb24e3fe9c4e3cd4772fe608c737ea36ec Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 4 Jan 2024 00:31:35 +0100 Subject: [PATCH 1/7] Implement more sophisticated abortion mechanic --- Cargo.toml | 1 + src/platform_impl/web/async/abortable.rs | 82 ++++++++++++++++++ src/platform_impl/web/async/mod.rs | 2 + src/platform_impl/web/cursor.rs | 101 +++++++++++++---------- 4 files changed, 142 insertions(+), 44 deletions(-) create mode 100644 src/platform_impl/web/async/abortable.rs diff --git a/Cargo.toml b/Cargo.toml index b74698d96d..62a1bce7fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -242,6 +242,7 @@ features = [ [target.'cfg(target_family = "wasm")'.dependencies] atomic-waker = "1" js-sys = "0.3.64" +pin-project = "1" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" web-time = "0.2" diff --git a/src/platform_impl/web/async/abortable.rs b/src/platform_impl/web/async/abortable.rs new file mode 100644 index 0000000000..29b7a028f8 --- /dev/null +++ b/src/platform_impl/web/async/abortable.rs @@ -0,0 +1,82 @@ +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 atomic_waker::AtomicWaker; +use pin_project::pin_project; + +#[pin_project] +pub struct Abortable { + #[pin] + future: F, + shared: Arc, +} + +impl Abortable { + pub fn new(handle: AbortHandle, future: F) -> Self { + Self { + future, + shared: handle.0, + } + } +} + +impl Future for Abortable { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + 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); + +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(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 {} diff --git a/src/platform_impl/web/async/mod.rs b/src/platform_impl/web/async/mod.rs index b4fe698daf..f682e55048 100644 --- a/src/platform_impl/web/async/mod.rs +++ b/src/platform_impl/web/async/mod.rs @@ -1,8 +1,10 @@ +mod abortable; mod channel; mod dispatcher; mod waker; mod wrapper; +pub use self::abortable::{AbortHandle, Abortable}; pub use self::channel::{channel, AsyncReceiver, AsyncSender}; pub use self::dispatcher::{DispatchRunner, Dispatcher}; pub use self::waker::{Waker, WakerSpawner}; diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index e077429740..10e4542f6e 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -1,6 +1,7 @@ use super::backend::Style; use super::event_loop::runner::{EventWrapper, WeakShared}; use super::main_thread::{MainThreadMarker, MainThreadSafe}; +use super::r#async::{AbortHandle, Abortable}; use super::EventLoopWindowTarget; use crate::cursor::{BadImage, Cursor, CursorImage}; use cursor_icon::CursorIcon; @@ -65,13 +66,6 @@ impl PartialEq for CustomCursor { impl Eq for 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( builder: CustomCursorBuilder, window_target: &EventLoopWindowTarget, @@ -146,8 +140,8 @@ impl CursorHandler { let mut image = cursor.0.get(self.main_thread).borrow_mut(); match image.deref_mut() { - ImageState::Loading(state) => { - *state = Some(self.runner.clone()); + ImageState::Loading { runner, .. } => { + *runner = Some(self.runner.clone()); drop(image); self.cursor = SelectedCursor::ImageLoading { cursor, @@ -273,7 +267,10 @@ impl From for Previous { #[derive(Debug)] enum ImageState { - Loading(Option), + Loading { + runner: Option, + handle: AbortHandle, + }, Failed, Ready { style: String, @@ -340,9 +337,16 @@ impl ImageState { .expect("unexpected exception in `createImageBitmap()`"), ); - let this = CustomCursor::new(main_thread); + let handle = AbortHandle::new(); + let this = CustomCursor(Arc::new(MainThreadSafe::new( + main_thread, + RefCell::new(ImageState::Loading { + runner: None, + handle: handle.clone(), + }), + ))); - wasm_bindgen_futures::spawn_local({ + let task = Abortable::new(handle, { let weak = Arc::downgrade(&this.0); let CursorImage { width, @@ -352,20 +356,11 @@ impl ImageState { .. } = *image; async move { - // Keep checking if all references are dropped between every `await` call. - if weak.strong_count() == 0 { - return; - } - let bitmap: ImageBitmap = bitmap .await .expect("found invalid state in `ImageData`") .unchecked_into(); - if weak.strong_count() == 0 { - return; - } - let canvas: HtmlCanvasElement = document .create_element("canvas") .expect("invalid tag name") @@ -417,17 +412,13 @@ impl ImageState { .await; drop(canvas); - if weak.strong_count() == 0 { - return; - } - let Some(blob) = blob else { log::error!("creating object URL from custom cursor failed"); - let Some(this) = weak.upgrade() else { - return; - }; + let this = weak + .upgrade() + .expect("`CursorHandler` invalidated without aborting"); let mut this = this.get(main_thread).borrow_mut(); - let ImageState::Loading(runner) = this.deref_mut() else { + let ImageState::Loading { runner, .. } = this.deref_mut() else { unreachable!("found invalid state"); }; let runner = runner.take(); @@ -449,6 +440,10 @@ impl ImageState { } }); + wasm_bindgen_futures::spawn_local(async move { + let _ = task.await; + }); + this } @@ -458,14 +453,28 @@ impl ImageState { hotspot_x: u16, hotspot_y: u16, ) -> CustomCursor { - let this = CustomCursor::new(main_thread); - wasm_bindgen_futures::spawn_local(Self::decode( + let handle = AbortHandle::new(); + let this = CustomCursor(Arc::new(MainThreadSafe::new( main_thread, - Arc::downgrade(&this.0), - UrlType::Plain(url), - hotspot_x, - hotspot_y, - )); + RefCell::new(ImageState::Loading { + runner: None, + handle: handle.clone(), + }), + ))); + + let task = Abortable::new( + handle, + Self::decode( + main_thread, + Arc::downgrade(&this.0), + UrlType::Plain(url), + hotspot_x, + hotspot_y, + ), + ); + wasm_bindgen_futures::spawn_local(async move { + let _ = task.await; + }); this } @@ -477,22 +486,18 @@ impl ImageState { hotspot_x: u16, hotspot_y: u16, ) { - if weak.strong_count() == 0 { - return; - } - // 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; - let Some(this) = weak.upgrade() else { - return; - }; + let this = weak + .upgrade() + .expect("`CursorHandler` invalidated without aborting"); let mut this = this.get(main_thread).borrow_mut(); - let ImageState::Loading(runner) = this.deref_mut() else { + let ImageState::Loading { runner, .. } = this.deref_mut() else { unreachable!("found invalid state"); }; let runner = runner.take(); @@ -524,6 +529,14 @@ impl ImageState { } } +impl Drop for ImageState { + fn drop(&mut self) { + if let Self::Loading { handle, .. } = self { + handle.abort(); + } + } +} + #[derive(Clone)] pub struct CustomCursorHandle(Weak>>); From c1e305b4e52358d38178d82de58cb6b863257a80 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Fri, 5 Jan 2024 14:43:26 +0100 Subject: [PATCH 2/7] Rename `Async(Receiver|Sender)` to `Receiver|Sender` --- src/platform_impl/web/async/channel.rs | 26 +++++++++++------------ src/platform_impl/web/async/dispatcher.rs | 8 +++---- src/platform_impl/web/async/mod.rs | 2 +- src/platform_impl/web/main_thread.rs | 4 ++-- 4 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/platform_impl/web/async/channel.rs b/src/platform_impl/web/async/channel.rs index 0592a20822..fcd8a7b81e 100644 --- a/src/platform_impl/web/async/channel.rs +++ b/src/platform_impl/web/async/channel.rs @@ -2,22 +2,22 @@ use atomic_waker::AtomicWaker; use std::future; use std::rc::Rc; 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::task::Poll; -pub fn channel() -> (AsyncSender, AsyncReceiver) { +pub fn channel() -> (Sender, Receiver) { let (sender, receiver) = mpsc::channel(); let shared = Arc::new(Shared { closed: AtomicBool::new(false), waker: AtomicWaker::new(), }); - let sender = AsyncSender(Arc::new(SenderInner { + let sender = Sender(Arc::new(SenderInner { sender: Mutex::new(sender), shared: Arc::clone(&shared), })); - let receiver = AsyncReceiver { + let receiver = Receiver { receiver: Rc::new(receiver), shared, }; @@ -25,18 +25,18 @@ pub fn channel() -> (AsyncSender, AsyncReceiver) { (sender, receiver) } -pub struct AsyncSender(Arc>); +pub struct Sender(Arc>); struct SenderInner { // 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 // to wrap `Sender` in an `Arc` to make it clonable on the main thread without // having to block. - sender: Mutex>, + sender: Mutex>, shared: Arc, } -impl AsyncSender { +impl Sender { pub fn send(&self, event: T) -> Result<(), SendError> { self.0.sender.lock().unwrap().send(event)?; self.0.shared.waker.wake(); @@ -52,7 +52,7 @@ impl SenderInner { } } -impl Clone for AsyncSender { +impl Clone for Sender { fn clone(&self) -> Self { Self(Arc::clone(&self.0)) } @@ -64,12 +64,12 @@ impl Drop for SenderInner { } } -pub struct AsyncReceiver { - receiver: Rc>, +pub struct Receiver { + receiver: Rc>, shared: Arc, } -impl AsyncReceiver { +impl Receiver { pub async fn next(&self) -> Result { future::poll_fn(|cx| match self.receiver.try_recv() { Ok(event) => Poll::Ready(Ok(event)), @@ -102,7 +102,7 @@ impl AsyncReceiver { } } -impl Clone for AsyncReceiver { +impl Clone for Receiver { fn clone(&self) -> Self { Self { receiver: Rc::clone(&self.receiver), @@ -111,7 +111,7 @@ impl Clone for AsyncReceiver { } } -impl Drop for AsyncReceiver { +impl Drop for Receiver { fn drop(&mut self) { self.shared.closed.store(true, Ordering::Relaxed); } diff --git a/src/platform_impl/web/async/dispatcher.rs b/src/platform_impl/web/async/dispatcher.rs index 9ec9b6a365..f7307d9f3a 100644 --- a/src/platform_impl/web/async/dispatcher.rs +++ b/src/platform_impl/web/async/dispatcher.rs @@ -1,11 +1,11 @@ use super::super::main_thread::MainThreadMarker; -use super::{channel, AsyncReceiver, AsyncSender, Wrapper}; +use super::{channel, Receiver, Sender, Wrapper}; use std::{ cell::Ref, sync::{Arc, Condvar, Mutex}, }; -pub struct Dispatcher(Wrapper>, Closure>); +pub struct Dispatcher(Wrapper>, Closure>); struct Closure(Box); @@ -85,8 +85,8 @@ impl Dispatcher { } pub struct DispatchRunner { - wrapper: Wrapper>, Closure>, - receiver: AsyncReceiver>, + wrapper: Wrapper>, Closure>, + receiver: Receiver>, } impl DispatchRunner { diff --git a/src/platform_impl/web/async/mod.rs b/src/platform_impl/web/async/mod.rs index f682e55048..7dff1f6e0f 100644 --- a/src/platform_impl/web/async/mod.rs +++ b/src/platform_impl/web/async/mod.rs @@ -5,7 +5,7 @@ mod waker; mod wrapper; pub use self::abortable::{AbortHandle, Abortable}; -pub use self::channel::{channel, AsyncReceiver, AsyncSender}; +pub use self::channel::{channel, Receiver, Sender}; pub use self::dispatcher::{DispatchRunner, Dispatcher}; pub use self::waker::{Waker, WakerSpawner}; use self::wrapper::Wrapper; diff --git a/src/platform_impl/web/main_thread.rs b/src/platform_impl/web/main_thread.rs index 140a1e9e17..59a2ac59f8 100644 --- a/src/platform_impl/web/main_thread.rs +++ b/src/platform_impl/web/main_thread.rs @@ -6,7 +6,7 @@ use std::sync::OnceLock; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::{JsCast, JsValue}; -use super::r#async::{self, AsyncSender}; +use super::r#async::{self, Sender}; thread_local! { static MAIN_THREAD: bool = { @@ -85,7 +85,7 @@ impl Drop for MainThreadSafe { unsafe impl Send for MainThreadSafe {} unsafe impl Sync for MainThreadSafe {} -static DROP_HANDLER: OnceLock> = OnceLock::new(); +static DROP_HANDLER: OnceLock> = OnceLock::new(); struct DropBox(#[allow(dead_code)] Box); From 3ea3ab306955893485d1aaa3606e0c3f757cb6c2 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Sat, 6 Jan 2024 01:32:30 +0100 Subject: [PATCH 3/7] Switch to async-based custom cursor notification --- Cargo.toml | 1 + examples/custom_cursors.rs | 19 ++ src/platform_impl/web/async/abortable.rs | 19 ++ src/platform_impl/web/async/mod.rs | 4 +- src/platform_impl/web/async/notifier.rs | 76 ++++++++ src/platform_impl/web/cursor.rs | 206 ++++++++++----------- src/platform_impl/web/event_loop/runner.rs | 25 --- src/platform_impl/web/web_sys/canvas.rs | 4 +- src/platform_impl/web/window.rs | 1 - 9 files changed, 222 insertions(+), 133 deletions(-) create mode 100644 src/platform_impl/web/async/notifier.rs diff --git a/Cargo.toml b/Cargo.toml index 62a1bce7fb..4ddbb0c3fc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -241,6 +241,7 @@ features = [ [target.'cfg(target_family = "wasm")'.dependencies] atomic-waker = "1" +concurrent-queue = { version = "2", default-features = false } js-sys = "0.3.64" pin-project = "1" wasm-bindgen = "0.2" diff --git a/examples/custom_cursors.rs b/examples/custom_cursors.rs index 4c47f362d7..268d8c335f 100644 --- a/examples/custom_cursors.rs +++ b/examples/custom_cursors.rs @@ -74,6 +74,25 @@ fn main() -> Result<(), impl std::error::Error> { log::debug!("Setting cursor visibility to {:?}", cursor_visible); window.set_cursor_visible(cursor_visible); } + #[cfg(wasm_platform)] + Key::Character("4") => { + use std::sync::atomic::{AtomicU64, Ordering}; + use winit::platform::web::CustomCursorExtWebSys; + static COUNTER: AtomicU64 = AtomicU64::new(0); + + 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), + ); + } _ => {} }, WindowEvent::RedrawRequested => { diff --git a/src/platform_impl/web/async/abortable.rs b/src/platform_impl/web/async/abortable.rs index 29b7a028f8..5894b90be8 100644 --- a/src/platform_impl/web/async/abortable.rs +++ b/src/platform_impl/web/async/abortable.rs @@ -70,6 +70,25 @@ impl AbortHandle { } } +#[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; diff --git a/src/platform_impl/web/async/mod.rs b/src/platform_impl/web/async/mod.rs index 7dff1f6e0f..a6d52d822c 100644 --- a/src/platform_impl/web/async/mod.rs +++ b/src/platform_impl/web/async/mod.rs @@ -1,11 +1,13 @@ mod abortable; mod channel; mod dispatcher; +mod notifier; mod waker; mod wrapper; -pub use self::abortable::{AbortHandle, Abortable}; +pub use self::abortable::{AbortHandle, Abortable, DropAbortHandle}; pub use self::channel::{channel, Receiver, Sender}; pub use self::dispatcher::{DispatchRunner, Dispatcher}; +pub use self::notifier::Notifier; pub use self::waker::{Waker, WakerSpawner}; use self::wrapper::Wrapper; diff --git a/src/platform_impl/web/async/notifier.rs b/src/platform_impl/web/async/notifier.rs new file mode 100644 index 0000000000..b189000968 --- /dev/null +++ b/src/platform_impl/web/async/notifier.rs @@ -0,0 +1,76 @@ +use std::future::Future; +use std::pin::Pin; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::task::Context; +use std::task::Poll; +use std::task::Waker; + +use concurrent_queue::ConcurrentQueue; +use concurrent_queue::PushError; + +#[derive(Debug)] +pub struct Notifier(Arc); + +impl Notifier { + pub fn new() -> Self { + Self(Arc::new(Inner { + queue: ConcurrentQueue::unbounded(), + ready: AtomicBool::new(false), + })) + } + + pub fn notify(self) { + self.0.ready.store(true, Ordering::Relaxed); + + self.0.queue.close(); + + while let Ok(waker) = self.0.queue.pop() { + waker.wake() + } + } + + pub fn notified(&self) -> Notified { + Notified(Some(Arc::clone(&self.0))) + } +} + +#[derive(Clone)] +pub struct Notified(Option>); + +impl Future for Notified { + type Output = (); + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.0.take().expect("`Receiver` polled after completion"); + + if this.ready.load(Ordering::Relaxed) { + return Poll::Ready(()); + } + + match this.queue.push(cx.waker().clone()) { + Ok(()) => { + if this.ready.load(Ordering::Relaxed) { + return Poll::Ready(()); + } + + self.0 = Some(this); + Poll::Pending + } + Err(PushError::Closed(_)) => { + debug_assert!(this.ready.load(Ordering::Relaxed)); + Poll::Ready(()) + } + Err(PushError::Full(_)) => { + unreachable!("found full queue despite using unbounded queue") + } + } + } +} + +#[derive(Debug)] +struct Inner { + queue: ConcurrentQueue, + ready: AtomicBool, +} diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index 10e4542f6e..1b9d02d31b 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -1,7 +1,6 @@ use super::backend::Style; -use super::event_loop::runner::{EventWrapper, WeakShared}; use super::main_thread::{MainThreadMarker, MainThreadSafe}; -use super::r#async::{AbortHandle, Abortable}; +use super::r#async::{AbortHandle, Abortable, DropAbortHandle, Notifier}; use super::EventLoopWindowTarget; use crate::cursor::{BadImage, Cursor, CursorImage}; use cursor_icon::CursorIcon; @@ -89,41 +88,44 @@ impl CustomCursor { } #[derive(Debug)] -pub struct CursorHandler { +pub struct CursorHandler(Rc>); + +#[derive(Debug)] +struct Inner { main_thread: MainThreadMarker, - runner: WeakShared, style: Style, visible: bool, cursor: SelectedCursor, } impl CursorHandler { - pub(crate) fn new(main_thread: MainThreadMarker, runner: WeakShared, style: Style) -> Self { - Self { + pub(crate) fn new(main_thread: MainThreadMarker, style: Style) -> Self { + Self(Rc::new(RefCell::new(Inner { main_thread, - runner, style, visible: true, 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 { Cursor::Icon(icon) => { if let SelectedCursor::Icon(old_icon) | SelectedCursor::ImageLoading { previous: Previous::Icon(old_icon), .. - } = &self.cursor + } = &this.cursor { if *old_icon == icon { return; } } - self.cursor = SelectedCursor::Icon(icon); - self.set_style(); + this.cursor = SelectedCursor::Icon(icon); + this.set_style(); } Cursor::Custom(cursor) => { let cursor = cursor.inner; @@ -131,73 +133,65 @@ impl CursorHandler { if let SelectedCursor::ImageLoading { cursor: old_cursor, .. } - | SelectedCursor::ImageReady(old_cursor) = &self.cursor + | SelectedCursor::ImageReady(old_cursor) = &this.cursor { if *old_cursor == cursor { return; } } - let mut image = cursor.0.get(self.main_thread).borrow_mut(); - match image.deref_mut() { - ImageState::Loading { runner, .. } => { - *runner = Some(self.runner.clone()); - drop(image); - self.cursor = SelectedCursor::ImageLoading { + let state = cursor.0.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 { + 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::ImageLoading { 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::Ready { .. } => { - drop(image); - self.cursor = SelectedCursor::ImageReady(cursor); - self.set_style(); + drop(state); + this.cursor = SelectedCursor::ImageReady(cursor); + this.set_style(); } }; } } } - pub fn set_cursor_visible(&mut self, visible: bool) { - if !visible && self.visible { - self.visible = false; - self.style.set("cursor", "none"); - } else if visible && !self.visible { - self.visible = true; - self.set_style(); - } - } - - pub fn handle_cursor_ready(&mut self, result: Result) { - if let SelectedCursor::ImageLoading { - cursor: current_cursor, - .. - } = &self.cursor - { - let current_cursor = Arc::downgrade(¤t_cursor.0); - - let (Ok(new_cursor) | Err(new_cursor)) = &result; - - if !new_cursor.0.ptr_eq(¤t_cursor) { - return; - } + pub fn set_cursor_visible(&self, visible: bool) { + let mut this = self.0.borrow_mut(); - 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(), - } + if !visible && this.visible { + this.visible = false; + this.style.set("cursor", "none"); + } else if visible && !this.visible { + this.visible = true; + this.set_style(); } } +} +impl Inner { fn set_style(&self) { if self.visible { match &self.cursor { @@ -222,6 +216,26 @@ impl CursorHandler { } } } + + fn notify(&mut self) { + let SelectedCursor::ImageLoading { + cursor, previous, .. + } = mem::take(&mut self.cursor) + else { + unreachable!("found wrong state") + }; + + let state = cursor.0.get(self.main_thread).borrow(); + match state.deref() { + ImageState::Failed => self.cursor = previous.into(), + ImageState::Ready { .. } => { + drop(state); + self.cursor = SelectedCursor::ImageReady(cursor); + self.set_style(); + } + ImageState::Loading { .. } => unreachable!("notified without being ready"), + } + } } #[derive(Debug)] @@ -230,6 +244,7 @@ enum SelectedCursor { ImageLoading { cursor: CustomCursor, previous: Previous, + _handle: DropAbortHandle, }, ImageReady(CustomCursor), } @@ -268,8 +283,8 @@ impl From for Previous { #[derive(Debug)] enum ImageState { Loading { - runner: Option, - handle: AbortHandle, + notifier: Notifier, + _handle: DropAbortHandle, }, Failed, Ready { @@ -292,7 +307,7 @@ impl ImageState { // 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. - // 7. Notify event loop if one is registered. + // 7. Notify listeners. // 1. Create an `ImageData` from the RGBA data. // Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223 @@ -341,8 +356,8 @@ impl ImageState { let this = CustomCursor(Arc::new(MainThreadSafe::new( main_thread, RefCell::new(ImageState::Loading { - runner: None, - handle: handle.clone(), + notifier: Notifier::new(), + _handle: DropAbortHandle::new(handle.clone()), }), ))); @@ -418,15 +433,12 @@ impl ImageState { .upgrade() .expect("`CursorHandler` invalidated without aborting"); let mut this = this.get(main_thread).borrow_mut(); - let ImageState::Loading { runner, .. } = this.deref_mut() else { + let ImageState::Loading { notifier, .. } = + mem::replace(this.deref_mut(), ImageState::Failed) + 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)))); - } + notifier.notify(); return; }; @@ -457,8 +469,8 @@ impl ImageState { let this = CustomCursor(Arc::new(MainThreadSafe::new( main_thread, RefCell::new(ImageState::Loading { - runner: None, - handle: handle.clone(), + notifier: Notifier::new(), + _handle: DropAbortHandle::new(handle.clone()), }), ))); @@ -497,49 +509,37 @@ impl ImageState { .expect("`CursorHandler` invalidated without aborting"); 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 { log::error!("decoding custom cursor failed: {error:?}"); - *this = ImageState::Failed; - - if let Some(runner) = runner.and_then(|weak| weak.upgrade()) { - runner.send_event(EventWrapper::CursorReady(Err(CustomCursorHandle(weak)))); - } + let ImageState::Loading { notifier, .. } = + mem::replace(this.deref_mut(), ImageState::Failed) + else { + unreachable!("found invalid state"); + }; + notifier.notify(); return; } - *this = ImageState::Ready { - style: format!("url({}) {hotspot_x} {hotspot_y}, auto", url.url()), - _object_url: match url { - UrlType::Plain(_) => None, - UrlType::Object(object_url) => Some(object_url), + let ImageState::Loading { notifier, .. } = mem::replace( + this.deref_mut(), + ImageState::Ready { + 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, }, - _image: image, + ) else { + unreachable!("found invalid state"); }; - // 7. Notify event loop if one is registered. - if let Some(runner) = runner.and_then(|weak| weak.upgrade()) { - runner.send_event(EventWrapper::CursorReady(Ok(CustomCursorHandle(weak)))); - } - } -} - -impl Drop for ImageState { - fn drop(&mut self) { - if let Self::Loading { handle, .. } = self { - handle.abort(); - } + // 7. Notify listeners. + notifier.notify(); } } -#[derive(Clone)] -pub struct CustomCursorHandle(Weak>>); - enum UrlType { Plain(String), Object(ObjectUrl), diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index 43e8c4a21d..f7932ff76e 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -1,4 +1,3 @@ -use super::super::cursor::CustomCursorHandle; use super::super::main_thread::MainThreadMarker; use super::super::DeviceId; 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> { self.0.proxy_spawner.waker() } - - pub(crate) fn weak(&self) -> WeakShared { - WeakShared(Rc::downgrade(&self.0)) - } -} - -#[derive(Clone, Debug)] -pub(crate) struct WeakShared(Weak); - -impl WeakShared { - pub(crate) fn upgrade(&self) -> Option { - self.0.upgrade().map(Shared) - } } pub(crate) enum EventWrapper { @@ -844,7 +820,6 @@ pub(crate) enum EventWrapper { size: PhysicalSize, scale: f64, }, - CursorReady(Result), } impl From> for EventWrapper { diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 195af6fe88..6a9adc66c9 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -18,7 +18,6 @@ use crate::platform_impl::{OsError, PlatformSpecificWindowBuilderAttributes}; use crate::window::{WindowAttributes, WindowId as RootWindowId}; use super::super::cursor::CursorHandler; -use super::super::event_loop::runner::WeakShared; use super::super::main_thread::MainThreadMarker; use super::super::WindowId; use super::animation_frame::AnimationFrameHandler; @@ -71,7 +70,6 @@ pub struct Style { impl Canvas { pub(crate) fn create( main_thread: MainThreadMarker, - runner: WeakShared, id: WindowId, window: web_sys::Window, document: Document, @@ -111,7 +109,7 @@ impl Canvas { let style = Style::new(&window, &canvas); - let cursor = CursorHandler::new(main_thread, runner, style.clone()); + let cursor = CursorHandler::new(main_thread, style.clone()); let common = Common { window: window.clone(), diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 1e82f21f3d..d391687f80 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -39,7 +39,6 @@ impl Window { let document = target.runner.document(); let canvas = backend::Canvas::create( target.runner.main_thread(), - target.runner.weak(), id, window.clone(), document.clone(), From d514161792ad77b332ae22aa152f95fe5474fd18 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Wed, 10 Jan 2024 14:31:20 +0100 Subject: [PATCH 4/7] Disable dependencies for single-threaded Wasm --- Cargo.toml | 6 +- src/platform_impl/web/async/abortable.rs | 3 +- src/platform_impl/web/async/atomic_waker.rs | 35 ++++++++++++ src/platform_impl/web/async/channel.rs | 3 +- .../web/async/concurrent_queue.rs | 55 +++++++++++++++++++ src/platform_impl/web/async/mod.rs | 6 ++ src/platform_impl/web/async/notifier.rs | 3 +- src/platform_impl/web/async/waker.rs | 6 +- 8 files changed, 108 insertions(+), 9 deletions(-) create mode 100644 src/platform_impl/web/async/atomic_waker.rs create mode 100644 src/platform_impl/web/async/concurrent_queue.rs diff --git a/Cargo.toml b/Cargo.toml index 4ddbb0c3fc..d383b5ad28 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -240,14 +240,16 @@ features = [ ] [target.'cfg(target_family = "wasm")'.dependencies] -atomic-waker = "1" -concurrent-queue = { version = "2", default-features = false } js-sys = "0.3.64" pin-project = "1" wasm-bindgen = "0.2" wasm-bindgen-futures = "0.4" 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] console_log = "1" web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] } diff --git a/src/platform_impl/web/async/abortable.rs b/src/platform_impl/web/async/abortable.rs index 5894b90be8..2608fc4046 100644 --- a/src/platform_impl/web/async/abortable.rs +++ b/src/platform_impl/web/async/abortable.rs @@ -6,9 +6,10 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Arc; use std::task::{Context, Poll}; -use atomic_waker::AtomicWaker; use pin_project::pin_project; +use super::AtomicWaker; + #[pin_project] pub struct Abortable { #[pin] diff --git a/src/platform_impl/web/async/atomic_waker.rs b/src/platform_impl/web/async/atomic_waker.rs new file mode 100644 index 0000000000..87b836121e --- /dev/null +++ b/src/platform_impl/web/async/atomic_waker.rs @@ -0,0 +1,35 @@ +use std::cell::RefCell; +use std::ops::Deref; +use std::task::Waker; + +#[derive(Debug)] +pub struct AtomicWaker(RefCell>); + +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 {} diff --git a/src/platform_impl/web/async/channel.rs b/src/platform_impl/web/async/channel.rs index fcd8a7b81e..6b313f32e7 100644 --- a/src/platform_impl/web/async/channel.rs +++ b/src/platform_impl/web/async/channel.rs @@ -1,4 +1,3 @@ -use atomic_waker::AtomicWaker; use std::future; use std::rc::Rc; use std::sync::atomic::{AtomicBool, Ordering}; @@ -6,6 +5,8 @@ use std::sync::mpsc::{self, RecvError, SendError, TryRecvError}; use std::sync::{Arc, Mutex}; use std::task::Poll; +use super::AtomicWaker; + pub fn channel() -> (Sender, Receiver) { let (sender, receiver) = mpsc::channel(); let shared = Arc::new(Shared { diff --git a/src/platform_impl/web/async/concurrent_queue.rs b/src/platform_impl/web/async/concurrent_queue.rs new file mode 100644 index 0000000000..2d7efacd50 --- /dev/null +++ b/src/platform_impl/web/async/concurrent_queue.rs @@ -0,0 +1,55 @@ +use std::cell::{Cell, RefCell}; + +#[derive(Debug)] +pub struct ConcurrentQueue { + queue: RefCell>, + closed: Cell, +} + +pub enum PushError { + #[allow(dead_code)] + Full(T), + Closed(T), +} + +pub enum PopError { + Empty, + Closed, +} + +impl ConcurrentQueue { + pub fn unbounded() -> Self { + Self { + queue: RefCell::new(Vec::new()), + closed: Cell::new(false), + } + } + + pub fn push(&self, value: T) -> Result<(), PushError> { + if self.closed.get() { + return Err(PushError::Closed(value)); + } + + self.queue.borrow_mut().push(value); + Ok(()) + } + + pub fn pop(&self) -> Result { + 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 Send for ConcurrentQueue {} +// SAFETY: Wasm without the `atomics` target feature is single-threaded. +unsafe impl Sync for ConcurrentQueue {} diff --git a/src/platform_impl/web/async/mod.rs b/src/platform_impl/web/async/mod.rs index a6d52d822c..1e21eb18aa 100644 --- a/src/platform_impl/web/async/mod.rs +++ b/src/platform_impl/web/async/mod.rs @@ -1,5 +1,9 @@ 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 waker; @@ -11,3 +15,5 @@ pub use self::dispatcher::{DispatchRunner, Dispatcher}; pub use self::notifier::Notifier; pub use self::waker::{Waker, WakerSpawner}; use self::wrapper::Wrapper; +use atomic_waker::AtomicWaker; +use concurrent_queue::{ConcurrentQueue, PushError}; diff --git a/src/platform_impl/web/async/notifier.rs b/src/platform_impl/web/async/notifier.rs index b189000968..a73e7b1c50 100644 --- a/src/platform_impl/web/async/notifier.rs +++ b/src/platform_impl/web/async/notifier.rs @@ -7,8 +7,7 @@ use std::task::Context; use std::task::Poll; use std::task::Waker; -use concurrent_queue::ConcurrentQueue; -use concurrent_queue::PushError; +use super::{ConcurrentQueue, PushError}; #[derive(Debug)] pub struct Notifier(Arc); diff --git a/src/platform_impl/web/async/waker.rs b/src/platform_impl/web/async/waker.rs index 79062d8456..fd8733af97 100644 --- a/src/platform_impl/web/async/waker.rs +++ b/src/platform_impl/web/async/waker.rs @@ -1,11 +1,11 @@ -use super::super::main_thread::MainThreadMarker; -use super::Wrapper; -use atomic_waker::AtomicWaker; use std::future; use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering}; use std::sync::Arc; use std::task::Poll; +use super::super::main_thread::MainThreadMarker; +use super::{AtomicWaker, Wrapper}; + pub struct WakerSpawner(Wrapper, Sender, usize>); pub struct Waker(Wrapper, Sender, usize>); From b31ad7723b6038cd61109c07cb81785e0fec9c8b Mon Sep 17 00:00:00 2001 From: daxpedda Date: Wed, 10 Jan 2024 16:24:29 +0100 Subject: [PATCH 5/7] Add `CustomCursorBuilder::build_async()` --- Cargo.toml | 1 + src/platform/web.rs | 65 +++++++++++++++-- src/platform_impl/web/async/mod.rs | 2 +- src/platform_impl/web/async/notifier.rs | 67 +++++++++--------- src/platform_impl/web/cursor.rs | 94 ++++++++++++++++++++----- src/platform_impl/web/mod.rs | 1 + 6 files changed, 171 insertions(+), 59 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index d383b5ad28..c2075c9792 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -205,6 +205,7 @@ features = [ 'console', 'CssStyleDeclaration', 'Document', + 'DomException', 'DomRect', 'DomRectReadOnly', 'Element', diff --git a/src/platform/web.rs b/src/platform/web.rs index f24eb51287..78260d5d69 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -27,17 +27,22 @@ //! [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border //! [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding -use crate::cursor::CustomCursorBuilder; -use crate::event::Event; -use crate::event_loop::EventLoop; -use crate::event_loop::EventLoopWindowTarget; -use crate::platform_impl::PlatformCustomCursorBuilder; -use crate::window::CustomCursor; -use crate::window::{Window, WindowBuilder}; +use std::fmt::{self, Display, Formatter}; +use std::future::Future; +use std::pin::Pin; +use std::task::{Context, Poll}; #[cfg(wasm_platform)] 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))] #[doc(hidden)] pub struct HtmlCanvasElement; @@ -253,3 +258,49 @@ impl CustomCursorExtWebSys for CustomCursor { } } } + +pub trait CustomCursorBuilderExtWebSys { + /// Async version of [`CustomCursorBuilder::build()`] which waits until the + /// cursor has completely finished loading. + fn build_async(self, window_target: &EventLoopWindowTarget) -> CustomCursorFuture; +} + +impl CustomCursorBuilderExtWebSys for CustomCursorBuilder { + fn build_async(self, window_target: &EventLoopWindowTarget) -> 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; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.0) + .poll(cx) + .map_ok(|cursor| CustomCursor { inner: cursor }) + } +} + +#[derive(Clone, Debug)] +pub enum CustomCursorError { + Blob, + Decode(String), +} + +impl Display for CustomCursorError { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { + match self { + CustomCursorError::Blob => write!(f, "failed to create `Blob`"), + CustomCursorError::Decode(error) => write!(f, "failed to decode image: {error}"), + } + } +} diff --git a/src/platform_impl/web/async/mod.rs b/src/platform_impl/web/async/mod.rs index 1e21eb18aa..4681cf9240 100644 --- a/src/platform_impl/web/async/mod.rs +++ b/src/platform_impl/web/async/mod.rs @@ -12,7 +12,7 @@ mod wrapper; pub use self::abortable::{AbortHandle, Abortable, DropAbortHandle}; pub use self::channel::{channel, Receiver, Sender}; pub use self::dispatcher::{DispatchRunner, Dispatcher}; -pub use self::notifier::Notifier; +pub use self::notifier::{Notified, Notifier}; pub use self::waker::{Waker, WakerSpawner}; use self::wrapper::Wrapper; use atomic_waker::AtomicWaker; diff --git a/src/platform_impl/web/async/notifier.rs b/src/platform_impl/web/async/notifier.rs index a73e7b1c50..6387b7e5da 100644 --- a/src/platform_impl/web/async/notifier.rs +++ b/src/platform_impl/web/async/notifier.rs @@ -1,8 +1,7 @@ use std::future::Future; use std::pin::Pin; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; use std::sync::Arc; +use std::sync::OnceLock; use std::task::Context; use std::task::Poll; use std::task::Waker; @@ -10,18 +9,20 @@ use std::task::Waker; use super::{ConcurrentQueue, PushError}; #[derive(Debug)] -pub struct Notifier(Arc); +pub struct Notifier(Arc>); -impl Notifier { +impl Notifier { pub fn new() -> Self { Self(Arc::new(Inner { queue: ConcurrentQueue::unbounded(), - ready: AtomicBool::new(false), + value: OnceLock::new(), })) } - pub fn notify(self) { - self.0.ready.store(true, Ordering::Relaxed); + pub fn notify(self, value: T) { + if self.0.value.set(value).is_err() { + unreachable!("value set before") + } self.0.queue.close(); @@ -30,46 +31,48 @@ impl Notifier { } } - pub fn notified(&self) -> Notified { + pub fn notified(&self) -> Notified { Notified(Some(Arc::clone(&self.0))) } } -#[derive(Clone)] -pub struct Notified(Option>); +#[derive(Clone, Debug)] +pub struct Notified(Option>>); -impl Future for Notified { - type Output = (); +impl Future for Notified { + type Output = T; fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.0.take().expect("`Receiver` polled after completion"); - if this.ready.load(Ordering::Relaxed) { - return Poll::Ready(()); - } - - match this.queue.push(cx.waker().clone()) { - Ok(()) => { - if this.ready.load(Ordering::Relaxed) { - return Poll::Ready(()); + 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") } - - self.0 = Some(this); - Poll::Pending - } - Err(PushError::Closed(_)) => { - debug_assert!(this.ready.load(Ordering::Relaxed)); - Poll::Ready(()) - } - 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 { +struct Inner { queue: ConcurrentQueue, - ready: AtomicBool, + value: OnceLock, } diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index 1b9d02d31b..e67740bbbd 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -1,11 +1,15 @@ use super::backend::Style; use super::main_thread::{MainThreadMarker, MainThreadSafe}; -use super::r#async::{AbortHandle, Abortable, DropAbortHandle, Notifier}; +use super::r#async::{AbortHandle, Abortable, DropAbortHandle, Notified, Notifier}; use super::EventLoopWindowTarget; use crate::cursor::{BadImage, Cursor, CursorImage}; +use crate::platform::web::CustomCursorError; use cursor_icon::CursorIcon; +use std::future::Future; use std::ops::Deref; +use std::pin::Pin; use std::sync::Weak; +use std::task::{ready, Context}; use std::{ cell::RefCell, future, @@ -19,8 +23,8 @@ use std::{ use wasm_bindgen::{closure::Closure, JsCast}; use wasm_bindgen_futures::JsFuture; use web_sys::{ - Blob, Document, HtmlCanvasElement, HtmlImageElement, ImageBitmap, ImageBitmapOptions, - ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window, + Blob, Document, DomException, HtmlCanvasElement, HtmlImageElement, ImageBitmap, + ImageBitmapOptions, ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window, }; #[derive(Debug)] @@ -85,6 +89,48 @@ impl CustomCursor { } => ImageState::from_url(main_thread, url, hotspot_x, hotspot_y), } } + + pub(crate) fn build_async( + builder: CustomCursorBuilder, + window_target: &EventLoopWindowTarget, + ) -> CustomCursorFuture { + let state = Self::build(builder, window_target).0; + let binding = state.get(window_target.runner.main_thread()).borrow(); + let ImageState::Loading { notifier, .. } = binding.deref() else { + unreachable!("found invalid state") + }; + let notify = notifier.notified(); + drop(binding); + + CustomCursorFuture { + notify, + state: Some(state), + } + } +} + +#[derive(Debug)] +pub struct CustomCursorFuture { + notify: Notified>, + state: Option>>>, +} + +impl Future for CustomCursorFuture { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + if self.state.is_none() { + panic!("`CustomCursorFuture` polled after completion") + } + + let result = ready!(Pin::new(&mut self.notify).poll(cx)); + let state = self + .state + .take() + .expect("`CustomCursorFuture` polled after completion"); + + Poll::Ready(result.map(|_| CustomCursor(state))) + } } #[derive(Debug)] @@ -149,7 +195,7 @@ impl CursorHandler { let task = Abortable::new(handle.handle(), { let weak = Rc::downgrade(&self.0); async move { - notified.await; + let _ = notified.await; let handler = weak .upgrade() .expect("`CursorHandler` invalidated without aborting"); @@ -167,7 +213,9 @@ impl CursorHandler { _handle: handle, }; } - ImageState::Failed => log::error!("tried to load invalid cursor"), + ImageState::Failed(error) => { + log::error!("trying to load custom cursor that has failed to load: {error}") + } ImageState::Ready { .. } => { drop(state); this.cursor = SelectedCursor::ImageReady(cursor); @@ -227,7 +275,10 @@ impl Inner { let state = cursor.0.get(self.main_thread).borrow(); match state.deref() { - ImageState::Failed => self.cursor = previous.into(), + ImageState::Failed(error) => { + log::error!("custom cursor failed to load: {error}"); + self.cursor = previous.into() + } ImageState::Ready { .. } => { drop(state); self.cursor = SelectedCursor::ImageReady(cursor); @@ -283,10 +334,10 @@ impl From for Previous { #[derive(Debug)] enum ImageState { Loading { - notifier: Notifier, + notifier: Notifier>, _handle: DropAbortHandle, }, - Failed, + Failed(CustomCursorError), Ready { style: String, _object_url: Option, @@ -428,17 +479,17 @@ impl ImageState { drop(canvas); let Some(blob) = blob else { - log::error!("creating object URL from custom cursor failed"); let this = weak .upgrade() .expect("`CursorHandler` invalidated without aborting"); let mut this = this.get(main_thread).borrow_mut(); - let ImageState::Loading { notifier, .. } = - mem::replace(this.deref_mut(), ImageState::Failed) - else { + let ImageState::Loading { notifier, .. } = mem::replace( + this.deref_mut(), + ImageState::Failed(CustomCursorError::Blob), + ) else { unreachable!("found invalid state"); }; - notifier.notify(); + notifier.notify(Err(CustomCursorError::Blob)); return; }; @@ -510,13 +561,18 @@ impl ImageState { let mut this = this.get(main_thread).borrow_mut(); if let Err(error) = result { - log::error!("decoding custom cursor failed: {error:?}"); - let ImageState::Loading { notifier, .. } = - mem::replace(this.deref_mut(), ImageState::Failed) - else { + debug_assert!(error.has_type::()); + let error: DomException = error.unchecked_into(); + debug_assert_eq!(error.name(), "EncodingError"); + let error = error.message(); + + let ImageState::Loading { notifier, .. } = mem::replace( + this.deref_mut(), + ImageState::Failed(CustomCursorError::Decode(error.clone())), + ) else { unreachable!("found invalid state"); }; - notifier.notify(); + notifier.notify(Err(CustomCursorError::Decode(error))); return; } @@ -536,7 +592,7 @@ impl ImageState { }; // 7. Notify listeners. - notifier.notify(); + notifier.notify(Ok(())); } } diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs index dc4b2a50a8..3d23351efe 100644 --- a/src/platform_impl/web/mod.rs +++ b/src/platform_impl/web/mod.rs @@ -43,3 +43,4 @@ pub(crate) use crate::icon::NoIcon as PlatformIcon; pub(crate) use crate::platform_impl::Fullscreen; pub(crate) use cursor::CustomCursor as PlatformCustomCursor; pub(crate) use cursor::CustomCursorBuilder as PlatformCustomCursorBuilder; +pub(crate) use cursor::CustomCursorFuture; From 4df01ffbf7847a02b8e0f5c4a0cb127d352466a4 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 11 Jan 2024 14:23:19 +0100 Subject: [PATCH 6/7] Refactor to avoid code duplication --- src/platform_impl/web/cursor.rs | 540 +++++++++++++++----------------- 1 file changed, 252 insertions(+), 288 deletions(-) diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index e67740bbbd..5923e072d6 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -1,25 +1,14 @@ -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}; -use crate::platform::web::CustomCursorError; -use cursor_icon::CursorIcon; -use std::future::Future; -use std::ops::Deref; +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::sync::Weak; -use std::task::{ready, Context}; -use std::{ - cell::RefCell, - future, - hash::{Hash, Hasher}, - mem, - ops::DerefMut, - rc::Rc, - sync::Arc, - task::{Poll, Waker}, -}; +use std::rc::Rc; +use std::sync::Arc; +use std::task::{ready, Context, Poll, Waker}; + +use cursor_icon::CursorIcon; use wasm_bindgen::{closure::Closure, JsCast}; use wasm_bindgen_futures::JsFuture; use web_sys::{ @@ -27,6 +16,13 @@ use web_sys::{ 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}; +use crate::platform::web::CustomCursorError; + #[derive(Debug)] pub(crate) enum CustomCursorBuilder { Image(CursorImage), @@ -73,23 +69,78 @@ impl CustomCursor { builder: CustomCursorBuilder, window_target: &EventLoopWindowTarget, ) -> Self { - let main_thread = window_target.runner.main_thread(); - match builder { - CustomCursorBuilder::Image(image) => ImageState::from_rgba( - main_thread, - window_target.runner.window(), - window_target.runner.document().clone(), - &image, + CustomCursorBuilder::Image(image) => Self::build_spawn( + window_target, + from_rgba( + window_target.runner.window(), + window_target.runner.document().clone(), + &image, + ), ), CustomCursorBuilder::Url { url, hotspot_x, 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), + ), } } + fn build_spawn>>( + window_target: &EventLoopWindowTarget, + task: F, + ) -> CustomCursor { + let handle = AbortHandle::new(); + let this = CustomCursor(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.0); + 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(image) => { + let ImageState::Loading { notifier, .. } = + mem::replace(this.deref_mut(), ImageState::Ready(image)) + 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( builder: CustomCursorBuilder, window_target: &EventLoopWindowTarget, @@ -99,11 +150,11 @@ impl CustomCursor { let ImageState::Loading { notifier, .. } = binding.deref() else { unreachable!("found invalid state") }; - let notify = notifier.notified(); + let notified = notifier.notified(); drop(binding); CustomCursorFuture { - notify, + notified, state: Some(state), } } @@ -111,7 +162,7 @@ impl CustomCursor { #[derive(Debug)] pub struct CustomCursorFuture { - notify: Notified>, + notified: Notified>, state: Option>>>, } @@ -123,7 +174,7 @@ impl Future for CustomCursorFuture { panic!("`CustomCursorFuture` polled after completion") } - let result = ready!(Pin::new(&mut self.notify).poll(cx)); + let result = ready!(Pin::new(&mut self.notified).poll(cx)); let state = self .state .take() @@ -253,7 +304,7 @@ impl Inner { .. } | SelectedCursor::ImageReady(cursor) => { - if let ImageState::Ready { style, .. } = + if let ImageState::Ready(Image { style, .. }) = cursor.0.get(self.main_thread).borrow().deref() { self.style.set("cursor", style) @@ -338,262 +389,14 @@ enum ImageState { _handle: DropAbortHandle, }, Failed(CustomCursorError), - Ready { - style: String, - _object_url: Option, - _image: HtmlImageElement, - }, + Ready(Image), } -impl ImageState { - fn from_rgba( - main_thread: MainThreadMarker, - window: &Window, - document: Document, - image: &CursorImage, - ) -> CustomCursor { - // 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. - // 7. Notify listeners. - - // 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::prelude::wasm_bindgen; - 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; - } - - let array = Uint8Array::new_with_length(image.rgba.len() as u32); - array.copy_from(&image.rgba); - 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.rgba), - 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 mut options = ImageBitmapOptions::new(); - options.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 handle = AbortHandle::new(); - let this = CustomCursor(Arc::new(MainThreadSafe::new( - main_thread, - RefCell::new(ImageState::Loading { - notifier: Notifier::new(), - _handle: DropAbortHandle::new(handle.clone()), - }), - ))); - - let task = Abortable::new(handle, { - let weak = Arc::downgrade(&this.0); - let CursorImage { - width, - height, - hotspot_x, - hotspot_y, - .. - } = *image; - 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::>::new(None)); - let callback = Closure::once({ - let value = value.clone(); - let waker = waker.clone(); - move |blob: Option| { - *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 { - let this = weak - .upgrade() - .expect("`CursorHandler` invalidated without aborting"); - let mut this = this.get(main_thread).borrow_mut(); - let ImageState::Loading { notifier, .. } = mem::replace( - this.deref_mut(), - ImageState::Failed(CustomCursorError::Blob), - ) else { - unreachable!("found invalid state"); - }; - notifier.notify(Err(CustomCursorError::Blob)); - - return; - }; - - // 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)); - - Self::decode(main_thread, weak, url, hotspot_x, hotspot_y).await; - } - }); - - wasm_bindgen_futures::spawn_local(async move { - let _ = task.await; - }); - - this - } - - fn from_url( - main_thread: MainThreadMarker, - url: String, - hotspot_x: u16, - hotspot_y: u16, - ) -> CustomCursor { - let handle = AbortHandle::new(); - let this = CustomCursor(Arc::new(MainThreadSafe::new( - main_thread, - RefCell::new(ImageState::Loading { - notifier: Notifier::new(), - _handle: DropAbortHandle::new(handle.clone()), - }), - ))); - - let task = Abortable::new( - handle, - Self::decode( - main_thread, - Arc::downgrade(&this.0), - UrlType::Plain(url), - hotspot_x, - hotspot_y, - ), - ); - wasm_bindgen_futures::spawn_local(async move { - let _ = task.await; - }); - - this - } - - async fn decode( - main_thread: MainThreadMarker, - weak: Weak>>, - url: UrlType, - hotspot_x: u16, - hotspot_y: u16, - ) { - // 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; - - let this = weak - .upgrade() - .expect("`CursorHandler` invalidated without aborting"); - let mut this = this.get(main_thread).borrow_mut(); - - if let Err(error) = result { - debug_assert!(error.has_type::()); - let error: DomException = error.unchecked_into(); - debug_assert_eq!(error.name(), "EncodingError"); - let error = error.message(); - - let ImageState::Loading { notifier, .. } = mem::replace( - this.deref_mut(), - ImageState::Failed(CustomCursorError::Decode(error.clone())), - ) else { - unreachable!("found invalid state"); - }; - notifier.notify(Err(CustomCursorError::Decode(error))); - - return; - } - - let ImageState::Loading { notifier, .. } = mem::replace( - this.deref_mut(), - ImageState::Ready { - 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, - }, - ) else { - unreachable!("found invalid state"); - }; - - // 7. Notify listeners. - notifier.notify(Ok(())); - } +#[derive(Debug)] +struct Image { + style: String, + _object_url: Option, + _image: HtmlImageElement, } enum UrlType { @@ -618,3 +421,164 @@ impl Drop for ObjectUrl { Url::revoke_object_url(&self.0).expect("unexpected exception in `URL.revokeObjectURL()`"); } } + +fn from_rgba( + window: &Window, + document: Document, + image: &CursorImage, +) -> impl Future> { + // 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::prelude::wasm_bindgen; + 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; + } + + let array = Uint8Array::new_with_length(image.rgba.len() as u32); + array.copy_from(&image.rgba); + 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.rgba), + 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 mut options = ImageBitmapOptions::new(); + options.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 CursorImage { + width, + height, + hotspot_x, + hotspot_y, + .. + } = *image; + 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::>::new(None)); + let callback = Closure::once({ + let value = value.clone(); + let waker = waker.clone(); + move |blob: Option| { + *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 { + // 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::()); + 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, + }) +} From d8d3243434f337756f3754a4256aaf5cd4292dbc Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 11 Jan 2024 23:03:57 +0100 Subject: [PATCH 7/7] Add `CustomCursor::from_animation()` --- CHANGELOG.md | 1 + examples/custom_cursors.rs | 37 ++- src/platform/web.rs | 62 ++++- src/platform_impl/web/cursor.rs | 316 ++++++++++++++++++++---- src/platform_impl/web/web_sys/canvas.rs | 2 +- 5 files changed, 361 insertions(+), 57 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a1f683eb84..f6475e3d64 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Unreleased` header. - Add `CustomCursor` - 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_animation` to allow creating animated cursors from other `CustomCursor`s. - On macOS, add services menu. - **Breaking:** On Web, remove queuing fullscreen request in absence of transient activation. - On Web, fix setting cursor icon overriding cursor visibility. diff --git a/examples/custom_cursors.rs b/examples/custom_cursors.rs index 268d8c335f..6f4cbb824d 100644 --- a/examples/custom_cursors.rs +++ b/examples/custom_cursors.rs @@ -8,6 +8,15 @@ use winit::{ keyboard::Key, 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(bytes: &[u8], window_target: &EventLoopWindowTarget) -> CustomCursor { let img = image::load_from_memory(bytes).unwrap().to_rgba8(); @@ -76,10 +85,6 @@ fn main() -> Result<(), impl std::error::Error> { } #[cfg(wasm_platform)] Key::Character("4") => { - use std::sync::atomic::{AtomicU64, Ordering}; - use winit::platform::web::CustomCursorExtWebSys; - static COUNTER: AtomicU64 = AtomicU64::new(0); - log::debug!("Setting cursor to a random image from an URL"); window.set_cursor( CustomCursor::from_url( @@ -93,6 +98,30 @@ fn main() -> Result<(), impl std::error::Error> { .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 => { diff --git a/src/platform/web.rs b/src/platform/web.rs index 78260d5d69..b1f030c45d 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -27,10 +27,12 @@ //! [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border //! [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding +use std::error::Error; use std::fmt::{self, Display, Formatter}; use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; +use std::time::Duration; #[cfg(wasm_platform)] use web_sys::HtmlCanvasElement; @@ -239,15 +241,29 @@ pub enum PollStrategy { } 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. /// 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. /// /// [PNG]: https://en.wikipedia.org/wiki/PNG 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, + ) -> Result; } impl CustomCursorExtWebSys for CustomCursor { + fn is_animation(&self) -> bool { + self.inner.animation + } + fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder { CustomCursorBuilder { inner: PlatformCustomCursorBuilder::Url { @@ -257,8 +273,45 @@ impl CustomCursorExtWebSys for CustomCursor { }, } } + + fn from_animation( + duration: Duration, + cursors: Vec, + ) -> Result { + 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. @@ -294,13 +347,18 @@ impl Future for CustomCursorFuture { pub enum CustomCursorError { Blob, Decode(String), + Animation, } impl Display for CustomCursorError { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { - CustomCursorError::Blob => write!(f, "failed to create `Blob`"), - CustomCursorError::Decode(error) => write!(f, "failed to decode image: {error}"), + 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" + ), } } } diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index 5923e072d6..8881c8cd33 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -7,8 +7,11 @@ 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::prelude::wasm_bindgen; use wasm_bindgen::{closure::Closure, JsCast}; use wasm_bindgen_futures::JsFuture; use web_sys::{ @@ -20,7 +23,7 @@ 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}; +use crate::cursor::{BadImage, Cursor, CursorImage, CustomCursor as RootCustomCursor}; use crate::platform::web::CustomCursorError; #[derive(Debug)] @@ -31,6 +34,10 @@ pub(crate) enum CustomCursorBuilder { hotspot_x: u16, hotspot_y: u16, }, + Animation { + duration: Duration, + cursors: Vec, + }, } impl CustomCursorBuilder { @@ -48,17 +55,20 @@ impl CustomCursorBuilder { } #[derive(Clone, Debug)] -pub struct CustomCursor(Arc>>); +pub struct CustomCursor { + pub(crate) animation: bool, + state: Arc>>, +} impl Hash for CustomCursor { fn hash(&self, state: &mut H) { - Arc::as_ptr(&self.0).hash(state); + Arc::as_ptr(&self.state).hash(state); } } impl PartialEq for CustomCursor { fn eq(&self, other: &Self) -> bool { - Arc::ptr_eq(&self.0, &other.0) + Arc::ptr_eq(&self.state, &other.state) } } @@ -77,6 +87,7 @@ impl CustomCursor { window_target.runner.document().clone(), &image, ), + false, ), CustomCursorBuilder::Url { url, @@ -85,23 +96,41 @@ impl CustomCursor { } => 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>>( + fn build_spawn( window_target: &EventLoopWindowTarget, task: F, - ) -> CustomCursor { + animation: bool, + ) -> CustomCursor + where + F: 'static + Future>, + S: Into, + { let handle = AbortHandle::new(); - let this = CustomCursor(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.0); + 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, { @@ -114,9 +143,9 @@ impl CustomCursor { let mut this = this.get(main_thread).borrow_mut(); match result { - Ok(image) => { + Ok(new_state) => { let ImageState::Loading { notifier, .. } = - mem::replace(this.deref_mut(), ImageState::Ready(image)) + mem::replace(this.deref_mut(), new_state.into()) else { unreachable!("found invalid state"); }; @@ -145,7 +174,7 @@ impl CustomCursor { builder: CustomCursorBuilder, window_target: &EventLoopWindowTarget, ) -> CustomCursorFuture { - let state = Self::build(builder, window_target).0; + 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") @@ -155,6 +184,7 @@ impl CustomCursor { CustomCursorFuture { notified, + animation, state: Some(state), } } @@ -163,6 +193,7 @@ impl CustomCursor { #[derive(Debug)] pub struct CustomCursorFuture { notified: Notified>, + animation: bool, state: Option>>>, } @@ -180,7 +211,10 @@ impl Future for CustomCursorFuture { .take() .expect("`CustomCursorFuture` polled after completion"); - Poll::Ready(result.map(|_| CustomCursor(state))) + Poll::Ready(result.map(|_| CustomCursor { + animation: self.animation, + state, + })) } } @@ -190,15 +224,21 @@ pub struct CursorHandler(Rc>); #[derive(Debug)] struct Inner { main_thread: MainThreadMarker, + canvas: HtmlCanvasElement, style: Style, visible: bool, cursor: SelectedCursor, } impl CursorHandler { - pub(crate) fn new(main_thread: MainThreadMarker, style: Style) -> Self { + 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(), @@ -211,7 +251,7 @@ impl CursorHandler { match cursor { Cursor::Icon(icon) => { if let SelectedCursor::Icon(old_icon) - | SelectedCursor::ImageLoading { + | SelectedCursor::Loading { previous: Previous::Icon(old_icon), .. } = &this.cursor @@ -227,17 +267,20 @@ impl CursorHandler { Cursor::Custom(cursor) => { let cursor = cursor.inner; - if let SelectedCursor::ImageLoading { + if let SelectedCursor::Loading { cursor: old_cursor, .. } - | SelectedCursor::ImageReady(old_cursor) = &this.cursor + | SelectedCursor::Image(old_cursor) + | SelectedCursor::Animation { + cursor: old_cursor, .. + } = &this.cursor { if *old_cursor == cursor { return; } } - let state = cursor.0.get(this.main_thread).borrow(); + let state = cursor.state.get(this.main_thread).borrow(); match state.deref() { ImageState::Loading { notifier, .. } => { @@ -258,7 +301,7 @@ impl CursorHandler { }); drop(state); - this.cursor = SelectedCursor::ImageLoading { + this.cursor = SelectedCursor::Loading { cursor, previous: mem::take(&mut this.cursor).into(), _handle: handle, @@ -267,9 +310,27 @@ impl CursorHandler { ImageState::Failed(error) => { log::error!("trying to load custom cursor that has failed to load: {error}") } - ImageState::Ready { .. } => { + ImageState::Image(_) => { + 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); - this.cursor = SelectedCursor::ImageReady(cursor); + + if !this.visible { + animation.cancel(); + } + + this.cursor = SelectedCursor::Animation { + animation: AnimationDropper(animation), + cursor, + }; this.set_style(); } }; @@ -283,6 +344,10 @@ impl CursorHandler { 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(); @@ -295,46 +360,69 @@ impl Inner { if self.visible { match &self.cursor { SelectedCursor::Icon(icon) - | SelectedCursor::ImageLoading { + | SelectedCursor::Loading { previous: Previous::Icon(icon), .. } => self.style.set("cursor", icon.name()), - SelectedCursor::ImageLoading { + SelectedCursor::Loading { previous: Previous::Image(cursor), .. } - | SelectedCursor::ImageReady(cursor) => { - if let ImageState::Ready(Image { style, .. }) = - cursor.0.get(self.main_thread).borrow().deref() - { - self.style.set("cursor", style) - } else { - unreachable!("found invalid saved state") + | 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::ImageLoading { + let SelectedCursor::Loading { cursor, previous, .. } = mem::take(&mut self.cursor) else { unreachable!("found wrong state") }; - let state = cursor.0.get(self.main_thread).borrow(); + let state = cursor.state.get(self.main_thread).borrow(); match state.deref() { - ImageState::Failed(error) => { - log::error!("custom cursor failed to load: {error}"); - self.cursor = previous.into() + ImageState::Image(_) => { + drop(state); + self.cursor = SelectedCursor::Image(cursor); + self.set_style(); } - ImageState::Ready { .. } => { + 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); - self.cursor = SelectedCursor::ImageReady(cursor); + + 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"), } } @@ -343,12 +431,16 @@ impl Inner { #[derive(Debug)] enum SelectedCursor { Icon(CursorIcon), - ImageLoading { + Loading { cursor: CustomCursor, previous: Previous, _handle: DropAbortHandle, }, - ImageReady(CustomCursor), + Image(CustomCursor), + Animation { + cursor: CustomCursor, + animation: AnimationDropper, + }, } impl Default for SelectedCursor { @@ -361,23 +453,31 @@ impl From for SelectedCursor { fn from(previous: Previous) -> Self { match previous { 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)] -pub enum Previous { +enum Previous { Icon(CursorIcon), Image(CustomCursor), + Animation { + cursor: CustomCursor, + animation: AnimationDropper, + }, } impl From for Previous { fn from(value: SelectedCursor) -> Self { match value { SelectedCursor::Icon(icon) => Self::Icon(icon), - SelectedCursor::ImageLoading { previous, .. } => previous, - SelectedCursor::ImageReady(image) => Self::Image(image), + SelectedCursor::Loading { previous, .. } => previous, + SelectedCursor::Image(image) => Self::Image(image), + SelectedCursor::Animation { cursor, animation } => { + Self::Animation { cursor, animation } + } } } } @@ -389,7 +489,8 @@ enum ImageState { _handle: DropAbortHandle, }, Failed(CustomCursorError), - Ready(Image), + Image(Image), + Animation(Animation), } #[derive(Debug)] @@ -399,6 +500,26 @@ struct Image { _image: HtmlImageElement, } +impl From for ImageState { + fn from(image: Image) -> Self { + Self::Image(image) + } +} + +#[derive(Debug)] +struct Animation { + keyframes: Array, + options: KeyframeAnimationOptions, + _images: Vec, +} + +impl From for ImageState { + fn from(animation: Animation) -> Self { + Self::Animation(animation) + } +} + +#[derive(Debug)] enum UrlType { Plain(String), Object(ObjectUrl), @@ -422,6 +543,15 @@ impl Drop for ObjectUrl { } } +#[derive(Debug)] +struct AnimationDropper(WebAnimation); + +impl Drop for AnimationDropper { + fn drop(&mut self) { + self.0.cancel() + } +} + fn from_rgba( window: &Window, document: Document, @@ -440,7 +570,6 @@ fn from_rgba( // Can't share `SharedArrayBuffer` with `ImageData`. let result = { use js_sys::{Uint8Array, Uint8ClampedArray}; - use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; #[wasm_bindgen] @@ -582,3 +711,90 @@ async fn from_url( _image: image, }) } + +#[allow(clippy::await_holding_refcell_ref)] // false-positive +async fn from_animation( + main_thread: MainThreadMarker, + duration: Duration, + cursors: impl Iterator + ExactSizeIterator, +) -> Result { + 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"), + }; + + let keyframe: Keyframe = Object::new().unchecked_into(); + keyframe.set_cursor(style); + keyframes.push(&keyframe); + drop(state); + + images.push(cursor); + } + + 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); +} diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 6a9adc66c9..bbd59419e5 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -109,7 +109,7 @@ impl Canvas { let style = Style::new(&window, &canvas); - let cursor = CursorHandler::new(main_thread, style.clone()); + let cursor = CursorHandler::new(main_thread, canvas.clone(), style.clone()); let common = Common { window: window.clone(),