2024-01-12 11:51:19 +01:00
|
|
|
use std::cell::RefCell;
|
|
|
|
|
use std::future::{self, Future};
|
|
|
|
|
use std::hash::{Hash, Hasher};
|
|
|
|
|
use std::mem;
|
|
|
|
|
use std::ops::{Deref, DerefMut};
|
|
|
|
|
use std::pin::Pin;
|
|
|
|
|
use std::rc::Rc;
|
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
use std::task::{ready, Context, Poll, Waker};
|
|
|
|
|
use std::time::Duration;
|
|
|
|
|
|
2023-12-26 03:49:20 +01:00
|
|
|
use cursor_icon::CursorIcon;
|
2024-01-12 11:51:19 +01:00
|
|
|
use js_sys::{Array, Object};
|
2023-12-16 22:02:17 +02:00
|
|
|
use wasm_bindgen::closure::Closure;
|
2024-01-12 11:51:19 +01:00
|
|
|
use wasm_bindgen::prelude::wasm_bindgen;
|
2023-12-16 22:02:17 +02:00
|
|
|
use wasm_bindgen::JsCast;
|
|
|
|
|
use wasm_bindgen_futures::JsFuture;
|
|
|
|
|
use web_sys::{
|
2024-01-12 11:51:19 +01:00
|
|
|
Blob, Document, DomException, HtmlCanvasElement, HtmlImageElement, ImageBitmap,
|
|
|
|
|
ImageBitmapOptions, ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window,
|
2023-12-16 22:02:17 +02:00
|
|
|
};
|
|
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
use super::backend::Style;
|
|
|
|
|
use super::main_thread::{MainThreadMarker, MainThreadSafe};
|
|
|
|
|
use super::r#async::{AbortHandle, Abortable, DropAbortHandle, Notified, Notifier};
|
2024-01-31 17:29:59 +04:00
|
|
|
use super::ActiveEventLoop;
|
2024-01-12 11:51:19 +01:00
|
|
|
use crate::cursor::{BadImage, Cursor, CursorImage, CustomCursor as RootCustomCursor};
|
|
|
|
|
use crate::platform::web::CustomCursorError;
|
|
|
|
|
|
2024-08-08 00:46:28 +02:00
|
|
|
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
2024-02-03 07:27:17 +04:00
|
|
|
pub(crate) enum CustomCursorSource {
|
2023-12-16 22:02:17 +02:00
|
|
|
Image(CursorImage),
|
|
|
|
|
Url { url: String, hotspot_x: u16, hotspot_y: u16 },
|
2024-01-12 11:51:19 +01:00
|
|
|
Animation { duration: Duration, cursors: Vec<RootCustomCursor> },
|
2023-12-16 22:02:17 +02:00
|
|
|
}
|
|
|
|
|
|
2024-02-03 07:27:17 +04:00
|
|
|
impl CustomCursorSource {
|
2023-12-23 16:12:29 +01:00
|
|
|
pub fn from_rgba(
|
|
|
|
|
rgba: Vec<u8>,
|
|
|
|
|
width: u16,
|
|
|
|
|
height: u16,
|
|
|
|
|
hotspot_x: u16,
|
|
|
|
|
hotspot_y: u16,
|
2024-02-03 07:27:17 +04:00
|
|
|
) -> Result<CustomCursorSource, BadImage> {
|
|
|
|
|
Ok(CustomCursorSource::Image(CursorImage::from_rgba(
|
2023-12-23 16:12:29 +01:00
|
|
|
rgba, width, height, hotspot_x, hotspot_y,
|
|
|
|
|
)?))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Clone, Debug)]
|
2024-01-12 11:51:19 +01:00
|
|
|
pub struct CustomCursor {
|
|
|
|
|
pub(crate) animation: bool,
|
|
|
|
|
state: Arc<MainThreadSafe<RefCell<ImageState>>>,
|
|
|
|
|
}
|
2023-12-23 16:12:29 +01:00
|
|
|
|
|
|
|
|
impl Hash for CustomCursor {
|
|
|
|
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
2024-01-12 11:51:19 +01:00
|
|
|
Arc::as_ptr(&self.state).hash(state);
|
2023-12-23 16:12:29 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl PartialEq for CustomCursor {
|
|
|
|
|
fn eq(&self, other: &Self) -> bool {
|
2024-01-12 11:51:19 +01:00
|
|
|
Arc::ptr_eq(&self.state, &other.state)
|
2023-12-23 16:12:29 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Eq for CustomCursor {}
|
|
|
|
|
|
|
|
|
|
impl CustomCursor {
|
2024-02-03 07:27:17 +04:00
|
|
|
pub(crate) fn new(event_loop: &ActiveEventLoop, source: CustomCursorSource) -> Self {
|
|
|
|
|
match source {
|
|
|
|
|
CustomCursorSource::Image(image) => Self::build_spawn(
|
|
|
|
|
event_loop,
|
|
|
|
|
from_rgba(event_loop.runner.window(), event_loop.runner.document().clone(), &image),
|
2024-01-12 11:51:19 +01:00
|
|
|
false,
|
2023-12-22 22:20:41 +01:00
|
|
|
),
|
2024-02-03 07:27:17 +04:00
|
|
|
CustomCursorSource::Url { url, hotspot_x, hotspot_y } => Self::build_spawn(
|
|
|
|
|
event_loop,
|
2024-01-12 11:51:19 +01:00
|
|
|
from_url(UrlType::Plain(url), hotspot_x, hotspot_y),
|
|
|
|
|
false,
|
|
|
|
|
),
|
2024-02-03 07:27:17 +04:00
|
|
|
CustomCursorSource::Animation { duration, cursors } => Self::build_spawn(
|
|
|
|
|
event_loop,
|
2024-01-12 11:51:19 +01:00
|
|
|
from_animation(
|
2024-02-03 07:27:17 +04:00
|
|
|
event_loop.runner.main_thread(),
|
2024-01-12 11:51:19 +01:00
|
|
|
duration,
|
|
|
|
|
cursors.into_iter().map(|cursor| cursor.inner),
|
|
|
|
|
),
|
|
|
|
|
true,
|
|
|
|
|
),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-31 17:29:59 +04:00
|
|
|
fn build_spawn<F, S>(window_target: &ActiveEventLoop, task: F, animation: bool) -> CustomCursor
|
2024-01-12 11:51:19 +01:00
|
|
|
where
|
|
|
|
|
F: 'static + Future<Output = Result<S, CustomCursorError>>,
|
|
|
|
|
S: Into<ImageState>,
|
|
|
|
|
{
|
|
|
|
|
let handle = AbortHandle::new();
|
|
|
|
|
let this = CustomCursor {
|
|
|
|
|
animation,
|
|
|
|
|
state: Arc::new(MainThreadSafe::new(
|
|
|
|
|
window_target.runner.main_thread(),
|
|
|
|
|
RefCell::new(ImageState::Loading {
|
|
|
|
|
notifier: Notifier::new(),
|
|
|
|
|
_handle: DropAbortHandle::new(handle.clone()),
|
|
|
|
|
}),
|
|
|
|
|
)),
|
|
|
|
|
};
|
|
|
|
|
let weak = Arc::downgrade(&this.state);
|
|
|
|
|
let main_thread = window_target.runner.main_thread();
|
|
|
|
|
|
|
|
|
|
let task = Abortable::new(handle, {
|
|
|
|
|
async move {
|
|
|
|
|
let result = task.await;
|
|
|
|
|
|
|
|
|
|
let this = weak.upgrade().expect("`CursorHandler` invalidated without aborting");
|
|
|
|
|
let mut this = this.get(main_thread).borrow_mut();
|
|
|
|
|
|
|
|
|
|
match result {
|
|
|
|
|
Ok(new_state) => {
|
|
|
|
|
let ImageState::Loading { notifier, .. } =
|
|
|
|
|
mem::replace(this.deref_mut(), new_state.into())
|
|
|
|
|
else {
|
|
|
|
|
unreachable!("found invalid state");
|
|
|
|
|
};
|
|
|
|
|
notifier.notify(Ok(()));
|
|
|
|
|
},
|
|
|
|
|
Err(error) => {
|
|
|
|
|
let ImageState::Loading { notifier, .. } =
|
|
|
|
|
mem::replace(this.deref_mut(), ImageState::Failed(error.clone()))
|
|
|
|
|
else {
|
|
|
|
|
unreachable!("found invalid state");
|
|
|
|
|
};
|
|
|
|
|
notifier.notify(Err(error));
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
|
|
|
let _ = task.await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
this
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-03 07:27:17 +04:00
|
|
|
pub(crate) fn new_async(
|
|
|
|
|
event_loop: &ActiveEventLoop,
|
|
|
|
|
source: CustomCursorSource,
|
2024-01-12 11:51:19 +01:00
|
|
|
) -> CustomCursorFuture {
|
2024-02-03 07:27:17 +04:00
|
|
|
let CustomCursor { animation, state } = Self::new(event_loop, source);
|
|
|
|
|
let binding = state.get(event_loop.runner.main_thread()).borrow();
|
2024-01-12 11:51:19 +01:00
|
|
|
let ImageState::Loading { notifier, .. } = binding.deref() else {
|
|
|
|
|
unreachable!("found invalid state")
|
|
|
|
|
};
|
|
|
|
|
let notified = notifier.notified();
|
|
|
|
|
drop(binding);
|
|
|
|
|
|
|
|
|
|
CustomCursorFuture { notified, animation, state: Some(state) }
|
2023-12-22 22:20:41 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-17 18:49:45 +01:00
|
|
|
|
2023-12-22 22:20:41 +01:00
|
|
|
#[derive(Debug)]
|
2024-01-12 11:51:19 +01:00
|
|
|
pub struct CustomCursorFuture {
|
|
|
|
|
notified: Notified<Result<(), CustomCursorError>>,
|
|
|
|
|
animation: bool,
|
|
|
|
|
state: Option<Arc<MainThreadSafe<RefCell<ImageState>>>>,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Future for CustomCursorFuture {
|
|
|
|
|
type Output = Result<CustomCursor, CustomCursorError>;
|
|
|
|
|
|
|
|
|
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
|
|
|
|
if self.state.is_none() {
|
|
|
|
|
panic!("`CustomCursorFuture` polled after completion")
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-23 16:47:35 +02:00
|
|
|
let result = ready!(Pin::new(&mut self.notified).poll(cx)).unwrap();
|
2024-01-12 11:51:19 +01:00
|
|
|
let state = self.state.take().expect("`CustomCursorFuture` polled after completion");
|
2024-04-26 19:11:44 +04:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
Poll::Ready(result.map(|_| CustomCursor { animation: self.animation, state }))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
pub struct CursorHandler(Rc<RefCell<Inner>>);
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct Inner {
|
2023-12-26 03:49:20 +01:00
|
|
|
main_thread: MainThreadMarker,
|
2024-01-12 11:51:19 +01:00
|
|
|
canvas: HtmlCanvasElement,
|
2023-12-26 03:49:20 +01:00
|
|
|
style: Style,
|
|
|
|
|
visible: bool,
|
|
|
|
|
cursor: SelectedCursor,
|
|
|
|
|
}
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2023-12-26 03:49:20 +01:00
|
|
|
impl CursorHandler {
|
2024-01-12 11:51:19 +01:00
|
|
|
pub(crate) fn new(
|
|
|
|
|
main_thread: MainThreadMarker,
|
|
|
|
|
canvas: HtmlCanvasElement,
|
|
|
|
|
style: Style,
|
|
|
|
|
) -> Self {
|
|
|
|
|
Self(Rc::new(RefCell::new(Inner {
|
2023-12-26 01:22:10 +01:00
|
|
|
main_thread,
|
2024-01-12 11:51:19 +01:00
|
|
|
canvas,
|
2023-12-22 22:20:41 +01:00
|
|
|
style,
|
|
|
|
|
visible: true,
|
|
|
|
|
cursor: SelectedCursor::default(),
|
2024-01-12 11:51:19 +01:00
|
|
|
})))
|
2023-12-22 22:20:41 +01:00
|
|
|
}
|
|
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
pub fn set_cursor(&self, cursor: Cursor) {
|
|
|
|
|
let mut this = self.0.borrow_mut();
|
|
|
|
|
|
2023-12-25 07:20:52 +01:00
|
|
|
match cursor {
|
|
|
|
|
Cursor::Icon(icon) => {
|
2023-12-26 03:49:20 +01:00
|
|
|
if let SelectedCursor::Icon(old_icon)
|
|
|
|
|
| SelectedCursor::Loading { previous: Previous::Icon(old_icon), .. } =
|
2024-01-12 11:51:19 +01:00
|
|
|
&this.cursor
|
2023-12-26 03:49:20 +01:00
|
|
|
{
|
|
|
|
|
if *old_icon == icon {
|
|
|
|
|
return;
|
2023-12-25 07:20:52 +01:00
|
|
|
}
|
|
|
|
|
}
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
this.cursor = SelectedCursor::Icon(icon);
|
|
|
|
|
this.set_style();
|
2023-12-22 22:20:41 +01:00
|
|
|
},
|
2023-12-26 03:49:20 +01:00
|
|
|
Cursor::Custom(cursor) => {
|
|
|
|
|
let cursor = cursor.inner;
|
|
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
if let SelectedCursor::Loading { cursor: old_cursor, .. }
|
|
|
|
|
| SelectedCursor::Image(old_cursor)
|
|
|
|
|
| SelectedCursor::Animation { cursor: old_cursor, .. } = &this.cursor
|
2023-12-26 03:49:20 +01:00
|
|
|
{
|
|
|
|
|
if *old_cursor == cursor {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2023-12-25 07:20:52 +01:00
|
|
|
}
|
2023-12-26 03:49:20 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
let state = cursor.state.get(this.main_thread).borrow();
|
|
|
|
|
|
|
|
|
|
match state.deref() {
|
|
|
|
|
ImageState::Loading { notifier, .. } => {
|
|
|
|
|
let notified = notifier.notified();
|
|
|
|
|
let handle = DropAbortHandle::new(AbortHandle::new());
|
|
|
|
|
let task = Abortable::new(handle.handle(), {
|
|
|
|
|
let weak = Rc::downgrade(&self.0);
|
|
|
|
|
async move {
|
|
|
|
|
let _ = notified.await;
|
|
|
|
|
let handler = weak
|
|
|
|
|
.upgrade()
|
|
|
|
|
.expect("`CursorHandler` invalidated without aborting");
|
|
|
|
|
handler.borrow_mut().notify();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
wasm_bindgen_futures::spawn_local(async move {
|
|
|
|
|
let _ = task.await;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
drop(state);
|
|
|
|
|
this.cursor = SelectedCursor::Loading {
|
2023-12-26 03:49:20 +01:00
|
|
|
cursor,
|
2024-01-12 11:51:19 +01:00
|
|
|
previous: mem::take(&mut this.cursor).into(),
|
|
|
|
|
_handle: handle,
|
2023-12-26 03:49:20 +01:00
|
|
|
};
|
|
|
|
|
},
|
2024-01-12 11:51:19 +01:00
|
|
|
ImageState::Failed(error) => {
|
2024-02-25 19:20:39 -08:00
|
|
|
tracing::error!(
|
|
|
|
|
"trying to load custom cursor that has failed to load: {error}"
|
|
|
|
|
)
|
2024-01-12 11:51:19 +01:00
|
|
|
},
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
if !this.visible {
|
|
|
|
|
animation.cancel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
this.cursor = SelectedCursor::Animation {
|
|
|
|
|
animation: AnimationDropper(animation),
|
|
|
|
|
cursor,
|
|
|
|
|
};
|
|
|
|
|
this.set_style();
|
2023-12-26 03:49:20 +01:00
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
},
|
2023-12-22 22:20:41 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
pub fn set_cursor_visible(&self, visible: bool) {
|
|
|
|
|
let mut this = self.0.borrow_mut();
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
if !visible && this.visible {
|
|
|
|
|
this.visible = false;
|
|
|
|
|
this.style.set("cursor", "none");
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
if let SelectedCursor::Animation { animation, .. } = &this.cursor {
|
|
|
|
|
animation.0.cancel();
|
2023-12-26 03:49:20 +01:00
|
|
|
}
|
2024-01-12 11:51:19 +01:00
|
|
|
} else if visible && !this.visible {
|
|
|
|
|
this.visible = true;
|
|
|
|
|
this.set_style();
|
2023-12-26 03:49:20 +01:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-12 11:51:19 +01:00
|
|
|
}
|
2023-12-26 03:49:20 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
impl Inner {
|
2023-12-26 03:49:20 +01:00
|
|
|
fn set_style(&self) {
|
|
|
|
|
if self.visible {
|
|
|
|
|
match &self.cursor {
|
|
|
|
|
SelectedCursor::Icon(icon)
|
|
|
|
|
| SelectedCursor::Loading { previous: Previous::Icon(icon), .. } => {
|
2024-06-12 00:12:14 +02:00
|
|
|
if let CursorIcon::Default = icon {
|
|
|
|
|
self.style.remove("cursor")
|
|
|
|
|
} else {
|
|
|
|
|
self.style.set("cursor", icon.name())
|
|
|
|
|
}
|
2023-12-26 03:49:20 +01:00
|
|
|
},
|
|
|
|
|
SelectedCursor::Loading { previous: Previous::Image(cursor), .. }
|
2024-01-12 11:51:19 +01:00
|
|
|
| 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"),
|
2023-12-26 03:49:20 +01:00
|
|
|
}
|
|
|
|
|
},
|
2024-01-12 11:51:19 +01:00
|
|
|
SelectedCursor::Loading {
|
|
|
|
|
previous: Previous::Animation { animation, .. }, ..
|
|
|
|
|
}
|
|
|
|
|
| SelectedCursor::Animation { animation, .. } => {
|
|
|
|
|
self.style.remove("cursor");
|
|
|
|
|
animation.0.play()
|
|
|
|
|
},
|
2023-12-26 03:49:20 +01:00
|
|
|
}
|
2023-12-16 22:02:17 +02:00
|
|
|
}
|
|
|
|
|
}
|
2024-01-12 11:51:19 +01:00
|
|
|
|
|
|
|
|
fn notify(&mut self) {
|
|
|
|
|
let SelectedCursor::Loading { cursor, previous, .. } = mem::take(&mut self.cursor) else {
|
|
|
|
|
unreachable!("found wrong state")
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
let state = cursor.state.get(self.main_thread).borrow();
|
|
|
|
|
match state.deref() {
|
|
|
|
|
ImageState::Image(_) => {
|
|
|
|
|
drop(state);
|
|
|
|
|
self.cursor = SelectedCursor::Image(cursor);
|
|
|
|
|
self.set_style();
|
|
|
|
|
},
|
|
|
|
|
ImageState::Animation(animation) => {
|
|
|
|
|
let canvas: &CanvasAnimateExt = self.canvas.unchecked_ref();
|
|
|
|
|
let animation = canvas.animate_with_keyframe_animation_options(
|
|
|
|
|
Some(&animation.keyframes),
|
|
|
|
|
&animation.options,
|
|
|
|
|
);
|
|
|
|
|
drop(state);
|
|
|
|
|
|
|
|
|
|
if !self.visible {
|
|
|
|
|
animation.cancel();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.cursor =
|
|
|
|
|
SelectedCursor::Animation { animation: AnimationDropper(animation), cursor };
|
|
|
|
|
self.set_style();
|
|
|
|
|
},
|
|
|
|
|
ImageState::Failed(error) => {
|
2024-02-25 19:20:39 -08:00
|
|
|
tracing::error!("custom cursor failed to load: {error}");
|
2024-01-12 11:51:19 +01:00
|
|
|
self.cursor = previous.into()
|
|
|
|
|
},
|
|
|
|
|
ImageState::Loading { .. } => unreachable!("notified without being ready"),
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-16 22:02:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2023-12-22 22:20:41 +01:00
|
|
|
enum SelectedCursor {
|
2023-12-26 03:49:20 +01:00
|
|
|
Icon(CursorIcon),
|
2024-01-12 11:51:19 +01:00
|
|
|
Loading { cursor: CustomCursor, previous: Previous, _handle: DropAbortHandle },
|
|
|
|
|
Image(CustomCursor),
|
|
|
|
|
Animation { cursor: CustomCursor, animation: AnimationDropper },
|
2023-12-16 22:02:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Default for SelectedCursor {
|
|
|
|
|
fn default() -> Self {
|
2023-12-26 03:49:20 +01:00
|
|
|
Self::Icon(Default::default())
|
2023-12-16 22:02:17 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-22 22:20:41 +01:00
|
|
|
impl From<Previous> for SelectedCursor {
|
|
|
|
|
fn from(previous: Previous) -> Self {
|
|
|
|
|
match previous {
|
2023-12-26 03:49:20 +01:00
|
|
|
Previous::Icon(icon) => Self::Icon(icon),
|
2024-01-12 11:51:19 +01:00
|
|
|
Previous::Image(cursor) => Self::Image(cursor),
|
|
|
|
|
Previous::Animation { cursor, animation } => Self::Animation { cursor, animation },
|
2023-12-22 22:20:41 +01:00
|
|
|
}
|
2023-12-16 22:02:17 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2024-01-12 11:51:19 +01:00
|
|
|
enum Previous {
|
2023-12-26 03:49:20 +01:00
|
|
|
Icon(CursorIcon),
|
|
|
|
|
Image(CustomCursor),
|
2024-01-12 11:51:19 +01:00
|
|
|
Animation { cursor: CustomCursor, animation: AnimationDropper },
|
2023-12-16 22:02:17 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl From<SelectedCursor> for Previous {
|
|
|
|
|
fn from(value: SelectedCursor) -> Self {
|
|
|
|
|
match value {
|
2023-12-26 03:49:20 +01:00
|
|
|
SelectedCursor::Icon(icon) => Self::Icon(icon),
|
2024-01-12 11:51:19 +01:00
|
|
|
SelectedCursor::Loading { previous, .. } => previous,
|
|
|
|
|
SelectedCursor::Image(image) => Self::Image(image),
|
|
|
|
|
SelectedCursor::Animation { cursor, animation } => {
|
|
|
|
|
Self::Animation { cursor, animation }
|
|
|
|
|
},
|
2023-12-16 22:02:17 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[derive(Debug)]
|
2023-12-22 22:20:41 +01:00
|
|
|
enum ImageState {
|
2024-01-12 11:51:19 +01:00
|
|
|
Loading { notifier: Notifier<Result<(), CustomCursorError>>, _handle: DropAbortHandle },
|
|
|
|
|
Failed(CustomCursorError),
|
|
|
|
|
Image(Image),
|
|
|
|
|
Animation(Animation),
|
2023-12-16 22:02:17 +02:00
|
|
|
}
|
|
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct Image {
|
|
|
|
|
style: String,
|
|
|
|
|
_object_url: Option<ObjectUrl>,
|
|
|
|
|
_image: HtmlImageElement,
|
|
|
|
|
}
|
2023-12-16 22:02:17 +02:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
impl From<Image> for ImageState {
|
|
|
|
|
fn from(image: Image) -> Self {
|
|
|
|
|
Self::Image(image)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct Animation {
|
|
|
|
|
keyframes: Array,
|
|
|
|
|
options: KeyframeAnimationOptions,
|
|
|
|
|
_images: Vec<CustomCursor>,
|
|
|
|
|
}
|
2023-12-26 03:49:20 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
impl From<Animation> for ImageState {
|
|
|
|
|
fn from(animation: Animation) -> Self {
|
|
|
|
|
Self::Animation(animation)
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
enum UrlType {
|
|
|
|
|
Plain(String),
|
|
|
|
|
Object(ObjectUrl),
|
|
|
|
|
}
|
2023-12-16 22:02:17 +02:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
impl UrlType {
|
|
|
|
|
fn url(&self) -> &str {
|
|
|
|
|
match &self {
|
|
|
|
|
UrlType::Plain(url) => url,
|
|
|
|
|
UrlType::Object(object_url) => &object_url.0,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-26 03:49:20 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct ObjectUrl(String);
|
2023-12-16 22:02:17 +02:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
impl Drop for ObjectUrl {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
Url::revoke_object_url(&self.0).expect("unexpected exception in `URL.revokeObjectURL()`");
|
2023-12-22 22:20:41 +01:00
|
|
|
}
|
2024-01-12 11:51:19 +01:00
|
|
|
}
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
#[derive(Debug)]
|
|
|
|
|
struct AnimationDropper(WebAnimation);
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
impl Drop for AnimationDropper {
|
|
|
|
|
fn drop(&mut self) {
|
|
|
|
|
self.0.cancel()
|
2023-12-22 22:20:41 +01:00
|
|
|
}
|
2024-01-12 11:51:19 +01:00
|
|
|
}
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
fn from_rgba(
|
|
|
|
|
window: &Window,
|
|
|
|
|
document: Document,
|
|
|
|
|
image: &CursorImage,
|
|
|
|
|
) -> impl Future<Output = Result<Image, CustomCursorError>> {
|
|
|
|
|
// 1. Create an `ImageData` from the RGBA data.
|
|
|
|
|
// 2. Create an `ImageBitmap` from the `ImageData`.
|
|
|
|
|
// 3. Draw `ImageBitmap` on an `HTMLCanvasElement`.
|
|
|
|
|
// 4. Create a `Blob` from the `HTMLCanvasElement`.
|
|
|
|
|
// 5. Create an object URL from the `Blob`.
|
|
|
|
|
// 6. Decode the image on an `HTMLImageElement` from the URL.
|
|
|
|
|
|
|
|
|
|
// 1. Create an `ImageData` from the RGBA data.
|
|
|
|
|
// Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223
|
|
|
|
|
#[cfg(target_feature = "atomics")]
|
|
|
|
|
// Can't share `SharedArrayBuffer` with `ImageData`.
|
|
|
|
|
let result = {
|
|
|
|
|
use js_sys::{Uint8Array, Uint8ClampedArray};
|
|
|
|
|
use wasm_bindgen::JsValue;
|
|
|
|
|
|
|
|
|
|
#[wasm_bindgen]
|
|
|
|
|
extern "C" {
|
|
|
|
|
#[wasm_bindgen(js_namespace = ImageData)]
|
|
|
|
|
type ImageDataExt;
|
|
|
|
|
#[wasm_bindgen(catch, constructor, js_class = ImageData)]
|
|
|
|
|
fn new(array: Uint8ClampedArray, sw: u32) -> Result<ImageDataExt, JsValue>;
|
2023-12-22 22:20:41 +01:00
|
|
|
}
|
|
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
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);
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
// 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);
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
// 4. Create a `Blob` from the `HTMLCanvasElement`.
|
|
|
|
|
//
|
|
|
|
|
// To keep the `Closure` alive until `HTMLCanvasElement.toBlob()` is done,
|
|
|
|
|
// we do the whole `Waker` strategy. Commonly on `Drop` the callback is aborted,
|
|
|
|
|
// but it would increase complexity and isn't possible in this case.
|
|
|
|
|
// Keep in mind that `HTMLCanvasElement.toBlob()` can call the callback immediately.
|
|
|
|
|
let value = Rc::new(RefCell::new(None));
|
|
|
|
|
let waker = Rc::new(RefCell::<Option<Waker>>::new(None));
|
|
|
|
|
let callback = Closure::once({
|
|
|
|
|
let value = value.clone();
|
|
|
|
|
let waker = waker.clone();
|
|
|
|
|
move |blob: Option<Blob>| {
|
|
|
|
|
*value.borrow_mut() = Some(blob);
|
|
|
|
|
if let Some(waker) = waker.borrow_mut().take() {
|
|
|
|
|
waker.wake();
|
|
|
|
|
}
|
2023-12-22 22:20:41 +01:00
|
|
|
}
|
2024-01-12 11:51:19 +01:00
|
|
|
});
|
|
|
|
|
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);
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
let Some(blob) = blob else {
|
|
|
|
|
return Err(CustomCursorError::Blob);
|
2023-12-26 03:49:20 +01:00
|
|
|
};
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
// 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
|
2023-12-16 22:02:17 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
async fn from_url(
|
|
|
|
|
url: UrlType,
|
|
|
|
|
hotspot_x: u16,
|
|
|
|
|
hotspot_y: u16,
|
|
|
|
|
) -> Result<Image, CustomCursorError> {
|
|
|
|
|
// 6. Decode the image on an `HTMLImageElement` from the URL.
|
|
|
|
|
let image = HtmlImageElement::new().expect("unexpected exception in `new HtmlImageElement`");
|
|
|
|
|
image.set_src(url.url());
|
|
|
|
|
let result = JsFuture::from(image.decode()).await;
|
|
|
|
|
|
|
|
|
|
if let Err(error) = result {
|
|
|
|
|
debug_assert!(error.has_type::<DomException>());
|
|
|
|
|
let error: DomException = error.unchecked_into();
|
|
|
|
|
debug_assert_eq!(error.name(), "EncodingError");
|
|
|
|
|
let error = error.message();
|
|
|
|
|
|
|
|
|
|
return Err(CustomCursorError::Decode(error));
|
|
|
|
|
}
|
2023-12-26 03:49:20 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
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,
|
|
|
|
|
})
|
2023-12-16 22:02:17 +02:00
|
|
|
}
|
|
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
#[allow(clippy::await_holding_refcell_ref)] // false-positive
|
|
|
|
|
async fn from_animation(
|
|
|
|
|
main_thread: MainThreadMarker,
|
|
|
|
|
duration: Duration,
|
2024-03-27 01:20:21 -07:00
|
|
|
cursors: impl ExactSizeIterator<Item = CustomCursor>,
|
2024-01-12 11:51:19 +01:00
|
|
|
) -> Result<Animation, CustomCursorError> {
|
|
|
|
|
let keyframes = Array::new();
|
|
|
|
|
let mut images = Vec::with_capacity(cursors.len());
|
|
|
|
|
|
|
|
|
|
for cursor in cursors {
|
|
|
|
|
let state = cursor.state.get(main_thread).borrow();
|
|
|
|
|
|
|
|
|
|
match state.deref() {
|
|
|
|
|
ImageState::Loading { notifier, .. } => {
|
|
|
|
|
let notified = notifier.notified();
|
|
|
|
|
drop(state);
|
2024-07-23 16:47:35 +02:00
|
|
|
notified.await.unwrap()?;
|
2024-01-12 11:51:19 +01:00
|
|
|
},
|
|
|
|
|
ImageState::Failed(error) => return Err(error.clone()),
|
|
|
|
|
ImageState::Image(_) => drop(state),
|
2024-02-03 07:27:17 +04:00
|
|
|
ImageState::Animation(_) => unreachable!("check in `CustomCursorSource` failed"),
|
2023-12-22 22:20:41 +01:00
|
|
|
}
|
2024-01-12 11:51:19 +01:00
|
|
|
|
|
|
|
|
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);
|
2023-12-22 22:20:41 +01:00
|
|
|
}
|
2024-01-12 11:51:19 +01:00
|
|
|
|
|
|
|
|
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 })
|
2023-12-22 22:20:41 +01:00
|
|
|
}
|
|
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
#[wasm_bindgen]
|
|
|
|
|
extern "C" {
|
|
|
|
|
type CanvasAnimateExt;
|
2023-12-22 22:20:41 +01:00
|
|
|
|
2024-01-12 11:51:19 +01:00
|
|
|
#[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);
|
2023-12-22 22:20:41 +01:00
|
|
|
}
|