From 6f7f7c3d7291f045d364b2347fba5f09d15b796b Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Fri, 17 Nov 2023 21:45:13 +0200 Subject: [PATCH 01/35] Implement custom cursor images for all desktop platforms --- Cargo.toml | 3 +- examples/custom_cursors.rs | 93 +++++++++++ examples/data/cross.png | Bin 0 -> 159 bytes examples/data/cross2.png | Bin 0 -> 129 bytes src/cursor.rs | 150 ++++++++++++++++++ src/lib.rs | 1 + src/platform/web.rs | 19 +++ 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 + .../linux/wayland/types/cursor.rs | 113 +++++++++++++ src/platform_impl/linux/wayland/types/mod.rs | 1 + src/platform_impl/linux/wayland/window/mod.rs | 6 + .../linux/wayland/window/state.rs | 71 ++++++++- src/platform_impl/linux/x11/util/cursor.rs | 63 +++++++- src/platform_impl/linux/x11/util/mod.rs | 2 +- src/platform_impl/linux/x11/window.rs | 75 +++++++-- .../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 | 135 ++++++++++++++++ src/platform_impl/web/mod.rs | 2 + src/platform_impl/web/window.rs | 31 ++-- src/platform_impl/windows/event_loop.rs | 39 +++-- src/platform_impl/windows/icon.rs | 101 +++++++++++- src/platform_impl/windows/mod.rs | 3 +- src/platform_impl/windows/window.rs | 17 +- src/platform_impl/windows/window_state.rs | 8 +- src/window.rs | 8 + tests/send_objects.rs | 5 + tests/sync_object.rs | 5 + 37 files changed, 1007 insertions(+), 68 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/Cargo.toml b/Cargo.toml index 36009c00b0..4ac89f1502 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -168,6 +168,7 @@ wayland-protocols-plasma = { version = "0.2.0", features = [ "client" ], optiona x11-dl = { version = "2.18.5", optional = true } x11rb = { version = "0.13.0", default-features = false, features = ["allow-unsafe-code", "dl-libxcb", "randr", "resource_manager", "xinput", "xkb"], optional = true } xkbcommon-dl = "0.4.0" +memfd = "0.6.4" [target.'cfg(target_os = "redox")'.dependencies] orbclient = { version = "0.3.42", default-features = false } @@ -218,7 +219,7 @@ web-time = "0.2" [target.'cfg(target_family = "wasm")'.dev-dependencies] console_log = "1" -web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] } +web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d', 'Url', 'ImageData'] } [workspace] members = [ diff --git a/examples/custom_cursors.rs b/examples/custom_cursors.rs new file mode 100644 index 0000000000..3a01c91ef8 --- /dev/null +++ b/examples/custom_cursors.rs @@ -0,0 +1,93 @@ +#![allow(clippy::single_match, clippy::disallowed_methods)] + +#[cfg(not(wasm_platform))] +use simple_logger::SimpleLogger; +use winit::{ + cursor::CustomCursor, + event::{ElementState, Event, KeyEvent, WindowEvent}, + event_loop::EventLoop, + keyboard::{KeyCode, PhysicalKey}, + window::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 u32, h as u32); + 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, + physical_key: PhysicalKey::Code(code), + .. + }, + .. + } => match code { + KeyCode::KeyA => { + log::debug!("Setting cursor to {:?}", cursor_idx); + window.set_custom_cursor(&custom_cursors[cursor_idx]); + cursor_idx = (cursor_idx + 1) % 2; + } + KeyCode::KeyS => { + log::debug!("Setting cursor icon to default"); + window.set_cursor_icon(Default::default()); + } + KeyCode::KeyD => { + 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 0000000000..41da75d989 --- /dev/null +++ b/src/cursor.rs @@ -0,0 +1,150 @@ +use core::fmt; +use std::{error::Error, sync::Arc}; + +use crate::{icon::PIXEL_SIZE, platform_impl::PlatformCustomCursor}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CustomCursor { + pub(crate) inner: Arc, +} + +impl CustomCursor { + pub fn from_rgba( + rgba: Vec, + width: u32, + height: u32, + hotspot_x: u32, + hotspot_y: u32, + ) -> Result { + Ok(Self { + inner: PlatformCustomCursor::from_rgba(rgba, width, height, hotspot_x, hotspot_y)? + .into(), + }) + } +} + +/// 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: u32, + height: u32, + hotspot_x: u32, + hotspot_y: u32, + ) -> Result { + CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?; + Ok(Self) + } +} + +#[derive(Debug)] +/// An error produced when using [`Icon::from_rgba`] with invalid arguments. +pub enum BadImage { + /// 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: u32, + height: u32, + width_x_height: usize, + pixel_count: usize, + }, + /// Produced when the hotspot is outside the image bounds + HotspotOutOfBounds { + width: u32, + height: u32, + hotspot_x: u32, + hotspot_y: u32, + }, +} + +impl fmt::Display for BadImage { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + 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 { + fn source(&self) -> Option<&(dyn Error + 'static)> { + Some(self) + } +} + +/// 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: u32, + pub(crate) height: u32, + pub(crate) hotspot_x: u32, + pub(crate) hotspot_y: u32, +} + +#[allow(dead_code)] +impl CursorImage { + pub fn from_rgba( + rgba: Vec, + width: u32, + height: u32, + hotspot_x: u32, + hotspot_y: u32, + ) -> Result { + if rgba.len() % PIXEL_SIZE != 0 { + return Err(BadImage::ByteCountNotDivisibleBy4 { + byte_count: rgba.len(), + }); + } + let pixel_count = rgba.len() / PIXEL_SIZE; + if pixel_count != (width * height) as usize { + return Err(BadImage::DimensionsVsPixelCount { + width, + height, + width_x_height: (width * height) as usize, + 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, + }) + } +} diff --git a/src/lib.rs b/src/lib.rs index 03af6f4898..1ce5130491 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,6 +172,7 @@ extern crate bitflags; pub mod dpi; #[macro_use] pub mod error; +pub 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..21aa7655a6 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,20 @@ pub enum PollStrategy { #[default] Scheduler, } + +pub trait CustomCursorExtWebSys { + fn from_url(url: String, hotspot_x: u32, hotspot_y: u32) -> Self; +} + +impl CustomCursorExtWebSys for CustomCursor { + fn from_url(url: String, hotspot_x: u32, hotspot_y: u32) -> 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/types/cursor.rs b/src/platform_impl/linux/wayland/types/cursor.rs new file mode 100644 index 0000000000..7fbce1dd4a --- /dev/null +++ b/src/platform_impl/linux/wayland/types/cursor.rs @@ -0,0 +1,113 @@ +use std::{fs::File, io::Write, sync::Arc}; + +use cursor_icon::CursorIcon; +use rustix::fd::{AsFd, OwnedFd}; +use wayland_backend::client::ObjectData; +use wayland_client::{ + protocol::{ + wl_buffer::{self, WlBuffer}, + wl_shm::{self, Format, WlShm}, + wl_shm_pool::{self, WlShmPool}, + }, + Connection, Proxy, WEnum, +}; + +use crate::cursor::CursorImage; + +#[derive(Debug)] +pub struct CustomCursorInternal { + _file: File, + shm_pool: WlShmPool, + pub buffer: WlBuffer, + pub w: i32, + pub h: i32, + pub hot_x: i32, + pub hot_y: i32, +} + +impl CustomCursorInternal { + pub fn new(connection: &Connection, shm: &WlShm, image: &CursorImage) -> Self { + let mfd = memfd::MemfdOptions::default() + .close_on_exec(true) + .create("winit-custom-cursor") + .unwrap(); + let mut file = mfd.into_file(); + file.set_len(image.rgba.len() as u64).unwrap(); + for chunk in image.rgba.chunks_exact(4) { + file.write_all(&[chunk[2], chunk[1], chunk[0], chunk[3]]) + .unwrap(); + } + file.flush().unwrap(); + + let pool_id = connection + .send_request( + shm, + wl_shm::Request::CreatePool { + size: image.rgba.len() as i32, + fd: file.as_fd(), + }, + Some(Arc::new(IgnoreObjectData)), + ) + .unwrap(); + let shm_pool = WlShmPool::from_id(connection, pool_id).unwrap(); + + let buffer_id = connection + .send_request( + &shm_pool, + wl_shm_pool::Request::CreateBuffer { + offset: 0, + width: image.width as i32, + height: image.width as i32, + stride: (image.width as i32 * 4), + format: WEnum::Value(Format::Argb8888), + }, + Some(Arc::new(IgnoreObjectData)), + ) + .unwrap(); + let buffer = WlBuffer::from_id(connection, buffer_id).unwrap(); + + CustomCursorInternal { + _file: file, + shm_pool, + buffer, + w: image.width as i32, + h: image.height as i32, + hot_x: image.hotspot_x as i32, + hot_y: image.hotspot_y as i32, + } + } + + pub fn destroy(&self, connection: &Connection) { + connection + .send_request(&self.buffer, wl_buffer::Request::Destroy, None) + .unwrap(); + connection + .send_request(&self.shm_pool, wl_shm_pool::Request::Destroy, None) + .unwrap(); + } +} + +#[derive(Debug)] +pub enum SelectedCursor { + Named(CursorIcon), + Custom(CustomCursorInternal), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } +} + +struct IgnoreObjectData; + +impl ObjectData for IgnoreObjectData { + fn event( + self: Arc, + _: &wayland_client::backend::Backend, + _: wayland_client::backend::protocol::Message, + ) -> Option> { + None + } + fn destroyed(&self, _: wayland_client::backend::ObjectId) {} +} 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..2e3cca3b1a 100644 --- a/src/platform_impl/linux/wayland/window/state.rs +++ b/src/platform_impl/linux/wayland/window/state.rs @@ -18,8 +18,8 @@ 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; @@ -27,11 +27,13 @@ use sctk::shm::Shm; use sctk::subcompositor::SubcompositorState; use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; +use crate::cursor::CustomCursor; 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::{CustomCursorInternal, SelectedCursor}; use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager; use crate::platform_impl::WindowId; use crate::window::{CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme}; @@ -66,8 +68,7 @@ pub struct WindowState { /// 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 +179,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, @@ -603,7 +604,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); } @@ -692,7 +696,11 @@ impl WindowState { /// /// Providing `None` will hide the cursor. pub fn set_cursor(&mut self, cursor_icon: CursorIcon) { - self.cursor_icon = cursor_icon; + if let SelectedCursor::Custom(cursor) = &self.selected_cursor { + cursor.destroy(&self.connection); + } + + self.selected_cursor = SelectedCursor::Named(cursor_icon); if !self.cursor_visible { return; @@ -705,6 +713,46 @@ impl WindowState { }) } + pub fn set_custom_cursor(&mut self, cursor: CustomCursor) { + if let SelectedCursor::Custom(cursor) = &self.selected_cursor { + cursor.destroy(&self.connection); + } + + let cursor = CustomCursorInternal::new(&self.connection, &self.shm, &cursor.inner); + + if self.cursor_visible { + self.apply_custom_cursor(&cursor); + } + + self.selected_cursor = SelectedCursor::Custom(cursor); + } + + pub fn apply_custom_cursor(&self, cursor: &CustomCursorInternal) { + 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), 0, 0); + surface.damage_buffer(0, 0, cursor.w, cursor.h); + 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.hot_x, cursor.hot_y); + }); + } + /// 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 +887,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(); @@ -1028,6 +1079,10 @@ impl WindowState { impl Drop for WindowState { fn drop(&mut self) { + if let SelectedCursor::Custom(cursor) = &self.selected_cursor { + cursor.destroy(&self.connection); + } + if let Some(blur) = self.blur.take() { blur.release(); } diff --git a/src/platform_impl/linux/x11/util/cursor.rs b/src/platform_impl/linux/x11/util/cursor.rs index 8d62cfa7f0..92b5e03e5e 100644 --- a/src/platform_impl/linux/x11/util/cursor.rs +++ b/src/platform_impl/linux/x11/util/cursor.rs @@ -1,12 +1,64 @@ -use std::ffi::CString; -use std::iter; +use core::slice; +use std::{ffi::CString, iter}; use x11rb::connection::Connection; -use crate::window::CursorIcon; +use crate::{cursor::CursorImage, window::CursorIcon}; use super::*; +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct CustomCursorInternal(ffi::Cursor); + +impl CustomCursorInternal { + pub unsafe fn new( + xcursor: &ffi::Xcursor, + display: *mut ffi::Display, + image: &CursorImage, + ) -> Self { + unsafe { + let ximage = (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; + (*ximage).yhot = image.hotspot_y; + (*ximage).delay = 0; + + let dst = + slice::from_raw_parts_mut((*ximage).pixels, (image.width * image.height) as usize); + for (i, chunk) in image.rgba.chunks_exact(4).enumerate() { + dst[i] = (chunk[0] as u32) << 16 + | (chunk[1] as u32) << 8 + | (chunk[2] as u32) + | (chunk[3] as u32) << 24; + } + + let cursor = (xcursor.XcursorImageLoadCursor)(display, ximage); + (xcursor.XcursorImageDestroy)(ximage); + Self(cursor) + } + } + + pub unsafe fn destroy(&self, xlib: &ffi::Xlib, display: *mut ffi::Display) { + unsafe { + (xlib.XFreeCursor)(display, self.0); + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SelectedCursor { + Custom(CustomCursorInternal), + Named(CursorIcon), +} + +impl Default for SelectedCursor { + fn default() -> Self { + SelectedCursor::Named(Default::default()) + } +} + impl XConnection { pub fn set_cursor_icon(&self, window: xproto::Window, cursor: Option) { let cursor = *self @@ -20,6 +72,11 @@ impl XConnection { .expect("Failed to set cursor"); } + pub fn set_custom_cursor(&self, window: xproto::Window, cursor: CustomCursorInternal) { + self.update_cursor(window, cursor.0) + .expect("Failed to set cursor"); + } + fn create_empty_cursor(&self) -> ffi::Cursor { let data = 0; let pixmap = unsafe { 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..e4268d3fa5 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; + +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, CustomCursorInternal, 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,44 @@ 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), + ); + + if let SelectedCursor::Custom(old_cursor) = old_cursor { + unsafe { + old_cursor.destroy(&self.xconn.xlib, self.xconn.display); + } + } + #[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: CustomCursor) { + let new_cursor = unsafe { + CustomCursorInternal::new(&self.xconn.xcursor, self.xconn.display, &cursor.inner) + }; + let old_cursor = replace( + &mut *self.selected_cursor.lock().unwrap(), + SelectedCursor::Custom(new_cursor), + ); + if let SelectedCursor::Custom(old_cursor) = old_cursor { + unsafe { + old_cursor.destroy(&self.xconn.xlib, self.xconn.display); + } + } + + #[allow(clippy::mutex_atomic)] + if *self.cursor_visible.lock().unwrap() { + self.xconn.set_custom_cursor(self.xwindow, 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 +1664,23 @@ impl UnownedWindow { return; } let cursor = if visible { - Some(*self.cursor.lock().unwrap()) + Some(*self.selected_cursor.lock().unwrap()) } 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] @@ -1948,6 +1994,17 @@ impl UnownedWindow { } } +impl Drop for UnownedWindow { + #[inline] + fn drop(&mut self) { + if let SelectedCursor::Custom(cursor) = *self.selected_cursor.lock().unwrap() { + unsafe { + cursor.destroy(&self.xconn.xlib, self.xconn.display); + } + } + } +} + /// Cast a dimension value into a hinted dimension for `WmSizeHints`, clamping if too large. fn cast_dimension_to_hint(val: u32) -> i32 { val.try_into().unwrap_or(i32::MAX) 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..c922b487e7 --- /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() as *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..2e59d0faaa 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(image: &CursorImage) -> Id { + let w = image.width; + let h = image.height; + + let bitmap = NSBitmapImageRep::init_rgba(w as isize, h as isize); + let bitmap_data = + unsafe { std::slice::from_raw_parts_mut(bitmap.bitmap_data(), (w * h * 4) as usize) }; + bitmap_data.copy_from_slice(&image.rgba); + + let nsimage = NSImage::init_with_size(NSSize::new(w.into(), h.into())); + nsimage.add_representation(&bitmap); + + let hotspot = NSPoint::new(image.hotspot_x as f64, image.hotspot_y as f64); + + NSCursor::new(&nsimage, 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..22ffc0117a --- /dev/null +++ b/src/platform_impl/web/cursor.rs @@ -0,0 +1,135 @@ +use crate::cursor::{BadImage, CursorImage}; +use cursor_icon::CursorIcon; +use wasm_bindgen::JsCast; +use web_sys::{CanvasRenderingContext2d, Document, HtmlCanvasElement, ImageData, Url}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum WebCustomCursor { + Image(CursorImage), + Url { + url: String, + hotspot_x: u32, + hotspot_y: u32, + }, +} + +impl WebCustomCursor { + pub fn from_rgba( + rgba: Vec, + width: u32, + height: u32, + hotspot_x: u32, + hotspot_y: u32, + ) -> Result { + Ok(Self::Image(CursorImage::from_rgba( + rgba, width, height, hotspot_x, hotspot_y, + )?)) + } +} + +#[derive(Debug)] +pub enum SelectedCursor { + Named(CursorIcon), + Custom(CustomCursorInternal), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } +} + +#[derive(Debug)] +pub struct CustomCursorInternal { + style: String, + data_url: Option, +} + +impl CustomCursorInternal { + pub fn new(document: &Document, cursor: &WebCustomCursor) -> Self { + match cursor { + WebCustomCursor::Image(image) => Self::from_image(document, image), + WebCustomCursor::Url { + url, + hotspot_x, + hotspot_y, + } => Self { + style: format!("url({}) {} {}, auto", url, hotspot_x, hotspot_y), + data_url: None, + }, + } + } + + fn from_image(document: &Document, image: &CursorImage) -> Self { + let cursor_icon_canvas = document + .create_element("canvas") + .unwrap() + .dyn_into::() + .unwrap(); + + #[allow(clippy::disallowed_methods)] + cursor_icon_canvas.set_width(image.width); + #[allow(clippy::disallowed_methods)] + cursor_icon_canvas.set_height(image.height); + + let context = cursor_icon_canvas + .get_context("2d") + .unwrap() + .unwrap() + .dyn_into::() + .unwrap(); + + #[cfg(target_feature = "atomics")] + let image_data = { + // Can't create array directly when backed by SharedArrayBuffer. + // Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223 + 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) + .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) + .unwrap(); + + context.put_image_data(&image_data, 0.0, 0.0).unwrap(); + + let data_url = cursor_icon_canvas.to_data_url().unwrap(); + + Self { + style: format!( + "url({}) {} {}, auto", + data_url, image.hotspot_x, image.hotspot_y + ), + data_url: Some(data_url), + } + } + + pub fn style(&self) -> &str { + &self.style + } +} + +impl Drop for CustomCursorInternal { + fn drop(&mut self) { + if let Some(data_url) = &self.data_url { + Url::revoke_object_url(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/window.rs b/src/platform_impl/web/window.rs index 6536bf42c0..f2912ebdaf 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::{CustomCursorInternal, 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,20 @@ 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 new_cursor = CustomCursorInternal::new(self.canvas.borrow().document(), &cursor.inner); + self.canvas + .borrow() + .style() + .set("cursor", new_cursor.style()); + *self.selected_cursor.borrow_mut() = SelectedCursor::Custom(new_cursor); + } + #[inline] pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> { Err(ExternalError::NotSupported(NotSupportedError::new())) @@ -225,10 +236,12 @@ impl Inner { if !visible { self.canvas.borrow().style().set("cursor", "none"); } else { - self.canvas - .borrow() - .style() - .set("cursor", &self.previous_pointer.borrow()); + let selected = &*self.selected_cursor.borrow(); + let style = match selected { + SelectedCursor::Named(cursor) => cursor.name(), + SelectedCursor::Custom(cursor) => cursor.style(), + }; + self.canvas.borrow().style().set("cursor", style); } } diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index d4263920af..98c99f8a85 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, @@ -2004,26 +2004,23 @@ unsafe fn public_window_callback_inner( } WM_SETCURSOR => { - let set_cursor_to = { - let window_state = userdata.window_state_lock(); - // The return value for the preceding `WM_NCHITTEST` message is conveniently - // provided through the low-order word of lParam. We use that here since - // `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) - } else { - None - } - }; - - match set_cursor_to { - Some(cursor) => { - let cursor = unsafe { LoadCursorW(0, util::to_windows_cursor(cursor)) }; - unsafe { SetCursor(cursor) }; - result = ProcResult::Value(0); - } - None => result = ProcResult::DefWindowProc(wparam), + let window_state = userdata.window_state_lock(); + // The return value for the preceding `WM_NCHITTEST` message is conveniently + // provided through the low-order word of lParam. We use that here since + // `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 { + // No cursor + result = ProcResult::DefWindowProc(wparam); + } else { + let cursor = match &window_state.mouse.selected_cursor { + SelectedCursor::Named(cursor_icon) => unsafe { + LoadCursorW(0, util::to_windows_cursor(*cursor_icon)) + }, + SelectedCursor::Custom(icon) => icon.as_raw_handle(), + }; + unsafe { SetCursor(cursor) }; + result = ProcResult::Value(0); } } diff --git a/src/platform_impl/windows/icon.rs b/src/platform_impl/windows/icon.rs index 0be4fd1f5b..8531d11abb 100644 --- a/src/platform_impl/windows/icon.rs +++ b/src/platform_impl/windows/icon.rs @@ -1,18 +1,21 @@ -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, 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 +163,93 @@ pub fn unset_for_window(hwnd: HWND, icon_type: IconType) { SendMessageW(hwnd, WM_SETICON, icon_type as usize, 0); } } + +#[derive(Debug)] +struct RaiiCursor { + handle: HCURSOR, +} + +#[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) -> Self { + 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; + + let handle = unsafe { + 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 { + panic!("Failed to create mask bitmap"); + } + + let hdc_screen = GetDC(0); + if hdc_screen == 0 { + panic!("Failed to get screen DC"); + } + + let hbm_color = CreateCompatibleBitmap(hdc_screen, w, h); + if hbm_color == 0 { + panic!("Failed to create color bitmap"); + } + + if SetBitmapBits(hbm_color, bgra.len() as u32, bgra.as_ptr() as *const c_void) == 0 { + panic!("Failed to set bitmap bits"); + }; + + if ReleaseDC(0, hdc_screen) == 0 { + panic!("Failed to release screen DC"); + } + + let icon_info = ICONINFO { + fIcon: 0, + xHotspot: image.hotspot_x, + yHotspot: image.hotspot_y, + hbmMask: hbm_mask, + hbmColor: hbm_color, + }; + + CreateIconIndirect(&icon_info as *const _) + }; + + if handle == 0 { + panic!("Failed to create icon"); + } + + Self::from_handle(handle) + } +} + +impl Drop for RaiiCursor { + fn drop(&mut self) { + unsafe { DestroyCursor(self.handle) }; + } +} + +#[derive(Debug, Clone)] +pub enum SelectedCursor { + Named(CursorIcon), + Custom(WinCursor), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } +} 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..1b12ea088d 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,23 @@ 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 = WinCursor::new(&cursor.inner); + let handle = new_cursor.as_raw_handle(); + self.window_state_lock().mouse.selected_cursor = SelectedCursor::Custom(new_cursor); + self.thread_executor.execute_in_thread(move || unsafe { + SetCursor(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..6f61652884 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2,6 +2,7 @@ use std::fmt; use crate::{ + cursor::CustomCursor, dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError}, event_loop::EventLoopWindowTarget, @@ -1346,6 +1347,13 @@ impl Window { .maybe_queue_on_main(move |w| w.set_cursor_icon(cursor)) } + #[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..1815097b77 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..daec42278c 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::(); +} From 9da54a8966393a6337f706e16f42760c70dd2262 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Sat, 18 Nov 2023 11:46:29 +0100 Subject: [PATCH 02/35] Web: general improvements --- Cargo.toml | 6 ++++-- src/platform_impl/web/cursor.rs | 16 ++++++---------- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4ac89f1502..05d9fa40d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -191,6 +191,7 @@ features = [ 'FocusEvent', 'HtmlCanvasElement', 'HtmlElement', + 'ImageData', 'IntersectionObserver', 'IntersectionObserverEntry', 'KeyboardEvent', @@ -207,7 +208,8 @@ features = [ 'ResizeObserverSize', 'VisibilityState', 'Window', - 'WheelEvent' + 'WheelEvent', + 'Url', ] [target.'cfg(target_family = "wasm")'.dependencies] @@ -219,7 +221,7 @@ web-time = "0.2" [target.'cfg(target_family = "wasm")'.dev-dependencies] console_log = "1" -web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d', 'Url', 'ImageData'] } +web-sys = { version = "0.3.22", features = ['CanvasRenderingContext2d'] } [workspace] members = [ diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index 22ffc0117a..af212a22ca 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -61,28 +61,24 @@ impl CustomCursorInternal { } fn from_image(document: &Document, image: &CursorImage) -> Self { - let cursor_icon_canvas = document - .create_element("canvas") - .unwrap() - .dyn_into::() - .unwrap(); + let cursor_icon_canvas: HtmlCanvasElement = + document.create_element("canvas").unwrap().unchecked_into(); #[allow(clippy::disallowed_methods)] cursor_icon_canvas.set_width(image.width); #[allow(clippy::disallowed_methods)] cursor_icon_canvas.set_height(image.height); - let context = cursor_icon_canvas + let context: CanvasRenderingContext2d = cursor_icon_canvas .get_context("2d") .unwrap() .unwrap() - .dyn_into::() - .unwrap(); + .unchecked_into(); + // 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 = { - // Can't create array directly when backed by SharedArrayBuffer. - // Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223 use js_sys::{Uint8Array, Uint8ClampedArray}; use wasm_bindgen::prelude::wasm_bindgen; use wasm_bindgen::JsValue; From c3267b9515157189258302a36e0a964a4aed8f7c Mon Sep 17 00:00:00 2001 From: daxpedda Date: Sat, 18 Nov 2023 14:52:18 +0100 Subject: [PATCH 03/35] Web: generate a cached data URL --- Cargo.toml | 5 + src/platform_impl/web/cursor.rs | 241 +++++++++++++++++++----- src/platform_impl/web/web_sys/canvas.rs | 2 +- src/platform_impl/web/web_sys/mod.rs | 2 +- src/platform_impl/web/window.rs | 25 ++- 5 files changed, 213 insertions(+), 62 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 05d9fa40d9..9b75a4b69f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -180,6 +180,7 @@ version = "0.3.64" features = [ 'AbortController', 'AbortSignal', + 'Blob', 'console', 'CssStyleDeclaration', 'Document', @@ -191,6 +192,9 @@ features = [ 'FocusEvent', 'HtmlCanvasElement', 'HtmlElement', + 'ImageBitmap', + 'ImageBitmapOptions', + 'ImageBitmapRenderingContext', 'ImageData', 'IntersectionObserver', 'IntersectionObserverEntry', @@ -201,6 +205,7 @@ features = [ 'Node', 'PageTransitionEvent', 'PointerEvent', + 'PremultiplyAlpha', 'ResizeObserver', 'ResizeObserverBoxOptions', 'ResizeObserverEntry', diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index af212a22ca..9897a85a5a 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -1,7 +1,20 @@ +use std::{ + cell::RefCell, + mem, + ops::{Deref, DerefMut}, + rc::{Rc, Weak}, +}; + use crate::cursor::{BadImage, CursorImage}; use cursor_icon::CursorIcon; -use wasm_bindgen::JsCast; -use web_sys::{CanvasRenderingContext2d, Document, HtmlCanvasElement, ImageData, Url}; +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 { @@ -25,12 +38,40 @@ impl WebCustomCursor { rgba, width, height, hotspot_x, hotspot_y, )?)) } + + pub(super) fn build( + &self, + window: &Window, + document: &Document, + style: &Style, + previous: SelectedCursor, + ) -> SelectedCursor { + 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 = format!("url({}) {} {}, auto", url, hotspot_x, hotspot_y); + style.set("cursor", &value); + SelectedCursor::Url(value) + } + } + } } #[derive(Debug)] pub enum SelectedCursor { Named(CursorIcon), - Custom(CustomCursorInternal), + Url(String), + Image(Rc>), } impl Default for SelectedCursor { @@ -39,42 +80,74 @@ impl Default for SelectedCursor { } } +impl SelectedCursor { + pub fn set_style(&self, style: &Style) { + let value = match self { + SelectedCursor::Named(icon) => icon.name(), + SelectedCursor::Url(url) => url, + SelectedCursor::Image(image) => { + let image = image.borrow(); + let value = match image.deref() { + CursorImageState::Loading { previous, .. } => previous.style(), + CursorImageState::Ready(WebCursorImage { style, .. }) => style, + }; + return style.set("cursor", value); + } + }; + + style.set("cursor", value); + } +} + #[derive(Debug)] -pub struct CustomCursorInternal { - style: String, - data_url: Option, +pub enum Previous { + Named(CursorIcon), + Url(String), + Image(WebCursorImage), } -impl CustomCursorInternal { - pub fn new(document: &Document, cursor: &WebCustomCursor) -> Self { - match cursor { - WebCustomCursor::Image(image) => Self::from_image(document, image), - WebCustomCursor::Url { - url, - hotspot_x, - hotspot_y, - } => Self { - style: format!("url({}) {} {}, auto", url, hotspot_x, hotspot_y), - data_url: None, - }, +impl Previous { + fn style(&self) -> &str { + match self { + Previous::Named(icon) => icon.name(), + Previous::Url(url) => url, + Previous::Image(WebCursorImage { style, .. }) => style, } } +} - fn from_image(document: &Document, image: &CursorImage) -> Self { - let cursor_icon_canvas: HtmlCanvasElement = - document.create_element("canvas").unwrap().unchecked_into(); - - #[allow(clippy::disallowed_methods)] - cursor_icon_canvas.set_width(image.width); - #[allow(clippy::disallowed_methods)] - cursor_icon_canvas.set_height(image.height); +impl From for Previous { + fn from(value: SelectedCursor) -> Self { + match value { + SelectedCursor::Named(icon) => Self::Named(icon), + SelectedCursor::Url(url) => Self::Url(url), + SelectedCursor::Image(image) => match Rc::into_inner(image).unwrap().into_inner() { + CursorImageState::Loading { previous, .. } => previous, + CursorImageState::Ready(internal) => Self::Image(internal), + }, + } + } +} - let context: CanvasRenderingContext2d = cursor_icon_canvas - .get_context("2d") - .unwrap() - .unwrap() - .unchecked_into(); +#[derive(Debug)] +pub enum CursorImageState { + Loading { + style: Style, + previous: Previous, + hotspot_x: u32, + hotspot_y: u32, + }, + Ready(WebCursorImage), +} +impl CursorImageState { + fn from_image( + window: &Window, + document: Document, + style: Style, + image: &CursorImage, + previous: SelectedCursor, + ) -> 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")] @@ -104,28 +177,102 @@ impl CustomCursorInternal { ImageData::new_with_u8_clamped_array(wasm_bindgen::Clamped(&image.rgba), image.width) .unwrap(); - context.put_image_data(&image_data, 0.0, 0.0).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 data_url = cursor_icon_canvas.to_data_url().unwrap(); + let state = Rc::new(RefCell::new(Self::Loading { + style, + previous: previous.into(), + hotspot_x: image.hotspot_x, + hotspot_y: image.hotspot_y, + })); - Self { - style: format!( - "url({}) {} {}, auto", - data_url, image.hotspot_x, image.hotspot_y - ), - data_url: Some(data_url), - } - } + 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(); - pub fn style(&self) -> &str { - &self.style + if weak.strong_count() == 0 { + return; + } + + let canvas: HtmlCanvasElement = + document.create_element("canvas").unwrap().unchecked_into(); + #[allow(clippy::disallowed_methods)] + canvas.set_width(width); + #[allow(clippy::disallowed_methods)] + canvas.set_height(height); + + 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(blob) = blob else { + return; + }; + let Some(state) = weak.borrow_mut().take().and_then(|weak| weak.upgrade()) else { + return; + }; + let mut state = state.borrow_mut(); + // Extract the hotspots. + let CursorImageState::Loading { hotspot_x, hotspot_y, .. } = *state else { + unreachable!("found invalid state") + }; + + let data_url = Url::create_object_url_with_blob(&blob).unwrap(); + // Extract `Style`, which we couldn't do earlier without cloning it. + let CursorImageState::Loading { style, .. } = mem::replace(state.deref_mut(), CursorImageState::Ready(WebCursorImage { + style: format!("url({}) {} {}, auto", data_url, hotspot_x, hotspot_y), + data_url, + })) else { + unreachable!("found invalid state") + }; + // Extract the `cursor` property value, which we couldn't cache earlier without cloning. + let CursorImageState::Ready(WebCursorImage { style: value, .. }) = state.deref() else { + unreachable!("found invalid state") + }; + style.set("cursor", value); + }); + }); + } + + CURRENT_STATE.with(|state| *state.borrow_mut() = Some(weak)); + CALLBACK + .with(|callback| canvas.to_blob(callback.as_ref().unchecked_ref()).unwrap()); + } + }); + + state } } -impl Drop for CustomCursorInternal { +#[derive(Debug)] +pub struct WebCursorImage { + style: String, + data_url: String, +} + +impl Drop for WebCursorImage { fn drop(&mut self) { - if let Some(data_url) = &self.data_url { - Url::revoke_object_url(data_url).unwrap(); - }; + Url::revoke_object_url(&self.data_url).unwrap(); } } 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 f2912ebdaf..882db237b9 100644 --- a/src/platform_impl/web/window.rs +++ b/src/platform_impl/web/window.rs @@ -8,7 +8,7 @@ use crate::window::{ }; use crate::SendSyncWrapper; -use super::cursor::{CustomCursorInternal, SelectedCursor}; +use super::cursor::SelectedCursor; use super::r#async::Dispatcher; use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget, Fullscreen}; use web_sys::HtmlCanvasElement; @@ -202,12 +202,14 @@ impl Inner { #[inline] pub fn set_custom_cursor(&self, cursor: CustomCursor) { - let new_cursor = CustomCursorInternal::new(self.canvas.borrow().document(), &cursor.inner); - self.canvas - .borrow() - .style() - .set("cursor", new_cursor.style()); - *self.selected_cursor.borrow_mut() = SelectedCursor::Custom(new_cursor); + 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] @@ -236,12 +238,9 @@ impl Inner { if !visible { self.canvas.borrow().style().set("cursor", "none"); } else { - let selected = &*self.selected_cursor.borrow(); - let style = match selected { - SelectedCursor::Named(cursor) => cursor.name(), - SelectedCursor::Custom(cursor) => cursor.style(), - }; - self.canvas.borrow().style().set("cursor", style); + self.selected_cursor + .borrow() + .set_style(self.canvas.borrow().style()); } } From 28b7a610cd9c9c86efbd9e3fb608ba827c2d3650 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Mon, 20 Nov 2023 23:43:41 +0200 Subject: [PATCH 04/35] Handle windows custom cursor creation errors better --- src/platform_impl/windows/icon.rs | 16 ++++++++-------- src/platform_impl/windows/window.rs | 8 +++++++- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/platform_impl/windows/icon.rs b/src/platform_impl/windows/icon.rs index 8531d11abb..cd1ac91f19 100644 --- a/src/platform_impl/windows/icon.rs +++ b/src/platform_impl/windows/icon.rs @@ -185,7 +185,7 @@ impl WinCursor { } } - pub fn new(image: &CursorImage) -> Self { + pub fn new(image: &CursorImage) -> Result { let mut bgra = image.rgba.clone(); bgra.chunks_exact_mut(4).for_each(|chunk| chunk.swap(0, 2)); @@ -196,25 +196,25 @@ impl WinCursor { 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 { - panic!("Failed to create mask bitmap"); + return Err(io::Error::last_os_error()); } let hdc_screen = GetDC(0); if hdc_screen == 0 { - panic!("Failed to get screen DC"); + return Err(io::Error::last_os_error()); } let hbm_color = CreateCompatibleBitmap(hdc_screen, w, h); if hbm_color == 0 { - panic!("Failed to create color bitmap"); + return Err(io::Error::last_os_error()); } if SetBitmapBits(hbm_color, bgra.len() as u32, bgra.as_ptr() as *const c_void) == 0 { - panic!("Failed to set bitmap bits"); + return Err(io::Error::last_os_error()); }; if ReleaseDC(0, hdc_screen) == 0 { - panic!("Failed to release screen DC"); + return Err(io::Error::last_os_error()); } let icon_info = ICONINFO { @@ -229,10 +229,10 @@ impl WinCursor { }; if handle == 0 { - panic!("Failed to create icon"); + return Err(io::Error::last_os_error()); } - Self::from_handle(handle) + Ok(Self::from_handle(handle)) } } diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index 1b12ea088d..b01c57ec2a 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -406,7 +406,13 @@ impl Window { #[inline] pub fn set_custom_cursor(&self, cursor: CustomCursor) { - let new_cursor = WinCursor::new(&cursor.inner); + let new_cursor = match WinCursor::new(&cursor.inner) { + Ok(cursor) => cursor, + Err(err) => { + warn!("Failed to create custom cursor: {err}"); + return; + } + }; let handle = new_cursor.as_raw_handle(); self.window_state_lock().mouse.selected_cursor = SelectedCursor::Custom(new_cursor); self.thread_executor.execute_in_thread(move || unsafe { From c031a2de432313b5cf737fcd37f28212b3d5ae91 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Tue, 21 Nov 2023 01:58:14 +0200 Subject: [PATCH 05/35] Simplify wayland custom cursor creation --- Cargo.toml | 1 - .../linux/wayland/types/cursor.rs | 87 +++++-------------- .../linux/wayland/window/state.rs | 21 ++--- 3 files changed, 29 insertions(+), 80 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9b75a4b69f..6997b34fbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -168,7 +168,6 @@ wayland-protocols-plasma = { version = "0.2.0", features = [ "client" ], optiona x11-dl = { version = "2.18.5", optional = true } x11rb = { version = "0.13.0", default-features = false, features = ["allow-unsafe-code", "dl-libxcb", "randr", "resource_manager", "xinput", "xkb"], optional = true } xkbcommon-dl = "0.4.0" -memfd = "0.6.4" [target.'cfg(target_os = "redox")'.dependencies] orbclient = { version = "0.3.42", default-features = false } diff --git a/src/platform_impl/linux/wayland/types/cursor.rs b/src/platform_impl/linux/wayland/types/cursor.rs index 7fbce1dd4a..71867bca02 100644 --- a/src/platform_impl/linux/wayland/types/cursor.rs +++ b/src/platform_impl/linux/wayland/types/cursor.rs @@ -1,90 +1,49 @@ -use std::{fs::File, io::Write, sync::Arc}; +use std::sync::Arc; use cursor_icon::CursorIcon; -use rustix::fd::{AsFd, OwnedFd}; +use rustix::fd::OwnedFd; +use sctk::shm::slot::{Buffer, SlotPool}; use wayland_backend::client::ObjectData; -use wayland_client::{ - protocol::{ - wl_buffer::{self, WlBuffer}, - wl_shm::{self, Format, WlShm}, - wl_shm_pool::{self, WlShmPool}, - }, - Connection, Proxy, WEnum, -}; +use wayland_client::protocol::wl_shm::Format; use crate::cursor::CursorImage; #[derive(Debug)] pub struct CustomCursorInternal { - _file: File, - shm_pool: WlShmPool, - pub buffer: WlBuffer, + pub buffer: Buffer, pub w: i32, pub h: i32, - pub hot_x: i32, - pub hot_y: i32, + pub hotspot_x: i32, + pub hotspot_y: i32, } impl CustomCursorInternal { - pub fn new(connection: &Connection, shm: &WlShm, image: &CursorImage) -> Self { - let mfd = memfd::MemfdOptions::default() - .close_on_exec(true) - .create("winit-custom-cursor") - .unwrap(); - let mut file = mfd.into_file(); - file.set_len(image.rgba.len() as u64).unwrap(); - for chunk in image.rgba.chunks_exact(4) { - file.write_all(&[chunk[2], chunk[1], chunk[0], chunk[3]]) - .unwrap(); - } - file.flush().unwrap(); - - let pool_id = connection - .send_request( - shm, - wl_shm::Request::CreatePool { - size: image.rgba.len() as i32, - fd: file.as_fd(), - }, - Some(Arc::new(IgnoreObjectData)), + 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(); - let shm_pool = WlShmPool::from_id(connection, pool_id).unwrap(); - let buffer_id = connection - .send_request( - &shm_pool, - wl_shm_pool::Request::CreateBuffer { - offset: 0, - width: image.width as i32, - height: image.width as i32, - stride: (image.width as i32 * 4), - format: WEnum::Value(Format::Argb8888), - }, - Some(Arc::new(IgnoreObjectData)), - ) - .unwrap(); - let buffer = WlBuffer::from_id(connection, buffer_id).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]; + } CustomCursorInternal { - _file: file, - shm_pool, buffer, w: image.width as i32, h: image.height as i32, - hot_x: image.hotspot_x as i32, - hot_y: image.hotspot_y as i32, + hotspot_x: image.hotspot_x as i32, + hotspot_y: image.hotspot_y as i32, } } - - pub fn destroy(&self, connection: &Connection) { - connection - .send_request(&self.buffer, wl_buffer::Request::Destroy, None) - .unwrap(); - connection - .send_request(&self.shm_pool, wl_shm_pool::Request::Destroy, None) - .unwrap(); - } } #[derive(Debug)] diff --git a/src/platform_impl/linux/wayland/window/state.rs b/src/platform_impl/linux/wayland/window/state.rs index 2e3cca3b1a..61da5d81d9 100644 --- a/src/platform_impl/linux/wayland/window/state.rs +++ b/src/platform_impl/linux/wayland/window/state.rs @@ -23,6 +23,7 @@ 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; @@ -61,6 +62,7 @@ pub struct WindowState { /// The `Shm` to set cursor. pub shm: WlShm, + pool: SlotPool, /// The last received configure. pub last_configure: Option, @@ -198,6 +200,7 @@ impl WindowState { resizable: true, scale_factor: 1., shm: winit_state.shm.wl_shm().clone(), + pool: SlotPool::new(4, &winit_state.shm).unwrap(), size: initial_size.to_logical(1.), stateless_size: initial_size.to_logical(1.), initial_size: Some(initial_size), @@ -696,10 +699,6 @@ impl WindowState { /// /// Providing `None` will hide the cursor. pub fn set_cursor(&mut self, cursor_icon: CursorIcon) { - if let SelectedCursor::Custom(cursor) = &self.selected_cursor { - cursor.destroy(&self.connection); - } - self.selected_cursor = SelectedCursor::Named(cursor_icon); if !self.cursor_visible { @@ -714,11 +713,7 @@ impl WindowState { } pub fn set_custom_cursor(&mut self, cursor: CustomCursor) { - if let SelectedCursor::Custom(cursor) = &self.selected_cursor { - cursor.destroy(&self.connection); - } - - let cursor = CustomCursorInternal::new(&self.connection, &self.shm, &cursor.inner); + let cursor = CustomCursorInternal::new(&mut self.pool, &cursor.inner); if self.cursor_visible { self.apply_custom_cursor(&cursor); @@ -737,7 +732,7 @@ impl WindowState { .surface_data() .scale_factor(); surface.set_buffer_scale(scale); - surface.attach(Some(&cursor.buffer), 0, 0); + surface.attach(Some(cursor.buffer.wl_buffer()), 0, 0); surface.damage_buffer(0, 0, cursor.w, cursor.h); surface.commit(); @@ -749,7 +744,7 @@ impl WindowState { pointer .pointer() - .set_cursor(serial, Some(surface), cursor.hot_x, cursor.hot_y); + .set_cursor(serial, Some(surface), cursor.hotspot_x, cursor.hotspot_y); }); } @@ -1079,10 +1074,6 @@ impl WindowState { impl Drop for WindowState { fn drop(&mut self) { - if let SelectedCursor::Custom(cursor) = &self.selected_cursor { - cursor.destroy(&self.connection); - } - if let Some(blur) = self.blur.take() { blur.release(); } From a0579b7548b29bd9024a6ffd00a36a2b38adeaab Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Tue, 21 Nov 2023 15:36:25 +0200 Subject: [PATCH 06/35] Fix documentation error --- src/cursor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cursor.rs b/src/cursor.rs index 41da75d989..68246984fd 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -42,7 +42,7 @@ impl NoCustomCursor { } #[derive(Debug)] -/// An error produced when using [`Icon::from_rgba`] with invalid arguments. +/// An error produced when using [`CustomCursor::from_rgba`] with invalid arguments. pub enum BadImage { /// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be /// safely interpreted as 32bpp RGBA pixels. From c86503a9fe7dfc20efcffbdffeb7044f1932a290 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Wed, 22 Nov 2023 00:23:41 +0200 Subject: [PATCH 07/35] macOS: work around clippy false positive --- src/platform_impl/macos/appkit/bitmap_image_rep.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform_impl/macos/appkit/bitmap_image_rep.rs b/src/platform_impl/macos/appkit/bitmap_image_rep.rs index c922b487e7..aa6e48486e 100644 --- a/src/platform_impl/macos/appkit/bitmap_image_rep.rs +++ b/src/platform_impl/macos/appkit/bitmap_image_rep.rs @@ -35,7 +35,7 @@ extern_methods!( pub fn init_rgba(width: NSInteger, height: NSInteger) -> Id { unsafe { msg_send_id![Self::alloc(), - initWithBitmapDataPlanes: std::ptr::null_mut() as *mut *mut c_uchar, + initWithBitmapDataPlanes: std::ptr::null_mut::<*mut c_uchar>(), pixelsWide: width, pixelsHigh: height, bitsPerSample: 8 as NSInteger, From a813832bb8dddbb0991606aa9f9e4503102a0507 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Wed, 22 Nov 2023 00:49:31 +0200 Subject: [PATCH 08/35] Web: change Rc usage to be 1.65 compatible --- src/platform_impl/web/cursor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index 9897a85a5a..b151c19a25 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -121,7 +121,7 @@ impl From for Previous { match value { SelectedCursor::Named(icon) => Self::Named(icon), SelectedCursor::Url(url) => Self::Url(url), - SelectedCursor::Image(image) => match Rc::into_inner(image).unwrap().into_inner() { + SelectedCursor::Image(image) => match Rc::try_unwrap(image).unwrap().into_inner() { CursorImageState::Loading { previous, .. } => previous, CursorImageState::Ready(internal) => Self::Image(internal), }, From c96738b3248cc6e06ee24b9942265543fc034604 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Wed, 22 Nov 2023 01:45:22 +0200 Subject: [PATCH 09/35] Fix BadImage Error implementation Co-authored-by: Mads Marquart --- src/cursor.rs | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/cursor.rs b/src/cursor.rs index 68246984fd..7b0558bff7 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -90,11 +90,7 @@ impl fmt::Display for BadImage { } } -impl Error for BadImage { - fn source(&self) -> Option<&(dyn Error + 'static)> { - Some(self) - } -} +impl Error for BadImage {} /// Platforms export this directly as `PlatformCustomCursor` if they need to only work with images. #[derive(Debug, Clone, PartialEq, Eq)] From f68d7317f0abda9e0ad6c2f74a76fb3b10ae3dfd Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Wed, 22 Nov 2023 20:48:11 +0200 Subject: [PATCH 10/35] Make custom cursors example keyboard layout agnostic --- examples/custom_cursors.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/examples/custom_cursors.rs b/examples/custom_cursors.rs index 3a01c91ef8..880cbb0800 100644 --- a/examples/custom_cursors.rs +++ b/examples/custom_cursors.rs @@ -6,7 +6,7 @@ use winit::{ cursor::CustomCursor, event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::EventLoop, - keyboard::{KeyCode, PhysicalKey}, + keyboard::Key, window::WindowBuilder, }; @@ -54,21 +54,21 @@ fn main() -> Result<(), impl std::error::Error> { event: KeyEvent { state: ElementState::Pressed, - physical_key: PhysicalKey::Code(code), + logical_key: key, .. }, .. - } => match code { - KeyCode::KeyA => { + } => 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; } - KeyCode::KeyS => { + Key::Character("2") => { log::debug!("Setting cursor icon to default"); window.set_cursor_icon(Default::default()); } - KeyCode::KeyD => { + Key::Character("3") => { cursor_visible = !cursor_visible; log::debug!("Setting cursor visibility to {:?}", cursor_visible); window.set_cursor_visible(cursor_visible); From fda16f4063c3d355cc64bae52ae0d8063cf4ffdd Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Wed, 22 Nov 2023 21:37:56 +0200 Subject: [PATCH 11/35] Windows: Cursor creation improvements --- src/platform_impl/windows/icon.rs | 52 ++++++++++++++----------------- 1 file changed, 24 insertions(+), 28 deletions(-) diff --git a/src/platform_impl/windows/icon.rs b/src/platform_impl/windows/icon.rs index cd1ac91f19..affb134316 100644 --- a/src/platform_impl/windows/icon.rs +++ b/src/platform_impl/windows/icon.rs @@ -5,7 +5,9 @@ use windows_sys::{ core::PCWSTR, Win32::{ Foundation::HWND, - Graphics::Gdi::{CreateBitmap, CreateCompatibleBitmap, GetDC, ReleaseDC, SetBitmapBits}, + Graphics::Gdi::{ + CreateBitmap, CreateCompatibleBitmap, DeleteObject, GetDC, ReleaseDC, SetBitmapBits, + }, UI::WindowsAndMessaging::{ CreateIcon, CreateIconIndirect, DestroyCursor, DestroyIcon, LoadImageW, SendMessageW, HCURSOR, HICON, ICONINFO, ICON_BIG, ICON_SMALL, IMAGE_ICON, LR_DEFAULTSIZE, @@ -164,24 +166,19 @@ pub fn unset_for_window(hwnd: HWND, icon_type: IconType) { } } -#[derive(Debug)] -struct RaiiCursor { - handle: HCURSOR, -} - #[derive(Clone, Debug)] pub struct WinCursor { - inner: Arc, + handle: Arc, } impl WinCursor { - pub fn as_raw_handle(&self) -> HICON { - self.inner.handle + pub fn as_raw_handle(&self) -> HCURSOR { + *self.handle } fn from_handle(handle: HCURSOR) -> Self { Self { - inner: Arc::new(RaiiCursor { handle }), + handle: Arc::new(handle), } } @@ -192,28 +189,26 @@ impl WinCursor { let w = image.width as i32; let h = image.height as i32; - let handle = unsafe { - 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 { - return Err(io::Error::last_os_error()); - } - + 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()); }; - if ReleaseDC(0, hdc_screen) == 0 { + // 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()); } @@ -225,20 +220,21 @@ impl WinCursor { hbmColor: hbm_color, }; - CreateIconIndirect(&icon_info as *const _) - }; + let handle = CreateIconIndirect(&icon_info as *const _); + DeleteObject(hbm_color); + DeleteObject(hbm_mask); + if handle == 0 { + return Err(io::Error::last_os_error()); + } - if handle == 0 { - return Err(io::Error::last_os_error()); + Ok(Self::from_handle(handle)) } - - Ok(Self::from_handle(handle)) } } -impl Drop for RaiiCursor { +impl Drop for WinCursor { fn drop(&mut self) { - unsafe { DestroyCursor(self.handle) }; + unsafe { DestroyCursor(self.as_raw_handle()) }; } } From 7304d5d9f29bef7235aae2c47e3e1935a95735c2 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Wed, 22 Nov 2023 23:09:41 +0200 Subject: [PATCH 12/35] Wayland: Improve cursor allocation & fix bugs --- src/platform_impl/linux/wayland/state.rs | 10 +++++- .../linux/wayland/types/cursor.rs | 19 +----------- .../linux/wayland/window/state.rs | 31 ++++++++++++++----- 3 files changed, 33 insertions(+), 27 deletions(-) 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 index 71867bca02..6232523778 100644 --- a/src/platform_impl/linux/wayland/types/cursor.rs +++ b/src/platform_impl/linux/wayland/types/cursor.rs @@ -1,10 +1,6 @@ -use std::sync::Arc; - use cursor_icon::CursorIcon; -use rustix::fd::OwnedFd; +use sctk::reexports::client::protocol::wl_shm::Format; use sctk::shm::slot::{Buffer, SlotPool}; -use wayland_backend::client::ObjectData; -use wayland_client::protocol::wl_shm::Format; use crate::cursor::CursorImage; @@ -57,16 +53,3 @@ impl Default for SelectedCursor { Self::Named(Default::default()) } } - -struct IgnoreObjectData; - -impl ObjectData for IgnoreObjectData { - fn event( - self: Arc, - _: &wayland_client::backend::Backend, - _: wayland_client::backend::protocol::Message, - ) -> Option> { - None - } - fn destroyed(&self, _: wayland_client::backend::ObjectId) {} -} diff --git a/src/platform_impl/linux/wayland/window/state.rs b/src/platform_impl/linux/wayland/window/state.rs index 61da5d81d9..70a2bb4763 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}; @@ -62,7 +62,9 @@ pub struct WindowState { /// The `Shm` to set cursor. pub shm: WlShm, - pool: SlotPool, + + // A shared pool where to allocate custom cursors. + custom_cursor_pool: Arc>, /// The last received configure. pub last_configure: Option, @@ -200,7 +202,7 @@ impl WindowState { resizable: true, scale_factor: 1., shm: winit_state.shm.wl_shm().clone(), - pool: SlotPool::new(4, &winit_state.shm).unwrap(), + 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), @@ -713,7 +715,10 @@ impl WindowState { } pub fn set_custom_cursor(&mut self, cursor: CustomCursor) { - let cursor = CustomCursorInternal::new(&mut self.pool, &cursor.inner); + let cursor = { + let mut pool = self.custom_cursor_pool.lock().unwrap(); + CustomCursorInternal::new(&mut pool, &cursor.inner) + }; if self.cursor_visible { self.apply_custom_cursor(&cursor); @@ -724,6 +729,8 @@ impl WindowState { pub fn apply_custom_cursor(&self, cursor: &CustomCursorInternal) { self.apply_on_poiner(|pointer, _| { + // Adapted from https://github.com/Smithay/client-toolkit/blob/6e4b95c967b1e6893fcb596db8888b16962c302a/src/seat/pointer/mod.rs#L452-L496 + let surface = pointer.surface(); let scale = surface @@ -731,9 +738,14 @@ impl WindowState { .unwrap() .surface_data() .scale_factor(); + surface.set_buffer_scale(scale); surface.attach(Some(cursor.buffer.wl_buffer()), 0, 0); - surface.damage_buffer(0, 0, cursor.w, cursor.h); + 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 @@ -742,9 +754,12 @@ impl WindowState { .and_then(|data| data.pointer_data().latest_enter_serial()) .unwrap(); - pointer - .pointer() - .set_cursor(serial, Some(surface), cursor.hotspot_x, cursor.hotspot_y); + pointer.pointer().set_cursor( + serial, + Some(surface), + cursor.hotspot_x / scale, + cursor.hotspot_y / scale, + ); }); } From bb5db5ba5b4ea1371ce2000df97e24c28aa628da Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Wed, 22 Nov 2023 23:20:17 +0200 Subject: [PATCH 13/35] X11: fix import --- src/platform_impl/linux/x11/util/cursor.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/platform_impl/linux/x11/util/cursor.rs b/src/platform_impl/linux/x11/util/cursor.rs index 92b5e03e5e..10c144c942 100644 --- a/src/platform_impl/linux/x11/util/cursor.rs +++ b/src/platform_impl/linux/x11/util/cursor.rs @@ -1,5 +1,4 @@ -use core::slice; -use std::{ffi::CString, iter}; +use std::{ffi::CString, iter, slice}; use x11rb::connection::Connection; From 6412a016226bcc1e72284ebf9fee52e10ba359b9 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Thu, 23 Nov 2023 19:06:31 +0200 Subject: [PATCH 14/35] Wayland: remove useless comment --- src/platform_impl/linux/wayland/window/state.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/platform_impl/linux/wayland/window/state.rs b/src/platform_impl/linux/wayland/window/state.rs index 70a2bb4763..4c4199b3df 100644 --- a/src/platform_impl/linux/wayland/window/state.rs +++ b/src/platform_impl/linux/wayland/window/state.rs @@ -729,8 +729,6 @@ impl WindowState { pub fn apply_custom_cursor(&self, cursor: &CustomCursorInternal) { self.apply_on_poiner(|pointer, _| { - // Adapted from https://github.com/Smithay/client-toolkit/blob/6e4b95c967b1e6893fcb596db8888b16962c302a/src/seat/pointer/mod.rs#L452-L496 - let surface = pointer.surface(); let scale = surface From 2c435191a8f18f72b0cf4f9f763d2f9836184141 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Thu, 23 Nov 2023 19:18:10 +0200 Subject: [PATCH 15/35] Windows: fix cursor destruction --- src/platform_impl/windows/icon.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/platform_impl/windows/icon.rs b/src/platform_impl/windows/icon.rs index affb134316..543e0b2407 100644 --- a/src/platform_impl/windows/icon.rs +++ b/src/platform_impl/windows/icon.rs @@ -166,19 +166,24 @@ pub fn unset_for_window(hwnd: HWND, icon_type: IconType) { } } +#[derive(Debug)] +struct RaiiCursor { + handle: HCURSOR, +} + #[derive(Clone, Debug)] pub struct WinCursor { - handle: Arc, + inner: Arc, } impl WinCursor { - pub fn as_raw_handle(&self) -> HCURSOR { - *self.handle + pub fn as_raw_handle(&self) -> HICON { + self.inner.handle } fn from_handle(handle: HCURSOR) -> Self { Self { - handle: Arc::new(handle), + inner: Arc::new(RaiiCursor { handle }), } } @@ -232,9 +237,9 @@ impl WinCursor { } } -impl Drop for WinCursor { +impl Drop for RaiiCursor { fn drop(&mut self) { - unsafe { DestroyCursor(self.as_raw_handle()) }; + unsafe { DestroyCursor(self.handle) }; } } From 5c04cd393184342cb24d21a9f01518a12676e522 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Thu, 23 Nov 2023 20:12:25 +0200 Subject: [PATCH 16/35] Update custom cursors documentation --- examples/custom_cursors.rs | 3 +-- src/cursor.rs | 24 ++++++++++++++++++++++++ src/lib.rs | 2 +- src/window.rs | 9 ++++++++- tests/send_objects.rs | 2 +- tests/sync_object.rs | 2 +- 6 files changed, 36 insertions(+), 6 deletions(-) diff --git a/examples/custom_cursors.rs b/examples/custom_cursors.rs index 880cbb0800..9d09ec1d3d 100644 --- a/examples/custom_cursors.rs +++ b/examples/custom_cursors.rs @@ -3,11 +3,10 @@ #[cfg(not(wasm_platform))] use simple_logger::SimpleLogger; use winit::{ - cursor::CustomCursor, event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::EventLoop, keyboard::Key, - window::WindowBuilder, + window::{CustomCursor, WindowBuilder}, }; fn decode_cursor(bytes: &[u8]) -> CustomCursor { diff --git a/src/cursor.rs b/src/cursor.rs index 7b0558bff7..20f349637d 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -3,6 +3,30 @@ use std::{error::Error, sync::Arc}; use crate::{icon::PIXEL_SIZE, platform_impl::PlatformCustomCursor}; +/// Use a custom image as a cursor (mouse pointer). +/// +/// ## Platform-specific +/// +/// - **Web** Data URLs are used with [`CustomCursor::from_rgba`]. These have limited maximum sizes +/// in browsers. They are generated asynchronously, so there can be latency when setting a +/// cursor. On Firefox, a bug causes the cursor to not change until the mouse is moved. +/// +/// # Examples +/// +/// ``` +/// use winit::window::CustomCursor; +/// +/// let w = 10; +/// let h = 10; +/// let rgba = vec![0xff_u8; (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, diff --git a/src/lib.rs b/src/lib.rs index 1ce5130491..ce26ef0e9d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -172,7 +172,7 @@ extern crate bitflags; pub mod dpi; #[macro_use] pub mod error; -pub mod cursor; +mod cursor; pub mod event; pub mod event_loop; mod icon; diff --git a/src/window.rs b/src/window.rs index 6f61652884..d980ed0ea1 100644 --- a/src/window.rs +++ b/src/window.rs @@ -2,7 +2,6 @@ use std::fmt; use crate::{ - cursor::CustomCursor, dpi::{PhysicalPosition, PhysicalSize, Position, Size}, error::{ExternalError, NotSupportedError, OsError}, event_loop::EventLoopWindowTarget, @@ -10,6 +9,7 @@ use crate::{ platform_impl, SendSyncWrapper, }; +pub use crate::cursor::{BadImage, CustomCursor}; pub use crate::icon::{BadIcon, Icon}; #[doc(inline)] @@ -1337,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 /// @@ -1347,6 +1348,12 @@ 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(); diff --git a/tests/send_objects.rs b/tests/send_objects.rs index 1815097b77..0288c6c3c4 100644 --- a/tests/send_objects.rs +++ b/tests/send_objects.rs @@ -31,5 +31,5 @@ fn ids_send() { #[test] fn custom_cursor_send() { - needs_send::(); + needs_send::(); } diff --git a/tests/sync_object.rs b/tests/sync_object.rs index daec42278c..96b3b7df3b 100644 --- a/tests/sync_object.rs +++ b/tests/sync_object.rs @@ -14,5 +14,5 @@ fn window_builder_sync() { #[test] fn custom_cursor_sync() { - needs_sync::(); + needs_sync::(); } From d2e9f41af618f9f5a2f05ba5e426b87174c32caa Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Fri, 24 Nov 2023 03:23:38 +0200 Subject: [PATCH 17/35] Add missing custom cursor documentation --- src/cursor.rs | 12 +++++++++--- src/platform/web.rs | 3 +++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/cursor.rs b/src/cursor.rs index 20f349637d..e912ef93eb 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -7,9 +7,9 @@ use crate::{icon::PIXEL_SIZE, platform_impl::PlatformCustomCursor}; /// /// ## Platform-specific /// -/// - **Web** Data URLs are used with [`CustomCursor::from_rgba`]. These have limited maximum sizes -/// in browsers. They are generated asynchronously, so there can be latency when setting a -/// cursor. On Firefox, a bug causes the cursor to not change until the mouse is moved. +/// - **Web** On Firefox, [a bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1336764) causes +/// the cursor to not change until the mouse is moved. +/// /// /// # Examples /// @@ -33,6 +33,12 @@ pub struct CustomCursor { } impl CustomCursor { + /// Creates a new cursor from an rgba buffer. + /// + /// ## Platform-specific + /// + /// - **Web** Uses data URLs. They have limited maximum sizes in browsers. They are + /// generated asynchronously, so there can be latency when setting a cursor. pub fn from_rgba( rgba: Vec, width: u32, diff --git a/src/platform/web.rs b/src/platform/web.rs index 21aa7655a6..94c03eca96 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -204,6 +204,9 @@ pub enum PollStrategy { } 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), + /// so the same image formats and paths are supported. fn from_url(url: String, hotspot_x: u32, hotspot_y: u32) -> Self; } From 735704951a1c50f408fdee4bdc892aa74037efd1 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Fri, 24 Nov 2023 03:54:59 +0200 Subject: [PATCH 18/35] Fix possible overflow in cursor size checks --- src/cursor.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/cursor.rs b/src/cursor.rs index e912ef93eb..c1af65f265 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -1,7 +1,7 @@ use core::fmt; use std::{error::Error, sync::Arc}; -use crate::{icon::PIXEL_SIZE, platform_impl::PlatformCustomCursor}; +use crate::platform_impl::PlatformCustomCursor; /// Use a custom image as a cursor (mouse pointer). /// @@ -82,8 +82,8 @@ pub enum BadImage { DimensionsVsPixelCount { width: u32, height: u32, - width_x_height: usize, - pixel_count: usize, + width_x_height: u64, + pixel_count: u64, }, /// Produced when the hotspot is outside the image bounds HotspotOutOfBounds { @@ -132,6 +132,8 @@ pub struct CursorImage { pub(crate) hotspot_y: u32, } +pub const PIXEL_SIZE: usize = 4; + #[allow(dead_code)] impl CursorImage { pub fn from_rgba( @@ -146,12 +148,14 @@ impl CursorImage { byte_count: rgba.len(), }); } - let pixel_count = rgba.len() / PIXEL_SIZE; - if pixel_count != (width * height) as usize { + + 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: (width * height) as usize, + width_x_height, pixel_count, }); } From de364fea33e539c25e6acd736b2c8ba8299651e6 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Fri, 24 Nov 2023 03:57:21 +0200 Subject: [PATCH 19/35] Allow cursors to be created with impl Into> --- src/cursor.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/cursor.rs b/src/cursor.rs index c1af65f265..69e4fb5ef6 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -40,15 +40,21 @@ impl CustomCursor { /// - **Web** Uses data URLs. They have limited maximum sizes in browsers. They are /// generated asynchronously, so there can be latency when setting a cursor. pub fn from_rgba( - rgba: Vec, + rgba: impl Into>, width: u32, height: u32, hotspot_x: u32, hotspot_y: u32, ) -> Result { Ok(Self { - inner: PlatformCustomCursor::from_rgba(rgba, width, height, hotspot_x, hotspot_y)? - .into(), + inner: PlatformCustomCursor::from_rgba( + rgba.into(), + width, + height, + hotspot_x, + hotspot_y, + )? + .into(), }) } } From d03b0419eb1b46ff89d512bb7829b48ce560712b Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Fri, 24 Nov 2023 22:05:50 +0200 Subject: [PATCH 20/35] X11: Destroy custom cursor on drop --- src/platform_impl/linux/x11/util/cursor.rs | 62 ++++++++++++++-------- src/platform_impl/linux/x11/window.rs | 38 +++---------- 2 files changed, 47 insertions(+), 53 deletions(-) diff --git a/src/platform_impl/linux/x11/util/cursor.rs b/src/platform_impl/linux/x11/util/cursor.rs index 10c144c942..ec532bedfe 100644 --- a/src/platform_impl/linux/x11/util/cursor.rs +++ b/src/platform_impl/linux/x11/util/cursor.rs @@ -1,4 +1,4 @@ -use std::{ffi::CString, iter, slice}; +use std::{ffi::CString, iter, slice, sync::Arc}; use x11rb::connection::Connection; @@ -6,17 +6,38 @@ use crate::{cursor::CursorImage, window::CursorIcon}; use super::*; -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub struct CustomCursorInternal(ffi::Cursor); +#[derive(Debug)] +struct RaiiCursor { + xconn: Arc, + cursor: ffi::Cursor, +} + +impl Drop for RaiiCursor { + fn drop(&mut self) { + unsafe { + (self.xconn.xlib.XFreeCursor)(self.xconn.display, self.cursor); + } + } +} + +impl PartialEq for RaiiCursor { + fn eq(&self, other: &Self) -> bool { + self.cursor == other.cursor + } +} + +impl Eq for RaiiCursor {} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct CustomCursorInternal { + inner: Arc, +} impl CustomCursorInternal { - pub unsafe fn new( - xcursor: &ffi::Xcursor, - display: *mut ffi::Display, - image: &CursorImage, - ) -> Self { + pub(crate) unsafe fn new(xconn: &Arc, image: &CursorImage) -> Self { unsafe { - let ximage = (xcursor.XcursorImageCreate)(image.width as i32, image.height as i32); + let ximage = + (xconn.xcursor.XcursorImageCreate)(image.width as i32, image.height as i32); if ximage.is_null() { panic!("failed to allocate cursor image"); } @@ -33,20 +54,19 @@ impl CustomCursorInternal { | (chunk[3] as u32) << 24; } - let cursor = (xcursor.XcursorImageLoadCursor)(display, ximage); - (xcursor.XcursorImageDestroy)(ximage); - Self(cursor) - } - } - - pub unsafe fn destroy(&self, xlib: &ffi::Xlib, display: *mut ffi::Display) { - unsafe { - (xlib.XFreeCursor)(display, self.0); + let cursor = (xconn.xcursor.XcursorImageLoadCursor)(xconn.display, ximage); + (xconn.xcursor.XcursorImageDestroy)(ximage); + Self { + inner: Arc::new(RaiiCursor { + xconn: xconn.clone(), + cursor, + }), + } } } } -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum SelectedCursor { Custom(CustomCursorInternal), Named(CursorIcon), @@ -71,8 +91,8 @@ impl XConnection { .expect("Failed to set cursor"); } - pub fn set_custom_cursor(&self, window: xproto::Window, cursor: CustomCursorInternal) { - self.update_cursor(window, cursor.0) + pub fn set_custom_cursor(&self, window: xproto::Window, cursor: &CustomCursorInternal) { + self.update_cursor(window, cursor.inner.cursor) .expect("Failed to set cursor"); } diff --git a/src/platform_impl/linux/x11/window.rs b/src/platform_impl/linux/x11/window.rs index e4268d3fa5..f96c0d340c 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -1545,12 +1545,6 @@ impl UnownedWindow { SelectedCursor::Named(cursor), ); - if let SelectedCursor::Custom(old_cursor) = old_cursor { - unsafe { - old_cursor.destroy(&self.xconn.xlib, self.xconn.display); - } - } - #[allow(clippy::mutex_atomic)] if SelectedCursor::Named(cursor) != old_cursor && *self.cursor_visible.lock().unwrap() { self.xconn.set_cursor_icon(self.xwindow, Some(cursor)); @@ -1559,23 +1553,14 @@ impl UnownedWindow { #[inline] pub fn set_custom_cursor(&self, cursor: CustomCursor) { - let new_cursor = unsafe { - CustomCursorInternal::new(&self.xconn.xcursor, self.xconn.display, &cursor.inner) - }; - let old_cursor = replace( - &mut *self.selected_cursor.lock().unwrap(), - SelectedCursor::Custom(new_cursor), - ); - if let SelectedCursor::Custom(old_cursor) = old_cursor { - unsafe { - old_cursor.destroy(&self.xconn.xlib, self.xconn.display); - } - } + let new_cursor = unsafe { CustomCursorInternal::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.xconn.set_custom_cursor(self.xwindow, &new_cursor); } + + *self.selected_cursor.lock().unwrap() = SelectedCursor::Custom(new_cursor); } #[inline] @@ -1664,7 +1649,7 @@ impl UnownedWindow { return; } let cursor = if visible { - Some(*self.selected_cursor.lock().unwrap()) + Some((*self.selected_cursor.lock().unwrap()).clone()) } else { None }; @@ -1672,7 +1657,7 @@ impl UnownedWindow { drop(visible_lock); match cursor { Some(SelectedCursor::Custom(cursor)) => { - self.xconn.set_custom_cursor(self.xwindow, cursor); + self.xconn.set_custom_cursor(self.xwindow, &cursor); } Some(SelectedCursor::Named(cursor)) => { self.xconn.set_cursor_icon(self.xwindow, Some(cursor)); @@ -1994,17 +1979,6 @@ impl UnownedWindow { } } -impl Drop for UnownedWindow { - #[inline] - fn drop(&mut self) { - if let SelectedCursor::Custom(cursor) = *self.selected_cursor.lock().unwrap() { - unsafe { - cursor.destroy(&self.xconn.xlib, self.xconn.display); - } - } - } -} - /// Cast a dimension value into a hinted dimension for `WmSizeHints`, clamping if too large. fn cast_dimension_to_hint(val: u32) -> i32 { val.try_into().unwrap_or(i32::MAX) From 2245b5e7bd1a4370b06f51554a7e0b7d03f184d1 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Sat, 25 Nov 2023 00:40:36 +0200 Subject: [PATCH 21/35] Add documentation about maximum cursor sizes --- src/cursor.rs | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/cursor.rs b/src/cursor.rs index 69e4fb5ef6..f126b82794 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -7,7 +7,11 @@ use crate::platform_impl::PlatformCustomCursor; /// /// ## Platform-specific /// -/// - **Web** On Firefox, [a bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1336764) causes +/// - **Web** +/// Browsers have limits on cursor sizes. On Chromium, Firefox, and Safari, the maximum +/// size in 128x128. +/// +/// On Firefox, [a bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1336764) causes /// the cursor to not change until the mouse is moved. /// /// @@ -37,8 +41,8 @@ impl CustomCursor { /// /// ## Platform-specific /// - /// - **Web** Uses data URLs. They have limited maximum sizes in browsers. They are - /// generated asynchronously, so there can be latency when setting a cursor. + /// - **Web** Uses data URLs. They are generated asynchronously, so there can be latency when + /// setting a cursor. pub fn from_rgba( rgba: impl Into>, width: u32, From 3b56af423f17666fb32f5850985cd805d7576451 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Sat, 25 Nov 2023 01:24:58 +0200 Subject: [PATCH 22/35] Update custom cursors documentation Co-authored-by: daxpedda --- src/cursor.rs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/cursor.rs b/src/cursor.rs index f126b82794..852e9d1de0 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -7,12 +7,7 @@ use crate::platform_impl::PlatformCustomCursor; /// /// ## Platform-specific /// -/// - **Web** -/// Browsers have limits on cursor sizes. On Chromium, Firefox, and Safari, the maximum -/// size in 128x128. -/// -/// On Firefox, [a bug](https://bugzilla.mozilla.org/show_bug.cgi?id=1336764) causes -/// the cursor to not change until the mouse is moved. +/// **Web**: Some browsers have limits on cursor sizes typically at 128x128. /// /// /// # Examples From 51fce13a8ceec6a0c7cfee0f023e7d71823cda89 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Sat, 25 Nov 2023 01:37:35 +0200 Subject: [PATCH 23/35] Update custom cursor from_url docs Co-authored-by: daxpedda --- src/platform/web.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/platform/web.rs b/src/platform/web.rs index 94c03eca96..11709ce0cd 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -206,7 +206,9 @@ pub enum PollStrategy { 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), - /// so the same image formats and paths are supported. + /// 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: u32, hotspot_y: u32) -> Self; } From fd1bff36091b788d11d75c02817b268eeb565a32 Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Sat, 25 Nov 2023 18:19:49 +0200 Subject: [PATCH 24/35] Ensure that way too large custom cursors can't be created --- examples/custom_cursors.rs | 2 +- src/cursor.rs | 58 ++++++++++++------- src/platform/web.rs | 4 +- .../linux/wayland/types/cursor.rs | 2 +- src/platform_impl/linux/x11/util/cursor.rs | 11 ++-- src/platform_impl/macos/appkit/cursor.rs | 2 +- src/platform_impl/web/cursor.rs | 30 +++++----- src/platform_impl/windows/icon.rs | 4 +- src/window.rs | 2 +- 9 files changed, 65 insertions(+), 50 deletions(-) diff --git a/examples/custom_cursors.rs b/examples/custom_cursors.rs index 9d09ec1d3d..c685cd7540 100644 --- a/examples/custom_cursors.rs +++ b/examples/custom_cursors.rs @@ -13,7 +13,7 @@ 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 u32, h as u32); + let (w, h) = (w as u16, h as u16); CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap() } diff --git a/src/cursor.rs b/src/cursor.rs index 852e9d1de0..f7a079ffc0 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -40,10 +40,10 @@ impl CustomCursor { /// setting a cursor. pub fn from_rgba( rgba: impl Into>, - width: u32, - height: u32, - hotspot_x: u32, - hotspot_y: u32, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, ) -> Result { Ok(Self { inner: PlatformCustomCursor::from_rgba( @@ -66,42 +66,52 @@ pub(crate) struct NoCustomCursor; impl NoCustomCursor { pub fn from_rgba( rgba: Vec, - width: u32, - height: u32, - hotspot_x: u32, - hotspot_y: u32, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, ) -> Result { CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?; Ok(Self) } } +/// The maximum width and height for a cursor when using [`CustomCursor::from_rgba`]. +pub const MAX_CURSOR_SIZE: u16 = 2048; + #[derive(Debug)] /// An error produced when using [`CustomCursor::from_rgba`] with invalid arguments. 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: u32, - height: u32, + width: u16, + height: u16, width_x_height: u64, pixel_count: u64, }, /// Produced when the hotspot is outside the image bounds HotspotOutOfBounds { - width: u32, - height: u32, - hotspot_x: u32, - hotspot_y: u32, + 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.", ), @@ -131,10 +141,10 @@ impl Error for BadImage {} #[derive(Debug, Clone, PartialEq, Eq)] pub struct CursorImage { pub(crate) rgba: Vec, - pub(crate) width: u32, - pub(crate) height: u32, - pub(crate) hotspot_x: u32, - pub(crate) hotspot_y: u32, + pub(crate) width: u16, + pub(crate) height: u16, + pub(crate) hotspot_x: u16, + pub(crate) hotspot_y: u16, } pub const PIXEL_SIZE: usize = 4; @@ -143,11 +153,15 @@ pub const PIXEL_SIZE: usize = 4; impl CursorImage { pub fn from_rgba( rgba: Vec, - width: u32, - height: u32, - hotspot_x: u32, - hotspot_y: u32, + 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(), diff --git a/src/platform/web.rs b/src/platform/web.rs index 11709ce0cd..c01308e972 100644 --- a/src/platform/web.rs +++ b/src/platform/web.rs @@ -209,11 +209,11 @@ pub trait CustomCursorExtWebSys { /// but browser support for image formats is inconsistent. Using [PNG] is recommended. /// /// [PNG]: https://en.wikipedia.org/wiki/PNG - fn from_url(url: String, hotspot_x: u32, hotspot_y: u32) -> Self; + fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> Self; } impl CustomCursorExtWebSys for CustomCursor { - fn from_url(url: String, hotspot_x: u32, hotspot_y: u32) -> Self { + fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> Self { Self { inner: PlatformCustomCursor::Url { url, diff --git a/src/platform_impl/linux/wayland/types/cursor.rs b/src/platform_impl/linux/wayland/types/cursor.rs index 6232523778..b2d0b1dbba 100644 --- a/src/platform_impl/linux/wayland/types/cursor.rs +++ b/src/platform_impl/linux/wayland/types/cursor.rs @@ -19,7 +19,7 @@ impl CustomCursorInternal { .create_buffer( image.width as i32, image.height as i32, - 4 * image.width as i32, + 4 * (image.width as i32), Format::Argb8888, ) .unwrap(); diff --git a/src/platform_impl/linux/x11/util/cursor.rs b/src/platform_impl/linux/x11/util/cursor.rs index ec532bedfe..accf783d56 100644 --- a/src/platform_impl/linux/x11/util/cursor.rs +++ b/src/platform_impl/linux/x11/util/cursor.rs @@ -41,14 +41,13 @@ impl CustomCursorInternal { if ximage.is_null() { panic!("failed to allocate cursor image"); } - (*ximage).xhot = image.hotspot_x; - (*ximage).yhot = image.hotspot_y; + (*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.width * image.height) as usize); - for (i, chunk) in image.rgba.chunks_exact(4).enumerate() { - dst[i] = (chunk[0] as u32) << 16 + 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; diff --git a/src/platform_impl/macos/appkit/cursor.rs b/src/platform_impl/macos/appkit/cursor.rs index 2e59d0faaa..95b1180b19 100644 --- a/src/platform_impl/macos/appkit/cursor.rs +++ b/src/platform_impl/macos/appkit/cursor.rs @@ -240,7 +240,7 @@ impl NSCursor { let bitmap = NSBitmapImageRep::init_rgba(w as isize, h as isize); let bitmap_data = - unsafe { std::slice::from_raw_parts_mut(bitmap.bitmap_data(), (w * h * 4) as usize) }; + unsafe { std::slice::from_raw_parts_mut(bitmap.bitmap_data(), image.rgba.len()) }; bitmap_data.copy_from_slice(&image.rgba); let nsimage = NSImage::init_with_size(NSSize::new(w.into(), h.into())); diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index b151c19a25..5ecb359899 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -21,18 +21,18 @@ pub enum WebCustomCursor { Image(CursorImage), Url { url: String, - hotspot_x: u32, - hotspot_y: u32, + hotspot_x: u16, + hotspot_y: u16, }, } impl WebCustomCursor { pub fn from_rgba( rgba: Vec, - width: u32, - height: u32, - hotspot_x: u32, - hotspot_y: u32, + width: u16, + height: u16, + hotspot_x: u16, + hotspot_y: u16, ) -> Result { Ok(Self::Image(CursorImage::from_rgba( rgba, width, height, hotspot_x, hotspot_y, @@ -134,8 +134,8 @@ pub enum CursorImageState { Loading { style: Style, previous: Previous, - hotspot_x: u32, - hotspot_y: u32, + hotspot_x: u16, + hotspot_y: u16, }, Ready(WebCursorImage), } @@ -167,15 +167,17 @@ impl CursorImageState { 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) + 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) - .unwrap(); + 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); @@ -209,9 +211,9 @@ impl CursorImageState { let canvas: HtmlCanvasElement = document.create_element("canvas").unwrap().unchecked_into(); #[allow(clippy::disallowed_methods)] - canvas.set_width(width); + canvas.set_width(width as u32); #[allow(clippy::disallowed_methods)] - canvas.set_height(height); + canvas.set_height(height as u32); let context: ImageBitmapRenderingContext = canvas .get_context("bitmaprenderer") diff --git a/src/platform_impl/windows/icon.rs b/src/platform_impl/windows/icon.rs index 543e0b2407..5a026b14ea 100644 --- a/src/platform_impl/windows/icon.rs +++ b/src/platform_impl/windows/icon.rs @@ -219,8 +219,8 @@ impl WinCursor { let icon_info = ICONINFO { fIcon: 0, - xHotspot: image.hotspot_x, - yHotspot: image.hotspot_y, + xHotspot: image.hotspot_x as u32, + yHotspot: image.hotspot_y as u32, hbmMask: hbm_mask, hbmColor: hbm_color, }; diff --git a/src/window.rs b/src/window.rs index d980ed0ea1..5b2e66edf1 100644 --- a/src/window.rs +++ b/src/window.rs @@ -9,7 +9,7 @@ use crate::{ platform_impl, SendSyncWrapper, }; -pub use crate::cursor::{BadImage, CustomCursor}; +pub use crate::cursor::{BadImage, CustomCursor, MAX_CURSOR_SIZE}; pub use crate::icon::{BadIcon, Icon}; #[doc(inline)] From b6be37274a52ce864e87500fdc5d53bbbd48148c Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Sun, 26 Nov 2023 00:20:41 +0200 Subject: [PATCH 25/35] Update changelog and features --- CHANGELOG.md | 5 +++++ FEATURES.md | 1 + 2 files changed, 6 insertions(+) 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/FEATURES.md b/FEATURES.md index 97286ceb22..3ce516a719 100644 --- a/FEATURES.md +++ b/FEATURES.md @@ -206,6 +206,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** | From 134e94d0e04efc7fdcd8fa6cd553e2c79229785b Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Sun, 26 Nov 2023 00:33:51 +0200 Subject: [PATCH 26/35] Add feature description --- FEATURES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/FEATURES.md b/FEATURES.md index 3ce516a719..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. From a4ff89034f2d2e9b34a1101bf3131f244b498f6f Mon Sep 17 00:00:00 2001 From: daxpedda Date: Mon, 27 Nov 2023 15:10:32 +0100 Subject: [PATCH 27/35] Web: don't drop previous cursor until new cursor is applied --- src/platform_impl/web/cursor.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index 5ecb359899..bcd1b9308d 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -242,7 +242,7 @@ impl CursorImageState { let data_url = Url::create_object_url_with_blob(&blob).unwrap(); // Extract `Style`, which we couldn't do earlier without cloning it. - let CursorImageState::Loading { style, .. } = mem::replace(state.deref_mut(), CursorImageState::Ready(WebCursorImage { + let CursorImageState::Loading { style, previous: _previous, .. } = mem::replace(state.deref_mut(), CursorImageState::Ready(WebCursorImage { style: format!("url({}) {} {}, auto", data_url, hotspot_x, hotspot_y), data_url, })) else { From d2565b9e28a11ec8088671a65aa7d1e4b8241efa Mon Sep 17 00:00:00 2001 From: daxpedda Date: Mon, 27 Nov 2023 15:15:13 +0100 Subject: [PATCH 28/35] Web: simplify state machine --- src/platform_impl/web/cursor.rs | 45 ++++++++++++++------------------- 1 file changed, 19 insertions(+), 26 deletions(-) diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index bcd1b9308d..da77605994 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -1,7 +1,6 @@ use std::{ cell::RefCell, - mem, - ops::{Deref, DerefMut}, + ops::Deref, rc::{Rc, Weak}, }; @@ -71,7 +70,7 @@ impl WebCustomCursor { pub enum SelectedCursor { Named(CursorIcon), Url(String), - Image(Rc>), + Image(Rc>>), } impl Default for SelectedCursor { @@ -87,7 +86,7 @@ impl SelectedCursor { SelectedCursor::Url(url) => url, SelectedCursor::Image(image) => { let image = image.borrow(); - let value = match image.deref() { + let value = match image.deref().as_ref().unwrap() { CursorImageState::Loading { previous, .. } => previous.style(), CursorImageState::Ready(WebCursorImage { style, .. }) => style, }; @@ -121,10 +120,12 @@ impl From for Previous { match value { SelectedCursor::Named(icon) => Self::Named(icon), SelectedCursor::Url(url) => Self::Url(url), - SelectedCursor::Image(image) => match Rc::try_unwrap(image).unwrap().into_inner() { - CursorImageState::Loading { previous, .. } => previous, - CursorImageState::Ready(internal) => Self::Image(internal), - }, + SelectedCursor::Image(image) => { + match Rc::try_unwrap(image).unwrap().into_inner().unwrap() { + CursorImageState::Loading { previous, .. } => previous, + CursorImageState::Ready(internal) => Self::Image(internal), + } + } } } } @@ -147,7 +148,7 @@ impl CursorImageState { style: Style, image: &CursorImage, previous: SelectedCursor, - ) -> Rc> { + ) -> 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")] @@ -187,12 +188,12 @@ impl CursorImageState { .unwrap(), ); - let state = Rc::new(RefCell::new(Self::Loading { + let state = Rc::new(RefCell::new(Some(Self::Loading { style, previous: previous.into(), hotspot_x: image.hotspot_x, hotspot_y: image.hotspot_y, - })); + }))); wasm_bindgen_futures::spawn_local({ let weak = Rc::downgrade(&state); @@ -223,7 +224,7 @@ impl CursorImageState { context.transfer_from_image_bitmap(&bitmap); thread_local! { - static CURRENT_STATE: RefCell>>> = RefCell::new(None); + 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| { @@ -235,24 +236,16 @@ impl CursorImageState { return; }; let mut state = state.borrow_mut(); - // Extract the hotspots. - let CursorImageState::Loading { hotspot_x, hotspot_y, .. } = *state else { - unreachable!("found invalid state") - }; let data_url = Url::create_object_url_with_blob(&blob).unwrap(); - // Extract `Style`, which we couldn't do earlier without cloning it. - let CursorImageState::Loading { style, previous: _previous, .. } = mem::replace(state.deref_mut(), CursorImageState::Ready(WebCursorImage { - style: format!("url({}) {} {}, auto", data_url, hotspot_x, hotspot_y), - data_url, - })) else { - unreachable!("found invalid state") - }; - // Extract the `cursor` property value, which we couldn't cache earlier without cloning. - let CursorImageState::Ready(WebCursorImage { style: value, .. }) = state.deref() else { + + // Extract old state. + let CursorImageState::Loading { style, previous: _previous, hotspot_x, hotspot_y } = state.take().unwrap() else { unreachable!("found invalid state") }; - style.set("cursor", value); + let value = format!("url({}) {} {}, auto", data_url, hotspot_x, hotspot_y); + style.set("cursor", &value); + *state = Some(CursorImageState::Ready(WebCursorImage { style: value, data_url })); }); }); } From 43e39fffd387158f3a622737c44376f89da8b21a Mon Sep 17 00:00:00 2001 From: daxpedda Date: Mon, 27 Nov 2023 15:19:14 +0100 Subject: [PATCH 29/35] Web: retain previous cursor as a fallback --- src/platform_impl/web/cursor.rs | 98 ++++++++++++++++++++++++++++----- 1 file changed, 83 insertions(+), 15 deletions(-) diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index da77605994..a3a9267bba 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Cow, cell::RefCell, ops::Deref, rc::{Rc, Weak}, @@ -60,7 +61,12 @@ impl WebCustomCursor { } => { let value = format!("url({}) {} {}, auto", url, hotspot_x, hotspot_y); style.set("cursor", &value); - SelectedCursor::Url(value) + SelectedCursor::Url { + style: value, + url: url.clone(), + hotspot_x: *hotspot_x, + hotspot_y: *hotspot_y, + } } } } @@ -69,7 +75,12 @@ impl WebCustomCursor { #[derive(Debug)] pub enum SelectedCursor { Named(CursorIcon), - Url(String), + Url { + style: String, + url: String, + hotspot_x: u16, + hotspot_y: u16, + }, Image(Rc>>), } @@ -83,12 +94,12 @@ impl SelectedCursor { pub fn set_style(&self, style: &Style) { let value = match self { SelectedCursor::Named(icon) => icon.name(), - SelectedCursor::Url(url) => url, + 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::Ready(WebCursorImage { style, .. }) => style, + CursorImageState::Ready { style, .. } => style, }; return style.set("cursor", value); } @@ -101,16 +112,46 @@ impl SelectedCursor { #[derive(Debug)] pub enum Previous { Named(CursorIcon), - Url(String), - Image(WebCursorImage), + 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(url) => url, - Previous::Image(WebCursorImage { style, .. }) => style, + Previous::Url { style: url, .. } => url, + Previous::Image { style, .. } => style, + } + } + + fn cursor(&self) -> Cow<'_, str> { + match self { + Previous::Named(icon) => Cow::Borrowed(icon.name()), + Previous::Url { + url, + hotspot_x, + hotspot_y, + .. + } + | Previous::Image { + image: + WebCursorImage { + data_url: url, + hotspot_x, + hotspot_y, + .. + }, + .. + } => Cow::Owned(format!("url({url}) {hotspot_x} {hotspot_y}")), } } } @@ -119,11 +160,24 @@ impl From for Previous { fn from(value: SelectedCursor) -> Self { match value { SelectedCursor::Named(icon) => Self::Named(icon), - SelectedCursor::Url(url) => Self::Url(url), + 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::Ready(internal) => Self::Image(internal), + CursorImageState::Ready { style, current, .. } => Self::Image { + style, + image: current, + }, } } } @@ -138,7 +192,11 @@ pub enum CursorImageState { hotspot_x: u16, hotspot_y: u16, }, - Ready(WebCursorImage), + Ready { + style: String, + current: WebCursorImage, + previous: Previous, + }, } impl CursorImageState { @@ -240,12 +298,21 @@ impl CursorImageState { let data_url = Url::create_object_url_with_blob(&blob).unwrap(); // Extract old state. - let CursorImageState::Loading { style, previous: _previous, hotspot_x, hotspot_y } = state.take().unwrap() else { + let CursorImageState::Loading { style, previous, hotspot_x, hotspot_y, .. } = state.take().unwrap() else { unreachable!("found invalid state") }; - let value = format!("url({}) {} {}, auto", data_url, hotspot_x, hotspot_y); + let value = if let Previous::Named { .. } = previous { + format!("url({data_url}) {hotspot_x} {hotspot_y}, {}", previous.cursor() ) + } else { + format!("url({data_url}) {hotspot_x} {hotspot_y}, {}, auto", previous.cursor() ) + }; style.set("cursor", &value); - *state = Some(CursorImageState::Ready(WebCursorImage { style: value, data_url })); + *state = Some( + CursorImageState::Ready { + style: value, + current: WebCursorImage{ data_url, hotspot_x, hotspot_y }, + previous, + }); }); }); } @@ -262,8 +329,9 @@ impl CursorImageState { #[derive(Debug)] pub struct WebCursorImage { - style: String, data_url: String, + hotspot_x: u16, + hotspot_y: u16, } impl Drop for WebCursorImage { From 3bea0ab5b466106515e8a2e456f9a5c1f409e9fc Mon Sep 17 00:00:00 2001 From: daxpedda Date: Mon, 27 Nov 2023 15:28:42 +0100 Subject: [PATCH 30/35] Web: add failed state in case of `toBlob()` failure --- src/platform_impl/web/cursor.rs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index a3a9267bba..291d4ccdb4 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -99,6 +99,7 @@ impl SelectedCursor { 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); @@ -174,6 +175,7 @@ impl From for Previous { SelectedCursor::Image(image) => { match Rc::try_unwrap(image).unwrap().into_inner().unwrap() { CursorImageState::Loading { previous, .. } => previous, + CursorImageState::Failed(previous) => previous, CursorImageState::Ready { style, current, .. } => Self::Image { style, image: current, @@ -192,6 +194,7 @@ pub enum CursorImageState { hotspot_x: u16, hotspot_y: u16, }, + Failed(Previous), Ready { style: String, current: WebCursorImage, @@ -287,20 +290,21 @@ impl CursorImageState { // `Closure` that doesn't need to be garbage-collected. static CALLBACK: Closure)> = Closure::new(|blob| { CURRENT_STATE.with(|weak| { - let Some(blob) = blob else { - return; - }; let Some(state) = weak.borrow_mut().take().and_then(|weak| weak.upgrade()) else { return; }; let mut state = state.borrow_mut(); - - let data_url = Url::create_object_url_with_blob(&blob).unwrap(); - // 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 = if let Previous::Named { .. } = previous { format!("url({data_url}) {hotspot_x} {hotspot_y}, {}", previous.cursor() ) } else { From 0678780ecaec1d18d4d9b6dc01b3276a356a2a1b Mon Sep 17 00:00:00 2001 From: daxpedda Date: Mon, 27 Nov 2023 16:00:47 +0100 Subject: [PATCH 31/35] Web: retain previous cursor as a fallback for URLs as well --- src/platform_impl/web/cursor.rs | 36 +++++++++++++++++++-------------- 1 file changed, 21 insertions(+), 15 deletions(-) diff --git a/src/platform_impl/web/cursor.rs b/src/platform_impl/web/cursor.rs index 291d4ccdb4..22ae3b2496 100644 --- a/src/platform_impl/web/cursor.rs +++ b/src/platform_impl/web/cursor.rs @@ -1,5 +1,4 @@ use std::{ - borrow::Cow, cell::RefCell, ops::Deref, rc::{Rc, Weak}, @@ -46,6 +45,8 @@ impl WebCustomCursor { style: &Style, previous: SelectedCursor, ) -> SelectedCursor { + let previous = previous.into(); + match self { WebCustomCursor::Image(image) => SelectedCursor::Image(CursorImageState::from_image( window, @@ -59,10 +60,11 @@ impl WebCustomCursor { hotspot_x, hotspot_y, } => { - let value = format!("url({}) {} {}, auto", 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, @@ -77,6 +79,7 @@ pub enum SelectedCursor { Named(CursorIcon), Url { style: String, + previous: Previous, url: String, hotspot_x: u16, hotspot_y: u16, @@ -134,9 +137,9 @@ impl Previous { } } - fn cursor(&self) -> Cow<'_, str> { + fn style_with_url(&self, new_url: &str, new_hotspot_x: u16, new_hotspot_y: u16) -> String { match self { - Previous::Named(icon) => Cow::Borrowed(icon.name()), + Previous::Named(icon) => format!("url({new_url}) {new_hotspot_x} {new_hotspot_y}, {}", icon.name()), Previous::Url { url, hotspot_x, @@ -152,7 +155,9 @@ impl Previous { .. }, .. - } => Cow::Owned(format!("url({url}) {hotspot_x} {hotspot_y}")), + } => format!( + "url({new_url}) {new_hotspot_x} {new_hotspot_y}, url({url}) {hotspot_x} {hotspot_y}, auto", + ), } } } @@ -166,6 +171,7 @@ impl From for Previous { url, hotspot_x, hotspot_y, + .. } => Self::Url { style, url, @@ -176,7 +182,11 @@ impl From for Previous { match Rc::try_unwrap(image).unwrap().into_inner().unwrap() { CursorImageState::Loading { previous, .. } => previous, CursorImageState::Failed(previous) => previous, - CursorImageState::Ready { style, current, .. } => Self::Image { + CursorImageState::Ready { + style, + image: current, + .. + } => Self::Image { style, image: current, }, @@ -197,7 +207,7 @@ pub enum CursorImageState { Failed(Previous), Ready { style: String, - current: WebCursorImage, + image: WebCursorImage, previous: Previous, }, } @@ -208,7 +218,7 @@ impl CursorImageState { document: Document, style: Style, image: &CursorImage, - previous: SelectedCursor, + 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 @@ -251,7 +261,7 @@ impl CursorImageState { let state = Rc::new(RefCell::new(Some(Self::Loading { style, - previous: previous.into(), + previous, hotspot_x: image.hotspot_x, hotspot_y: image.hotspot_y, }))); @@ -305,16 +315,12 @@ impl CursorImageState { }; let data_url = Url::create_object_url_with_blob(&blob).unwrap(); - let value = if let Previous::Named { .. } = previous { - format!("url({data_url}) {hotspot_x} {hotspot_y}, {}", previous.cursor() ) - } else { - format!("url({data_url}) {hotspot_x} {hotspot_y}, {}, auto", previous.cursor() ) - }; + let value = previous.style_with_url(&data_url, hotspot_x, hotspot_y); style.set("cursor", &value); *state = Some( CursorImageState::Ready { style: value, - current: WebCursorImage{ data_url, hotspot_x, hotspot_y }, + image: WebCursorImage{ data_url, hotspot_x, hotspot_y }, previous, }); }); From d374d60473d5f5d82d78c90f5751f13a24d4e79a Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Mon, 27 Nov 2023 20:39:26 +0200 Subject: [PATCH 32/35] Windows: fix cursor mutex scope --- src/platform_impl/windows/event_loop.rs | 42 +++++++++++++++---------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/platform_impl/windows/event_loop.rs b/src/platform_impl/windows/event_loop.rs index 98c99f8a85..3f5026e95a 100644 --- a/src/platform_impl/windows/event_loop.rs +++ b/src/platform_impl/windows/event_loop.rs @@ -2004,23 +2004,31 @@ unsafe fn public_window_callback_inner( } WM_SETCURSOR => { - let window_state = userdata.window_state_lock(); - // The return value for the preceding `WM_NCHITTEST` message is conveniently - // provided through the low-order word of lParam. We use that here since - // `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 { - // No cursor - result = ProcResult::DefWindowProc(wparam); - } else { - let cursor = match &window_state.mouse.selected_cursor { - SelectedCursor::Named(cursor_icon) => unsafe { - LoadCursorW(0, util::to_windows_cursor(*cursor_icon)) - }, - SelectedCursor::Custom(icon) => icon.as_raw_handle(), - }; - unsafe { SetCursor(cursor) }; - result = ProcResult::Value(0); + let set_cursor_to = { + let window_state = userdata.window_state_lock(); + // The return value for the preceding `WM_NCHITTEST` message is conveniently + // provided through the low-order word of lParam. We use that here since + // `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.selected_cursor.clone()) + } else { + None + } + }; + + match set_cursor_to { + 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), } } From 64c4826f7b91c40efd92de36c86b2ec8b414061b Mon Sep 17 00:00:00 2001 From: Eero Lehtinen Date: Mon, 27 Nov 2023 22:12:14 +0200 Subject: [PATCH 33/35] Windows: keep cursor handle alive when setting in another thread --- src/platform_impl/windows/window.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/platform_impl/windows/window.rs b/src/platform_impl/windows/window.rs index b01c57ec2a..144129b892 100644 --- a/src/platform_impl/windows/window.rs +++ b/src/platform_impl/windows/window.rs @@ -413,10 +413,9 @@ impl Window { return; } }; - let handle = new_cursor.as_raw_handle(); - self.window_state_lock().mouse.selected_cursor = SelectedCursor::Custom(new_cursor); + self.window_state_lock().mouse.selected_cursor = SelectedCursor::Custom(new_cursor.clone()); self.thread_executor.execute_in_thread(move || unsafe { - SetCursor(handle); + SetCursor(new_cursor.as_raw_handle()); }); } From d3370159d8622b74d870b6911a6b90af5ddad7db Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Sat, 16 Dec 2023 21:06:25 +0400 Subject: [PATCH 34/35] Fixup style issues --- src/cursor.rs | 59 ++++--- .../linux/wayland/types/cursor.rs | 31 ++-- .../linux/wayland/window/state.rs | 13 +- src/platform_impl/linux/x11/util/cursor.rs | 144 +++++++++--------- src/platform_impl/linux/x11/window.rs | 8 +- src/platform_impl/macos/appkit/cursor.rs | 20 +-- src/platform_impl/windows/icon.rs | 30 ++-- 7 files changed, 152 insertions(+), 153 deletions(-) diff --git a/src/cursor.rs b/src/cursor.rs index f7a079ffc0..1687689334 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -3,21 +3,25 @@ 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 typically at 128x128. -/// +/// **Web**: Some browsers have limits on cursor sizes usually at 128x128. /// -/// # Examples +/// # Example /// /// ``` /// use winit::window::CustomCursor; /// /// let w = 10; /// let h = 10; -/// let rgba = vec![0xff_u8; (w * h * 4) as usize]; +/// 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")] @@ -36,8 +40,8 @@ impl CustomCursor { /// /// ## Platform-specific /// - /// - **Web** Uses data URLs. They are generated asynchronously, so there can be latency when - /// setting a cursor. + /// - **Web:** Setting cursor could be delayed due to use of data URLs, which are async by + /// nature. pub fn from_rgba( rgba: impl Into>, width: u16, @@ -58,29 +62,8 @@ impl CustomCursor { } } -/// 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) - } -} - -/// The maximum width and height for a cursor when using [`CustomCursor::from_rgba`]. -pub const MAX_CURSOR_SIZE: u16 = 2048; - -#[derive(Debug)] /// 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 @@ -147,8 +130,6 @@ pub struct CursorImage { pub(crate) hotspot_y: u16, } -pub const PIXEL_SIZE: usize = 4; - #[allow(dead_code)] impl CursorImage { pub fn from_rgba( @@ -197,3 +178,21 @@ impl CursorImage { }) } } + +// 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/platform_impl/linux/wayland/types/cursor.rs b/src/platform_impl/linux/wayland/types/cursor.rs index b2d0b1dbba..483486197f 100644 --- a/src/platform_impl/linux/wayland/types/cursor.rs +++ b/src/platform_impl/linux/wayland/types/cursor.rs @@ -1,11 +1,24 @@ 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 struct CustomCursorInternal { +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, @@ -13,7 +26,7 @@ pub struct CustomCursorInternal { pub hotspot_y: i32, } -impl CustomCursorInternal { +impl CustomCursor { pub fn new(pool: &mut SlotPool, image: &CursorImage) -> Self { let (buffer, canvas) = pool .create_buffer( @@ -32,7 +45,7 @@ impl CustomCursorInternal { canvas_chunk[3] = rgba_chunk[3]; } - CustomCursorInternal { + CustomCursor { buffer, w: image.width as i32, h: image.height as i32, @@ -41,15 +54,3 @@ impl CustomCursorInternal { } } } - -#[derive(Debug)] -pub enum SelectedCursor { - Named(CursorIcon), - Custom(CustomCursorInternal), -} - -impl Default for SelectedCursor { - fn default() -> Self { - Self::Named(Default::default()) - } -} diff --git a/src/platform_impl/linux/wayland/window/state.rs b/src/platform_impl/linux/wayland/window/state.rs index 4c4199b3df..9355bb8ab4 100644 --- a/src/platform_impl/linux/wayland/window/state.rs +++ b/src/platform_impl/linux/wayland/window/state.rs @@ -28,13 +28,13 @@ use sctk::shm::Shm; use sctk::subcompositor::SubcompositorState; use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur; -use crate::cursor::CustomCursor; +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::{CustomCursorInternal, SelectedCursor}; +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}; @@ -698,8 +698,6 @@ impl WindowState { } /// Set the cursor icon. - /// - /// Providing `None` will hide the cursor. pub fn set_cursor(&mut self, cursor_icon: CursorIcon) { self.selected_cursor = SelectedCursor::Named(cursor_icon); @@ -714,10 +712,11 @@ impl WindowState { }) } - pub fn set_custom_cursor(&mut self, cursor: CustomCursor) { + /// 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(); - CustomCursorInternal::new(&mut pool, &cursor.inner) + CustomCursor::new(&mut pool, &cursor.inner) }; if self.cursor_visible { @@ -727,7 +726,7 @@ impl WindowState { self.selected_cursor = SelectedCursor::Custom(cursor); } - pub fn apply_custom_cursor(&self, cursor: &CustomCursorInternal) { + fn apply_custom_cursor(&self, cursor: &CustomCursor) { self.apply_on_poiner(|pointer, _| { let surface = pointer.surface(); diff --git a/src/platform_impl/linux/x11/util/cursor.rs b/src/platform_impl/linux/x11/util/cursor.rs index accf783d56..e9b457d967 100644 --- a/src/platform_impl/linux/x11/util/cursor.rs +++ b/src/platform_impl/linux/x11/util/cursor.rs @@ -6,77 +6,6 @@ use crate::{cursor::CursorImage, window::CursorIcon}; use super::*; -#[derive(Debug)] -struct RaiiCursor { - xconn: Arc, - cursor: ffi::Cursor, -} - -impl Drop for RaiiCursor { - fn drop(&mut self) { - unsafe { - (self.xconn.xlib.XFreeCursor)(self.xconn.display, self.cursor); - } - } -} - -impl PartialEq for RaiiCursor { - fn eq(&self, other: &Self) -> bool { - self.cursor == other.cursor - } -} - -impl Eq for RaiiCursor {} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct CustomCursorInternal { - inner: Arc, -} - -impl CustomCursorInternal { - 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(RaiiCursor { - xconn: xconn.clone(), - cursor, - }), - } - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum SelectedCursor { - Custom(CustomCursorInternal), - Named(CursorIcon), -} - -impl Default for SelectedCursor { - fn default() -> Self { - SelectedCursor::Named(Default::default()) - } -} - impl XConnection { pub fn set_cursor_icon(&self, window: xproto::Window, cursor: Option) { let cursor = *self @@ -90,7 +19,7 @@ impl XConnection { .expect("Failed to set cursor"); } - pub fn set_custom_cursor(&self, window: xproto::Window, cursor: &CustomCursorInternal) { + pub fn set_custom_cursor(&self, window: xproto::Window, cursor: &CustomCursor) { self.update_cursor(window, cursor.inner.cursor) .expect("Failed to set cursor"); } @@ -162,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/window.rs b/src/platform_impl/linux/x11/window.rs index f96c0d340c..2d4314c4fb 100644 --- a/src/platform_impl/linux/x11/window.rs +++ b/src/platform_impl/linux/x11/window.rs @@ -7,7 +7,7 @@ use std::{ sync::{Arc, Mutex, MutexGuard}, }; -use crate::cursor::CustomCursor; +use crate::cursor::CustomCursor as RootCustomCursor; use cursor_icon::CursorIcon; use x11rb::{ @@ -43,7 +43,7 @@ use crate::{ use super::{ ffi, - util::{self, CustomCursorInternal, SelectedCursor}, + util::{self, CustomCursor, SelectedCursor}, CookieResultExt, EventLoopWindowTarget, ImeRequest, ImeSender, VoidCookie, WindowId, XConnection, }; @@ -1552,8 +1552,8 @@ impl UnownedWindow { } #[inline] - pub fn set_custom_cursor(&self, cursor: CustomCursor) { - let new_cursor = unsafe { CustomCursorInternal::new(&self.xconn, &cursor.inner) }; + 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() { diff --git a/src/platform_impl/macos/appkit/cursor.rs b/src/platform_impl/macos/appkit/cursor.rs index 95b1180b19..de83f0a27f 100644 --- a/src/platform_impl/macos/appkit/cursor.rs +++ b/src/platform_impl/macos/appkit/cursor.rs @@ -234,21 +234,21 @@ impl NSCursor { } } - pub fn from_image(image: &CursorImage) -> Id { - let w = image.width; - let h = image.height; + pub fn from_image(cursor: &CursorImage) -> Id { + let width = cursor.width; + let height = cursor.height; - let bitmap = NSBitmapImageRep::init_rgba(w as isize, h as isize); + let bitmap = NSBitmapImageRep::init_rgba(width as isize, height as isize); let bitmap_data = - unsafe { std::slice::from_raw_parts_mut(bitmap.bitmap_data(), image.rgba.len()) }; - bitmap_data.copy_from_slice(&image.rgba); + unsafe { std::slice::from_raw_parts_mut(bitmap.bitmap_data(), cursor.rgba.len()) }; + bitmap_data.copy_from_slice(&cursor.rgba); - let nsimage = NSImage::init_with_size(NSSize::new(w.into(), h.into())); - nsimage.add_representation(&bitmap); + let image = NSImage::init_with_size(NSSize::new(width.into(), height.into())); + image.add_representation(&bitmap); - let hotspot = NSPoint::new(image.hotspot_x as f64, image.hotspot_y as f64); + let hotspot = NSPoint::new(cursor.hotspot_x as f64, cursor.hotspot_y as f64); - NSCursor::new(&nsimage, hotspot) + NSCursor::new(&image, hotspot) } } diff --git a/src/platform_impl/windows/icon.rs b/src/platform_impl/windows/icon.rs index 5a026b14ea..275f7fb699 100644 --- a/src/platform_impl/windows/icon.rs +++ b/src/platform_impl/windows/icon.rs @@ -166,9 +166,16 @@ pub fn unset_for_window(hwnd: HWND, icon_type: IconType) { } } -#[derive(Debug)] -struct RaiiCursor { - handle: HCURSOR, +#[derive(Debug, Clone)] +pub enum SelectedCursor { + Named(CursorIcon), + Custom(WinCursor), +} + +impl Default for SelectedCursor { + fn default() -> Self { + Self::Named(Default::default()) + } } #[derive(Clone, Debug)] @@ -237,20 +244,13 @@ impl WinCursor { } } +#[derive(Debug)] +struct RaiiCursor { + handle: HCURSOR, +} + impl Drop for RaiiCursor { fn drop(&mut self) { unsafe { DestroyCursor(self.handle) }; } } - -#[derive(Debug, Clone)] -pub enum SelectedCursor { - Named(CursorIcon), - Custom(WinCursor), -} - -impl Default for SelectedCursor { - fn default() -> Self { - Self::Named(Default::default()) - } -} From 951a012da747290bccefdab37b43758bdda720f4 Mon Sep 17 00:00:00 2001 From: Kirill Chibisov Date: Sat, 16 Dec 2023 21:30:55 +0400 Subject: [PATCH 35/35] Update src/cursor.rs Co-authored-by: daxpedda --- src/cursor.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/cursor.rs b/src/cursor.rs index 1687689334..ca55a61f14 100644 --- a/src/cursor.rs +++ b/src/cursor.rs @@ -40,8 +40,8 @@ impl CustomCursor { /// /// ## Platform-specific /// - /// - **Web:** Setting cursor could be delayed due to use of data URLs, which are async by - /// nature. + /// - **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,