Skip to content

Commit

Permalink
Web: improve custom cursor handling and add animated cursors (#3384)
Browse files Browse the repository at this point in the history
  • Loading branch information
daxpedda authored Jan 12, 2024
1 parent bdeb257 commit 169cd39
Show file tree
Hide file tree
Showing 18 changed files with 1,078 additions and 412 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ Unreleased` header.
- Add `CustomCursor`
- Add `CustomCursor::from_rgba` to allow creating cursor images from RGBA data.
- Add `CustomCursorExtWebSys::from_url` to allow loading cursor images from URLs.
- Add `CustomCursorExtWebSys::from_animation` to allow creating animated cursors from other `CustomCursor`s.
- On macOS, add services menu.
- **Breaking:** On Web, remove queuing fullscreen request in absence of transient activation.
- On Web, fix setting cursor icon overriding cursor visibility.
Expand Down
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ features = [
'console',
'CssStyleDeclaration',
'Document',
'DomException',
'DomRect',
'DomRectReadOnly',
'Element',
Expand Down Expand Up @@ -240,12 +241,16 @@ features = [
]

[target.'cfg(target_family = "wasm")'.dependencies]
atomic-waker = "1"
js-sys = "0.3.64"
pin-project = "1"
wasm-bindgen = "0.2"
wasm-bindgen-futures = "0.4"
web-time = "0.2"

[target.'cfg(all(target_family = "wasm", target_feature = "atomics"))'.dependencies]
atomic-waker = "1"
concurrent-queue = { version = "2", default-features = false }

[target.'cfg(target_family = "wasm")'.dev-dependencies]
console_log = "1"
web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] }
Expand Down
48 changes: 48 additions & 0 deletions examples/custom_cursors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,15 @@ use winit::{
keyboard::Key,
window::{CursorIcon, CustomCursor, WindowBuilder},
};
#[cfg(wasm_platform)]
use {
std::sync::atomic::{AtomicU64, Ordering},
std::time::Duration,
winit::platform::web::CustomCursorExtWebSys,
};

#[cfg(wasm_platform)]
static COUNTER: AtomicU64 = AtomicU64::new(0);

fn decode_cursor<T>(bytes: &[u8], window_target: &EventLoopWindowTarget<T>) -> CustomCursor {
let img = image::load_from_memory(bytes).unwrap().to_rgba8();
Expand Down Expand Up @@ -74,6 +83,45 @@ fn main() -> Result<(), impl std::error::Error> {
log::debug!("Setting cursor visibility to {:?}", cursor_visible);
window.set_cursor_visible(cursor_visible);
}
#[cfg(wasm_platform)]
Key::Character("4") => {
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 => {
Expand Down
123 changes: 116 additions & 7 deletions src/platform/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,17 +27,24 @@
//! [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border
//! [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding

use crate::cursor::CustomCursorBuilder;
use crate::event::Event;
use crate::event_loop::EventLoop;
use crate::event_loop::EventLoopWindowTarget;
use crate::platform_impl::PlatformCustomCursorBuilder;
use crate::window::CustomCursor;
use crate::window::{Window, WindowBuilder};
use std::error::Error;
use std::fmt::{self, Display, Formatter};
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::time::Duration;

#[cfg(wasm_platform)]
use web_sys::HtmlCanvasElement;

use crate::cursor::CustomCursorBuilder;
use crate::event::Event;
use crate::event_loop::{EventLoop, EventLoopWindowTarget};
#[cfg(wasm_platform)]
use crate::platform_impl::CustomCursorFuture as PlatformCustomCursorFuture;
use crate::platform_impl::{PlatformCustomCursor, PlatformCustomCursorBuilder};
use crate::window::{CustomCursor, Window, WindowBuilder};

#[cfg(not(wasm_platform))]
#[doc(hidden)]
pub struct HtmlCanvasElement;
Expand Down Expand Up @@ -234,15 +241,29 @@ pub enum PollStrategy {
}

pub trait CustomCursorExtWebSys {
/// Returns if this cursor is an animation.
fn is_animation(&self) -> bool;

/// Creates a new cursor from a URL pointing to an image.
/// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url),
/// but browser support for image formats is inconsistent. Using [PNG] is recommended.
///
/// [PNG]: https://en.wikipedia.org/wiki/PNG
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder;

/// Crates a new animated cursor from multiple [`CustomCursor`]s.
/// Supplied `cursors` can't be empty or other animations.
fn from_animation(
duration: Duration,
cursors: Vec<CustomCursor>,
) -> Result<CustomCursorBuilder, BadAnimation>;
}

impl CustomCursorExtWebSys for CustomCursor {
fn is_animation(&self) -> bool {
self.inner.animation
}

fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorBuilder {
CustomCursorBuilder {
inner: PlatformCustomCursorBuilder::Url {
Expand All @@ -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 changes: 102 additions & 0 deletions src/platform_impl/web/async/abortable.rs
Original file line number Diff line number Diff line change
@@ -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 changes: 35 additions & 0 deletions src/platform_impl/web/async/atomic_waker.rs
Original file line number Diff line number Diff line change
@@ -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 {}
Loading

0 comments on commit 169cd39

Please sign in to comment.