diff --git a/CHANGELOG.md b/CHANGELOG.md index 3c109e2098..964f360f64 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 36009c00b0..6997b34fbd 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 97286ceb22..5855979c1f 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 0000000000..c685cd7540 --- /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 0000000000..9bfdf369b3 Binary files /dev/null and b/examples/data/cross.png differ diff --git a/examples/data/cross2.png b/examples/data/cross2.png new file mode 100644 index 0000000000..b9f7a486b1 Binary files /dev/null and b/examples/data/cross2.png differ diff --git a/src/cursor.rs b/src/cursor.rs new file mode 100644 index 0000000000..ca55a61f14 --- /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 03af6f4898..ce26ef0e9d 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 06e3f682a4..c01308e972 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 72570c4984..68be1a7534 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 dec71cdeff..5fc9d7431d 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 28c7f318cf..e70d843f60 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 a6068bc762..6b6186cf44 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 967ee93f17..a3ff57c330 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 0000000000..483486197f --- /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 ea74588823..77e67f48be 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 ae6558a810..13d74c201c 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 73c0c92c5f..9355bb8ab4 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 8d62cfa7f0..e9b457d967 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 f806b0825c..1bff92eb0b 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 3f0fa32606..2d4314c4fb 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 0000000000..aa6e48486e --- /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 6377ad420c..de83f0a27f 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 0b5944c3da..d108eb1f09 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 832fc149c1..8c6eb12ada 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 7169ca9dbf..31bee1f03e 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 7e7d6f383e..c98db38fba 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 5aa56328a7..121d020380 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 26ab72681a..9d800af7cf 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 0000000000..22ae3b2496 --- /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 3abd268414..433a4e8bed 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 6cf5d339b0..ada3cc5f31 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 d86f991c47..089d6e8525 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 6536bf42c0..882db237b9 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 d4263920af..3f5026e95a 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 0be4fd1f5b..275f7fb699 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 bc6caeb6f5..b378494264 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 f19272aba1..144129b892 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 cefaab6873..9384fead6e 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 87531e8ad8..5b2e66edf1 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 7941b16769..0288c6c3c4 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 23c6012545..96b3b7df3b 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::(); +}