From af93167237e37a701ccb933571f66336a031ac10 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Sat, 16 Dec 2023 22:02:17 +0200 Subject: [PATCH] feat(all): Custom cursor images for all desktop platforms There seems to be many PRs relating to this issue, but they don't include all platforms and for some reason lost steam. This PR again tries to make this feature happen, and does it for all desktop platforms (x11, wayland, macos, windows, web). I think the best user of this feature and the reason I'm doing this is Bevy and game engines in general. There non laggy hardware cursors with custom images are very important. Game devs also like their PNGs so supporting platform native cursor files is not that important, but I guess could be added too. Co-authored-by: daxpedda Co-authored-by: Mads Marquart Co-authored-by: Kirill Chibisov --- CHANGELOG.md | 5 + Cargo.toml | 9 +- FEATURES.md | 2 + examples/custom_cursors.rs | 92 +++++ examples/data/cross.png | Bin 0 -> 159 bytes examples/data/cross2.png | Bin 0 -> 129 bytes src/cursor.rs | 198 ++++++++++ src/lib.rs | 1 + src/platform/web.rs | 24 ++ src/platform_impl/android/mod.rs | 4 + src/platform_impl/ios/mod.rs | 1 + src/platform_impl/ios/window.rs | 5 + src/platform_impl/linux/mod.rs | 7 + src/platform_impl/linux/wayland/state.rs | 10 +- .../linux/wayland/types/cursor.rs | 56 +++ src/platform_impl/linux/wayland/types/mod.rs | 1 + src/platform_impl/linux/wayland/window/mod.rs | 6 + .../linux/wayland/window/state.rs | 80 +++- src/platform_impl/linux/x11/util/cursor.rs | 81 +++- src/platform_impl/linux/x11/util/mod.rs | 2 +- src/platform_impl/linux/x11/window.rs | 49 ++- .../macos/appkit/bitmap_image_rep.rs | 56 +++ src/platform_impl/macos/appkit/cursor.rs | 22 +- src/platform_impl/macos/appkit/image.rs | 14 +- src/platform_impl/macos/appkit/mod.rs | 2 + src/platform_impl/macos/mod.rs | 1 + src/platform_impl/macos/window.rs | 8 + src/platform_impl/orbital/mod.rs | 1 + src/platform_impl/orbital/window.rs | 3 + src/platform_impl/web/cursor.rs | 351 ++++++++++++++++++ src/platform_impl/web/mod.rs | 2 + src/platform_impl/web/web_sys/canvas.rs | 2 +- src/platform_impl/web/web_sys/mod.rs | 2 +- src/platform_impl/web/window.rs | 28 +- src/platform_impl/windows/event_loop.rs | 15 +- src/platform_impl/windows/icon.rs | 102 ++++- src/platform_impl/windows/mod.rs | 3 +- src/platform_impl/windows/window.rs | 22 +- src/platform_impl/windows/window_state.rs | 8 +- src/window.rs | 15 + tests/send_objects.rs | 5 + tests/sync_object.rs | 5 + 42 files changed, 1243 insertions(+), 57 deletions(-) create mode 100644 examples/custom_cursors.rs create mode 100644 examples/data/cross.png create mode 100644 examples/data/cross2.png create mode 100644 src/cursor.rs create mode 100644 src/platform_impl/linux/wayland/types/cursor.rs create mode 100644 src/platform_impl/macos/appkit/bitmap_image_rep.rs create mode 100644 src/platform_impl/web/cursor.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c109e20..964f360f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,11 @@ Unreleased` header. # Unreleased +- On Windows, macOS, X11, Wayland and Web, implement setting images as cursors. See the `custom_cursors.rs` example. + - Add `Window::set_custom_cursor` + - 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. - On macOS, add services menu. - On macOS, remove spurious error logging when handling `Fn`. - On X11, fix an issue where floating point data from the server is diff --git a/Cargo.toml b/Cargo.toml index 36009c00..6997b34f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -179,6 +179,7 @@ version = "0.3.64" features = [ 'AbortController', 'AbortSignal', + 'Blob', 'console', 'CssStyleDeclaration', 'Document', @@ -190,6 +191,10 @@ features = [ 'FocusEvent', 'HtmlCanvasElement', 'HtmlElement', + 'ImageBitmap', + 'ImageBitmapOptions', + 'ImageBitmapRenderingContext', + 'ImageData', 'IntersectionObserver', 'IntersectionObserverEntry', 'KeyboardEvent', @@ -199,6 +204,7 @@ features = [ 'Node', 'PageTransitionEvent', 'PointerEvent', + 'PremultiplyAlpha', 'ResizeObserver', 'ResizeObserverBoxOptions', 'ResizeObserverEntry', @@ -206,7 +212,8 @@ features = [ 'ResizeObserverSize', 'VisibilityState', 'Window', - 'WheelEvent' + 'WheelEvent', + 'Url', ] [target.'cfg(target_family = "wasm")'.dependencies] diff --git a/FEATURES.md b/FEATURES.md index 97286ceb..5855979c 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -106,6 +106,7 @@ If your PR makes notable changes to Winit's features, please update this section - **Cursor locking**: Locking the cursor inside the window so it cannot move. - **Cursor confining**: Confining the cursor to the window bounds so it cannot leave them. - **Cursor icon**: Changing the cursor icon or hiding the cursor. +- **Cursor image**: Changing the cursor to your own image. - **Cursor hittest**: Handle or ignore mouse events for a window. - **Touch events**: Single-touch events. - **Touch pressure**: Touch events contain information about the amount of force being applied. @@ -206,6 +207,7 @@ Legend: |Cursor locking |❌ |✔️ |❌ |✔️ |**N/A**|**N/A**|✔️ |❌ | |Cursor confining |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ | |Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |**N/A** | +|Cursor image |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |**N/A** | |Cursor hittest |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ | |Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A** | |Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |✔️ |**N/A** | diff --git a/examples/custom_cursors.rs b/examples/custom_cursors.rs new file mode 100644 index 00000000..c685cd75 --- /dev/null +++ b/examples/custom_cursors.rs @@ -0,0 +1,92 @@ +#![allow(clippy::single_match, clippy::disallowed_methods)] + +#[cfg(not(wasm_platform))] +use simple_logger::SimpleLogger; +use winit::{ + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::EventLoop, + keyboard::Key, + window::{CustomCursor, WindowBuilder}, +}; + +fn decode_cursor(bytes: &[u8]) -> CustomCursor { + 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); + CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap() +} + +#[cfg(not(wasm_platform))] +#[path = "util/fill.rs"] +mod fill; + +fn main() -> Result<(), impl std::error::Error> { + #[cfg(not(wasm_platform))] + SimpleLogger::new() + .with_level(log::LevelFilter::Info) + .init() + .unwrap(); + #[cfg(wasm_platform)] + console_log::init_with_level(log::Level::Debug).unwrap(); + + let event_loop = EventLoop::new().unwrap(); + let builder = WindowBuilder::new().with_title("A fantastic window!"); + #[cfg(wasm_platform)] + let builder = { + use winit::platform::web::WindowBuilderExtWebSys; + builder.with_append(true) + }; + let window = builder.build(&event_loop).unwrap(); + + let mut cursor_idx = 0; + let mut cursor_visible = true; + + let custom_cursors = [ + decode_cursor(include_bytes!("data/cross.png")), + decode_cursor(include_bytes!("data/cross2.png")), + ]; + + event_loop.run(move |event, _elwt| match event { + Event::WindowEvent { event, .. } => match event { + WindowEvent::KeyboardInput { + event: + KeyEvent { + state: ElementState::Pressed, + logical_key: key, + .. + }, + .. + } => match key.as_ref() { + Key::Character("1") => { + log::debug!("Setting cursor to {:?}", cursor_idx); + window.set_custom_cursor(&custom_cursors[cursor_idx]); + cursor_idx = (cursor_idx + 1) % 2; + } + Key::Character("2") => { + log::debug!("Setting cursor icon to default"); + window.set_cursor_icon(Default::default()); + } + Key::Character("3") => { + cursor_visible = !cursor_visible; + log::debug!("Setting cursor visibility to {:?}", cursor_visible); + window.set_cursor_visible(cursor_visible); + } + _ => {} + }, + WindowEvent::RedrawRequested => { + #[cfg(not(wasm_platform))] + fill::fill_window(&window); + } + WindowEvent::CloseRequested => { + #[cfg(not(wasm_platform))] + _elwt.exit(); + } + _ => (), + }, + Event::AboutToWait => { + window.request_redraw(); + } + _ => {} + }) +} diff --git a/examples/data/cross.png b/examples/data/cross.png new file mode 100644 index 0000000000000000000000000000000000000000..9bfdf369b3ba297216a2859beda741508d02a43d GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`X`U{QAr_~T6Am!{GdB41|NMau z^7EzC*ti&vvTaZ(KEp44U~cuLZ%oRY=Jfp%+;L~>Y_p!`k*U0k}|>c=$^ c{fhOa9A!#8wWZVSfMzmyy85}Sb4q9e07=*?^Z)<= literal 0 HcmV?d00001 diff --git a/src/cursor.rs b/src/cursor.rs new file mode 100644 index 00000000..ca55a61f --- /dev/null +++ b/src/cursor.rs @@ -0,0 +1,198 @@ +use core::fmt; +use std::{error::Error, sync::Arc}; + +use crate::platform_impl::PlatformCustomCursor; + +/// The maximum width and height for a cursor when using [`CustomCursor::from_rgba`]. +pub const MAX_CURSOR_SIZE: u16 = 2048; + +const PIXEL_SIZE: usize = 4; + +/// Use a custom image as a cursor (mouse pointer). +/// +/// ## Platform-specific +/// +/// **Web**: Some browsers have limits on cursor sizes usually at 128x128. +/// +/// # Example +/// +/// ``` +/// use winit::window::CustomCursor; +/// +/// let w = 10; +/// let h = 10; +/// let rgba = vec![255; (w * h * 4) as usize]; +/// let custom_cursor = CustomCursor::from_rgba(rgba, w, h, w / 2, h / 2).unwrap(); +/// +/// #[cfg(target_family = "wasm")] +/// let custom_cursor_url = { +/// use winit::platform::web::CustomCursorExtWebSys; +/// CustomCursor::from_url("http://localhost:3000/cursor.png", 0, 0).unwrap() +/// }; +/// ``` +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CustomCursor { + pub(crate) inner: Arc, +} + +impl CustomCursor { + /// Creates a new cursor from an rgba buffer. + /// + /// ## Platform-specific + /// + /// - **Web:** Setting cursor could be delayed due to the creation of `Blob` objects, + /// which are async by nature. + pub fn from_rgba( + rgba: impl Into>, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + Ok(Self { + inner: PlatformCustomCursor::from_rgba( + rgba.into(), + width, + height, + hotspot_x, + hotspot_y, + )? + .into(), + }) + } +} + +/// An error produced when using [`CustomCursor::from_rgba`] with invalid arguments. +#[derive(Debug, Clone)] +pub enum BadImage { + /// Produced when the image dimensions are larger than [`MAX_CURSOR_SIZE`]. This doesn't + /// guarantee that the cursor will work, but should avoid many platform and device specific + /// limits. + TooLarge { width: u16, height: u16 }, + /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be + /// safely interpreted as 32bpp RGBA pixels. + ByteCountNotDivisibleBy4 { byte_count: usize }, + /// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`. + /// At least one of your arguments is incorrect. + DimensionsVsPixelCount { + width: u16, + height: u16, + width_x_height: u64, + pixel_count: u64, + }, + /// Produced when the hotspot is outside the image bounds + HotspotOutOfBounds { + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + }, +} + +impl fmt::Display for BadImage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + BadImage::TooLarge { width, height } => write!(f, + "The specified dimensions ({width:?}x{height:?}) are too large. The maximum is {MAX_CURSOR_SIZE:?}x{MAX_CURSOR_SIZE:?}.", + ), + BadImage::ByteCountNotDivisibleBy4 { byte_count } => write!(f, + "The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making it impossible to interpret as 32bpp RGBA pixels.", + ), + BadImage::DimensionsVsPixelCount { + width, + height, + width_x_height, + pixel_count, + } => write!(f, + "The specified dimensions ({width:?}x{height:?}) don't match the number of pixels supplied by the `rgba` argument ({pixel_count:?}). For those dimensions, the expected pixel count is {width_x_height:?}.", + ), + BadImage::HotspotOutOfBounds { + width, + height, + hotspot_x, + hotspot_y, + } => write!(f, + "The specified hotspot ({hotspot_x:?}, {hotspot_y:?}) is outside the image bounds ({width:?}x{height:?}).", + ), + } + } +} + +impl Error for BadImage {} + +/// Platforms export this directly as `PlatformCustomCursor` if they need to only work with images. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CursorImage { + pub(crate) rgba: Vec, + pub(crate) width: u16, + pub(crate) height: u16, + pub(crate) hotspot_x: u16, + pub(crate) hotspot_y: u16, +} + +#[allow(dead_code)] +impl CursorImage { + pub fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + if width > MAX_CURSOR_SIZE || height > MAX_CURSOR_SIZE { + return Err(BadImage::TooLarge { width, height }); + } + + if rgba.len() % PIXEL_SIZE != 0 { + return Err(BadImage::ByteCountNotDivisibleBy4 { + byte_count: rgba.len(), + }); + } + + let pixel_count = (rgba.len() / PIXEL_SIZE) as u64; + let width_x_height = width as u64 * height as u64; + if pixel_count != width_x_height { + return Err(BadImage::DimensionsVsPixelCount { + width, + height, + width_x_height, + pixel_count, + }); + } + + if hotspot_x >= width || hotspot_y >= height { + return Err(BadImage::HotspotOutOfBounds { + width, + height, + hotspot_x, + hotspot_y, + }); + } + + Ok(CursorImage { + rgba, + width, + height, + hotspot_x, + hotspot_y, + }) + } +} + +// Platforms that don't support cursors will export this as `PlatformCustomCursor`. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct NoCustomCursor; + +#[allow(dead_code)] +impl NoCustomCursor { + pub fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?; + Ok(Self) + } +} diff --git a/src/lib.rs b/src/lib.rs index 03af6f48..ce26ef0e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,6 +172,7 @@ extern crate bitflags; pub mod dpi; #[macro_use] pub mod error; +mod cursor; pub mod event; pub mod event_loop; mod icon; diff --git a/src/platform/web.rs b/src/platform/web.rs index 06e3f682..c01308e9 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -27,9 +27,11 @@ //! [`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::CustomCursor; use crate::event::Event; use crate::event_loop::EventLoop; use crate::event_loop::EventLoopWindowTarget; +use crate::platform_impl::PlatformCustomCursor; use crate::window::{Window, WindowBuilder}; use crate::SendSyncWrapper; @@ -200,3 +202,25 @@ pub enum PollStrategy { #[default] Scheduler, } + +pub trait CustomCursorExtWebSys { + /// 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) -> Self; +} + +impl CustomCursorExtWebSys for CustomCursor { + fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> Self { + Self { + inner: PlatformCustomCursor::Url { + url, + hotspot_x, + hotspot_y, + } + .into(), + } + } +} diff --git a/src/platform_impl/android/mod.rs b/src/platform_impl/android/mod.rs index 72570c49..68be1a75 100644 --- a/src/platform_impl/android/mod.rs +++ b/src/platform_impl/android/mod.rs @@ -18,6 +18,7 @@ use android_activity::{ use once_cell::sync::Lazy; use crate::{ + cursor::CustomCursor, dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error, event::{self, Force, InnerSizeWriter, StartCause}, @@ -906,6 +907,8 @@ impl Window { pub fn set_cursor_icon(&self, _: window::CursorIcon) {} + pub fn set_custom_cursor(&self, _: CustomCursor) {} + pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> { Err(error::ExternalError::NotSupported( error::NotSupportedError::new(), @@ -1031,6 +1034,7 @@ impl Display for OsError { } } +pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursor; pub(crate) use crate::icon::NoIcon as PlatformIcon; #[derive(Clone, Debug, PartialEq, Eq, Hash)] diff --git a/src/platform_impl/ios/mod.rs b/src/platform_impl/ios/mod.rs index dec71cde..5fc9d743 100644 --- a/src/platform_impl/ios/mod.rs +++ b/src/platform_impl/ios/mod.rs @@ -77,6 +77,7 @@ pub(crate) use self::{ }; use self::uikit::UIScreen; +pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursor; pub(crate) use crate::icon::NoIcon as PlatformIcon; pub(crate) use crate::platform_impl::Fullscreen; diff --git a/src/platform_impl/ios/window.rs b/src/platform_impl/ios/window.rs index 28c7f318..e70d843f 100644 --- a/src/platform_impl/ios/window.rs +++ b/src/platform_impl/ios/window.rs @@ -11,6 +11,7 @@ use super::app_state::EventWrapper; use super::uikit::{UIApplication, UIScreen, UIScreenOverscanCompensation}; use super::view::{WinitUIWindow, WinitView, WinitViewController}; use crate::{ + cursor::CustomCursor, dpi::{self, LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, event::{Event, WindowEvent}, @@ -177,6 +178,10 @@ impl Inner { debug!("`Window::set_cursor_icon` ignored on iOS") } + pub fn set_custom_cursor(&self, _: CustomCursor) { + debug!("`Window::set_custom_cursor` ignored on iOS") + } + pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { Err(ExternalError::NotSupported(NotSupportedError::new())) } diff --git a/src/platform_impl/linux/mod.rs b/src/platform_impl/linux/mod.rs index a6068bc7..6b6186cf 100644 --- a/src/platform_impl/linux/mod.rs +++ b/src/platform_impl/linux/mod.rs @@ -14,6 +14,7 @@ use std::{ffi::CStr, mem::MaybeUninit, os::raw::*, sync::Mutex}; use once_cell::sync::Lazy; use smol_str::SmolStr; +use crate::cursor::CustomCursor; #[cfg(x11_platform)] use crate::platform::x11::XlibErrorHook; use crate::{ @@ -40,6 +41,7 @@ pub use x11::XNotSupported; #[cfg(x11_platform)] use x11::{util::WindowType as XWindowType, X11Error, XConnection, XError}; +pub(crate) use crate::cursor::CursorImage as PlatformCustomCursor; pub(crate) use crate::icon::RgbaIcon as PlatformIcon; pub(crate) use crate::platform_impl::Fullscreen; @@ -424,6 +426,11 @@ impl Window { x11_or_wayland!(match self; Window(w) => w.set_cursor_icon(cursor)) } + #[inline] + pub fn set_custom_cursor(&self, cursor: CustomCursor) { + x11_or_wayland!(match self; Window(w) => w.set_custom_cursor(cursor)) + } + #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { x11_or_wayland!(match self; Window(window) => window.set_cursor_grab(mode)) diff --git a/src/platform_impl/linux/wayland/state.rs b/src/platform_impl/linux/wayland/state.rs index 967ee93f..a3ff57c3 100644 --- a/src/platform_impl/linux/wayland/state.rs +++ b/src/platform_impl/linux/wayland/state.rs @@ -19,6 +19,7 @@ use sctk::seat::SeatState; use sctk::shell::xdg::window::{Window, WindowConfigure, WindowHandler}; use sctk::shell::xdg::XdgShell; use sctk::shell::WaylandSurface; +use sctk::shm::slot::SlotPool; use sctk::shm::{Shm, ShmHandler}; use sctk::subcompositor::SubcompositorState; @@ -58,6 +59,9 @@ pub struct WinitState { /// The shm for software buffers, such as cursors. pub shm: Shm, + /// The pool where custom cursors are allocated. + pub custom_cursor_pool: Arc>, + /// The XDG shell that is used for widnows. pub xdg_shell: XdgShell, @@ -153,13 +157,17 @@ impl WinitState { (None, None) }; + let shm = Shm::bind(globals, queue_handle).map_err(WaylandError::Bind)?; + let custom_cursor_pool = Arc::new(Mutex::new(SlotPool::new(2, &shm).unwrap())); + Ok(Self { registry_state, compositor_state: Arc::new(compositor_state), subcompositor_state: subcompositor_state.map(Arc::new), output_state, seat_state, - shm: Shm::bind(globals, queue_handle).map_err(WaylandError::Bind)?, + shm, + custom_cursor_pool, xdg_shell: XdgShell::bind(globals, queue_handle).map_err(WaylandError::Bind)?, xdg_activation: XdgActivationState::bind(globals, queue_handle).ok(), diff --git a/src/platform_impl/linux/wayland/types/cursor.rs b/src/platform_impl/linux/wayland/types/cursor.rs new file mode 100644 index 00000000..48348619 --- /dev/null +++ b/src/platform_impl/linux/wayland/types/cursor.rs @@ -0,0 +1,56 @@ +use cursor_icon::CursorIcon; + +use sctk::reexports::client::protocol::wl_shm::Format; +use sctk::shm::slot::{Buffer, SlotPool}; + +use crate::cursor::CursorImage; + +#[derive(Debug)] +pub enum SelectedCursor { + Named(CursorIcon), + Custom(CustomCursor), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } +} + +#[derive(Debug)] +pub struct CustomCursor { + pub buffer: Buffer, + pub w: i32, + pub h: i32, + pub hotspot_x: i32, + pub hotspot_y: i32, +} + +impl CustomCursor { + pub fn new(pool: &mut SlotPool, image: &CursorImage) -> Self { + let (buffer, canvas) = pool + .create_buffer( + image.width as i32, + image.height as i32, + 4 * (image.width as i32), + Format::Argb8888, + ) + .unwrap(); + + for (canvas_chunk, rgba_chunk) in canvas.chunks_exact_mut(4).zip(image.rgba.chunks_exact(4)) + { + canvas_chunk[0] = rgba_chunk[2]; + canvas_chunk[1] = rgba_chunk[1]; + canvas_chunk[2] = rgba_chunk[0]; + canvas_chunk[3] = rgba_chunk[3]; + } + + CustomCursor { + buffer, + w: image.width as i32, + h: image.height as i32, + hotspot_x: image.hotspot_x as i32, + hotspot_y: image.hotspot_y as i32, + } + } +} diff --git a/src/platform_impl/linux/wayland/types/mod.rs b/src/platform_impl/linux/wayland/types/mod.rs index ea745888..77e67f48 100644 --- a/src/platform_impl/linux/wayland/types/mod.rs +++ b/src/platform_impl/linux/wayland/types/mod.rs @@ -1,5 +1,6 @@ //! Wayland protocol implementation boilerplate. +pub mod cursor; pub mod kwin_blur; pub mod wp_fractional_scaling; pub mod wp_viewporter; diff --git a/src/platform_impl/linux/wayland/window/mod.rs b/src/platform_impl/linux/wayland/window/mod.rs index ae6558a8..13d74c20 100644 --- a/src/platform_impl/linux/wayland/window/mod.rs +++ b/src/platform_impl/linux/wayland/window/mod.rs @@ -15,6 +15,7 @@ use sctk::shell::xdg::window::Window as SctkWindow; use sctk::shell::xdg::window::WindowDecorations; use sctk::shell::WaylandSurface; +use crate::cursor::CustomCursor; use crate::dpi::{LogicalSize, PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{ExternalError, NotSupportedError, OsError as RootOsError}; use crate::event::{Ime, WindowEvent}; @@ -506,6 +507,11 @@ impl Window { self.window_state.lock().unwrap().set_cursor(cursor); } + #[inline] + pub fn set_custom_cursor(&self, cursor: CustomCursor) { + self.window_state.lock().unwrap().set_custom_cursor(cursor); + } + #[inline] pub fn set_cursor_visible(&self, visible: bool) { self.window_state diff --git a/src/platform_impl/linux/wayland/window/state.rs b/src/platform_impl/linux/wayland/window/state.rs index 73c0c92c..9355bb8a 100644 --- a/src/platform_impl/linux/wayland/window/state.rs +++ b/src/platform_impl/linux/wayland/window/state.rs @@ -1,7 +1,7 @@ //! The state of the window, which is shared with the event-loop. use std::num::NonZeroU32; -use std::sync::{Arc, Weak}; +use std::sync::{Arc, Mutex, Weak}; use std::time::Duration; use log::{info, warn}; @@ -18,20 +18,23 @@ use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3:: use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport; use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as XdgResizeEdge; -use sctk::compositor::{CompositorState, Region}; -use sctk::seat::pointer::ThemedPointer; +use sctk::compositor::{CompositorState, Region, SurfaceData, SurfaceDataExt}; +use sctk::seat::pointer::{PointerDataExt, ThemedPointer}; use sctk::shell::xdg::window::{DecorationMode, Window, WindowConfigure}; use sctk::shell::xdg::XdgSurface; use sctk::shell::WaylandSurface; +use sctk::shm::slot::SlotPool; use sctk::shm::Shm; use sctk::subcompositor::SubcompositorState; use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; +use crate::cursor::CustomCursor as RootCustomCursor; use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize, Size}; use crate::error::{ExternalError, NotSupportedError}; use crate::event::WindowEvent; use crate::platform_impl::wayland::event_loop::sink::EventSink; use crate::platform_impl::wayland::make_wid; +use crate::platform_impl::wayland::types::cursor::{CustomCursor, SelectedCursor}; use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager; use crate::platform_impl::WindowId; use crate::window::{CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme}; @@ -60,14 +63,16 @@ pub struct WindowState { /// The `Shm` to set cursor. pub shm: WlShm, + // A shared pool where to allocate custom cursors. + custom_cursor_pool: Arc>, + /// The last received configure. pub last_configure: Option, /// The pointers observed on the window. pub pointers: Vec>>, - /// Cursor icon. - pub cursor_icon: CursorIcon, + selected_cursor: SelectedCursor, /// Wether the cursor is visible. pub cursor_visible: bool, @@ -178,7 +183,7 @@ impl WindowState { connection, csd_fails: false, cursor_grab_mode: GrabState::new(), - cursor_icon: CursorIcon::Default, + selected_cursor: Default::default(), cursor_visible: true, decorate: true, fractional_scale, @@ -197,6 +202,7 @@ impl WindowState { resizable: true, scale_factor: 1., shm: winit_state.shm.wl_shm().clone(), + custom_cursor_pool: winit_state.custom_cursor_pool.clone(), size: initial_size.to_logical(1.), stateless_size: initial_size.to_logical(1.), initial_size: Some(initial_size), @@ -603,7 +609,10 @@ impl WindowState { /// Reload the cursor style on the given window. pub fn reload_cursor_style(&mut self) { if self.cursor_visible { - self.set_cursor(self.cursor_icon); + match &self.selected_cursor { + SelectedCursor::Named(icon) => self.set_cursor(*icon), + SelectedCursor::Custom(cursor) => self.apply_custom_cursor(cursor), + } } else { self.set_cursor_visible(self.cursor_visible); } @@ -689,10 +698,8 @@ impl WindowState { } /// Set the cursor icon. - /// - /// Providing `None` will hide the cursor. pub fn set_cursor(&mut self, cursor_icon: CursorIcon) { - self.cursor_icon = cursor_icon; + self.selected_cursor = SelectedCursor::Named(cursor_icon); if !self.cursor_visible { return; @@ -705,6 +712,54 @@ impl WindowState { }) } + /// Set the custom cursor icon. + pub fn set_custom_cursor(&mut self, cursor: RootCustomCursor) { + let cursor = { + let mut pool = self.custom_cursor_pool.lock().unwrap(); + CustomCursor::new(&mut pool, &cursor.inner) + }; + + if self.cursor_visible { + self.apply_custom_cursor(&cursor); + } + + self.selected_cursor = SelectedCursor::Custom(cursor); + } + + fn apply_custom_cursor(&self, cursor: &CustomCursor) { + self.apply_on_poiner(|pointer, _| { + let surface = pointer.surface(); + + let scale = surface + .data::() + .unwrap() + .surface_data() + .scale_factor(); + + surface.set_buffer_scale(scale); + surface.attach(Some(cursor.buffer.wl_buffer()), 0, 0); + if surface.version() >= 4 { + surface.damage_buffer(0, 0, cursor.w, cursor.h); + } else { + surface.damage(0, 0, cursor.w / scale, cursor.h / scale); + } + surface.commit(); + + let serial = pointer + .pointer() + .data::() + .and_then(|data| data.pointer_data().latest_enter_serial()) + .unwrap(); + + pointer.pointer().set_cursor( + serial, + Some(surface), + cursor.hotspot_x / scale, + cursor.hotspot_y / scale, + ); + }); + } + /// Set maximum inner window size. pub fn set_min_inner_size(&mut self, size: Option>) { // Ensure that the window has the right minimum size. @@ -839,7 +894,10 @@ impl WindowState { self.cursor_visible = cursor_visible; if self.cursor_visible { - self.set_cursor(self.cursor_icon); + match &self.selected_cursor { + SelectedCursor::Named(icon) => self.set_cursor(*icon), + SelectedCursor::Custom(cursor) => self.apply_custom_cursor(cursor), + } } else { for pointer in self.pointers.iter().filter_map(|pointer| pointer.upgrade()) { let latest_enter_serial = pointer.pointer().winit_data().latest_enter_serial(); diff --git a/src/platform_impl/linux/x11/util/cursor.rs b/src/platform_impl/linux/x11/util/cursor.rs index 8d62cfa7..e9b457d9 100644 --- a/src/platform_impl/linux/x11/util/cursor.rs +++ b/src/platform_impl/linux/x11/util/cursor.rs @@ -1,9 +1,8 @@ -use std::ffi::CString; -use std::iter; +use std::{ffi::CString, iter, slice, sync::Arc}; use x11rb::connection::Connection; -use crate::window::CursorIcon; +use crate::{cursor::CursorImage, window::CursorIcon}; use super::*; @@ -20,6 +19,11 @@ impl XConnection { .expect("Failed to set cursor"); } + pub fn set_custom_cursor(&self, window: xproto::Window, cursor: &CustomCursor) { + self.update_cursor(window, cursor.inner.cursor) + .expect("Failed to set cursor"); + } + fn create_empty_cursor(&self) -> ffi::Cursor { let data = 0; let pixmap = unsafe { @@ -87,3 +91,74 @@ impl XConnection { Ok(()) } } + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum SelectedCursor { + Custom(CustomCursor), + Named(CursorIcon), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CustomCursor { + inner: Arc, +} + +impl CustomCursor { + pub(crate) unsafe fn new(xconn: &Arc, image: &CursorImage) -> Self { + unsafe { + let ximage = + (xconn.xcursor.XcursorImageCreate)(image.width as i32, image.height as i32); + if ximage.is_null() { + panic!("failed to allocate cursor image"); + } + (*ximage).xhot = image.hotspot_x as u32; + (*ximage).yhot = image.hotspot_y as u32; + (*ximage).delay = 0; + + let dst = slice::from_raw_parts_mut((*ximage).pixels, image.rgba.len() / 4); + for (dst, chunk) in dst.iter_mut().zip(image.rgba.chunks_exact(4)) { + *dst = (chunk[0] as u32) << 16 + | (chunk[1] as u32) << 8 + | (chunk[2] as u32) + | (chunk[3] as u32) << 24; + } + + let cursor = (xconn.xcursor.XcursorImageLoadCursor)(xconn.display, ximage); + (xconn.xcursor.XcursorImageDestroy)(ximage); + Self { + inner: Arc::new(CustomCursorInner { + xconn: xconn.clone(), + cursor, + }), + } + } + } +} + +#[derive(Debug)] +struct CustomCursorInner { + xconn: Arc, + cursor: ffi::Cursor, +} + +impl Drop for CustomCursorInner { + fn drop(&mut self) { + unsafe { + (self.xconn.xlib.XFreeCursor)(self.xconn.display, self.cursor); + } + } +} + +impl PartialEq for CustomCursorInner { + fn eq(&self, other: &Self) -> bool { + self.cursor == other.cursor + } +} + +impl Eq for CustomCursorInner {} + +impl Default for SelectedCursor { + fn default() -> Self { + SelectedCursor::Named(Default::default()) + } +} diff --git a/src/platform_impl/linux/x11/util/mod.rs b/src/platform_impl/linux/x11/util/mod.rs index f806b082..1bff92eb 100644 --- a/src/platform_impl/linux/x11/util/mod.rs +++ b/src/platform_impl/linux/x11/util/mod.rs @@ -13,7 +13,7 @@ mod randr; mod window_property; mod wm; -pub use self::{geometry::*, hint::*, input::*, window_property::*, wm::*}; +pub use self::{cursor::*, geometry::*, hint::*, input::*, window_property::*, wm::*}; use std::{ mem::{self, MaybeUninit}, diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index 3f0fa326..2d4314c4 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -7,6 +7,9 @@ use std::{ sync::{Arc, Mutex, MutexGuard}, }; +use crate::cursor::CustomCursor as RootCustomCursor; + +use cursor_icon::CursorIcon; use x11rb::{ connection::Connection, properties::{WmHints, WmHintsState, WmSizeHints, WmSizeHintsSpecification}, @@ -33,13 +36,15 @@ use crate::{ PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode, }, window::{ - CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, - WindowAttributes, WindowButtons, WindowLevel, + CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes, + WindowButtons, WindowLevel, }, }; use super::{ - ffi, util, CookieResultExt, EventLoopWindowTarget, ImeRequest, ImeSender, VoidCookie, WindowId, + ffi, + util::{self, CustomCursor, SelectedCursor}, + CookieResultExt, EventLoopWindowTarget, ImeRequest, ImeSender, VoidCookie, WindowId, XConnection, }; @@ -126,7 +131,7 @@ pub(crate) struct UnownedWindow { root: xproto::Window, // never changes #[allow(dead_code)] screen_id: i32, // never changes - cursor: Mutex, + selected_cursor: Mutex, cursor_grabbed_mode: Mutex, #[allow(clippy::mutex_atomic)] cursor_visible: Mutex, @@ -355,7 +360,7 @@ impl UnownedWindow { visual, root, screen_id, - cursor: Default::default(), + selected_cursor: Default::default(), cursor_grabbed_mode: Mutex::new(CursorGrabMode::None), cursor_visible: Mutex::new(true), ime_sender: Mutex::new(event_loop.ime_sender.clone()), @@ -1535,13 +1540,29 @@ impl UnownedWindow { #[inline] pub fn set_cursor_icon(&self, cursor: CursorIcon) { - let old_cursor = replace(&mut *self.cursor.lock().unwrap(), cursor); + let old_cursor = replace( + &mut *self.selected_cursor.lock().unwrap(), + SelectedCursor::Named(cursor), + ); + #[allow(clippy::mutex_atomic)] - if cursor != old_cursor && *self.cursor_visible.lock().unwrap() { + if SelectedCursor::Named(cursor) != old_cursor && *self.cursor_visible.lock().unwrap() { self.xconn.set_cursor_icon(self.xwindow, Some(cursor)); } } + #[inline] + pub fn set_custom_cursor(&self, cursor: RootCustomCursor) { + let new_cursor = unsafe { CustomCursor::new(&self.xconn, &cursor.inner) }; + + #[allow(clippy::mutex_atomic)] + if *self.cursor_visible.lock().unwrap() { + self.xconn.set_custom_cursor(self.xwindow, &new_cursor); + } + + *self.selected_cursor.lock().unwrap() = SelectedCursor::Custom(new_cursor); + } + #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap(); @@ -1628,13 +1649,23 @@ impl UnownedWindow { return; } let cursor = if visible { - Some(*self.cursor.lock().unwrap()) + Some((*self.selected_cursor.lock().unwrap()).clone()) } else { None }; *visible_lock = visible; drop(visible_lock); - self.xconn.set_cursor_icon(self.xwindow, cursor); + match cursor { + Some(SelectedCursor::Custom(cursor)) => { + self.xconn.set_custom_cursor(self.xwindow, &cursor); + } + Some(SelectedCursor::Named(cursor)) => { + self.xconn.set_cursor_icon(self.xwindow, Some(cursor)); + } + None => { + self.xconn.set_cursor_icon(self.xwindow, None); + } + } } #[inline] diff --git a/src/platform_impl/macos/appkit/bitmap_image_rep.rs b/src/platform_impl/macos/appkit/bitmap_image_rep.rs new file mode 100644 index 00000000..aa6e4848 --- /dev/null +++ b/src/platform_impl/macos/appkit/bitmap_image_rep.rs @@ -0,0 +1,56 @@ +use std::ffi::c_uchar; + +use icrate::Foundation::{NSInteger, NSObject, NSString}; +use objc2::rc::Id; +use objc2::runtime::Bool; +use objc2::{extern_class, extern_methods, msg_send, msg_send_id, mutability, ClassType}; + +extern_class!( + #[derive(Debug, PartialEq, Eq, Hash)] + pub struct NSImageRep; + + unsafe impl ClassType for NSImageRep { + type Super = NSObject; + type Mutability = mutability::InteriorMutable; + } +); + +extern "C" { + static NSDeviceRGBColorSpace: &'static NSString; +} + +extern_class!( + // + #[derive(Debug, PartialEq, Eq, Hash)] + pub(crate) struct NSBitmapImageRep; + + unsafe impl ClassType for NSBitmapImageRep { + type Super = NSImageRep; + type Mutability = mutability::InteriorMutable; + } +); + +extern_methods!( + unsafe impl NSBitmapImageRep { + pub fn init_rgba(width: NSInteger, height: NSInteger) -> Id { + unsafe { + msg_send_id![Self::alloc(), + initWithBitmapDataPlanes: std::ptr::null_mut::<*mut c_uchar>(), + pixelsWide: width, + pixelsHigh: height, + bitsPerSample: 8 as NSInteger, + samplesPerPixel: 4 as NSInteger, + hasAlpha: Bool::new(true), + isPlanar: Bool::new(false), + colorSpaceName: NSDeviceRGBColorSpace, + bytesPerRow: width * 4, + bitsPerPixel: 32 as NSInteger, + ] + } + } + + pub fn bitmap_data(&self) -> *mut u8 { + unsafe { msg_send![self, bitmapData] } + } + } +); diff --git a/src/platform_impl/macos/appkit/cursor.rs b/src/platform_impl/macos/appkit/cursor.rs index 6377ad42..de83f0a2 100644 --- a/src/platform_impl/macos/appkit/cursor.rs +++ b/src/platform_impl/macos/appkit/cursor.rs @@ -2,13 +2,14 @@ use once_cell::sync::Lazy; use icrate::ns_string; use icrate::Foundation::{ - NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSString, + NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSSize, NSString, }; use objc2::rc::{DefaultId, Id}; use objc2::runtime::Sel; use objc2::{extern_class, extern_methods, msg_send_id, mutability, sel, ClassType}; -use super::NSImage; +use super::{NSBitmapImageRep, NSImage}; +use crate::cursor::CursorImage; use crate::window::CursorIcon; extern_class!( @@ -232,6 +233,23 @@ impl NSCursor { _ => Default::default(), } } + + pub fn from_image(cursor: &CursorImage) -> Id { + let width = cursor.width; + let height = cursor.height; + + let bitmap = NSBitmapImageRep::init_rgba(width as isize, height as isize); + let bitmap_data = + unsafe { std::slice::from_raw_parts_mut(bitmap.bitmap_data(), cursor.rgba.len()) }; + bitmap_data.copy_from_slice(&cursor.rgba); + + let image = NSImage::init_with_size(NSSize::new(width.into(), height.into())); + image.add_representation(&bitmap); + + let hotspot = NSPoint::new(cursor.hotspot_x as f64, cursor.hotspot_y as f64); + + NSCursor::new(&image, hotspot) + } } impl DefaultId for NSCursor { diff --git a/src/platform_impl/macos/appkit/image.rs b/src/platform_impl/macos/appkit/image.rs index 0b5944c3..d108eb1f 100644 --- a/src/platform_impl/macos/appkit/image.rs +++ b/src/platform_impl/macos/appkit/image.rs @@ -1,6 +1,8 @@ -use icrate::Foundation::{NSData, NSObject, NSString}; +use icrate::Foundation::{NSData, NSObject, NSSize, NSString}; use objc2::rc::Id; -use objc2::{extern_class, extern_methods, msg_send_id, mutability, ClassType}; +use objc2::{extern_class, extern_methods, msg_send, msg_send_id, mutability, ClassType}; + +use super::NSBitmapImageRep; extern_class!( // TODO: Can this be mutable? @@ -32,5 +34,13 @@ extern_methods!( pub fn new_with_data(data: &NSData) -> Id { unsafe { msg_send_id![Self::alloc(), initWithData: data] } } + + pub fn init_with_size(size: NSSize) -> Id { + unsafe { msg_send_id![Self::alloc(), initWithSize: size] } + } + + pub fn add_representation(&self, representation: &NSBitmapImageRep) { + unsafe { msg_send![self, addRepresentation: representation] } + } } ); diff --git a/src/platform_impl/macos/appkit/mod.rs b/src/platform_impl/macos/appkit/mod.rs index 832fc149..8c6eb12a 100644 --- a/src/platform_impl/macos/appkit/mod.rs +++ b/src/platform_impl/macos/appkit/mod.rs @@ -13,6 +13,7 @@ mod appearance; mod application; +mod bitmap_image_rep; mod button; mod color; mod control; @@ -36,6 +37,7 @@ pub(crate) use self::application::{ NSApp, NSApplication, NSApplicationActivationPolicy, NSApplicationPresentationOptions, NSRequestUserAttentionType, }; +pub(crate) use self::bitmap_image_rep::NSBitmapImageRep; pub(crate) use self::button::NSButton; pub(crate) use self::color::NSColor; pub(crate) use self::control::NSControl; diff --git a/src/platform_impl/macos/mod.rs b/src/platform_impl/macos/mod.rs index 7169ca9d..31bee1f0 100644 --- a/src/platform_impl/macos/mod.rs +++ b/src/platform_impl/macos/mod.rs @@ -28,6 +28,7 @@ pub(crate) use self::{ use crate::event::DeviceId as RootDeviceId; pub(crate) use self::window::Window; +pub(crate) use crate::cursor::CursorImage as PlatformCustomCursor; pub(crate) use crate::icon::NoIcon as PlatformIcon; pub(crate) use crate::platform_impl::Fullscreen; diff --git a/src/platform_impl/macos/window.rs b/src/platform_impl/macos/window.rs index 7e7d6f38..c98db38f 100644 --- a/src/platform_impl/macos/window.rs +++ b/src/platform_impl/macos/window.rs @@ -7,6 +7,7 @@ use std::os::raw::c_void; use std::ptr::NonNull; use std::sync::{Mutex, MutexGuard}; +use crate::cursor::CustomCursor; use crate::{ dpi::{ LogicalPosition, LogicalSize, PhysicalPosition, PhysicalSize, Position, Size, Size::Logical, @@ -834,6 +835,13 @@ impl WinitWindow { self.invalidateCursorRectsForView(&view); } + #[inline] + pub fn set_custom_cursor(&self, cursor: CustomCursor) { + let view = self.view(); + view.set_cursor_icon(NSCursor::from_image(&cursor.inner)); + self.invalidateCursorRectsForView(&view); + } + #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { let associate_mouse_cursor = match mode { diff --git a/src/platform_impl/orbital/mod.rs b/src/platform_impl/orbital/mod.rs index 5aa56328..121d0203 100644 --- a/src/platform_impl/orbital/mod.rs +++ b/src/platform_impl/orbital/mod.rs @@ -193,6 +193,7 @@ impl Display for OsError { } } +pub(crate) use crate::cursor::NoCustomCursor as PlatformCustomCursor; pub(crate) use crate::icon::NoIcon as PlatformIcon; #[derive(Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)] diff --git a/src/platform_impl/orbital/window.rs b/src/platform_impl/orbital/window.rs index 26ab7268..9d800af7 100644 --- a/src/platform_impl/orbital/window.rs +++ b/src/platform_impl/orbital/window.rs @@ -4,6 +4,7 @@ use std::{ }; use crate::{ + cursor::CustomCursor, dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error, platform_impl::Fullscreen, @@ -352,6 +353,8 @@ impl Window { #[inline] pub fn set_cursor_icon(&self, _: window::CursorIcon) {} + pub fn set_custom_cursor(&self, _: CustomCursor) {} + #[inline] pub fn set_cursor_position(&self, _: Position) -> Result<(), error::ExternalError> { Err(error::ExternalError::NotSupported( diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs new file mode 100644 index 00000000..22ae3b24 --- /dev/null +++ b/src/platform_impl/web/cursor.rs @@ -0,0 +1,351 @@ +use std::{ + cell::RefCell, + ops::Deref, + rc::{Rc, Weak}, +}; + +use crate::cursor::{BadImage, CursorImage}; +use cursor_icon::CursorIcon; +use wasm_bindgen::{closure::Closure, JsCast}; +use wasm_bindgen_futures::JsFuture; +use web_sys::{ + Blob, Document, HtmlCanvasElement, ImageBitmap, ImageBitmapOptions, + ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window, +}; + +use super::backend::Style; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WebCustomCursor { + Image(CursorImage), + Url { + url: String, + hotspot_x: u16, + hotspot_y: u16, + }, +} + +impl WebCustomCursor { + pub fn from_rgba( + rgba: Vec, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, + ) -> Result { + Ok(Self::Image(CursorImage::from_rgba( + rgba, width, height, hotspot_x, hotspot_y, + )?)) + } + + pub(super) fn build( + &self, + window: &Window, + document: &Document, + style: &Style, + previous: SelectedCursor, + ) -> SelectedCursor { + let previous = previous.into(); + + match self { + WebCustomCursor::Image(image) => SelectedCursor::Image(CursorImageState::from_image( + window, + document.clone(), + style.clone(), + image, + previous, + )), + WebCustomCursor::Url { + url, + hotspot_x, + hotspot_y, + } => { + let value = previous.style_with_url(url, *hotspot_x, *hotspot_y); + style.set("cursor", &value); + SelectedCursor::Url { + style: value, + previous, + url: url.clone(), + hotspot_x: *hotspot_x, + hotspot_y: *hotspot_y, + } + } + } + } +} + +#[derive(Debug)] +pub enum SelectedCursor { + Named(CursorIcon), + Url { + style: String, + previous: Previous, + url: String, + hotspot_x: u16, + hotspot_y: u16, + }, + Image(Rc>>), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } +} + +impl SelectedCursor { + pub fn set_style(&self, style: &Style) { + let value = match self { + SelectedCursor::Named(icon) => icon.name(), + SelectedCursor::Url { style, .. } => style, + SelectedCursor::Image(image) => { + let image = image.borrow(); + let value = match image.deref().as_ref().unwrap() { + CursorImageState::Loading { previous, .. } => previous.style(), + CursorImageState::Failed(previous) => previous.style(), + CursorImageState::Ready { style, .. } => style, + }; + return style.set("cursor", value); + } + }; + + style.set("cursor", value); + } +} + +#[derive(Debug)] +pub enum Previous { + Named(CursorIcon), + Url { + style: String, + url: String, + hotspot_x: u16, + hotspot_y: u16, + }, + Image { + style: String, + image: WebCursorImage, + }, +} + +impl Previous { + fn style(&self) -> &str { + match self { + Previous::Named(icon) => icon.name(), + Previous::Url { style: url, .. } => url, + Previous::Image { style, .. } => style, + } + } + + fn style_with_url(&self, new_url: &str, new_hotspot_x: u16, new_hotspot_y: u16) -> String { + match self { + Previous::Named(icon) => format!("url({new_url}) {new_hotspot_x} {new_hotspot_y}, {}", icon.name()), + Previous::Url { + url, + hotspot_x, + hotspot_y, + .. + } + | Previous::Image { + image: + WebCursorImage { + data_url: url, + hotspot_x, + hotspot_y, + .. + }, + .. + } => format!( + "url({new_url}) {new_hotspot_x} {new_hotspot_y}, url({url}) {hotspot_x} {hotspot_y}, auto", + ), + } + } +} + +impl From for Previous { + fn from(value: SelectedCursor) -> Self { + match value { + SelectedCursor::Named(icon) => Self::Named(icon), + SelectedCursor::Url { + style, + url, + hotspot_x, + hotspot_y, + .. + } => Self::Url { + style, + url, + hotspot_x, + hotspot_y, + }, + SelectedCursor::Image(image) => { + match Rc::try_unwrap(image).unwrap().into_inner().unwrap() { + CursorImageState::Loading { previous, .. } => previous, + CursorImageState::Failed(previous) => previous, + CursorImageState::Ready { + style, + image: current, + .. + } => Self::Image { + style, + image: current, + }, + } + } + } + } +} + +#[derive(Debug)] +pub enum CursorImageState { + Loading { + style: Style, + previous: Previous, + hotspot_x: u16, + hotspot_y: u16, + }, + Failed(Previous), + Ready { + style: String, + image: WebCursorImage, + previous: Previous, + }, +} + +impl CursorImageState { + fn from_image( + window: &Window, + document: Document, + style: Style, + image: &CursorImage, + previous: Previous, + ) -> Rc>> { + // Can't create array directly when backed by SharedArrayBuffer. + // Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223 + #[cfg(target_feature = "atomics")] + let image_data = { + use js_sys::{Uint8Array, Uint8ClampedArray}; + use wasm_bindgen::prelude::wasm_bindgen; + use wasm_bindgen::JsValue; + + #[wasm_bindgen] + extern "C" { + #[wasm_bindgen(js_namespace = ImageData)] + type ImageDataExt; + #[wasm_bindgen(catch, constructor, js_class = ImageData)] + fn new(array: Uint8ClampedArray, sw: u32) -> Result; + } + + let array = Uint8Array::new_with_length(image.rgba.len() as u32); + array.copy_from(&image.rgba); + let array = Uint8ClampedArray::new(&array); + ImageDataExt::new(array, image.width as u32) + .map(JsValue::from) + .map(ImageData::unchecked_from_js) + .unwrap() + }; + #[cfg(not(target_feature = "atomics"))] + let image_data = ImageData::new_with_u8_clamped_array( + wasm_bindgen::Clamped(&image.rgba), + image.width as u32, + ) + .unwrap(); + + let mut options = ImageBitmapOptions::new(); + options.premultiply_alpha(PremultiplyAlpha::None); + let bitmap = JsFuture::from( + window + .create_image_bitmap_with_image_data_and_image_bitmap_options(&image_data, &options) + .unwrap(), + ); + + let state = Rc::new(RefCell::new(Some(Self::Loading { + style, + previous, + hotspot_x: image.hotspot_x, + hotspot_y: image.hotspot_y, + }))); + + wasm_bindgen_futures::spawn_local({ + let weak = Rc::downgrade(&state); + let CursorImage { width, height, .. } = *image; + async move { + if weak.strong_count() == 0 { + return; + } + + let bitmap: ImageBitmap = bitmap.await.unwrap().unchecked_into(); + + if weak.strong_count() == 0 { + return; + } + + let canvas: HtmlCanvasElement = + document.create_element("canvas").unwrap().unchecked_into(); + #[allow(clippy::disallowed_methods)] + canvas.set_width(width as u32); + #[allow(clippy::disallowed_methods)] + canvas.set_height(height as u32); + + let context: ImageBitmapRenderingContext = canvas + .get_context("bitmaprenderer") + .unwrap() + .unwrap() + .unchecked_into(); + context.transfer_from_image_bitmap(&bitmap); + + thread_local! { + static CURRENT_STATE: RefCell>>>> = RefCell::new(None); + // `HTMLCanvasElement.toBlob()` can't be interrupted. So we have to use a + // `Closure` that doesn't need to be garbage-collected. + static CALLBACK: Closure)> = Closure::new(|blob| { + CURRENT_STATE.with(|weak| { + let Some(state) = weak.borrow_mut().take().and_then(|weak| weak.upgrade()) else { + return; + }; + let mut state = state.borrow_mut(); + // Extract old state. + let CursorImageState::Loading { style, previous, hotspot_x, hotspot_y, .. } = state.take().unwrap() else { + unreachable!("found invalid state") + }; + + let Some(blob) = blob else { + *state = Some(CursorImageState::Failed(previous)); + return; + }; + let data_url = Url::create_object_url_with_blob(&blob).unwrap(); + + let value = previous.style_with_url(&data_url, hotspot_x, hotspot_y); + style.set("cursor", &value); + *state = Some( + CursorImageState::Ready { + style: value, + image: WebCursorImage{ data_url, hotspot_x, hotspot_y }, + previous, + }); + }); + }); + } + + CURRENT_STATE.with(|state| *state.borrow_mut() = Some(weak)); + CALLBACK + .with(|callback| canvas.to_blob(callback.as_ref().unchecked_ref()).unwrap()); + } + }); + + state + } +} + +#[derive(Debug)] +pub struct WebCursorImage { + data_url: String, + hotspot_x: u16, + hotspot_y: u16, +} + +impl Drop for WebCursorImage { + fn drop(&mut self) { + Url::revoke_object_url(&self.data_url).unwrap(); + } +} diff --git a/src/platform_impl/web/mod.rs b/src/platform_impl/web/mod.rs index 3abd2684..433a4e8b 100644 --- a/src/platform_impl/web/mod.rs +++ b/src/platform_impl/web/mod.rs @@ -18,6 +18,7 @@ // compliant way. mod r#async; +mod cursor; mod device; mod error; mod event_loop; @@ -39,3 +40,4 @@ pub use self::window::{PlatformSpecificWindowBuilderAttributes, Window, WindowId pub(crate) use self::keyboard::KeyEventExtra; pub(crate) use crate::icon::NoIcon as PlatformIcon; pub(crate) use crate::platform_impl::Fullscreen; +pub(crate) use cursor::WebCustomCursor as PlatformCustomCursor; diff --git a/src/platform_impl/web/web_sys/canvas.rs b/src/platform_impl/web/web_sys/canvas.rs index 6cf5d339..ada3cc5f 100644 --- a/src/platform_impl/web/web_sys/canvas.rs +++ b/src/platform_impl/web/web_sys/canvas.rs @@ -55,7 +55,7 @@ pub struct Common { fullscreen_handler: Rc, } -#[derive(Clone)] +#[derive(Clone, Debug)] pub struct Style { read: CssStyleDeclaration, write: CssStyleDeclaration, diff --git a/src/platform_impl/web/web_sys/mod.rs b/src/platform_impl/web/web_sys/mod.rs index d86f991c..089d6e85 100644 --- a/src/platform_impl/web/web_sys/mod.rs +++ b/src/platform_impl/web/web_sys/mod.rs @@ -10,7 +10,7 @@ mod resize_scaling; mod schedule; pub use self::canvas::Canvas; -use self::canvas::Style; +pub use self::canvas::Style; pub use self::event::ButtonsState; pub use self::event_handle::EventListenerHandle; pub use self::resize_scaling::ResizeScaleHandle; diff --git a/src/platform_impl/web/window.rs b/src/platform_impl/web/window.rs index 6536bf42..882db237 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -1,3 +1,4 @@ +use crate::cursor::CustomCursor; use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size}; use crate::error::{ExternalError, NotSupportedError, OsError as RootOE}; use crate::icon::Icon; @@ -7,10 +8,10 @@ use crate::window::{ }; use crate::SendSyncWrapper; -use web_sys::HtmlCanvasElement; - +use super::cursor::SelectedCursor; use super::r#async::Dispatcher; use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget, Fullscreen}; +use web_sys::HtmlCanvasElement; use std::cell::RefCell; use std::collections::VecDeque; @@ -24,7 +25,7 @@ pub struct Inner { id: WindowId, pub window: web_sys::Window, canvas: Rc>, - previous_pointer: RefCell<&'static str>, + selected_cursor: RefCell, destroy_fn: Option>, } @@ -53,7 +54,7 @@ impl Window { id, window: window.clone(), canvas, - previous_pointer: RefCell::new("auto"), + selected_cursor: Default::default(), destroy_fn: Some(destroy_fn), }; @@ -195,10 +196,22 @@ impl Inner { #[inline] pub fn set_cursor_icon(&self, cursor: CursorIcon) { - *self.previous_pointer.borrow_mut() = cursor.name(); + *self.selected_cursor.borrow_mut() = SelectedCursor::Named(cursor); self.canvas.borrow().style().set("cursor", cursor.name()); } + #[inline] + pub fn set_custom_cursor(&self, cursor: CustomCursor) { + let canvas = self.canvas.borrow(); + let new_cursor = cursor.inner.build( + canvas.window(), + canvas.document(), + canvas.style(), + self.selected_cursor.take(), + ); + *self.selected_cursor.borrow_mut() = new_cursor; + } + #[inline] pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { Err(ExternalError::NotSupported(NotSupportedError::new())) @@ -225,10 +238,9 @@ impl Inner { if !visible { self.canvas.borrow().style().set("cursor", "none"); } else { - self.canvas + self.selected_cursor .borrow() - .style() - .set("cursor", &self.previous_pointer.borrow()); + .set_style(self.canvas.borrow().style()); } } diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index d4263920..3f5026e9 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -101,7 +101,7 @@ use runner::{EventLoopRunner, EventLoopRunnerShared}; use self::runner::RunnerState; -use super::window::set_skip_taskbar; +use super::{window::set_skip_taskbar, SelectedCursor}; type GetPointerFrameInfoHistory = unsafe extern "system" fn( pointerId: u32, @@ -2011,16 +2011,21 @@ unsafe fn public_window_callback_inner( // `WM_MOUSEMOVE` seems to come after `WM_SETCURSOR` for a given cursor movement. let in_client_area = super::loword(lparam as u32) as u32 == HTCLIENT; if in_client_area { - Some(window_state.mouse.cursor) + Some(window_state.mouse.selected_cursor.clone()) } else { None } }; match set_cursor_to { - Some(cursor) => { - let cursor = unsafe { LoadCursorW(0, util::to_windows_cursor(cursor)) }; - unsafe { SetCursor(cursor) }; + Some(selected_cursor) => { + let hcursor = match selected_cursor { + SelectedCursor::Named(cursor_icon) => unsafe { + LoadCursorW(0, util::to_windows_cursor(cursor_icon)) + }, + SelectedCursor::Custom(cursor) => cursor.as_raw_handle(), + }; + unsafe { SetCursor(hcursor) }; result = ProcResult::Value(0); } None => result = ProcResult::DefWindowProc(wparam), diff --git a/src/platform_impl/windows/icon.rs b/src/platform_impl/windows/icon.rs index 0be4fd1f..275f7fb6 100644 --- a/src/platform_impl/windows/icon.rs +++ b/src/platform_impl/windows/icon.rs @@ -1,18 +1,23 @@ -use std::{fmt, io, mem, path::Path, sync::Arc}; +use std::{ffi::c_void, fmt, io, mem, path::Path, sync::Arc}; +use cursor_icon::CursorIcon; use windows_sys::{ core::PCWSTR, Win32::{ Foundation::HWND, + Graphics::Gdi::{ + CreateBitmap, CreateCompatibleBitmap, DeleteObject, GetDC, ReleaseDC, SetBitmapBits, + }, UI::WindowsAndMessaging::{ - CreateIcon, DestroyIcon, LoadImageW, SendMessageW, HICON, ICON_BIG, ICON_SMALL, - IMAGE_ICON, LR_DEFAULTSIZE, LR_LOADFROMFILE, WM_SETICON, + CreateIcon, CreateIconIndirect, DestroyCursor, DestroyIcon, LoadImageW, SendMessageW, + HCURSOR, HICON, ICONINFO, ICON_BIG, ICON_SMALL, IMAGE_ICON, LR_DEFAULTSIZE, + LR_LOADFROMFILE, WM_SETICON, }, }, }; -use crate::dpi::PhysicalSize; use crate::icon::*; +use crate::{cursor::CursorImage, dpi::PhysicalSize}; use super::util; @@ -160,3 +165,92 @@ pub fn unset_for_window(hwnd: HWND, icon_type: IconType) { SendMessageW(hwnd, WM_SETICON, icon_type as usize, 0); } } + +#[derive(Debug, Clone)] +pub enum SelectedCursor { + Named(CursorIcon), + Custom(WinCursor), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } +} + +#[derive(Clone, Debug)] +pub struct WinCursor { + inner: Arc, +} + +impl WinCursor { + pub fn as_raw_handle(&self) -> HICON { + self.inner.handle + } + + fn from_handle(handle: HCURSOR) -> Self { + Self { + inner: Arc::new(RaiiCursor { handle }), + } + } + + pub fn new(image: &CursorImage) -> Result { + let mut bgra = image.rgba.clone(); + bgra.chunks_exact_mut(4).for_each(|chunk| chunk.swap(0, 2)); + + let w = image.width as i32; + let h = image.height as i32; + + unsafe { + let hdc_screen = GetDC(0); + if hdc_screen == 0 { + return Err(io::Error::last_os_error()); + } + let hbm_color = CreateCompatibleBitmap(hdc_screen, w, h); + ReleaseDC(0, hdc_screen); + if hbm_color == 0 { + return Err(io::Error::last_os_error()); + } + if SetBitmapBits(hbm_color, bgra.len() as u32, bgra.as_ptr() as *const c_void) == 0 { + DeleteObject(hbm_color); + return Err(io::Error::last_os_error()); + }; + + // Mask created according to https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createbitmap#parameters + let mask_bits: Vec = vec![0xff; ((((w + 15) >> 4) << 1) * h) as usize]; + let hbm_mask = CreateBitmap(w, h, 1, 1, mask_bits.as_ptr() as *const _); + if hbm_mask == 0 { + DeleteObject(hbm_color); + return Err(io::Error::last_os_error()); + } + + let icon_info = ICONINFO { + fIcon: 0, + xHotspot: image.hotspot_x as u32, + yHotspot: image.hotspot_y as u32, + hbmMask: hbm_mask, + hbmColor: hbm_color, + }; + + let handle = CreateIconIndirect(&icon_info as *const _); + DeleteObject(hbm_color); + DeleteObject(hbm_mask); + if handle == 0 { + return Err(io::Error::last_os_error()); + } + + Ok(Self::from_handle(handle)) + } + } +} + +#[derive(Debug)] +struct RaiiCursor { + handle: HCURSOR, +} + +impl Drop for RaiiCursor { + fn drop(&mut self) { + unsafe { DestroyCursor(self.handle) }; + } +} diff --git a/src/platform_impl/windows/mod.rs b/src/platform_impl/windows/mod.rs index bc6caeb6..b3784942 100644 --- a/src/platform_impl/windows/mod.rs +++ b/src/platform_impl/windows/mod.rs @@ -10,12 +10,13 @@ pub(crate) use self::{ event_loop::{ EventLoop, EventLoopProxy, EventLoopWindowTarget, PlatformSpecificEventLoopAttributes, }, - icon::WinIcon, + icon::{SelectedCursor, WinIcon}, monitor::{MonitorHandle, VideoMode}, window::Window, }; pub use self::icon::WinIcon as PlatformIcon; +pub(crate) use crate::cursor::CursorImage as PlatformCustomCursor; use crate::platform_impl::Fullscreen; use crate::event::DeviceId as RootDeviceId; diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index f19272ab..144129b8 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -55,6 +55,7 @@ use windows_sys::Win32::{ }; use crate::{ + cursor::CustomCursor, dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError as RootOsError}, icon::Icon, @@ -66,13 +67,13 @@ use crate::{ dpi::{dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_dpi}, drop_handler::FileDropHandler, event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID}, - icon::{self, IconType}, + icon::{self, IconType, WinCursor}, ime::ImeContext, keyboard::KeyEventBuilder, monitor::{self, MonitorHandle}, util, window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState}, - Fullscreen, PlatformSpecificWindowBuilderAttributes, WindowId, + Fullscreen, PlatformSpecificWindowBuilderAttributes, SelectedCursor, WindowId, }, window::{ CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType, @@ -396,13 +397,28 @@ impl Window { #[inline] pub fn set_cursor_icon(&self, cursor: CursorIcon) { - self.window_state_lock().mouse.cursor = cursor; + self.window_state_lock().mouse.selected_cursor = SelectedCursor::Named(cursor); self.thread_executor.execute_in_thread(move || unsafe { let cursor = LoadCursorW(0, util::to_windows_cursor(cursor)); SetCursor(cursor); }); } + #[inline] + pub fn set_custom_cursor(&self, cursor: CustomCursor) { + let new_cursor = match WinCursor::new(&cursor.inner) { + Ok(cursor) => cursor, + Err(err) => { + warn!("Failed to create custom cursor: {err}"); + return; + } + }; + self.window_state_lock().mouse.selected_cursor = SelectedCursor::Custom(new_cursor.clone()); + self.thread_executor.execute_in_thread(move || unsafe { + SetCursor(new_cursor.as_raw_handle()); + }); + } + #[inline] pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> { let confine = match mode { diff --git a/src/platform_impl/windows/window_state.rs b/src/platform_impl/windows/window_state.rs index cefaab68..9384fead 100644 --- a/src/platform_impl/windows/window_state.rs +++ b/src/platform_impl/windows/window_state.rs @@ -2,8 +2,8 @@ use crate::{ dpi::{PhysicalPosition, PhysicalSize, Size}, icon::Icon, keyboard::ModifiersState, - platform_impl::platform::{event_loop, util, Fullscreen}, - window::{CursorIcon, Theme, WindowAttributes}, + platform_impl::platform::{event_loop, util, Fullscreen, SelectedCursor}, + window::{Theme, WindowAttributes}, }; use std::io; use std::sync::MutexGuard; @@ -67,7 +67,7 @@ pub struct SavedWindow { #[derive(Clone)] pub struct MouseProperties { - pub cursor: CursorIcon, + pub(crate) selected_cursor: SelectedCursor, pub capture_count: u32, cursor_flags: CursorFlags, pub last_position: Option>, @@ -143,7 +143,7 @@ impl WindowState { ) -> WindowState { WindowState { mouse: MouseProperties { - cursor: CursorIcon::default(), + selected_cursor: SelectedCursor::default(), capture_count: 0, cursor_flags: CursorFlags::empty(), last_position: None, diff --git a/src/window.rs b/src/window.rs index 87531e8a..5b2e66ed 100644 --- a/src/window.rs +++ b/src/window.rs @@ -9,6 +9,7 @@ use crate::{ platform_impl, SendSyncWrapper, }; +pub use crate::cursor::{BadImage, CustomCursor, MAX_CURSOR_SIZE}; pub use crate::icon::{BadIcon, Icon}; #[doc(inline)] @@ -1336,6 +1337,7 @@ impl Window { /// Cursor functions. impl Window { /// Modifies the cursor icon of the window. + /// Overwrites cursors set in [`Window::set_custom_cursor`]. /// /// ## Platform-specific /// @@ -1346,6 +1348,19 @@ impl Window { .maybe_queue_on_main(move |w| w.set_cursor_icon(cursor)) } + /// Modifies the cursor icon of the window with a custom cursor. + /// Overwrites cursors set in [`Window::set_cursor_icon`]. + /// + /// ## Platform-specific + /// + /// - **iOS / Android / Orbital:** Unsupported. + #[inline] + pub fn set_custom_cursor(&self, cursor: &CustomCursor) { + let cursor = cursor.clone(); + self.window + .maybe_queue_on_main(move |w| w.set_custom_cursor(cursor)) + } + /// Changes the position of the cursor in window coordinates. /// /// ```no_run diff --git a/tests/send_objects.rs b/tests/send_objects.rs index 7941b167..0288c6c3 100644 --- a/tests/send_objects.rs +++ b/tests/send_objects.rs @@ -28,3 +28,8 @@ fn ids_send() { needs_send::(); needs_send::(); } + +#[test] +fn custom_cursor_send() { + needs_send::(); +} diff --git a/tests/sync_object.rs b/tests/sync_object.rs index 23c60125..96b3b7df 100644 --- a/tests/sync_object.rs +++ b/tests/sync_object.rs @@ -11,3 +11,8 @@ fn window_sync() { fn window_builder_sync() { needs_sync::(); } + +#[test] +fn custom_cursor_sync() { + needs_sync::(); +}