use std::{ cell::RefCell, future, hash::{Hash, Hasher}, mem, ops::DerefMut, rc::{self, Rc}, sync::{self, 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::{ Blob, Document, HtmlCanvasElement, HtmlImageElement, ImageBitmap, ImageBitmapOptions, 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), Url { url: String, hotspot_x: u16, hotspot_y: u16, }, } impl CustomCursorBuilder { pub fn from_rgba( rgba: Vec, width: u16, height: u16, hotspot_x: u16, hotspot_y: u16, ) -> Result { Ok(CustomCursorBuilder::Image(CursorImage::from_rgba( rgba, width, height, hotspot_x, hotspot_y, )?)) } } #[derive(Clone, Debug)] pub struct CustomCursor(Arc>>); impl Hash for CustomCursor { fn hash(&self, state: &mut H) { Arc::as_ptr(&self.0).hash(state); } } impl PartialEq for CustomCursor { fn eq(&self, other: &Self) -> bool { Arc::ptr_eq(&self.0, &other.0) } } 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, ) -> 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::Url { url, hotspot_x, hotspot_y, } => ImageState::from_url(main_thread, url, hotspot_x, hotspot_y), } } } #[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 { main_thread: MainThreadMarker, 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, }; self.style.set("cursor", value); } } } #[derive(Debug)] enum SelectedCursor { Named(CursorIcon), ImageLoading { state: CustomCursor, previous: Previous, }, ImageReady(Rc), } impl Default for SelectedCursor { fn default() -> Self { Self::Named(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), } } } #[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, } } } impl From for Previous { fn from(value: SelectedCursor) -> Self { match value { SelectedCursor::Named(icon) => Self::Named(icon), SelectedCursor::ImageLoading { previous, .. } => previous, SelectedCursor::ImageReady(image) => Self::Image(image), } } } #[derive(Debug)] enum ImageState { Loading(Option>>), Failed, Ready(Rc), } 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. Change the `CursorState` if queued. // 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()`"), ); #[allow(clippy::arc_with_non_send_sync)] let this = CustomCursor::new(main_thread); wasm_bindgen_futures::spawn_local({ let weak = Arc::downgrade(&this.0); let CursorImage { width, height, hotspot_x, hotspot_y, .. } = *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") .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); // 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; let url = { 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; }; // 5. Create an object URL from the `Blob`. Url::create_object_url_with_blob(&blob) .expect("unexpected exception in `URL.createObjectURL()`") }; Self::decode(main_thread, weak, url, true, hotspot_x, hotspot_y).await; } }); this } fn from_url( main_thread: MainThreadMarker, url: String, 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, hotspot_x, hotspot_y, )); this } async fn decode( main_thread: MainThreadMarker, weak: sync::Weak>>, url: String, object: bool, 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); let result = JsFuture::from(image.decode()).await; let Some(this) = weak.upgrade() else { return; }; let mut this = this.get(main_thread).borrow_mut(); let ImageState::Loading(state) = this.deref_mut() else { unreachable!("found invalid state"); }; let state = state.take(); if let Err(error) = result { log::error!("creating 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(); } return; } let image = Image::new(url, object, image, hotspot_x, hotspot_y); // 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(); } *this = ImageState::Ready(image); } } #[derive(Debug)] pub struct Image { style: String, url: String, object: bool, _image: HtmlImageElement, } impl Drop for Image { 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, }) } }