diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index 7287c25f..e0774297 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -1,16 +1,21 @@ +use super::backend::Style; +use super::event_loop::runner::{EventWrapper, WeakShared}; +use super::main_thread::{MainThreadMarker, MainThreadSafe}; +use super::EventLoopWindowTarget; +use crate::cursor::{BadImage, Cursor, CursorImage}; +use cursor_icon::CursorIcon; +use std::ops::Deref; +use std::sync::Weak; use std::{ cell::RefCell, future, hash::{Hash, Hasher}, mem, ops::DerefMut, - rc::{self, Rc}, - sync::{self, Arc}, + rc::Rc, + sync::Arc, task::{Poll, Waker}, }; - -use crate::cursor::{BadImage, Cursor, CursorImage}; -use cursor_icon::CursorIcon; use wasm_bindgen::{closure::Closure, JsCast}; use wasm_bindgen_futures::JsFuture; use web_sys::{ @@ -18,10 +23,6 @@ use web_sys::{ ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window, }; -use super::backend::Style; -use super::main_thread::{MainThreadMarker, MainThreadSafe}; -use super::EventLoopWindowTarget; - #[derive(Debug)] pub(crate) enum CustomCursorBuilder { Image(CursorImage), @@ -94,136 +95,176 @@ impl CustomCursor { } #[derive(Debug)] -pub struct CursorState(Rc>); - -impl CursorState { - pub fn new(main_thread: MainThreadMarker, style: Style) -> Self { - Self(Rc::new(RefCell::new(State { - main_thread, - style, - visible: true, - cursor: SelectedCursor::default(), - }))) - } - - pub fn set_cursor(&self, cursor: Cursor) { - let mut this = self.0.borrow_mut(); - - match cursor { - Cursor::Icon(icon) => { - if let SelectedCursor::ImageLoading { state, .. } = &this.cursor { - if let ImageState::Loading(state) = - state.0.get(this.main_thread).borrow_mut().deref_mut() - { - state.take(); - } - } - - this.cursor = SelectedCursor::Named(icon); - this.set_style(); - } - Cursor::Custom(cursor) => match cursor - .inner - .0 - .get(this.main_thread) - .borrow_mut() - .deref_mut() - { - ImageState::Loading(state) => { - this.cursor = SelectedCursor::ImageLoading { - state: cursor.inner.clone(), - previous: mem::take(&mut this.cursor).into(), - }; - *state = Some(Rc::downgrade(&self.0)); - } - ImageState::Failed => log::error!("tried to load invalid cursor"), - ImageState::Ready(image) => { - this.cursor = SelectedCursor::ImageReady(image.clone()); - this.set_style(); - } - }, - } - } - - pub fn set_cursor_visible(&self, visible: bool) { - let mut state = self.0.borrow_mut(); - - if !visible && state.visible { - state.visible = false; - state.style.set("cursor", "none"); - } else if visible && !state.visible { - state.visible = true; - state.set_style(); - } - } -} - -#[derive(Debug)] -struct State { +pub struct CursorHandler { main_thread: MainThreadMarker, + runner: WeakShared, style: Style, visible: bool, cursor: SelectedCursor, } -impl State { - pub fn set_style(&self) { - if self.visible { - let value = match &self.cursor { - SelectedCursor::Named(icon) => icon.name(), - SelectedCursor::ImageLoading { previous, .. } => previous.style(), - SelectedCursor::ImageReady(image) => &image.style, +impl CursorHandler { + pub(crate) fn new(main_thread: MainThreadMarker, runner: WeakShared, style: Style) -> Self { + Self { + main_thread, + runner, + style, + visible: true, + cursor: SelectedCursor::default(), + } + } + + pub fn set_cursor(&mut self, cursor: Cursor) { + match cursor { + Cursor::Icon(icon) => { + if let SelectedCursor::Icon(old_icon) + | SelectedCursor::ImageLoading { + previous: Previous::Icon(old_icon), + .. + } = &self.cursor + { + if *old_icon == icon { + return; + } + } + + self.cursor = SelectedCursor::Icon(icon); + self.set_style(); + } + Cursor::Custom(cursor) => { + let cursor = cursor.inner; + + if let SelectedCursor::ImageLoading { + cursor: old_cursor, .. + } + | SelectedCursor::ImageReady(old_cursor) = &self.cursor + { + if *old_cursor == cursor { + return; + } + } + + let mut image = cursor.0.get(self.main_thread).borrow_mut(); + match image.deref_mut() { + ImageState::Loading(state) => { + *state = Some(self.runner.clone()); + drop(image); + self.cursor = SelectedCursor::ImageLoading { + cursor, + previous: mem::take(&mut self.cursor).into(), + }; + } + ImageState::Failed => log::error!("tried to load invalid cursor"), + ImageState::Ready { .. } => { + drop(image); + self.cursor = SelectedCursor::ImageReady(cursor); + self.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; + } + + let SelectedCursor::ImageLoading { cursor, previous } = mem::take(&mut self.cursor) + else { + unreachable!("found wrong state") }; - self.style.set("cursor", value); + match result { + Ok(_) => { + self.cursor = SelectedCursor::ImageReady(cursor); + self.set_style(); + } + Err(_) => self.cursor = previous.into(), + } + } + } + + fn set_style(&self) { + if self.visible { + match &self.cursor { + SelectedCursor::Icon(icon) + | SelectedCursor::ImageLoading { + previous: Previous::Icon(icon), + .. + } => self.style.set("cursor", icon.name()), + SelectedCursor::ImageLoading { + previous: Previous::Image(cursor), + .. + } + | SelectedCursor::ImageReady(cursor) => { + if let ImageState::Ready { style, .. } = + cursor.0.get(self.main_thread).borrow().deref() + { + self.style.set("cursor", style) + } else { + unreachable!("found invalid saved state") + } + } + } } } } #[derive(Debug)] enum SelectedCursor { - Named(CursorIcon), + Icon(CursorIcon), ImageLoading { - state: CustomCursor, + cursor: CustomCursor, previous: Previous, }, - ImageReady(Rc), + ImageReady(CustomCursor), } impl Default for SelectedCursor { fn default() -> Self { - Self::Named(Default::default()) + Self::Icon(Default::default()) } } impl From for SelectedCursor { fn from(previous: Previous) -> Self { match previous { - Previous::Named(icon) => Self::Named(icon), - Previous::Image(image) => Self::ImageReady(image), + Previous::Icon(icon) => Self::Icon(icon), + Previous::Image(cursor) => Self::ImageReady(cursor), } } } #[derive(Debug)] pub enum Previous { - Named(CursorIcon), - Image(Rc), -} - -impl Previous { - fn style(&self) -> &str { - match self { - Previous::Named(icon) => icon.name(), - Previous::Image(image) => &image.style, - } - } + Icon(CursorIcon), + Image(CustomCursor), } impl From for Previous { fn from(value: SelectedCursor) -> Self { match value { - SelectedCursor::Named(icon) => Self::Named(icon), + SelectedCursor::Icon(icon) => Self::Icon(icon), SelectedCursor::ImageLoading { previous, .. } => previous, SelectedCursor::ImageReady(image) => Self::Image(image), } @@ -232,9 +273,13 @@ impl From for Previous { #[derive(Debug)] enum ImageState { - Loading(Option>>), + Loading(Option), Failed, - Ready(Rc), + Ready { + style: String, + _object_url: Option, + _image: HtmlImageElement, + }, } impl ImageState { @@ -250,7 +295,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. Change the `CursorState` if queued. + // 7. Notify event loop if one is registered. // 1. Create an `ImageData` from the RGBA data. // Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223 @@ -295,7 +340,6 @@ impl ImageState { .expect("unexpected exception in `createImageBitmap()`"), ); - #[allow(clippy::arc_with_non_send_sync)] let this = CustomCursor::new(main_thread); wasm_bindgen_futures::spawn_local({ @@ -338,6 +382,8 @@ impl ImageState { .expect("`bitmaprenderer` context unsupported") .unchecked_into(); context.transfer_from_image_bitmap(&bitmap); + drop(bitmap); + drop(context); // 4. Create a `Blob` from the `HTMLCanvasElement`. // @@ -369,40 +415,37 @@ impl ImageState { } }) .await; + drop(canvas); - let url = { + 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 mut this = this.get(main_thread).borrow_mut(); - - let Some(blob) = blob else { - log::error!("creating custom cursor failed"); - let ImageState::Loading(state) = this.deref_mut() else { - unreachable!("found invalid state"); - }; - let state = state.take(); - *this = ImageState::Failed; - - if let Some(state) = state.and_then(|weak| weak.upgrade()) { - let mut state = state.borrow_mut(); - let SelectedCursor::ImageLoading { previous, .. } = - mem::take(&mut state.cursor) - else { - unreachable!("found invalid state"); - }; - state.cursor = previous.into(); - } - - return; + let ImageState::Loading(runner) = this.deref_mut() else { + unreachable!("found invalid state"); }; + let runner = runner.take(); + *this = ImageState::Failed; - // 5. Create an object URL from the `Blob`. - Url::create_object_url_with_blob(&blob) - .expect("unexpected exception in `URL.createObjectURL()`") + if let Some(runner) = runner.and_then(|weak| weak.upgrade()) { + runner.send_event(EventWrapper::CursorReady(Err(CustomCursorHandle(weak)))); + } + + return; }; - Self::decode(main_thread, weak, url, true, hotspot_x, hotspot_y).await; + // 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; } }); @@ -415,13 +458,11 @@ impl ImageState { hotspot_x: u16, hotspot_y: u16, ) -> CustomCursor { - #[allow(clippy::arc_with_non_send_sync)] let this = CustomCursor::new(main_thread); wasm_bindgen_futures::spawn_local(Self::decode( main_thread, Arc::downgrade(&this.0), - url, - false, + UrlType::Plain(url), hotspot_x, hotspot_y, )); @@ -431,9 +472,8 @@ impl ImageState { async fn decode( main_thread: MainThreadMarker, - weak: sync::Weak>>, - url: String, - object: bool, + weak: Weak>>, + url: UrlType, hotspot_x: u16, hotspot_y: u16, ) { @@ -444,7 +484,7 @@ impl ImageState { // 6. Decode the image on an `HTMLImageElement` from the URL. let image = HtmlImageElement::new().expect("unexpected exception in `new HtmlImageElement`"); - image.set_src(&url); + image.set_src(url.url()); let result = JsFuture::from(image.decode()).await; let Some(this) = weak.upgrade() else { @@ -452,72 +492,60 @@ impl ImageState { }; let mut this = this.get(main_thread).borrow_mut(); - let ImageState::Loading(state) = this.deref_mut() else { + let ImageState::Loading(runner) = this.deref_mut() else { unreachable!("found invalid state"); }; - let state = state.take(); + let runner = runner.take(); if let Err(error) = result { - log::error!("creating custom cursor failed: {error:?}"); + log::error!("decoding custom cursor failed: {error:?}"); *this = ImageState::Failed; - if let Some(state) = state.and_then(|weak| weak.upgrade()) { - let mut state = state.borrow_mut(); - let SelectedCursor::ImageLoading { previous, .. } = mem::take(&mut state.cursor) - else { - unreachable!("found invalid state"); - }; - state.cursor = previous.into(); + if let Some(runner) = runner.and_then(|weak| weak.upgrade()) { + runner.send_event(EventWrapper::CursorReady(Err(CustomCursorHandle(weak)))); } return; } - let image = Image::new(url, object, image, hotspot_x, hotspot_y); + *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), + }, + _image: image, + }; - // 7. Change the `CursorState` if queued. - if let Some(state) = state.and_then(|weak| weak.upgrade()) { - let mut state = state.borrow_mut(); - state.cursor = SelectedCursor::ImageReady(image.clone()); - state.set_style(); + // 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)))); } + } +} - *this = ImageState::Ready(image); +#[derive(Clone)] +pub struct CustomCursorHandle(Weak>>); + +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)] -pub struct Image { - style: String, - url: String, - object: bool, - _image: HtmlImageElement, -} +struct ObjectUrl(String); -impl Drop for Image { +impl Drop for ObjectUrl { fn drop(&mut self) { - if self.object { - Url::revoke_object_url(&self.url) - .expect("unexpected exception in `URL.revokeObjectURL()`"); - } - } -} - -impl Image { - fn new( - url: String, - object: bool, - image: HtmlImageElement, - hotspot_x: u16, - hotspot_y: u16, - ) -> Rc { - let style = format!("url({url}) {hotspot_x} {hotspot_y}, auto"); - - Rc::new(Self { - style, - url, - object, - _image: image, - }) + Url::revoke_object_url(&self.0).expect("unexpected exception in `URL.revokeObjectURL()`"); } } diff --git a/src/platform_impl/web/event_loop/runner.rs b/src/platform_impl/web/event_loop/runner.rs index f7932ff7..43e8c4a2 100644 --- a/src/platform_impl/web/event_loop/runner.rs +++ b/src/platform_impl/web/event_loop/runner.rs @@ -1,3 +1,4 @@ +use super::super::cursor::CustomCursorHandle; use super::super::main_thread::MainThreadMarker; use super::super::DeviceId; use super::{backend, state::State}; @@ -139,6 +140,16 @@ 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()) + } + } + } } } } @@ -811,6 +822,19 @@ 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 { @@ -820,6 +844,7 @@ 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 390a5456..46a335a8 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -17,6 +17,8 @@ use crate::keyboard::{Key, KeyLocation, ModifiersState, PhysicalKey}; 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; @@ -46,6 +48,7 @@ pub struct Canvas { animation_frame_handler: AnimationFrameHandler, on_touch_end: Option>, on_context_menu: Option>, + pub cursor: CursorHandler, } pub struct Common { @@ -66,8 +69,9 @@ pub struct Style { } impl Canvas { - pub fn create( + pub(crate) fn create( main_thread: MainThreadMarker, + runner: WeakShared, id: WindowId, window: web_sys::Window, document: Document, @@ -107,6 +111,8 @@ impl Canvas { let style = Style::new(&window, &canvas); + let cursor = CursorHandler::new(main_thread, runner, style.clone()); + let common = Common { window: window.clone(), document: document.clone(), @@ -163,6 +169,7 @@ impl Canvas { animation_frame_handler: AnimationFrameHandler::new(window), on_touch_end: None, on_context_menu: None, + cursor, }) } diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 645f2e85..04503bc2 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -6,7 +6,6 @@ use crate::window::{ WindowAttributes, WindowButtons, WindowId as RootWI, WindowLevel, }; -use super::cursor::CursorState; use super::main_thread::{MainThreadMarker, MainThreadSafe}; use super::r#async::Dispatcher; use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget, Fullscreen}; @@ -25,7 +24,6 @@ pub struct Inner { id: WindowId, pub window: web_sys::Window, canvas: Rc>, - cursor: CursorState, destroy_fn: Option>, } @@ -41,6 +39,7 @@ impl Window { let document = target.runner.document(); let canvas = backend::Canvas::create( target.runner.main_thread(), + target.runner.weak(), id, window.clone(), document.clone(), @@ -48,7 +47,6 @@ impl Window { platform_attr, )?; let canvas = Rc::new(RefCell::new(canvas)); - let cursor = CursorState::new(target.runner.main_thread(), canvas.borrow().style().clone()); target.register(&canvas, id); @@ -59,7 +57,6 @@ impl Window { id, window: window.clone(), canvas, - cursor, destroy_fn: Some(destroy_fn), }; @@ -227,7 +224,7 @@ impl Inner { #[inline] pub fn set_cursor(&self, cursor: Cursor) { - self.cursor.set_cursor(cursor) + self.canvas.borrow_mut().cursor.set_cursor(cursor) } #[inline] @@ -253,7 +250,7 @@ impl Inner { #[inline] pub fn set_cursor_visible(&self, visible: bool) { - self.cursor.set_cursor_visible(visible) + self.canvas.borrow_mut().cursor.set_cursor_visible(visible) } #[inline]