Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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