Web: improve custom cursor handling and add animated cursors (#3384)
This commit is contained in:
parent
bdeb2574dc
commit
169cd39f93
18 changed files with 1086 additions and 420 deletions
|
|
@ -17,6 +17,7 @@ Unreleased` header.
|
||||||
- Add `CustomCursor`
|
- Add `CustomCursor`
|
||||||
- Add `CustomCursor::from_rgba` to allow creating cursor images from RGBA data.
|
- Add `CustomCursor::from_rgba` to allow creating cursor images from RGBA data.
|
||||||
- Add `CustomCursorExtWebSys::from_url` to allow loading cursor images from URLs.
|
- Add `CustomCursorExtWebSys::from_url` to allow loading cursor images from URLs.
|
||||||
|
- Add `CustomCursorExtWebSys::from_animation` to allow creating animated cursors from other `CustomCursor`s.
|
||||||
- On macOS, add services menu.
|
- On macOS, add services menu.
|
||||||
- **Breaking:** On Web, remove queuing fullscreen request in absence of transient activation.
|
- **Breaking:** On Web, remove queuing fullscreen request in absence of transient activation.
|
||||||
- On Web, fix setting cursor icon overriding cursor visibility.
|
- On Web, fix setting cursor icon overriding cursor visibility.
|
||||||
|
|
|
||||||
|
|
@ -205,6 +205,7 @@ features = [
|
||||||
'console',
|
'console',
|
||||||
'CssStyleDeclaration',
|
'CssStyleDeclaration',
|
||||||
'Document',
|
'Document',
|
||||||
|
'DomException',
|
||||||
'DomRect',
|
'DomRect',
|
||||||
'DomRectReadOnly',
|
'DomRectReadOnly',
|
||||||
'Element',
|
'Element',
|
||||||
|
|
@ -240,12 +241,16 @@ features = [
|
||||||
]
|
]
|
||||||
|
|
||||||
[target.'cfg(target_family = "wasm")'.dependencies]
|
[target.'cfg(target_family = "wasm")'.dependencies]
|
||||||
atomic-waker = "1"
|
|
||||||
js-sys = "0.3.64"
|
js-sys = "0.3.64"
|
||||||
|
pin-project = "1"
|
||||||
wasm-bindgen = "0.2"
|
wasm-bindgen = "0.2"
|
||||||
wasm-bindgen-futures = "0.4"
|
wasm-bindgen-futures = "0.4"
|
||||||
web-time = "0.2"
|
web-time = "0.2"
|
||||||
|
|
||||||
|
[target.'cfg(all(target_family = "wasm", target_feature = "atomics"))'.dependencies]
|
||||||
|
atomic-waker = "1"
|
||||||
|
concurrent-queue = { version = "2", default-features = false }
|
||||||
|
|
||||||
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
[target.'cfg(target_family = "wasm")'.dev-dependencies]
|
||||||
console_log = "1"
|
console_log = "1"
|
||||||
web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] }
|
web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] }
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,15 @@ use winit::{
|
||||||
keyboard::Key,
|
keyboard::Key,
|
||||||
window::{CursorIcon, CustomCursor, WindowBuilder},
|
window::{CursorIcon, CustomCursor, WindowBuilder},
|
||||||
};
|
};
|
||||||
|
#[cfg(wasm_platform)]
|
||||||
|
use {
|
||||||
|
std::sync::atomic::{AtomicU64, Ordering},
|
||||||
|
std::time::Duration,
|
||||||
|
winit::platform::web::CustomCursorExtWebSys,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[cfg(wasm_platform)]
|
||||||
|
static COUNTER: AtomicU64 = AtomicU64::new(0);
|
||||||
|
|
||||||
fn decode_cursor<T>(bytes: &[u8], window_target: &EventLoopWindowTarget<T>) -> CustomCursor {
|
fn decode_cursor<T>(bytes: &[u8], window_target: &EventLoopWindowTarget<T>) -> CustomCursor {
|
||||||
let img = image::load_from_memory(bytes).unwrap().to_rgba8();
|
let img = image::load_from_memory(bytes).unwrap().to_rgba8();
|
||||||
|
|
@ -74,6 +83,45 @@ fn main() -> Result<(), impl std::error::Error> {
|
||||||
log::debug!("Setting cursor visibility to {:?}", cursor_visible);
|
log::debug!("Setting cursor visibility to {:?}", cursor_visible);
|
||||||
window.set_cursor_visible(cursor_visible);
|
window.set_cursor_visible(cursor_visible);
|
||||||
}
|
}
|
||||||
|
#[cfg(wasm_platform)]
|
||||||
|
Key::Character("4") => {
|
||||||
|
log::debug!("Setting cursor to a random image from an URL");
|
||||||
|
window.set_cursor(
|
||||||
|
CustomCursor::from_url(
|
||||||
|
format!(
|
||||||
|
"https://picsum.photos/128?random={}",
|
||||||
|
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||||
|
),
|
||||||
|
64,
|
||||||
|
64,
|
||||||
|
)
|
||||||
|
.build(_elwt),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
#[cfg(wasm_platform)]
|
||||||
|
Key::Character("5") => {
|
||||||
|
log::debug!("Setting cursor to an animation");
|
||||||
|
window.set_cursor(
|
||||||
|
CustomCursor::from_animation(
|
||||||
|
Duration::from_secs(3),
|
||||||
|
vec![
|
||||||
|
custom_cursors[0].clone(),
|
||||||
|
custom_cursors[1].clone(),
|
||||||
|
CustomCursor::from_url(
|
||||||
|
format!(
|
||||||
|
"https://picsum.photos/128?random={}",
|
||||||
|
COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||||
|
),
|
||||||
|
64,
|
||||||
|
64,
|
||||||
|
)
|
||||||
|
.build(_elwt),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
.build(_elwt),
|
||||||
|
);
|
||||||
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
WindowEvent::RedrawRequested => {
|
WindowEvent::RedrawRequested => {
|
||||||
|
|
|
||||||
|
|
@ -27,17 +27,24 @@
|
||||||
//! [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border
|
//! [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border
|
||||||
//! [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding
|
//! [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding
|
||||||
|
|
||||||
use crate::cursor::CustomCursorBuilder;
|
use std::error::Error;
|
||||||
use crate::event::Event;
|
use std::fmt::{self, Display, Formatter};
|
||||||
use crate::event_loop::EventLoop;
|
use std::future::Future;
|
||||||
use crate::event_loop::EventLoopWindowTarget;
|
use std::pin::Pin;
|
||||||
use crate::platform_impl::PlatformCustomCursorBuilder;
|
use std::task::{Context, Poll};
|
||||||
use crate::window::CustomCursor;
|
use std::time::Duration;
|
||||||
use crate::window::{Window, WindowBuilder};
|
|
||||||
|
|
||||||
#[cfg(wasm_platform)]
|
#[cfg(wasm_platform)]
|
||||||
use web_sys::HtmlCanvasElement;
|
use web_sys::HtmlCanvasElement;
|
||||||
|
|
||||||
|
use crate::cursor::CustomCursorBuilder;
|
||||||
|
use crate::event::Event;
|
||||||
|
use crate::event_loop::{EventLoop, EventLoopWindowTarget};
|
||||||
|
#[cfg(wasm_platform)]
|
||||||
|
use crate::platform_impl::CustomCursorFuture as PlatformCustomCursorFuture;
|
||||||
|
use crate::platform_impl::{PlatformCustomCursor, PlatformCustomCursorBuilder};
|
||||||
|
use crate::window::{CustomCursor, Window, WindowBuilder};
|
||||||
|
|
||||||
#[cfg(not(wasm_platform))]
|
#[cfg(not(wasm_platform))]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub struct HtmlCanvasElement;
|
pub struct HtmlCanvasElement;
|
||||||
|
|
@ -234,15 +241,29 @@ pub enum PollStrategy {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub trait CustomCursorExtWebSys {
|
pub trait CustomCursorExtWebSys {
|
||||||
|
/// Returns if this cursor is an animation.
|
||||||
|
fn is_animation(&self) -> bool;
|
||||||
|
|
||||||
/// Creates a new cursor from a URL pointing to an image.
|
/// Creates a new cursor from a URL pointing to an image.
|
||||||
/// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url),
|
/// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url),
|
||||||
/// but browser support for image formats is inconsistent. Using [PNG] is recommended.
|
/// but browser support for image formats is inconsistent. Using [PNG] is recommended.
|
||||||
///
|
///
|
||||||
/// [PNG]: https://en.wikipedia.org/wiki/PNG
|
/// [PNG]: https://en.wikipedia.org/wiki/PNG
|
||||||
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder;
|
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder;
|
||||||
|
|
||||||
|
/// Crates a new animated cursor from multiple [`CustomCursor`]s.
|
||||||
|
/// Supplied `cursors` can't be empty or other animations.
|
||||||
|
fn from_animation(
|
||||||
|
duration: Duration,
|
||||||
|
cursors: Vec<CustomCursor>,
|
||||||
|
) -> Result<CustomCursorBuilder, BadAnimation>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CustomCursorExtWebSys for CustomCursor {
|
impl CustomCursorExtWebSys for CustomCursor {
|
||||||
|
fn is_animation(&self) -> bool {
|
||||||
|
self.inner.animation
|
||||||
|
}
|
||||||
|
|
||||||
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder {
|
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder {
|
||||||
CustomCursorBuilder {
|
CustomCursorBuilder {
|
||||||
inner: PlatformCustomCursorBuilder::Url {
|
inner: PlatformCustomCursorBuilder::Url {
|
||||||
|
|
@ -252,4 +273,92 @@ impl CustomCursorExtWebSys for CustomCursor {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn from_animation(
|
||||||
|
duration: Duration,
|
||||||
|
cursors: Vec<CustomCursor>,
|
||||||
|
) -> Result<CustomCursorBuilder, BadAnimation> {
|
||||||
|
if cursors.is_empty() {
|
||||||
|
return Err(BadAnimation::Empty);
|
||||||
|
}
|
||||||
|
|
||||||
|
if cursors.iter().any(CustomCursor::is_animation) {
|
||||||
|
return Err(BadAnimation::Animation);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(CustomCursorBuilder {
|
||||||
|
inner: PlatformCustomCursorBuilder::Animation { duration, cursors },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// An error produced when using [`CustomCursor::from_animation`] with invalid arguments.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub enum BadAnimation {
|
||||||
|
/// Produced when no cursors were supplied.
|
||||||
|
Empty,
|
||||||
|
/// Produced when a supplied cursor is an animation.
|
||||||
|
Animation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Display for BadAnimation {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Empty => write!(f, "No cursors supplied"),
|
||||||
|
Self::Animation => write!(f, "A supplied cursor is an animtion"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for BadAnimation {}
|
||||||
|
|
||||||
|
pub trait CustomCursorBuilderExtWebSys {
|
||||||
|
/// Async version of [`CustomCursorBuilder::build()`] which waits until the
|
||||||
|
/// cursor has completely finished loading.
|
||||||
|
fn build_async<T>(self, window_target: &EventLoopWindowTarget<T>) -> CustomCursorFuture;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CustomCursorBuilderExtWebSys for CustomCursorBuilder {
|
||||||
|
fn build_async<T>(self, window_target: &EventLoopWindowTarget<T>) -> CustomCursorFuture {
|
||||||
|
CustomCursorFuture(PlatformCustomCursor::build_async(
|
||||||
|
self.inner,
|
||||||
|
&window_target.p,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(not(wasm_platform))]
|
||||||
|
struct PlatformCustomCursorFuture;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CustomCursorFuture(PlatformCustomCursorFuture);
|
||||||
|
|
||||||
|
impl Future for CustomCursorFuture {
|
||||||
|
type Output = Result<CustomCursor, CustomCursorError>;
|
||||||
|
|
||||||
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
Pin::new(&mut self.0)
|
||||||
|
.poll(cx)
|
||||||
|
.map_ok(|cursor| CustomCursor { inner: cursor })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub enum CustomCursorError {
|
||||||
|
Blob,
|
||||||
|
Decode(String),
|
||||||
|
Animation,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for CustomCursorError {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
match self {
|
||||||
|
Self::Blob => write!(f, "failed to create `Blob`"),
|
||||||
|
Self::Decode(error) => write!(f, "failed to decode image: {error}"),
|
||||||
|
Self::Animation => write!(
|
||||||
|
f,
|
||||||
|
"found `CustomCursor` that is an animation when building an animation"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
102
src/platform_impl/web/async/abortable.rs
Normal file
102
src/platform_impl/web/async/abortable.rs
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
use std::error::Error;
|
||||||
|
use std::fmt::{self, Display, Formatter};
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
|
use pin_project::pin_project;
|
||||||
|
|
||||||
|
use super::AtomicWaker;
|
||||||
|
|
||||||
|
#[pin_project]
|
||||||
|
pub struct Abortable<F: Future> {
|
||||||
|
#[pin]
|
||||||
|
future: F,
|
||||||
|
shared: Arc<Shared>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: Future> Abortable<F> {
|
||||||
|
pub fn new(handle: AbortHandle, future: F) -> Self {
|
||||||
|
Self {
|
||||||
|
future,
|
||||||
|
shared: handle.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<F: Future> Future for Abortable<F> {
|
||||||
|
type Output = Result<F::Output, Aborted>;
|
||||||
|
|
||||||
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
if self.shared.aborted.load(Ordering::Relaxed) {
|
||||||
|
return Poll::Ready(Err(Aborted));
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Poll::Ready(value) = self.as_mut().project().future.poll(cx) {
|
||||||
|
return Poll::Ready(Ok(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.shared.waker.register(cx.waker());
|
||||||
|
|
||||||
|
if self.shared.aborted.load(Ordering::Relaxed) {
|
||||||
|
return Poll::Ready(Err(Aborted));
|
||||||
|
}
|
||||||
|
|
||||||
|
Poll::Pending
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Shared {
|
||||||
|
waker: AtomicWaker,
|
||||||
|
aborted: AtomicBool,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct AbortHandle(Arc<Shared>);
|
||||||
|
|
||||||
|
impl AbortHandle {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(Arc::new(Shared {
|
||||||
|
waker: AtomicWaker::new(),
|
||||||
|
aborted: AtomicBool::new(false),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn abort(&self) {
|
||||||
|
self.0.aborted.store(true, Ordering::Relaxed);
|
||||||
|
self.0.waker.wake()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct DropAbortHandle(AbortHandle);
|
||||||
|
|
||||||
|
impl DropAbortHandle {
|
||||||
|
pub fn new(handle: AbortHandle) -> Self {
|
||||||
|
Self(handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn handle(&self) -> AbortHandle {
|
||||||
|
self.0.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for DropAbortHandle {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.abort()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
|
||||||
|
pub struct Aborted;
|
||||||
|
|
||||||
|
impl Display for Aborted {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "`Abortable` future has been aborted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Error for Aborted {}
|
||||||
35
src/platform_impl/web/async/atomic_waker.rs
Normal file
35
src/platform_impl/web/async/atomic_waker.rs
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::ops::Deref;
|
||||||
|
use std::task::Waker;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct AtomicWaker(RefCell<Option<Waker>>);
|
||||||
|
|
||||||
|
impl AtomicWaker {
|
||||||
|
pub const fn new() -> Self {
|
||||||
|
Self(RefCell::new(None))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn register(&self, waker: &Waker) {
|
||||||
|
let mut this = self.0.borrow_mut();
|
||||||
|
|
||||||
|
if let Some(old_waker) = this.deref() {
|
||||||
|
if old_waker.will_wake(waker) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
*this = Some(waker.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn wake(&self) {
|
||||||
|
if let Some(waker) = self.0.borrow_mut().take() {
|
||||||
|
waker.wake();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: Wasm without the `atomics` target feature is single-threaded.
|
||||||
|
unsafe impl Send for AtomicWaker {}
|
||||||
|
// SAFETY: Wasm without the `atomics` target feature is single-threaded.
|
||||||
|
unsafe impl Sync for AtomicWaker {}
|
||||||
|
|
@ -1,23 +1,24 @@
|
||||||
use atomic_waker::AtomicWaker;
|
|
||||||
use std::future;
|
use std::future;
|
||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
use std::sync::atomic::{AtomicBool, Ordering};
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::mpsc::{self, Receiver, RecvError, SendError, Sender, TryRecvError};
|
use std::sync::mpsc::{self, RecvError, SendError, TryRecvError};
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
use std::task::Poll;
|
use std::task::Poll;
|
||||||
|
|
||||||
pub fn channel<T>() -> (AsyncSender<T>, AsyncReceiver<T>) {
|
use super::AtomicWaker;
|
||||||
|
|
||||||
|
pub fn channel<T>() -> (Sender<T>, Receiver<T>) {
|
||||||
let (sender, receiver) = mpsc::channel();
|
let (sender, receiver) = mpsc::channel();
|
||||||
let shared = Arc::new(Shared {
|
let shared = Arc::new(Shared {
|
||||||
closed: AtomicBool::new(false),
|
closed: AtomicBool::new(false),
|
||||||
waker: AtomicWaker::new(),
|
waker: AtomicWaker::new(),
|
||||||
});
|
});
|
||||||
|
|
||||||
let sender = AsyncSender(Arc::new(SenderInner {
|
let sender = Sender(Arc::new(SenderInner {
|
||||||
sender: Mutex::new(sender),
|
sender: Mutex::new(sender),
|
||||||
shared: Arc::clone(&shared),
|
shared: Arc::clone(&shared),
|
||||||
}));
|
}));
|
||||||
let receiver = AsyncReceiver {
|
let receiver = Receiver {
|
||||||
receiver: Rc::new(receiver),
|
receiver: Rc::new(receiver),
|
||||||
shared,
|
shared,
|
||||||
};
|
};
|
||||||
|
|
@ -25,18 +26,18 @@ pub fn channel<T>() -> (AsyncSender<T>, AsyncReceiver<T>) {
|
||||||
(sender, receiver)
|
(sender, receiver)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AsyncSender<T>(Arc<SenderInner<T>>);
|
pub struct Sender<T>(Arc<SenderInner<T>>);
|
||||||
|
|
||||||
struct SenderInner<T> {
|
struct SenderInner<T> {
|
||||||
// We need to wrap it into a `Mutex` to make it `Sync`. So the sender can't
|
// We need to wrap it into a `Mutex` to make it `Sync`. So the sender can't
|
||||||
// be accessed on the main thread, as it could block. Additionally we need
|
// be accessed on the main thread, as it could block. Additionally we need
|
||||||
// to wrap `Sender` in an `Arc` to make it clonable on the main thread without
|
// to wrap `Sender` in an `Arc` to make it clonable on the main thread without
|
||||||
// having to block.
|
// having to block.
|
||||||
sender: Mutex<Sender<T>>,
|
sender: Mutex<mpsc::Sender<T>>,
|
||||||
shared: Arc<Shared>,
|
shared: Arc<Shared>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> AsyncSender<T> {
|
impl<T> Sender<T> {
|
||||||
pub fn send(&self, event: T) -> Result<(), SendError<T>> {
|
pub fn send(&self, event: T) -> Result<(), SendError<T>> {
|
||||||
self.0.sender.lock().unwrap().send(event)?;
|
self.0.sender.lock().unwrap().send(event)?;
|
||||||
self.0.shared.waker.wake();
|
self.0.shared.waker.wake();
|
||||||
|
|
@ -52,7 +53,7 @@ impl<T> SenderInner<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Clone for AsyncSender<T> {
|
impl<T> Clone for Sender<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self(Arc::clone(&self.0))
|
Self(Arc::clone(&self.0))
|
||||||
}
|
}
|
||||||
|
|
@ -64,12 +65,12 @@ impl<T> Drop for SenderInner<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct AsyncReceiver<T> {
|
pub struct Receiver<T> {
|
||||||
receiver: Rc<Receiver<T>>,
|
receiver: Rc<mpsc::Receiver<T>>,
|
||||||
shared: Arc<Shared>,
|
shared: Arc<Shared>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> AsyncReceiver<T> {
|
impl<T> Receiver<T> {
|
||||||
pub async fn next(&self) -> Result<T, RecvError> {
|
pub async fn next(&self) -> Result<T, RecvError> {
|
||||||
future::poll_fn(|cx| match self.receiver.try_recv() {
|
future::poll_fn(|cx| match self.receiver.try_recv() {
|
||||||
Ok(event) => Poll::Ready(Ok(event)),
|
Ok(event) => Poll::Ready(Ok(event)),
|
||||||
|
|
@ -102,7 +103,7 @@ impl<T> AsyncReceiver<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Clone for AsyncReceiver<T> {
|
impl<T> Clone for Receiver<T> {
|
||||||
fn clone(&self) -> Self {
|
fn clone(&self) -> Self {
|
||||||
Self {
|
Self {
|
||||||
receiver: Rc::clone(&self.receiver),
|
receiver: Rc::clone(&self.receiver),
|
||||||
|
|
@ -111,7 +112,7 @@ impl<T> Clone for AsyncReceiver<T> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Drop for AsyncReceiver<T> {
|
impl<T> Drop for Receiver<T> {
|
||||||
fn drop(&mut self) {
|
fn drop(&mut self) {
|
||||||
self.shared.closed.store(true, Ordering::Relaxed);
|
self.shared.closed.store(true, Ordering::Relaxed);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
55
src/platform_impl/web/async/concurrent_queue.rs
Normal file
55
src/platform_impl/web/async/concurrent_queue.rs
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
use std::cell::{Cell, RefCell};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ConcurrentQueue<T> {
|
||||||
|
queue: RefCell<Vec<T>>,
|
||||||
|
closed: Cell<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PushError<T> {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
Full(T),
|
||||||
|
Closed(T),
|
||||||
|
}
|
||||||
|
|
||||||
|
pub enum PopError {
|
||||||
|
Empty,
|
||||||
|
Closed,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> ConcurrentQueue<T> {
|
||||||
|
pub fn unbounded() -> Self {
|
||||||
|
Self {
|
||||||
|
queue: RefCell::new(Vec::new()),
|
||||||
|
closed: Cell::new(false),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn push(&self, value: T) -> Result<(), PushError<T>> {
|
||||||
|
if self.closed.get() {
|
||||||
|
return Err(PushError::Closed(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.queue.borrow_mut().push(value);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn pop(&self) -> Result<T, PopError> {
|
||||||
|
self.queue.borrow_mut().pop().ok_or_else(|| {
|
||||||
|
if self.closed.get() {
|
||||||
|
PopError::Closed
|
||||||
|
} else {
|
||||||
|
PopError::Empty
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn close(&self) -> bool {
|
||||||
|
!self.closed.replace(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// SAFETY: Wasm without the `atomics` target feature is single-threaded.
|
||||||
|
unsafe impl<T> Send for ConcurrentQueue<T> {}
|
||||||
|
// SAFETY: Wasm without the `atomics` target feature is single-threaded.
|
||||||
|
unsafe impl<T> Sync for ConcurrentQueue<T> {}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use super::super::main_thread::MainThreadMarker;
|
use super::super::main_thread::MainThreadMarker;
|
||||||
use super::{channel, AsyncReceiver, AsyncSender, Wrapper};
|
use super::{channel, Receiver, Sender, Wrapper};
|
||||||
use std::{
|
use std::{
|
||||||
cell::Ref,
|
cell::Ref,
|
||||||
sync::{Arc, Condvar, Mutex},
|
sync::{Arc, Condvar, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub struct Dispatcher<T: 'static>(Wrapper<true, T, AsyncSender<Closure<T>>, Closure<T>>);
|
pub struct Dispatcher<T: 'static>(Wrapper<true, T, Sender<Closure<T>>, Closure<T>>);
|
||||||
|
|
||||||
struct Closure<T>(Box<dyn FnOnce(&T) + Send>);
|
struct Closure<T>(Box<dyn FnOnce(&T) + Send>);
|
||||||
|
|
||||||
|
|
@ -85,8 +85,8 @@ impl<T> Dispatcher<T> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct DispatchRunner<T: 'static> {
|
pub struct DispatchRunner<T: 'static> {
|
||||||
wrapper: Wrapper<true, T, AsyncSender<Closure<T>>, Closure<T>>,
|
wrapper: Wrapper<true, T, Sender<Closure<T>>, Closure<T>>,
|
||||||
receiver: AsyncReceiver<Closure<T>>,
|
receiver: Receiver<Closure<T>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> DispatchRunner<T> {
|
impl<T> DispatchRunner<T> {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,19 @@
|
||||||
|
mod abortable;
|
||||||
|
#[cfg(not(target_feature = "atomics"))]
|
||||||
|
mod atomic_waker;
|
||||||
mod channel;
|
mod channel;
|
||||||
|
#[cfg(not(target_feature = "atomics"))]
|
||||||
|
mod concurrent_queue;
|
||||||
mod dispatcher;
|
mod dispatcher;
|
||||||
|
mod notifier;
|
||||||
mod waker;
|
mod waker;
|
||||||
mod wrapper;
|
mod wrapper;
|
||||||
|
|
||||||
pub use self::channel::{channel, AsyncReceiver, AsyncSender};
|
pub use self::abortable::{AbortHandle, Abortable, DropAbortHandle};
|
||||||
|
pub use self::channel::{channel, Receiver, Sender};
|
||||||
pub use self::dispatcher::{DispatchRunner, Dispatcher};
|
pub use self::dispatcher::{DispatchRunner, Dispatcher};
|
||||||
|
pub use self::notifier::{Notified, Notifier};
|
||||||
pub use self::waker::{Waker, WakerSpawner};
|
pub use self::waker::{Waker, WakerSpawner};
|
||||||
use self::wrapper::Wrapper;
|
use self::wrapper::Wrapper;
|
||||||
|
use atomic_waker::AtomicWaker;
|
||||||
|
use concurrent_queue::{ConcurrentQueue, PushError};
|
||||||
|
|
|
||||||
78
src/platform_impl/web/async/notifier.rs
Normal file
78
src/platform_impl/web/async/notifier.rs
Normal file
|
|
@ -0,0 +1,78 @@
|
||||||
|
use std::future::Future;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
use std::task::Context;
|
||||||
|
use std::task::Poll;
|
||||||
|
use std::task::Waker;
|
||||||
|
|
||||||
|
use super::{ConcurrentQueue, PushError};
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct Notifier<T: Clone>(Arc<Inner<T>>);
|
||||||
|
|
||||||
|
impl<T: Clone> Notifier<T> {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
Self(Arc::new(Inner {
|
||||||
|
queue: ConcurrentQueue::unbounded(),
|
||||||
|
value: OnceLock::new(),
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notify(self, value: T) {
|
||||||
|
if self.0.value.set(value).is_err() {
|
||||||
|
unreachable!("value set before")
|
||||||
|
}
|
||||||
|
|
||||||
|
self.0.queue.close();
|
||||||
|
|
||||||
|
while let Ok(waker) = self.0.queue.pop() {
|
||||||
|
waker.wake()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn notified(&self) -> Notified<T> {
|
||||||
|
Notified(Some(Arc::clone(&self.0)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Notified<T: Clone>(Option<Arc<Inner<T>>>);
|
||||||
|
|
||||||
|
impl<T: Clone> Future for Notified<T> {
|
||||||
|
type Output = T;
|
||||||
|
|
||||||
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
let this = self.0.take().expect("`Receiver` polled after completion");
|
||||||
|
|
||||||
|
if this.value.get().is_none() {
|
||||||
|
match this.queue.push(cx.waker().clone()) {
|
||||||
|
Ok(()) => {
|
||||||
|
if this.value.get().is_none() {
|
||||||
|
self.0 = Some(this);
|
||||||
|
return Poll::Pending;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(PushError::Closed(_)) => (),
|
||||||
|
Err(PushError::Full(_)) => {
|
||||||
|
unreachable!("found full queue despite using unbounded queue")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let (Ok(Some(value)) | Err(Some(value))) = Arc::try_unwrap(this)
|
||||||
|
.map(|mut inner| inner.value.take())
|
||||||
|
.map_err(|this| this.value.get().cloned())
|
||||||
|
else {
|
||||||
|
unreachable!("found no value despite being ready")
|
||||||
|
};
|
||||||
|
|
||||||
|
Poll::Ready(value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Inner<T> {
|
||||||
|
queue: ConcurrentQueue<Waker>,
|
||||||
|
value: OnceLock<T>,
|
||||||
|
}
|
||||||
|
|
@ -1,11 +1,11 @@
|
||||||
use super::super::main_thread::MainThreadMarker;
|
|
||||||
use super::Wrapper;
|
|
||||||
use atomic_waker::AtomicWaker;
|
|
||||||
use std::future;
|
use std::future;
|
||||||
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
use std::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::task::Poll;
|
use std::task::Poll;
|
||||||
|
|
||||||
|
use super::super::main_thread::MainThreadMarker;
|
||||||
|
use super::{AtomicWaker, Wrapper};
|
||||||
|
|
||||||
pub struct WakerSpawner<T: 'static>(Wrapper<false, Handler<T>, Sender, usize>);
|
pub struct WakerSpawner<T: 'static>(Wrapper<false, Handler<T>, Sender, usize>);
|
||||||
|
|
||||||
pub struct Waker<T: 'static>(Wrapper<false, Handler<T>, Sender, usize>);
|
pub struct Waker<T: 'static>(Wrapper<false, Handler<T>, Sender, usize>);
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,31 @@
|
||||||
use super::backend::Style;
|
use std::cell::RefCell;
|
||||||
use super::event_loop::runner::{EventWrapper, WeakShared};
|
use std::future::{self, Future};
|
||||||
use super::main_thread::{MainThreadMarker, MainThreadSafe};
|
use std::hash::{Hash, Hasher};
|
||||||
use super::EventLoopWindowTarget;
|
use std::mem;
|
||||||
use crate::cursor::{BadImage, Cursor, CursorImage};
|
use std::ops::{Deref, DerefMut};
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use std::sync::Arc;
|
||||||
|
use std::task::{ready, Context, Poll, Waker};
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
use cursor_icon::CursorIcon;
|
use cursor_icon::CursorIcon;
|
||||||
use std::ops::Deref;
|
use js_sys::{Array, Object};
|
||||||
use std::sync::Weak;
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
use std::{
|
|
||||||
cell::RefCell,
|
|
||||||
future,
|
|
||||||
hash::{Hash, Hasher},
|
|
||||||
mem,
|
|
||||||
ops::DerefMut,
|
|
||||||
rc::Rc,
|
|
||||||
sync::Arc,
|
|
||||||
task::{Poll, Waker},
|
|
||||||
};
|
|
||||||
use wasm_bindgen::{closure::Closure, JsCast};
|
use wasm_bindgen::{closure::Closure, JsCast};
|
||||||
use wasm_bindgen_futures::JsFuture;
|
use wasm_bindgen_futures::JsFuture;
|
||||||
use web_sys::{
|
use web_sys::{
|
||||||
Blob, Document, HtmlCanvasElement, HtmlImageElement, ImageBitmap, ImageBitmapOptions,
|
Blob, Document, DomException, HtmlCanvasElement, HtmlImageElement, ImageBitmap,
|
||||||
ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window,
|
ImageBitmapOptions, ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::backend::Style;
|
||||||
|
use super::main_thread::{MainThreadMarker, MainThreadSafe};
|
||||||
|
use super::r#async::{AbortHandle, Abortable, DropAbortHandle, Notified, Notifier};
|
||||||
|
use super::EventLoopWindowTarget;
|
||||||
|
use crate::cursor::{BadImage, Cursor, CursorImage, CustomCursor as RootCustomCursor};
|
||||||
|
use crate::platform::web::CustomCursorError;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(crate) enum CustomCursorBuilder {
|
pub(crate) enum CustomCursorBuilder {
|
||||||
Image(CursorImage),
|
Image(CursorImage),
|
||||||
|
|
@ -31,6 +34,10 @@ pub(crate) enum CustomCursorBuilder {
|
||||||
hotspot_x: u16,
|
hotspot_x: u16,
|
||||||
hotspot_y: u16,
|
hotspot_y: u16,
|
||||||
},
|
},
|
||||||
|
Animation {
|
||||||
|
duration: Duration,
|
||||||
|
cursors: Vec<RootCustomCursor>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CustomCursorBuilder {
|
impl CustomCursorBuilder {
|
||||||
|
|
@ -48,196 +55,392 @@ impl CustomCursorBuilder {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct CustomCursor(Arc<MainThreadSafe<RefCell<ImageState>>>);
|
pub struct CustomCursor {
|
||||||
|
pub(crate) animation: bool,
|
||||||
|
state: Arc<MainThreadSafe<RefCell<ImageState>>>,
|
||||||
|
}
|
||||||
|
|
||||||
impl Hash for CustomCursor {
|
impl Hash for CustomCursor {
|
||||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
Arc::as_ptr(&self.0).hash(state);
|
Arc::as_ptr(&self.state).hash(state);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PartialEq for CustomCursor {
|
impl PartialEq for CustomCursor {
|
||||||
fn eq(&self, other: &Self) -> bool {
|
fn eq(&self, other: &Self) -> bool {
|
||||||
Arc::ptr_eq(&self.0, &other.0)
|
Arc::ptr_eq(&self.state, &other.state)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Eq for CustomCursor {}
|
impl Eq for CustomCursor {}
|
||||||
|
|
||||||
impl CustomCursor {
|
impl CustomCursor {
|
||||||
fn new(main_thread: MainThreadMarker) -> Self {
|
|
||||||
Self(Arc::new(MainThreadSafe::new(
|
|
||||||
main_thread,
|
|
||||||
RefCell::new(ImageState::Loading(None)),
|
|
||||||
)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn build<T>(
|
pub(crate) fn build<T>(
|
||||||
builder: CustomCursorBuilder,
|
builder: CustomCursorBuilder,
|
||||||
window_target: &EventLoopWindowTarget<T>,
|
window_target: &EventLoopWindowTarget<T>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let main_thread = window_target.runner.main_thread();
|
|
||||||
|
|
||||||
match builder {
|
match builder {
|
||||||
CustomCursorBuilder::Image(image) => ImageState::from_rgba(
|
CustomCursorBuilder::Image(image) => Self::build_spawn(
|
||||||
main_thread,
|
window_target,
|
||||||
|
from_rgba(
|
||||||
window_target.runner.window(),
|
window_target.runner.window(),
|
||||||
window_target.runner.document().clone(),
|
window_target.runner.document().clone(),
|
||||||
&image,
|
&image,
|
||||||
),
|
),
|
||||||
|
false,
|
||||||
|
),
|
||||||
CustomCursorBuilder::Url {
|
CustomCursorBuilder::Url {
|
||||||
url,
|
url,
|
||||||
hotspot_x,
|
hotspot_x,
|
||||||
hotspot_y,
|
hotspot_y,
|
||||||
} => ImageState::from_url(main_thread, url, hotspot_x, hotspot_y),
|
} => Self::build_spawn(
|
||||||
|
window_target,
|
||||||
|
from_url(UrlType::Plain(url), hotspot_x, hotspot_y),
|
||||||
|
false,
|
||||||
|
),
|
||||||
|
CustomCursorBuilder::Animation { duration, cursors } => Self::build_spawn(
|
||||||
|
window_target,
|
||||||
|
from_animation(
|
||||||
|
window_target.runner.main_thread(),
|
||||||
|
duration,
|
||||||
|
cursors.into_iter().map(|cursor| cursor.inner),
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn build_spawn<T, F, S>(
|
||||||
|
window_target: &EventLoopWindowTarget<T>,
|
||||||
|
task: F,
|
||||||
|
animation: bool,
|
||||||
|
) -> CustomCursor
|
||||||
|
where
|
||||||
|
F: 'static + Future<Output = Result<S, CustomCursorError>>,
|
||||||
|
S: Into<ImageState>,
|
||||||
|
{
|
||||||
|
let handle = AbortHandle::new();
|
||||||
|
let this = CustomCursor {
|
||||||
|
animation,
|
||||||
|
state: Arc::new(MainThreadSafe::new(
|
||||||
|
window_target.runner.main_thread(),
|
||||||
|
RefCell::new(ImageState::Loading {
|
||||||
|
notifier: Notifier::new(),
|
||||||
|
_handle: DropAbortHandle::new(handle.clone()),
|
||||||
|
}),
|
||||||
|
)),
|
||||||
|
};
|
||||||
|
let weak = Arc::downgrade(&this.state);
|
||||||
|
let main_thread = window_target.runner.main_thread();
|
||||||
|
|
||||||
|
let task = Abortable::new(handle, {
|
||||||
|
async move {
|
||||||
|
let result = task.await;
|
||||||
|
|
||||||
|
let this = weak
|
||||||
|
.upgrade()
|
||||||
|
.expect("`CursorHandler` invalidated without aborting");
|
||||||
|
let mut this = this.get(main_thread).borrow_mut();
|
||||||
|
|
||||||
|
match result {
|
||||||
|
Ok(new_state) => {
|
||||||
|
let ImageState::Loading { notifier, .. } =
|
||||||
|
mem::replace(this.deref_mut(), new_state.into())
|
||||||
|
else {
|
||||||
|
unreachable!("found invalid state");
|
||||||
|
};
|
||||||
|
notifier.notify(Ok(()));
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
let ImageState::Loading { notifier, .. } =
|
||||||
|
mem::replace(this.deref_mut(), ImageState::Failed(error.clone()))
|
||||||
|
else {
|
||||||
|
unreachable!("found invalid state");
|
||||||
|
};
|
||||||
|
notifier.notify(Err(error));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let _ = task.await;
|
||||||
|
});
|
||||||
|
|
||||||
|
this
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn build_async<T>(
|
||||||
|
builder: CustomCursorBuilder,
|
||||||
|
window_target: &EventLoopWindowTarget<T>,
|
||||||
|
) -> CustomCursorFuture {
|
||||||
|
let CustomCursor { animation, state } = Self::build(builder, window_target);
|
||||||
|
let binding = state.get(window_target.runner.main_thread()).borrow();
|
||||||
|
let ImageState::Loading { notifier, .. } = binding.deref() else {
|
||||||
|
unreachable!("found invalid state")
|
||||||
|
};
|
||||||
|
let notified = notifier.notified();
|
||||||
|
drop(binding);
|
||||||
|
|
||||||
|
CustomCursorFuture {
|
||||||
|
notified,
|
||||||
|
animation,
|
||||||
|
state: Some(state),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct CursorHandler {
|
pub struct CustomCursorFuture {
|
||||||
|
notified: Notified<Result<(), CustomCursorError>>,
|
||||||
|
animation: bool,
|
||||||
|
state: Option<Arc<MainThreadSafe<RefCell<ImageState>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Future for CustomCursorFuture {
|
||||||
|
type Output = Result<CustomCursor, CustomCursorError>;
|
||||||
|
|
||||||
|
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||||
|
if self.state.is_none() {
|
||||||
|
panic!("`CustomCursorFuture` polled after completion")
|
||||||
|
}
|
||||||
|
|
||||||
|
let result = ready!(Pin::new(&mut self.notified).poll(cx));
|
||||||
|
let state = self
|
||||||
|
.state
|
||||||
|
.take()
|
||||||
|
.expect("`CustomCursorFuture` polled after completion");
|
||||||
|
|
||||||
|
Poll::Ready(result.map(|_| CustomCursor {
|
||||||
|
animation: self.animation,
|
||||||
|
state,
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct CursorHandler(Rc<RefCell<Inner>>);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Inner {
|
||||||
main_thread: MainThreadMarker,
|
main_thread: MainThreadMarker,
|
||||||
runner: WeakShared,
|
canvas: HtmlCanvasElement,
|
||||||
style: Style,
|
style: Style,
|
||||||
visible: bool,
|
visible: bool,
|
||||||
cursor: SelectedCursor,
|
cursor: SelectedCursor,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CursorHandler {
|
impl CursorHandler {
|
||||||
pub(crate) fn new(main_thread: MainThreadMarker, runner: WeakShared, style: Style) -> Self {
|
pub(crate) fn new(
|
||||||
Self {
|
main_thread: MainThreadMarker,
|
||||||
|
canvas: HtmlCanvasElement,
|
||||||
|
style: Style,
|
||||||
|
) -> Self {
|
||||||
|
Self(Rc::new(RefCell::new(Inner {
|
||||||
main_thread,
|
main_thread,
|
||||||
runner,
|
canvas,
|
||||||
style,
|
style,
|
||||||
visible: true,
|
visible: true,
|
||||||
cursor: SelectedCursor::default(),
|
cursor: SelectedCursor::default(),
|
||||||
}
|
})))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_cursor(&mut self, cursor: Cursor) {
|
pub fn set_cursor(&self, cursor: Cursor) {
|
||||||
|
let mut this = self.0.borrow_mut();
|
||||||
|
|
||||||
match cursor {
|
match cursor {
|
||||||
Cursor::Icon(icon) => {
|
Cursor::Icon(icon) => {
|
||||||
if let SelectedCursor::Icon(old_icon)
|
if let SelectedCursor::Icon(old_icon)
|
||||||
| SelectedCursor::ImageLoading {
|
| SelectedCursor::Loading {
|
||||||
previous: Previous::Icon(old_icon),
|
previous: Previous::Icon(old_icon),
|
||||||
..
|
..
|
||||||
} = &self.cursor
|
} = &this.cursor
|
||||||
{
|
{
|
||||||
if *old_icon == icon {
|
if *old_icon == icon {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.cursor = SelectedCursor::Icon(icon);
|
this.cursor = SelectedCursor::Icon(icon);
|
||||||
self.set_style();
|
this.set_style();
|
||||||
}
|
}
|
||||||
Cursor::Custom(cursor) => {
|
Cursor::Custom(cursor) => {
|
||||||
let cursor = cursor.inner;
|
let cursor = cursor.inner;
|
||||||
|
|
||||||
if let SelectedCursor::ImageLoading {
|
if let SelectedCursor::Loading {
|
||||||
cursor: old_cursor, ..
|
cursor: old_cursor, ..
|
||||||
}
|
}
|
||||||
| SelectedCursor::ImageReady(old_cursor) = &self.cursor
|
| SelectedCursor::Image(old_cursor)
|
||||||
|
| SelectedCursor::Animation {
|
||||||
|
cursor: old_cursor, ..
|
||||||
|
} = &this.cursor
|
||||||
{
|
{
|
||||||
if *old_cursor == cursor {
|
if *old_cursor == cursor {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut image = cursor.0.get(self.main_thread).borrow_mut();
|
let state = cursor.state.get(this.main_thread).borrow();
|
||||||
match image.deref_mut() {
|
|
||||||
ImageState::Loading(state) => {
|
match state.deref() {
|
||||||
*state = Some(self.runner.clone());
|
ImageState::Loading { notifier, .. } => {
|
||||||
drop(image);
|
let notified = notifier.notified();
|
||||||
self.cursor = SelectedCursor::ImageLoading {
|
let handle = DropAbortHandle::new(AbortHandle::new());
|
||||||
|
let task = Abortable::new(handle.handle(), {
|
||||||
|
let weak = Rc::downgrade(&self.0);
|
||||||
|
async move {
|
||||||
|
let _ = notified.await;
|
||||||
|
let handler = weak
|
||||||
|
.upgrade()
|
||||||
|
.expect("`CursorHandler` invalidated without aborting");
|
||||||
|
handler.borrow_mut().notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
wasm_bindgen_futures::spawn_local(async move {
|
||||||
|
let _ = task.await;
|
||||||
|
});
|
||||||
|
|
||||||
|
drop(state);
|
||||||
|
this.cursor = SelectedCursor::Loading {
|
||||||
cursor,
|
cursor,
|
||||||
previous: mem::take(&mut self.cursor).into(),
|
previous: mem::take(&mut this.cursor).into(),
|
||||||
|
_handle: handle,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
ImageState::Failed => log::error!("tried to load invalid cursor"),
|
ImageState::Failed(error) => {
|
||||||
ImageState::Ready { .. } => {
|
log::error!("trying to load custom cursor that has failed to load: {error}")
|
||||||
drop(image);
|
}
|
||||||
self.cursor = SelectedCursor::ImageReady(cursor);
|
ImageState::Image(_) => {
|
||||||
self.set_style();
|
drop(state);
|
||||||
|
this.cursor = SelectedCursor::Image(cursor);
|
||||||
|
this.set_style();
|
||||||
|
}
|
||||||
|
ImageState::Animation(animation) => {
|
||||||
|
let canvas: &CanvasAnimateExt = this.canvas.unchecked_ref();
|
||||||
|
let animation = canvas.animate_with_keyframe_animation_options(
|
||||||
|
Some(&animation.keyframes),
|
||||||
|
&animation.options,
|
||||||
|
);
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
if !this.visible {
|
||||||
|
animation.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.cursor = SelectedCursor::Animation {
|
||||||
|
animation: AnimationDropper(animation),
|
||||||
|
cursor,
|
||||||
|
};
|
||||||
|
this.set_style();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_cursor_visible(&mut self, visible: bool) {
|
pub fn set_cursor_visible(&self, visible: bool) {
|
||||||
if !visible && self.visible {
|
let mut this = self.0.borrow_mut();
|
||||||
self.visible = false;
|
|
||||||
self.style.set("cursor", "none");
|
if !visible && this.visible {
|
||||||
} else if visible && !self.visible {
|
this.visible = false;
|
||||||
self.visible = true;
|
this.style.set("cursor", "none");
|
||||||
self.set_style();
|
|
||||||
|
if let SelectedCursor::Animation { animation, .. } = &this.cursor {
|
||||||
|
animation.0.cancel();
|
||||||
}
|
}
|
||||||
}
|
} else if visible && !this.visible {
|
||||||
|
this.visible = true;
|
||||||
pub fn handle_cursor_ready(&mut self, result: Result<CustomCursorHandle, CustomCursorHandle>) {
|
this.set_style();
|
||||||
if let SelectedCursor::ImageLoading {
|
|
||||||
cursor: current_cursor,
|
|
||||||
..
|
|
||||||
} = &self.cursor
|
|
||||||
{
|
|
||||||
let current_cursor = Arc::downgrade(¤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")
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Ok(_) => {
|
|
||||||
self.cursor = SelectedCursor::ImageReady(cursor);
|
|
||||||
self.set_style();
|
|
||||||
}
|
|
||||||
Err(_) => self.cursor = previous.into(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Inner {
|
||||||
fn set_style(&self) {
|
fn set_style(&self) {
|
||||||
if self.visible {
|
if self.visible {
|
||||||
match &self.cursor {
|
match &self.cursor {
|
||||||
SelectedCursor::Icon(icon)
|
SelectedCursor::Icon(icon)
|
||||||
| SelectedCursor::ImageLoading {
|
| SelectedCursor::Loading {
|
||||||
previous: Previous::Icon(icon),
|
previous: Previous::Icon(icon),
|
||||||
..
|
..
|
||||||
} => self.style.set("cursor", icon.name()),
|
} => self.style.set("cursor", icon.name()),
|
||||||
SelectedCursor::ImageLoading {
|
SelectedCursor::Loading {
|
||||||
previous: Previous::Image(cursor),
|
previous: Previous::Image(cursor),
|
||||||
..
|
..
|
||||||
}
|
}
|
||||||
| SelectedCursor::ImageReady(cursor) => {
|
| SelectedCursor::Image(cursor) => {
|
||||||
if let ImageState::Ready { style, .. } =
|
match cursor.state.get(self.main_thread).borrow().deref() {
|
||||||
cursor.0.get(self.main_thread).borrow().deref()
|
ImageState::Image(Image { style, .. }) => self.style.set("cursor", style),
|
||||||
{
|
_ => unreachable!("found invalid saved state"),
|
||||||
self.style.set("cursor", style)
|
}
|
||||||
} else {
|
}
|
||||||
unreachable!("found invalid saved state")
|
SelectedCursor::Loading {
|
||||||
|
previous: Previous::Animation { animation, .. },
|
||||||
|
..
|
||||||
|
}
|
||||||
|
| SelectedCursor::Animation { animation, .. } => {
|
||||||
|
self.style.remove("cursor");
|
||||||
|
animation.0.play()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn notify(&mut self) {
|
||||||
|
let SelectedCursor::Loading {
|
||||||
|
cursor, previous, ..
|
||||||
|
} = mem::take(&mut self.cursor)
|
||||||
|
else {
|
||||||
|
unreachable!("found wrong state")
|
||||||
|
};
|
||||||
|
|
||||||
|
let state = cursor.state.get(self.main_thread).borrow();
|
||||||
|
match state.deref() {
|
||||||
|
ImageState::Image(_) => {
|
||||||
|
drop(state);
|
||||||
|
self.cursor = SelectedCursor::Image(cursor);
|
||||||
|
self.set_style();
|
||||||
|
}
|
||||||
|
ImageState::Animation(animation) => {
|
||||||
|
let canvas: &CanvasAnimateExt = self.canvas.unchecked_ref();
|
||||||
|
let animation = canvas.animate_with_keyframe_animation_options(
|
||||||
|
Some(&animation.keyframes),
|
||||||
|
&animation.options,
|
||||||
|
);
|
||||||
|
drop(state);
|
||||||
|
|
||||||
|
if !self.visible {
|
||||||
|
animation.cancel();
|
||||||
|
}
|
||||||
|
|
||||||
|
self.cursor = SelectedCursor::Animation {
|
||||||
|
animation: AnimationDropper(animation),
|
||||||
|
cursor,
|
||||||
|
};
|
||||||
|
self.set_style();
|
||||||
|
}
|
||||||
|
ImageState::Failed(error) => {
|
||||||
|
log::error!("custom cursor failed to load: {error}");
|
||||||
|
self.cursor = previous.into()
|
||||||
|
}
|
||||||
|
ImageState::Loading { .. } => unreachable!("notified without being ready"),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum SelectedCursor {
|
enum SelectedCursor {
|
||||||
Icon(CursorIcon),
|
Icon(CursorIcon),
|
||||||
ImageLoading {
|
Loading {
|
||||||
cursor: CustomCursor,
|
cursor: CustomCursor,
|
||||||
previous: Previous,
|
previous: Previous,
|
||||||
|
_handle: DropAbortHandle,
|
||||||
|
},
|
||||||
|
Image(CustomCursor),
|
||||||
|
Animation {
|
||||||
|
cursor: CustomCursor,
|
||||||
|
animation: AnimationDropper,
|
||||||
},
|
},
|
||||||
ImageReady(CustomCursor),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for SelectedCursor {
|
impl Default for SelectedCursor {
|
||||||
|
|
@ -250,52 +453,116 @@ impl From<Previous> for SelectedCursor {
|
||||||
fn from(previous: Previous) -> Self {
|
fn from(previous: Previous) -> Self {
|
||||||
match previous {
|
match previous {
|
||||||
Previous::Icon(icon) => Self::Icon(icon),
|
Previous::Icon(icon) => Self::Icon(icon),
|
||||||
Previous::Image(cursor) => Self::ImageReady(cursor),
|
Previous::Image(cursor) => Self::Image(cursor),
|
||||||
|
Previous::Animation { cursor, animation } => Self::Animation { cursor, animation },
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum Previous {
|
enum Previous {
|
||||||
Icon(CursorIcon),
|
Icon(CursorIcon),
|
||||||
Image(CustomCursor),
|
Image(CustomCursor),
|
||||||
|
Animation {
|
||||||
|
cursor: CustomCursor,
|
||||||
|
animation: AnimationDropper,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<SelectedCursor> for Previous {
|
impl From<SelectedCursor> for Previous {
|
||||||
fn from(value: SelectedCursor) -> Self {
|
fn from(value: SelectedCursor) -> Self {
|
||||||
match value {
|
match value {
|
||||||
SelectedCursor::Icon(icon) => Self::Icon(icon),
|
SelectedCursor::Icon(icon) => Self::Icon(icon),
|
||||||
SelectedCursor::ImageLoading { previous, .. } => previous,
|
SelectedCursor::Loading { previous, .. } => previous,
|
||||||
SelectedCursor::ImageReady(image) => Self::Image(image),
|
SelectedCursor::Image(image) => Self::Image(image),
|
||||||
|
SelectedCursor::Animation { cursor, animation } => {
|
||||||
|
Self::Animation { cursor, animation }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
enum ImageState {
|
enum ImageState {
|
||||||
Loading(Option<WeakShared>),
|
Loading {
|
||||||
Failed,
|
notifier: Notifier<Result<(), CustomCursorError>>,
|
||||||
Ready {
|
_handle: DropAbortHandle,
|
||||||
|
},
|
||||||
|
Failed(CustomCursorError),
|
||||||
|
Image(Image),
|
||||||
|
Animation(Animation),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Image {
|
||||||
style: String,
|
style: String,
|
||||||
_object_url: Option<ObjectUrl>,
|
_object_url: Option<ObjectUrl>,
|
||||||
_image: HtmlImageElement,
|
_image: HtmlImageElement,
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ImageState {
|
impl From<Image> for ImageState {
|
||||||
|
fn from(image: Image) -> Self {
|
||||||
|
Self::Image(image)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct Animation {
|
||||||
|
keyframes: Array,
|
||||||
|
options: KeyframeAnimationOptions,
|
||||||
|
_images: Vec<CustomCursor>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<Animation> for ImageState {
|
||||||
|
fn from(animation: Animation) -> Self {
|
||||||
|
Self::Animation(animation)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
enum UrlType {
|
||||||
|
Plain(String),
|
||||||
|
Object(ObjectUrl),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl UrlType {
|
||||||
|
fn url(&self) -> &str {
|
||||||
|
match &self {
|
||||||
|
UrlType::Plain(url) => url,
|
||||||
|
UrlType::Object(object_url) => &object_url.0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct ObjectUrl(String);
|
||||||
|
|
||||||
|
impl Drop for ObjectUrl {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
Url::revoke_object_url(&self.0).expect("unexpected exception in `URL.revokeObjectURL()`");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct AnimationDropper(WebAnimation);
|
||||||
|
|
||||||
|
impl Drop for AnimationDropper {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.0.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn from_rgba(
|
fn from_rgba(
|
||||||
main_thread: MainThreadMarker,
|
|
||||||
window: &Window,
|
window: &Window,
|
||||||
document: Document,
|
document: Document,
|
||||||
image: &CursorImage,
|
image: &CursorImage,
|
||||||
) -> CustomCursor {
|
) -> impl Future<Output = Result<Image, CustomCursorError>> {
|
||||||
// 1. Create an `ImageData` from the RGBA data.
|
// 1. Create an `ImageData` from the RGBA data.
|
||||||
// 2. Create an `ImageBitmap` from the `ImageData`.
|
// 2. Create an `ImageBitmap` from the `ImageData`.
|
||||||
// 3. Draw `ImageBitmap` on an `HTMLCanvasElement`.
|
// 3. Draw `ImageBitmap` on an `HTMLCanvasElement`.
|
||||||
// 4. Create a `Blob` from the `HTMLCanvasElement`.
|
// 4. Create a `Blob` from the `HTMLCanvasElement`.
|
||||||
// 5. Create an object URL from the `Blob`.
|
// 5. Create an object URL from the `Blob`.
|
||||||
// 6. Decode the image on an `HTMLImageElement` from the URL.
|
// 6. Decode the image on an `HTMLImageElement` from the URL.
|
||||||
// 7. Notify event loop if one is registered.
|
|
||||||
|
|
||||||
// 1. Create an `ImageData` from the RGBA data.
|
// 1. Create an `ImageData` from the RGBA data.
|
||||||
// Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223
|
// Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223
|
||||||
|
|
@ -303,7 +570,6 @@ impl ImageState {
|
||||||
// Can't share `SharedArrayBuffer` with `ImageData`.
|
// Can't share `SharedArrayBuffer` with `ImageData`.
|
||||||
let result = {
|
let result = {
|
||||||
use js_sys::{Uint8Array, Uint8ClampedArray};
|
use js_sys::{Uint8Array, Uint8ClampedArray};
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
|
||||||
use wasm_bindgen::JsValue;
|
use wasm_bindgen::JsValue;
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
|
|
@ -340,10 +606,6 @@ impl ImageState {
|
||||||
.expect("unexpected exception in `createImageBitmap()`"),
|
.expect("unexpected exception in `createImageBitmap()`"),
|
||||||
);
|
);
|
||||||
|
|
||||||
let this = CustomCursor::new(main_thread);
|
|
||||||
|
|
||||||
wasm_bindgen_futures::spawn_local({
|
|
||||||
let weak = Arc::downgrade(&this.0);
|
|
||||||
let CursorImage {
|
let CursorImage {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
|
|
@ -352,20 +614,11 @@ impl ImageState {
|
||||||
..
|
..
|
||||||
} = *image;
|
} = *image;
|
||||||
async move {
|
async move {
|
||||||
// Keep checking if all references are dropped between every `await` call.
|
|
||||||
if weak.strong_count() == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let bitmap: ImageBitmap = bitmap
|
let bitmap: ImageBitmap = bitmap
|
||||||
.await
|
.await
|
||||||
.expect("found invalid state in `ImageData`")
|
.expect("found invalid state in `ImageData`")
|
||||||
.unchecked_into();
|
.unchecked_into();
|
||||||
|
|
||||||
if weak.strong_count() == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let canvas: HtmlCanvasElement = document
|
let canvas: HtmlCanvasElement = document
|
||||||
.create_element("canvas")
|
.create_element("canvas")
|
||||||
.expect("invalid tag name")
|
.expect("invalid tag name")
|
||||||
|
|
@ -417,27 +670,8 @@ impl ImageState {
|
||||||
.await;
|
.await;
|
||||||
drop(canvas);
|
drop(canvas);
|
||||||
|
|
||||||
if weak.strong_count() == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let Some(blob) = blob else {
|
let Some(blob) = blob else {
|
||||||
log::error!("creating object URL from custom cursor failed");
|
return Err(CustomCursorError::Blob);
|
||||||
let Some(this) = weak.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let mut this = this.get(main_thread).borrow_mut();
|
|
||||||
let ImageState::Loading(runner) = this.deref_mut() else {
|
|
||||||
unreachable!("found invalid state");
|
|
||||||
};
|
|
||||||
let runner = runner.take();
|
|
||||||
*this = ImageState::Failed;
|
|
||||||
|
|
||||||
if let Some(runner) = runner.and_then(|weak| weak.upgrade()) {
|
|
||||||
runner.send_event(EventWrapper::CursorReady(Err(CustomCursorHandle(weak))));
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 5. Create an object URL from the `Blob`.
|
// 5. Create an object URL from the `Blob`.
|
||||||
|
|
@ -445,107 +679,122 @@ impl ImageState {
|
||||||
.expect("unexpected exception in `URL.createObjectURL()`");
|
.expect("unexpected exception in `URL.createObjectURL()`");
|
||||||
let url = UrlType::Object(ObjectUrl(url));
|
let url = UrlType::Object(ObjectUrl(url));
|
||||||
|
|
||||||
Self::decode(main_thread, weak, url, hotspot_x, hotspot_y).await;
|
from_url(url, hotspot_x, hotspot_y).await
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
|
||||||
this
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn from_url(
|
async fn from_url(
|
||||||
main_thread: MainThreadMarker,
|
|
||||||
url: String,
|
|
||||||
hotspot_x: u16,
|
|
||||||
hotspot_y: u16,
|
|
||||||
) -> CustomCursor {
|
|
||||||
let this = CustomCursor::new(main_thread);
|
|
||||||
wasm_bindgen_futures::spawn_local(Self::decode(
|
|
||||||
main_thread,
|
|
||||||
Arc::downgrade(&this.0),
|
|
||||||
UrlType::Plain(url),
|
|
||||||
hotspot_x,
|
|
||||||
hotspot_y,
|
|
||||||
));
|
|
||||||
|
|
||||||
this
|
|
||||||
}
|
|
||||||
|
|
||||||
async fn decode(
|
|
||||||
main_thread: MainThreadMarker,
|
|
||||||
weak: Weak<MainThreadSafe<RefCell<ImageState>>>,
|
|
||||||
url: UrlType,
|
url: UrlType,
|
||||||
hotspot_x: u16,
|
hotspot_x: u16,
|
||||||
hotspot_y: u16,
|
hotspot_y: u16,
|
||||||
) {
|
) -> Result<Image, CustomCursorError> {
|
||||||
if weak.strong_count() == 0 {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. Decode the image on an `HTMLImageElement` from the URL.
|
// 6. Decode the image on an `HTMLImageElement` from the URL.
|
||||||
let image =
|
let image = HtmlImageElement::new().expect("unexpected exception in `new HtmlImageElement`");
|
||||||
HtmlImageElement::new().expect("unexpected exception in `new HtmlImageElement`");
|
|
||||||
image.set_src(url.url());
|
image.set_src(url.url());
|
||||||
let result = JsFuture::from(image.decode()).await;
|
let result = JsFuture::from(image.decode()).await;
|
||||||
|
|
||||||
let Some(this) = weak.upgrade() else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
let mut this = this.get(main_thread).borrow_mut();
|
|
||||||
|
|
||||||
let ImageState::Loading(runner) = this.deref_mut() else {
|
|
||||||
unreachable!("found invalid state");
|
|
||||||
};
|
|
||||||
let runner = runner.take();
|
|
||||||
|
|
||||||
if let Err(error) = result {
|
if let Err(error) = result {
|
||||||
log::error!("decoding custom cursor failed: {error:?}");
|
debug_assert!(error.has_type::<DomException>());
|
||||||
*this = ImageState::Failed;
|
let error: DomException = error.unchecked_into();
|
||||||
|
debug_assert_eq!(error.name(), "EncodingError");
|
||||||
|
let error = error.message();
|
||||||
|
|
||||||
if let Some(runner) = runner.and_then(|weak| weak.upgrade()) {
|
return Err(CustomCursorError::Decode(error));
|
||||||
runner.send_event(EventWrapper::CursorReady(Err(CustomCursorHandle(weak))));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return;
|
Ok(Image {
|
||||||
}
|
|
||||||
|
|
||||||
*this = ImageState::Ready {
|
|
||||||
style: format!("url({}) {hotspot_x} {hotspot_y}, auto", url.url()),
|
style: format!("url({}) {hotspot_x} {hotspot_y}, auto", url.url()),
|
||||||
_object_url: match url {
|
_object_url: match url {
|
||||||
UrlType::Plain(_) => None,
|
UrlType::Plain(_) => None,
|
||||||
UrlType::Object(object_url) => Some(object_url),
|
UrlType::Object(object_url) => Some(object_url),
|
||||||
},
|
},
|
||||||
_image: image,
|
_image: image,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::await_holding_refcell_ref)] // false-positive
|
||||||
|
async fn from_animation(
|
||||||
|
main_thread: MainThreadMarker,
|
||||||
|
duration: Duration,
|
||||||
|
cursors: impl Iterator<Item = CustomCursor> + ExactSizeIterator,
|
||||||
|
) -> Result<Animation, CustomCursorError> {
|
||||||
|
let keyframes = Array::new();
|
||||||
|
let mut images = Vec::with_capacity(cursors.len());
|
||||||
|
|
||||||
|
for cursor in cursors {
|
||||||
|
let state = cursor.state.get(main_thread).borrow();
|
||||||
|
|
||||||
|
match state.deref() {
|
||||||
|
ImageState::Loading { notifier, .. } => {
|
||||||
|
let notified = notifier.notified();
|
||||||
|
drop(state);
|
||||||
|
notified.await?;
|
||||||
|
}
|
||||||
|
ImageState::Failed(error) => return Err(error.clone()),
|
||||||
|
ImageState::Image(_) => drop(state),
|
||||||
|
ImageState::Animation(_) => unreachable!("check in `CustomCursorBuilder` failed"),
|
||||||
|
}
|
||||||
|
|
||||||
|
let state = cursor.state.get(main_thread).borrow();
|
||||||
|
let style = match state.deref() {
|
||||||
|
ImageState::Image(Image { style, .. }) => style,
|
||||||
|
_ => unreachable!("found invalid state"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// 7. Notify event loop if one is registered.
|
let keyframe: Keyframe = Object::new().unchecked_into();
|
||||||
if let Some(runner) = runner.and_then(|weak| weak.upgrade()) {
|
keyframe.set_cursor(style);
|
||||||
runner.send_event(EventWrapper::CursorReady(Ok(CustomCursorHandle(weak))));
|
keyframes.push(&keyframe);
|
||||||
}
|
drop(state);
|
||||||
}
|
|
||||||
|
images.push(cursor);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
keyframes.push(&keyframes.get(0));
|
||||||
pub struct CustomCursorHandle(Weak<MainThreadSafe<RefCell<ImageState>>>);
|
|
||||||
|
|
||||||
enum UrlType {
|
let options: KeyframeAnimationOptions = Object::new().unchecked_into();
|
||||||
Plain(String),
|
options.set_duration(duration.as_millis() as f64);
|
||||||
Object(ObjectUrl),
|
options.set_iterations(f64::INFINITY);
|
||||||
|
|
||||||
|
Ok(Animation {
|
||||||
|
keyframes,
|
||||||
|
options,
|
||||||
|
_images: images,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
impl UrlType {
|
#[wasm_bindgen]
|
||||||
fn url(&self) -> &str {
|
extern "C" {
|
||||||
match &self {
|
type CanvasAnimateExt;
|
||||||
UrlType::Plain(url) => url,
|
|
||||||
UrlType::Object(object_url) => &object_url.0,
|
#[wasm_bindgen(method, js_name = animate)]
|
||||||
}
|
fn animate_with_keyframe_animation_options(
|
||||||
}
|
this: &CanvasAnimateExt,
|
||||||
}
|
keyframes: Option<&Object>,
|
||||||
|
options: &KeyframeAnimationOptions,
|
||||||
|
) -> WebAnimation;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
struct ObjectUrl(String);
|
type WebAnimation;
|
||||||
|
|
||||||
impl Drop for ObjectUrl {
|
#[wasm_bindgen(method)]
|
||||||
fn drop(&mut self) {
|
fn cancel(this: &WebAnimation);
|
||||||
Url::revoke_object_url(&self.0).expect("unexpected exception in `URL.revokeObjectURL()`");
|
|
||||||
}
|
#[wasm_bindgen(method)]
|
||||||
|
fn play(this: &WebAnimation);
|
||||||
|
|
||||||
|
#[wasm_bindgen(extends = Object)]
|
||||||
|
type Keyframe;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, setter, js_name = cursor)]
|
||||||
|
fn set_cursor(this: &Keyframe, value: &str);
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
#[wasm_bindgen(extends = Object)]
|
||||||
|
type KeyframeAnimationOptions;
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, setter, js_name = duration)]
|
||||||
|
fn set_duration(this: &KeyframeAnimationOptions, value: f64);
|
||||||
|
|
||||||
|
#[wasm_bindgen(method, setter, js_name = iterations)]
|
||||||
|
fn set_iterations(this: &KeyframeAnimationOptions, value: f64);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
use super::super::cursor::CustomCursorHandle;
|
|
||||||
use super::super::main_thread::MainThreadMarker;
|
use super::super::main_thread::MainThreadMarker;
|
||||||
use super::super::DeviceId;
|
use super::super::DeviceId;
|
||||||
use super::{backend, state::State};
|
use super::{backend, state::State};
|
||||||
|
|
@ -140,16 +139,6 @@ impl Runner {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EventWrapper::CursorReady(result) => {
|
|
||||||
for (_, canvas, _) in runner.0.all_canvases.borrow().deref() {
|
|
||||||
if let Some(canvas) = canvas.upgrade() {
|
|
||||||
canvas
|
|
||||||
.borrow_mut()
|
|
||||||
.cursor
|
|
||||||
.handle_cursor_ready(result.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -822,19 +811,6 @@ impl Shared {
|
||||||
pub(crate) fn waker(&self) -> Waker<Weak<Execution>> {
|
pub(crate) fn waker(&self) -> Waker<Weak<Execution>> {
|
||||||
self.0.proxy_spawner.waker()
|
self.0.proxy_spawner.waker()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn weak(&self) -> WeakShared {
|
|
||||||
WeakShared(Rc::downgrade(&self.0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub(crate) struct WeakShared(Weak<Execution>);
|
|
||||||
|
|
||||||
impl WeakShared {
|
|
||||||
pub(crate) fn upgrade(&self) -> Option<Shared> {
|
|
||||||
self.0.upgrade().map(Shared)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) enum EventWrapper {
|
pub(crate) enum EventWrapper {
|
||||||
|
|
@ -844,7 +820,6 @@ pub(crate) enum EventWrapper {
|
||||||
size: PhysicalSize<u32>,
|
size: PhysicalSize<u32>,
|
||||||
scale: f64,
|
scale: f64,
|
||||||
},
|
},
|
||||||
CursorReady(Result<CustomCursorHandle, CustomCursorHandle>),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Event<()>> for EventWrapper {
|
impl From<Event<()>> for EventWrapper {
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ use std::sync::OnceLock;
|
||||||
use wasm_bindgen::prelude::wasm_bindgen;
|
use wasm_bindgen::prelude::wasm_bindgen;
|
||||||
use wasm_bindgen::{JsCast, JsValue};
|
use wasm_bindgen::{JsCast, JsValue};
|
||||||
|
|
||||||
use super::r#async::{self, AsyncSender};
|
use super::r#async::{self, Sender};
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static MAIN_THREAD: bool = {
|
static MAIN_THREAD: bool = {
|
||||||
|
|
@ -85,7 +85,7 @@ impl<T> Drop for MainThreadSafe<T> {
|
||||||
unsafe impl<T> Send for MainThreadSafe<T> {}
|
unsafe impl<T> Send for MainThreadSafe<T> {}
|
||||||
unsafe impl<T> Sync for MainThreadSafe<T> {}
|
unsafe impl<T> Sync for MainThreadSafe<T> {}
|
||||||
|
|
||||||
static DROP_HANDLER: OnceLock<AsyncSender<DropBox>> = OnceLock::new();
|
static DROP_HANDLER: OnceLock<Sender<DropBox>> = OnceLock::new();
|
||||||
|
|
||||||
struct DropBox(#[allow(dead_code)] Box<dyn Any>);
|
struct DropBox(#[allow(dead_code)] Box<dyn Any>);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -43,3 +43,4 @@ pub(crate) use crate::icon::NoIcon as PlatformIcon;
|
||||||
pub(crate) use crate::platform_impl::Fullscreen;
|
pub(crate) use crate::platform_impl::Fullscreen;
|
||||||
pub(crate) use cursor::CustomCursor as PlatformCustomCursor;
|
pub(crate) use cursor::CustomCursor as PlatformCustomCursor;
|
||||||
pub(crate) use cursor::CustomCursorBuilder as PlatformCustomCursorBuilder;
|
pub(crate) use cursor::CustomCursorBuilder as PlatformCustomCursorBuilder;
|
||||||
|
pub(crate) use cursor::CustomCursorFuture;
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,6 @@ use crate::platform_impl::{OsError, PlatformSpecificWindowBuilderAttributes};
|
||||||
use crate::window::{WindowAttributes, WindowId as RootWindowId};
|
use crate::window::{WindowAttributes, WindowId as RootWindowId};
|
||||||
|
|
||||||
use super::super::cursor::CursorHandler;
|
use super::super::cursor::CursorHandler;
|
||||||
use super::super::event_loop::runner::WeakShared;
|
|
||||||
use super::super::main_thread::MainThreadMarker;
|
use super::super::main_thread::MainThreadMarker;
|
||||||
use super::super::WindowId;
|
use super::super::WindowId;
|
||||||
use super::animation_frame::AnimationFrameHandler;
|
use super::animation_frame::AnimationFrameHandler;
|
||||||
|
|
@ -71,7 +70,6 @@ pub struct Style {
|
||||||
impl Canvas {
|
impl Canvas {
|
||||||
pub(crate) fn create(
|
pub(crate) fn create(
|
||||||
main_thread: MainThreadMarker,
|
main_thread: MainThreadMarker,
|
||||||
runner: WeakShared,
|
|
||||||
id: WindowId,
|
id: WindowId,
|
||||||
window: web_sys::Window,
|
window: web_sys::Window,
|
||||||
document: Document,
|
document: Document,
|
||||||
|
|
@ -111,7 +109,7 @@ impl Canvas {
|
||||||
|
|
||||||
let style = Style::new(&window, &canvas);
|
let style = Style::new(&window, &canvas);
|
||||||
|
|
||||||
let cursor = CursorHandler::new(main_thread, runner, style.clone());
|
let cursor = CursorHandler::new(main_thread, canvas.clone(), style.clone());
|
||||||
|
|
||||||
let common = Common {
|
let common = Common {
|
||||||
window: window.clone(),
|
window: window.clone(),
|
||||||
|
|
|
||||||
|
|
@ -39,7 +39,6 @@ impl Window {
|
||||||
let document = target.runner.document();
|
let document = target.runner.document();
|
||||||
let canvas = backend::Canvas::create(
|
let canvas = backend::Canvas::create(
|
||||||
target.runner.main_thread(),
|
target.runner.main_thread(),
|
||||||
target.runner.weak(),
|
|
||||||
id,
|
id,
|
||||||
window.clone(),
|
window.clone(),
|
||||||
document.clone(),
|
document.clone(),
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue