Skip to content

Commit

Permalink
Create custom cursor with directly with event loop
Browse files Browse the repository at this point in the history
Replace the `CustomCursorBuilder` with the `CustomCursorSource` and
perform the loading of the cursor via the
`EventLoop::create_custom_cursor` instead of passing it to the builder
itself.

This follows the `EventLoop::create_window` API.
  • Loading branch information
kchibisov committed Feb 19, 2024
1 parent feb9f95 commit ec4fafe
Show file tree
Hide file tree
Showing 26 changed files with 213 additions and 175 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ Unreleased` header.
- On Windows, macOS, X11, Wayland and Web, implement setting images as cursors. See the `custom_cursors.rs` example.
- **Breaking:** Remove `Window::set_cursor_icon`
- Add `WindowBuilder::with_cursor` and `Window::set_cursor` which takes a `CursorIcon` or `CustomCursor`
- 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.
- Add `{Active,}EventLoop::create_custom_cursor` to load custom cursor image sources.
- 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
32 changes: 14 additions & 18 deletions examples/window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ use winit::event::{MouseButton, MouseScrollDelta};
use winit::event_loop::{ActiveEventLoop, EventLoop};
use winit::keyboard::{Key, ModifiersState};
use winit::window::{
Cursor, CursorGrabMode, CustomCursor, Fullscreen, Icon, ResizeDirection, Theme,
Cursor, CursorGrabMode, CustomCursor, CustomCursorSource, Fullscreen, Icon, ResizeDirection,
Theme,
};
use winit::window::{Window, WindowId};

Expand Down Expand Up @@ -57,7 +58,6 @@ fn main() -> Result<(), Box<dyn Error>> {
Event::Resumed => {
println!("Resumed the event loop");
state.dump_monitors(event_loop);
state.load_custom_cursors(event_loop);

// Create initial window.
state
Expand Down Expand Up @@ -108,11 +108,11 @@ struct Application {
}

impl Application {
fn new<T>(_event_loop: &EventLoop<T>) -> Self {
fn new<T>(event_loop: &EventLoop<T>) -> Self {
// SAFETY: the context is dropped inside the loop, since the state we're using
// is moved inside the closure.
#[cfg(not(any(android_platform, ios_platform)))]
let context = unsafe { Context::from_raw(_event_loop.raw_display_handle()).unwrap() };
let context = unsafe { Context::from_raw(event_loop.raw_display_handle()).unwrap() };

// You'll have to choose an icon size at your own discretion. On X11, the desired size varies
// by WM, and on Windows, you still have to account for screen scaling. Here we use 32px,
Expand All @@ -122,12 +122,19 @@ impl Application {

let icon = load_icon(Path::new(path));

println!("Loading cursor assets");
let custom_cursors = vec![
event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/cross.png"))),
event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/cross2.png"))),
event_loop.create_custom_cursor(decode_cursor(include_bytes!("data/gradient.png"))),
];

Self {
#[cfg(not(any(android_platform, ios_platform)))]
context,
custom_cursors,
icon,
windows: Default::default(),
custom_cursors: Vec::new(),
}
}

Expand Down Expand Up @@ -411,15 +418,6 @@ impl Application {
}
}

fn load_custom_cursors(&mut self, event_loop: &ActiveEventLoop) {
println!("Loading cursor assets");
self.custom_cursors = vec![
decode_cursor(include_bytes!("data/cross.png"), event_loop),
decode_cursor(include_bytes!("data/cross2.png"), event_loop),
decode_cursor(include_bytes!("data/gradient.png"), event_loop),
];
}

/// Process the key binding.
fn process_key_binding(key: &str, mods: &ModifiersState) -> Option<Action> {
KEY_BINDINGS.iter().find_map(|binding| {
Expand Down Expand Up @@ -852,14 +850,12 @@ impl fmt::Display for Action {
}
}

fn decode_cursor(bytes: &[u8], window_target: &ActiveEventLoop) -> CustomCursor {
fn decode_cursor(bytes: &[u8]) -> CustomCursorSource {
let img = image::load_from_memory(bytes).unwrap().to_rgba8();
let samples = img.into_flat_samples();
let (_, w, h) = samples.extents();
let (w, h) = (w as u16, h as u16);
let builder = CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap();

builder.build(window_target)
CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap()
}

fn load_icon(path: &Path) -> Icon {
Expand Down
51 changes: 15 additions & 36 deletions src/cursor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,7 @@ use std::{error::Error, hash::Hash};

use cursor_icon::CursorIcon;

use crate::event_loop::ActiveEventLoop;
use crate::platform_impl::{self, PlatformCustomCursor, PlatformCustomCursorBuilder};
use crate::platform_impl::{PlatformCustomCursor, PlatformCustomCursorSource};

/// The maximum width and height for a cursor when using [`CustomCursor::from_rgba`].
pub const MAX_CURSOR_SIZE: u16 = 2048;
Expand Down Expand Up @@ -59,15 +58,15 @@ impl From<CustomCursor> for Cursor {
/// let rgba = vec![255; (w * h * 4) as usize];
///
/// #[cfg(not(target_family = "wasm"))]
/// let builder = CustomCursor::from_rgba(rgba, w, h, w / 2, h / 2).unwrap();
/// let source = CustomCursor::from_rgba(rgba, w, h, w / 2, h / 2).unwrap();
///
/// #[cfg(target_family = "wasm")]
/// let builder = {
/// let source = {
/// use winit::platform::web::CustomCursorExtWebSys;
/// CustomCursor::from_url(String::from("http://localhost:3000/cursor.png"), 0, 0)
/// };
///
/// let custom_cursor = builder.build(&event_loop);
/// let custom_cursor = event_loop.create_custom_cursor(source);
///
/// window.set_cursor(custom_cursor.clone());
/// # }
Expand All @@ -88,9 +87,9 @@ impl CustomCursor {
height: u16,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<CustomCursorBuilder, BadImage> {
Ok(CustomCursorBuilder {
inner: PlatformCustomCursorBuilder::from_rgba(
) -> Result<CustomCursorSource, BadImage> {
Ok(CustomCursorSource {
inner: PlatformCustomCursorSource::from_rgba(
rgba.into(),
width,
height,
Expand All @@ -101,20 +100,12 @@ impl CustomCursor {
}
}

/// Builds a [`CustomCursor`].
/// Source for [`CustomCursor`].
///
/// See [`CustomCursor`] for more details.
#[derive(Debug)]
pub struct CustomCursorBuilder {
pub(crate) inner: PlatformCustomCursorBuilder,
}

impl CustomCursorBuilder {
pub fn build(self, window_target: &ActiveEventLoop) -> CustomCursor {
CustomCursor {
inner: PlatformCustomCursor::build(self.inner, &window_target.p),
}
}
pub struct CustomCursorSource {
pub(crate) inner: PlatformCustomCursorSource,
}

/// An error produced when using [`CustomCursor::from_rgba`] with invalid arguments.
Expand Down Expand Up @@ -175,12 +166,14 @@ impl fmt::Display for BadImage {

impl Error for BadImage {}

/// Platforms export this directly as `PlatformCustomCursorBuilder` if they need to only work with images.
/// Platforms export this directly as `PlatformCustomCursorSource` if they need to only work with
/// images.
#[allow(dead_code)]
#[derive(Debug)]
pub(crate) struct OnlyCursorImageBuilder(pub(crate) CursorImage);
pub(crate) struct OnlyCursorImageSource(pub(crate) CursorImage);

#[allow(dead_code)]
impl OnlyCursorImageBuilder {
impl OnlyCursorImageSource {
pub(crate) fn from_rgba(
rgba: Vec<u8>,
width: u16,
Expand Down Expand Up @@ -210,16 +203,6 @@ impl PartialEq for OnlyCursorImage {

impl Eq for OnlyCursorImage {}

#[allow(dead_code)]
impl OnlyCursorImage {
pub(crate) fn build(
builder: OnlyCursorImageBuilder,
_: &platform_impl::ActiveEventLoop,
) -> Self {
Self(Arc::new(builder.0))
}
}

#[derive(Debug)]
#[allow(dead_code)]
pub(crate) struct CursorImage {
Expand Down Expand Up @@ -294,8 +277,4 @@ impl NoCustomCursor {
CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?;
Ok(Self)
}

fn build(self, _: &platform_impl::ActiveEventLoop) -> NoCustomCursor {
self
}
}
15 changes: 14 additions & 1 deletion src/event_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ use std::time::{Duration, Instant};
use web_time::{Duration, Instant};

use crate::error::{EventLoopError, OsError};
use crate::window::{Window, WindowAttributes};
use crate::window::{CustomCursor, CustomCursorSource, Window, WindowAttributes};
use crate::{event::Event, monitor::MonitorHandle, platform_impl};

/// Provides a way to retrieve events from the system and from the windows that were registered to
Expand Down Expand Up @@ -299,6 +299,14 @@ impl<T> EventLoop<T> {
platform_impl::Window::new(&self.event_loop.window_target().p, window_attributes)?;
Ok(Window { window })
}

/// Create custom cursor.
pub fn create_custom_cursor(&self, custom_cursor: CustomCursorSource) -> CustomCursor {
self.event_loop
.window_target()
.p
.create_custom_cursor(custom_cursor)
}
}

#[cfg(feature = "rwh_06")]
Expand Down Expand Up @@ -360,6 +368,11 @@ impl ActiveEventLoop {
Ok(Window { window })
}

/// Create custom cursor.
pub fn create_custom_cursor(&self, custom_cursor: CustomCursorSource) -> CustomCursor {
self.p.create_custom_cursor(custom_cursor)
}

#[inline]
pub fn available_monitors(&self) -> impl Iterator<Item = MonitorHandle> {
#[allow(clippy::useless_conversion)] // false positive on some platforms
Expand Down
46 changes: 20 additions & 26 deletions src/platform/web.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,12 @@ use std::time::Duration;
#[cfg(web_platform)]
use web_sys::HtmlCanvasElement;

use crate::cursor::CustomCursorBuilder;
use crate::cursor::CustomCursorSource;
use crate::event::Event;
use crate::event_loop::{ActiveEventLoop, EventLoop};
#[cfg(web_platform)]
use crate::platform_impl::CustomCursorFuture as PlatformCustomCursorFuture;
use crate::platform_impl::{PlatformCustomCursor, PlatformCustomCursorBuilder};
use crate::platform_impl::PlatformCustomCursorSource;
use crate::window::{CustomCursor, Window, WindowAttributes};

#[cfg(not(web_platform))]
Expand Down Expand Up @@ -200,9 +200,18 @@ pub trait ActiveEventLoopExtWebSys {
///
/// [`ControlFlow::Poll`]: crate::event_loop::ControlFlow::Poll
fn poll_strategy(&self) -> PollStrategy;

/// Async version of [`ActiveEventLoop::create_custom_cursor()`] which waits until the
/// cursor has completely finished loading.
fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture;
}

impl ActiveEventLoopExtWebSys for ActiveEventLoop {
#[inline]
fn create_custom_cursor_async(&self, source: CustomCursorSource) -> CustomCursorFuture {
self.p.create_custom_cursor_async(source)
}

#[inline]
fn set_poll_strategy(&self, strategy: PollStrategy) {
self.p.set_poll_strategy(strategy);
Expand Down Expand Up @@ -249,24 +258,24 @@ pub trait CustomCursorExtWebSys {
/// 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;
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorSource;

/// 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>;
) -> Result<CustomCursorSource, 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 {
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> CustomCursorSource {
CustomCursorSource {
inner: PlatformCustomCursorSource::Url {
url,
hotspot_x,
hotspot_y,
Expand All @@ -277,7 +286,7 @@ impl CustomCursorExtWebSys for CustomCursor {
fn from_animation(
duration: Duration,
cursors: Vec<CustomCursor>,
) -> Result<CustomCursorBuilder, BadAnimation> {
) -> Result<CustomCursorSource, BadAnimation> {
if cursors.is_empty() {
return Err(BadAnimation::Empty);
}
Expand All @@ -286,8 +295,8 @@ impl CustomCursorExtWebSys for CustomCursor {
return Err(BadAnimation::Animation);
}

Ok(CustomCursorBuilder {
inner: PlatformCustomCursorBuilder::Animation { duration, cursors },
Ok(CustomCursorSource {
inner: PlatformCustomCursorSource::Animation { duration, cursors },
})
}
}
Expand All @@ -312,26 +321,11 @@ impl fmt::Display for BadAnimation {

impl Error for BadAnimation {}

pub trait CustomCursorBuilderExtWebSys {
/// Async version of [`CustomCursorBuilder::build()`] which waits until the
/// cursor has completely finished loading.
fn build_async(self, window_target: &ActiveEventLoop) -> CustomCursorFuture;
}

impl CustomCursorBuilderExtWebSys for CustomCursorBuilder {
fn build_async(self, window_target: &ActiveEventLoop) -> CustomCursorFuture {
CustomCursorFuture(PlatformCustomCursor::build_async(
self.inner,
&window_target.p,
))
}
}

#[cfg(not(web_platform))]
struct PlatformCustomCursorFuture;

#[derive(Debug)]
pub struct CustomCursorFuture(PlatformCustomCursorFuture);
pub struct CustomCursorFuture(pub(crate) PlatformCustomCursorFuture);

impl Future for CustomCursorFuture {
type Output = Result<CustomCursor, CustomCursorError>;
Expand Down
Loading

0 comments on commit ec4fafe

Please sign in to comment.