feat(all): Custom cursor images for all desktop platforms

There seems to be many PRs relating to this issue, but they don't include all
platforms and for some reason lost steam. This PR again tries to make this
feature happen, and does it for all desktop platforms (x11, wayland, macos,
windows, web).

I think the best user of this feature and the reason I'm doing this is Bevy and
game engines in general. There non laggy hardware cursors with custom images are
very important. Game devs also like their PNGs so supporting platform native
cursor files is not that important, but I guess could be added too.

Co-authored-by: daxpedda <daxpedda@gmail.com>
Co-authored-by: Mads Marquart <mads@marquart.dk>
Co-authored-by: Kirill Chibisov <contact@kchibisov.com>
This commit is contained in:
Eero Lehtinen 2023-12-16 22:02:17 +02:00 committed by GitHub
parent 7f6b16a6af
commit af93167237
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
42 changed files with 1243 additions and 57 deletions

View file

@ -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

View file

@ -179,6 +179,7 @@ version = "0.3.64"
features = [
'AbortController',
'AbortSignal',
'Blob',
'console',
'CssStyleDeclaration',
'Document',
@ -190,6 +191,10 @@ features = [
'FocusEvent',
'HtmlCanvasElement',
'HtmlElement',
'ImageBitmap',
'ImageBitmapOptions',
'ImageBitmapRenderingContext',
'ImageData',
'IntersectionObserver',
'IntersectionObserverEntry',
'KeyboardEvent',
@ -199,6 +204,7 @@ features = [
'Node',
'PageTransitionEvent',
'PointerEvent',
'PremultiplyAlpha',
'ResizeObserver',
'ResizeObserverBoxOptions',
'ResizeObserverEntry',
@ -206,7 +212,8 @@ features = [
'ResizeObserverSize',
'VisibilityState',
'Window',
'WheelEvent'
'WheelEvent',
'Url',
]
[target.'cfg(target_family = "wasm")'.dependencies]

View file

@ -106,6 +106,7 @@ If your PR makes notable changes to Winit's features, please update this section
- **Cursor locking**: Locking the cursor inside the window so it cannot move.
- **Cursor confining**: Confining the cursor to the window bounds so it cannot leave them.
- **Cursor icon**: Changing the cursor icon or hiding the cursor.
- **Cursor image**: Changing the cursor to your own image.
- **Cursor hittest**: Handle or ignore mouse events for a window.
- **Touch events**: Single-touch events.
- **Touch pressure**: Touch events contain information about the amount of force being applied.
@ -206,6 +207,7 @@ Legend:
|Cursor locking |❌ |✔️ |❌ |✔️ |**N/A**|**N/A**|✔️ |❌ |
|Cursor confining |✔️ |❌ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ |
|Cursor icon |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |**N/A** |
|Cursor image |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|✔️ |**N/A** |
|Cursor hittest |✔️ |✔️ |✔️ |✔️ |**N/A**|**N/A**|❌ |❌ |
|Touch events |✔️ |❌ |✔️ |✔️ |✔️ |✔️ |✔️ |**N/A** |
|Touch pressure |✔️ |❌ |❌ |❌ |❌ |✔️ |✔️ |**N/A** |

View file

@ -0,0 +1,92 @@
#![allow(clippy::single_match, clippy::disallowed_methods)]
#[cfg(not(wasm_platform))]
use simple_logger::SimpleLogger;
use winit::{
event::{ElementState, Event, KeyEvent, WindowEvent},
event_loop::EventLoop,
keyboard::Key,
window::{CustomCursor, WindowBuilder},
};
fn decode_cursor(bytes: &[u8]) -> CustomCursor {
let img = image::load_from_memory(bytes).unwrap().to_rgba8();
let samples = img.into_flat_samples();
let (_, w, h) = samples.extents();
let (w, h) = (w as u16, h as u16);
CustomCursor::from_rgba(samples.samples, w, h, w / 2, h / 2).unwrap()
}
#[cfg(not(wasm_platform))]
#[path = "util/fill.rs"]
mod fill;
fn main() -> Result<(), impl std::error::Error> {
#[cfg(not(wasm_platform))]
SimpleLogger::new()
.with_level(log::LevelFilter::Info)
.init()
.unwrap();
#[cfg(wasm_platform)]
console_log::init_with_level(log::Level::Debug).unwrap();
let event_loop = EventLoop::new().unwrap();
let builder = WindowBuilder::new().with_title("A fantastic window!");
#[cfg(wasm_platform)]
let builder = {
use winit::platform::web::WindowBuilderExtWebSys;
builder.with_append(true)
};
let window = builder.build(&event_loop).unwrap();
let mut cursor_idx = 0;
let mut cursor_visible = true;
let custom_cursors = [
decode_cursor(include_bytes!("data/cross.png")),
decode_cursor(include_bytes!("data/cross2.png")),
];
event_loop.run(move |event, _elwt| match event {
Event::WindowEvent { event, .. } => match event {
WindowEvent::KeyboardInput {
event:
KeyEvent {
state: ElementState::Pressed,
logical_key: key,
..
},
..
} => match key.as_ref() {
Key::Character("1") => {
log::debug!("Setting cursor to {:?}", cursor_idx);
window.set_custom_cursor(&custom_cursors[cursor_idx]);
cursor_idx = (cursor_idx + 1) % 2;
}
Key::Character("2") => {
log::debug!("Setting cursor icon to default");
window.set_cursor_icon(Default::default());
}
Key::Character("3") => {
cursor_visible = !cursor_visible;
log::debug!("Setting cursor visibility to {:?}", cursor_visible);
window.set_cursor_visible(cursor_visible);
}
_ => {}
},
WindowEvent::RedrawRequested => {
#[cfg(not(wasm_platform))]
fill::fill_window(&window);
}
WindowEvent::CloseRequested => {
#[cfg(not(wasm_platform))]
_elwt.exit();
}
_ => (),
},
Event::AboutToWait => {
window.request_redraw();
}
_ => {}
})
}

BIN
examples/data/cross.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 B

BIN
examples/data/cross2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 B

198
src/cursor.rs Normal file
View file

@ -0,0 +1,198 @@
use core::fmt;
use std::{error::Error, sync::Arc};
use crate::platform_impl::PlatformCustomCursor;
/// The maximum width and height for a cursor when using [`CustomCursor::from_rgba`].
pub const MAX_CURSOR_SIZE: u16 = 2048;
const PIXEL_SIZE: usize = 4;
/// Use a custom image as a cursor (mouse pointer).
///
/// ## Platform-specific
///
/// **Web**: Some browsers have limits on cursor sizes usually at 128x128.
///
/// # Example
///
/// ```
/// use winit::window::CustomCursor;
///
/// let w = 10;
/// let h = 10;
/// let rgba = vec![255; (w * h * 4) as usize];
/// let custom_cursor = CustomCursor::from_rgba(rgba, w, h, w / 2, h / 2).unwrap();
///
/// #[cfg(target_family = "wasm")]
/// let custom_cursor_url = {
/// use winit::platform::web::CustomCursorExtWebSys;
/// CustomCursor::from_url("http://localhost:3000/cursor.png", 0, 0).unwrap()
/// };
/// ```
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomCursor {
pub(crate) inner: Arc<PlatformCustomCursor>,
}
impl CustomCursor {
/// Creates a new cursor from an rgba buffer.
///
/// ## Platform-specific
///
/// - **Web:** Setting cursor could be delayed due to the creation of `Blob` objects,
/// which are async by nature.
pub fn from_rgba(
rgba: impl Into<Vec<u8>>,
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<Self, BadImage> {
Ok(Self {
inner: PlatformCustomCursor::from_rgba(
rgba.into(),
width,
height,
hotspot_x,
hotspot_y,
)?
.into(),
})
}
}
/// An error produced when using [`CustomCursor::from_rgba`] with invalid arguments.
#[derive(Debug, Clone)]
pub enum BadImage {
/// Produced when the image dimensions are larger than [`MAX_CURSOR_SIZE`]. This doesn't
/// guarantee that the cursor will work, but should avoid many platform and device specific
/// limits.
TooLarge { width: u16, height: u16 },
/// Produced when the length of the `rgba` argument isn't divisible by 4, thus `rgba` can't be
/// safely interpreted as 32bpp RGBA pixels.
ByteCountNotDivisibleBy4 { byte_count: usize },
/// Produced when the number of pixels (`rgba.len() / 4`) isn't equal to `width * height`.
/// At least one of your arguments is incorrect.
DimensionsVsPixelCount {
width: u16,
height: u16,
width_x_height: u64,
pixel_count: u64,
},
/// Produced when the hotspot is outside the image bounds
HotspotOutOfBounds {
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
},
}
impl fmt::Display for BadImage {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
BadImage::TooLarge { width, height } => write!(f,
"The specified dimensions ({width:?}x{height:?}) are too large. The maximum is {MAX_CURSOR_SIZE:?}x{MAX_CURSOR_SIZE:?}.",
),
BadImage::ByteCountNotDivisibleBy4 { byte_count } => write!(f,
"The length of the `rgba` argument ({byte_count:?}) isn't divisible by 4, making it impossible to interpret as 32bpp RGBA pixels.",
),
BadImage::DimensionsVsPixelCount {
width,
height,
width_x_height,
pixel_count,
} => write!(f,
"The specified dimensions ({width:?}x{height:?}) don't match the number of pixels supplied by the `rgba` argument ({pixel_count:?}). For those dimensions, the expected pixel count is {width_x_height:?}.",
),
BadImage::HotspotOutOfBounds {
width,
height,
hotspot_x,
hotspot_y,
} => write!(f,
"The specified hotspot ({hotspot_x:?}, {hotspot_y:?}) is outside the image bounds ({width:?}x{height:?}).",
),
}
}
}
impl Error for BadImage {}
/// Platforms export this directly as `PlatformCustomCursor` if they need to only work with images.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CursorImage {
pub(crate) rgba: Vec<u8>,
pub(crate) width: u16,
pub(crate) height: u16,
pub(crate) hotspot_x: u16,
pub(crate) hotspot_y: u16,
}
#[allow(dead_code)]
impl CursorImage {
pub fn from_rgba(
rgba: Vec<u8>,
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<Self, BadImage> {
if width > MAX_CURSOR_SIZE || height > MAX_CURSOR_SIZE {
return Err(BadImage::TooLarge { width, height });
}
if rgba.len() % PIXEL_SIZE != 0 {
return Err(BadImage::ByteCountNotDivisibleBy4 {
byte_count: rgba.len(),
});
}
let pixel_count = (rgba.len() / PIXEL_SIZE) as u64;
let width_x_height = width as u64 * height as u64;
if pixel_count != width_x_height {
return Err(BadImage::DimensionsVsPixelCount {
width,
height,
width_x_height,
pixel_count,
});
}
if hotspot_x >= width || hotspot_y >= height {
return Err(BadImage::HotspotOutOfBounds {
width,
height,
hotspot_x,
hotspot_y,
});
}
Ok(CursorImage {
rgba,
width,
height,
hotspot_x,
hotspot_y,
})
}
}
// Platforms that don't support cursors will export this as `PlatformCustomCursor`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct NoCustomCursor;
#[allow(dead_code)]
impl NoCustomCursor {
pub fn from_rgba(
rgba: Vec<u8>,
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<Self, BadImage> {
CursorImage::from_rgba(rgba, width, height, hotspot_x, hotspot_y)?;
Ok(Self)
}
}

View file

@ -172,6 +172,7 @@ extern crate bitflags;
pub mod dpi;
#[macro_use]
pub mod error;
mod cursor;
pub mod event;
pub mod event_loop;
mod icon;

View file

@ -27,9 +27,11 @@
//! [`border`]: https://developer.mozilla.org/en-US/docs/Web/CSS/border
//! [`padding`]: https://developer.mozilla.org/en-US/docs/Web/CSS/padding
use crate::cursor::CustomCursor;
use crate::event::Event;
use crate::event_loop::EventLoop;
use crate::event_loop::EventLoopWindowTarget;
use crate::platform_impl::PlatformCustomCursor;
use crate::window::{Window, WindowBuilder};
use crate::SendSyncWrapper;
@ -200,3 +202,25 @@ pub enum PollStrategy {
#[default]
Scheduler,
}
pub trait CustomCursorExtWebSys {
/// Creates a new cursor from a URL pointing to an image.
/// It uses the [url css function](https://developer.mozilla.org/en-US/docs/Web/CSS/url),
/// but browser support for image formats is inconsistent. Using [PNG] is recommended.
///
/// [PNG]: https://en.wikipedia.org/wiki/PNG
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> Self;
}
impl CustomCursorExtWebSys for CustomCursor {
fn from_url(url: String, hotspot_x: u16, hotspot_y: u16) -> Self {
Self {
inner: PlatformCustomCursor::Url {
url,
hotspot_x,
hotspot_y,
}
.into(),
}
}
}

View file

@ -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)]

View file

@ -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;

View file

@ -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()))
}

View file

@ -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))

View file

@ -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<Mutex<SlotPool>>,
/// 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(),

View file

@ -0,0 +1,56 @@
use cursor_icon::CursorIcon;
use sctk::reexports::client::protocol::wl_shm::Format;
use sctk::shm::slot::{Buffer, SlotPool};
use crate::cursor::CursorImage;
#[derive(Debug)]
pub enum SelectedCursor {
Named(CursorIcon),
Custom(CustomCursor),
}
impl Default for SelectedCursor {
fn default() -> Self {
Self::Named(Default::default())
}
}
#[derive(Debug)]
pub struct CustomCursor {
pub buffer: Buffer,
pub w: i32,
pub h: i32,
pub hotspot_x: i32,
pub hotspot_y: i32,
}
impl CustomCursor {
pub fn new(pool: &mut SlotPool, image: &CursorImage) -> Self {
let (buffer, canvas) = pool
.create_buffer(
image.width as i32,
image.height as i32,
4 * (image.width as i32),
Format::Argb8888,
)
.unwrap();
for (canvas_chunk, rgba_chunk) in canvas.chunks_exact_mut(4).zip(image.rgba.chunks_exact(4))
{
canvas_chunk[0] = rgba_chunk[2];
canvas_chunk[1] = rgba_chunk[1];
canvas_chunk[2] = rgba_chunk[0];
canvas_chunk[3] = rgba_chunk[3];
}
CustomCursor {
buffer,
w: image.width as i32,
h: image.height as i32,
hotspot_x: image.hotspot_x as i32,
hotspot_y: image.hotspot_y as i32,
}
}
}

View file

@ -1,5 +1,6 @@
//! Wayland protocol implementation boilerplate.
pub mod cursor;
pub mod kwin_blur;
pub mod wp_fractional_scaling;
pub mod wp_viewporter;

View file

@ -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

View file

@ -1,7 +1,7 @@
//! The state of the window, which is shared with the event-loop.
use std::num::NonZeroU32;
use std::sync::{Arc, Weak};
use std::sync::{Arc, Mutex, Weak};
use std::time::Duration;
use log::{info, warn};
@ -18,20 +18,23 @@ use sctk::reexports::protocols::wp::text_input::zv3::client::zwp_text_input_v3::
use sctk::reexports::protocols::wp::viewporter::client::wp_viewport::WpViewport;
use sctk::reexports::protocols::xdg::shell::client::xdg_toplevel::ResizeEdge as XdgResizeEdge;
use sctk::compositor::{CompositorState, Region};
use sctk::seat::pointer::ThemedPointer;
use sctk::compositor::{CompositorState, Region, SurfaceData, SurfaceDataExt};
use sctk::seat::pointer::{PointerDataExt, ThemedPointer};
use sctk::shell::xdg::window::{DecorationMode, Window, WindowConfigure};
use sctk::shell::xdg::XdgSurface;
use sctk::shell::WaylandSurface;
use sctk::shm::slot::SlotPool;
use sctk::shm::Shm;
use sctk::subcompositor::SubcompositorState;
use wayland_protocols_plasma::blur::client::org_kde_kwin_blur::OrgKdeKwinBlur;
use crate::cursor::CustomCursor as RootCustomCursor;
use crate::dpi::{LogicalPosition, LogicalSize, PhysicalSize, Size};
use crate::error::{ExternalError, NotSupportedError};
use crate::event::WindowEvent;
use crate::platform_impl::wayland::event_loop::sink::EventSink;
use crate::platform_impl::wayland::make_wid;
use crate::platform_impl::wayland::types::cursor::{CustomCursor, SelectedCursor};
use crate::platform_impl::wayland::types::kwin_blur::KWinBlurManager;
use crate::platform_impl::WindowId;
use crate::window::{CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme};
@ -60,14 +63,16 @@ pub struct WindowState {
/// The `Shm` to set cursor.
pub shm: WlShm,
// A shared pool where to allocate custom cursors.
custom_cursor_pool: Arc<Mutex<SlotPool>>,
/// The last received configure.
pub last_configure: Option<WindowConfigure>,
/// The pointers observed on the window.
pub pointers: Vec<Weak<ThemedPointer<WinitPointerData>>>,
/// Cursor icon.
pub cursor_icon: CursorIcon,
selected_cursor: SelectedCursor,
/// Wether the cursor is visible.
pub cursor_visible: bool,
@ -178,7 +183,7 @@ impl WindowState {
connection,
csd_fails: false,
cursor_grab_mode: GrabState::new(),
cursor_icon: CursorIcon::Default,
selected_cursor: Default::default(),
cursor_visible: true,
decorate: true,
fractional_scale,
@ -197,6 +202,7 @@ impl WindowState {
resizable: true,
scale_factor: 1.,
shm: winit_state.shm.wl_shm().clone(),
custom_cursor_pool: winit_state.custom_cursor_pool.clone(),
size: initial_size.to_logical(1.),
stateless_size: initial_size.to_logical(1.),
initial_size: Some(initial_size),
@ -603,7 +609,10 @@ impl WindowState {
/// Reload the cursor style on the given window.
pub fn reload_cursor_style(&mut self) {
if self.cursor_visible {
self.set_cursor(self.cursor_icon);
match &self.selected_cursor {
SelectedCursor::Named(icon) => self.set_cursor(*icon),
SelectedCursor::Custom(cursor) => self.apply_custom_cursor(cursor),
}
} else {
self.set_cursor_visible(self.cursor_visible);
}
@ -689,10 +698,8 @@ impl WindowState {
}
/// Set the cursor icon.
///
/// Providing `None` will hide the cursor.
pub fn set_cursor(&mut self, cursor_icon: CursorIcon) {
self.cursor_icon = cursor_icon;
self.selected_cursor = SelectedCursor::Named(cursor_icon);
if !self.cursor_visible {
return;
@ -705,6 +712,54 @@ impl WindowState {
})
}
/// Set the custom cursor icon.
pub fn set_custom_cursor(&mut self, cursor: RootCustomCursor) {
let cursor = {
let mut pool = self.custom_cursor_pool.lock().unwrap();
CustomCursor::new(&mut pool, &cursor.inner)
};
if self.cursor_visible {
self.apply_custom_cursor(&cursor);
}
self.selected_cursor = SelectedCursor::Custom(cursor);
}
fn apply_custom_cursor(&self, cursor: &CustomCursor) {
self.apply_on_poiner(|pointer, _| {
let surface = pointer.surface();
let scale = surface
.data::<SurfaceData>()
.unwrap()
.surface_data()
.scale_factor();
surface.set_buffer_scale(scale);
surface.attach(Some(cursor.buffer.wl_buffer()), 0, 0);
if surface.version() >= 4 {
surface.damage_buffer(0, 0, cursor.w, cursor.h);
} else {
surface.damage(0, 0, cursor.w / scale, cursor.h / scale);
}
surface.commit();
let serial = pointer
.pointer()
.data::<WinitPointerData>()
.and_then(|data| data.pointer_data().latest_enter_serial())
.unwrap();
pointer.pointer().set_cursor(
serial,
Some(surface),
cursor.hotspot_x / scale,
cursor.hotspot_y / scale,
);
});
}
/// Set maximum inner window size.
pub fn set_min_inner_size(&mut self, size: Option<LogicalSize<u32>>) {
// Ensure that the window has the right minimum size.
@ -839,7 +894,10 @@ impl WindowState {
self.cursor_visible = cursor_visible;
if self.cursor_visible {
self.set_cursor(self.cursor_icon);
match &self.selected_cursor {
SelectedCursor::Named(icon) => self.set_cursor(*icon),
SelectedCursor::Custom(cursor) => self.apply_custom_cursor(cursor),
}
} else {
for pointer in self.pointers.iter().filter_map(|pointer| pointer.upgrade()) {
let latest_enter_serial = pointer.pointer().winit_data().latest_enter_serial();

View file

@ -1,9 +1,8 @@
use std::ffi::CString;
use std::iter;
use std::{ffi::CString, iter, slice, sync::Arc};
use x11rb::connection::Connection;
use crate::window::CursorIcon;
use crate::{cursor::CursorImage, window::CursorIcon};
use super::*;
@ -20,6 +19,11 @@ impl XConnection {
.expect("Failed to set cursor");
}
pub fn set_custom_cursor(&self, window: xproto::Window, cursor: &CustomCursor) {
self.update_cursor(window, cursor.inner.cursor)
.expect("Failed to set cursor");
}
fn create_empty_cursor(&self) -> ffi::Cursor {
let data = 0;
let pixmap = unsafe {
@ -87,3 +91,74 @@ impl XConnection {
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SelectedCursor {
Custom(CustomCursor),
Named(CursorIcon),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CustomCursor {
inner: Arc<CustomCursorInner>,
}
impl CustomCursor {
pub(crate) unsafe fn new(xconn: &Arc<XConnection>, 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<XConnection>,
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())
}
}

View file

@ -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},

View file

@ -7,6 +7,9 @@ use std::{
sync::{Arc, Mutex, MutexGuard},
};
use crate::cursor::CustomCursor as RootCustomCursor;
use cursor_icon::CursorIcon;
use x11rb::{
connection::Connection,
properties::{WmHints, WmHintsState, WmSizeHints, WmSizeHintsSpecification},
@ -33,13 +36,15 @@ use crate::{
PlatformSpecificWindowBuilderAttributes, VideoMode as PlatformVideoMode,
},
window::{
CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType,
WindowAttributes, WindowButtons, WindowLevel,
CursorGrabMode, ImePurpose, ResizeDirection, Theme, UserAttentionType, WindowAttributes,
WindowButtons, WindowLevel,
},
};
use super::{
ffi, util, CookieResultExt, EventLoopWindowTarget, ImeRequest, ImeSender, VoidCookie, WindowId,
ffi,
util::{self, CustomCursor, SelectedCursor},
CookieResultExt, EventLoopWindowTarget, ImeRequest, ImeSender, VoidCookie, WindowId,
XConnection,
};
@ -126,7 +131,7 @@ pub(crate) struct UnownedWindow {
root: xproto::Window, // never changes
#[allow(dead_code)]
screen_id: i32, // never changes
cursor: Mutex<CursorIcon>,
selected_cursor: Mutex<SelectedCursor>,
cursor_grabbed_mode: Mutex<CursorGrabMode>,
#[allow(clippy::mutex_atomic)]
cursor_visible: Mutex<bool>,
@ -355,7 +360,7 @@ impl UnownedWindow {
visual,
root,
screen_id,
cursor: Default::default(),
selected_cursor: Default::default(),
cursor_grabbed_mode: Mutex::new(CursorGrabMode::None),
cursor_visible: Mutex::new(true),
ime_sender: Mutex::new(event_loop.ime_sender.clone()),
@ -1535,13 +1540,29 @@ impl UnownedWindow {
#[inline]
pub fn set_cursor_icon(&self, cursor: CursorIcon) {
let old_cursor = replace(&mut *self.cursor.lock().unwrap(), cursor);
let old_cursor = replace(
&mut *self.selected_cursor.lock().unwrap(),
SelectedCursor::Named(cursor),
);
#[allow(clippy::mutex_atomic)]
if cursor != old_cursor && *self.cursor_visible.lock().unwrap() {
if SelectedCursor::Named(cursor) != old_cursor && *self.cursor_visible.lock().unwrap() {
self.xconn.set_cursor_icon(self.xwindow, Some(cursor));
}
}
#[inline]
pub fn set_custom_cursor(&self, cursor: RootCustomCursor) {
let new_cursor = unsafe { CustomCursor::new(&self.xconn, &cursor.inner) };
#[allow(clippy::mutex_atomic)]
if *self.cursor_visible.lock().unwrap() {
self.xconn.set_custom_cursor(self.xwindow, &new_cursor);
}
*self.selected_cursor.lock().unwrap() = SelectedCursor::Custom(new_cursor);
}
#[inline]
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
let mut grabbed_lock = self.cursor_grabbed_mode.lock().unwrap();
@ -1628,13 +1649,23 @@ impl UnownedWindow {
return;
}
let cursor = if visible {
Some(*self.cursor.lock().unwrap())
Some((*self.selected_cursor.lock().unwrap()).clone())
} else {
None
};
*visible_lock = visible;
drop(visible_lock);
self.xconn.set_cursor_icon(self.xwindow, cursor);
match cursor {
Some(SelectedCursor::Custom(cursor)) => {
self.xconn.set_custom_cursor(self.xwindow, &cursor);
}
Some(SelectedCursor::Named(cursor)) => {
self.xconn.set_cursor_icon(self.xwindow, Some(cursor));
}
None => {
self.xconn.set_cursor_icon(self.xwindow, None);
}
}
}
#[inline]

View file

@ -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!(
// <https://developer.apple.com/documentation/appkit/nsbitmapimagerep?language=objc>
#[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<Self> {
unsafe {
msg_send_id![Self::alloc(),
initWithBitmapDataPlanes: std::ptr::null_mut::<*mut c_uchar>(),
pixelsWide: width,
pixelsHigh: height,
bitsPerSample: 8 as NSInteger,
samplesPerPixel: 4 as NSInteger,
hasAlpha: Bool::new(true),
isPlanar: Bool::new(false),
colorSpaceName: NSDeviceRGBColorSpace,
bytesPerRow: width * 4,
bitsPerPixel: 32 as NSInteger,
]
}
}
pub fn bitmap_data(&self) -> *mut u8 {
unsafe { msg_send![self, bitmapData] }
}
}
);

View file

@ -2,13 +2,14 @@ use once_cell::sync::Lazy;
use icrate::ns_string;
use icrate::Foundation::{
NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSString,
NSData, NSDictionary, NSNumber, NSObject, NSObjectProtocol, NSPoint, NSSize, NSString,
};
use objc2::rc::{DefaultId, Id};
use objc2::runtime::Sel;
use objc2::{extern_class, extern_methods, msg_send_id, mutability, sel, ClassType};
use super::NSImage;
use super::{NSBitmapImageRep, NSImage};
use crate::cursor::CursorImage;
use crate::window::CursorIcon;
extern_class!(
@ -232,6 +233,23 @@ impl NSCursor {
_ => Default::default(),
}
}
pub fn from_image(cursor: &CursorImage) -> Id<Self> {
let width = cursor.width;
let height = cursor.height;
let bitmap = NSBitmapImageRep::init_rgba(width as isize, height as isize);
let bitmap_data =
unsafe { std::slice::from_raw_parts_mut(bitmap.bitmap_data(), cursor.rgba.len()) };
bitmap_data.copy_from_slice(&cursor.rgba);
let image = NSImage::init_with_size(NSSize::new(width.into(), height.into()));
image.add_representation(&bitmap);
let hotspot = NSPoint::new(cursor.hotspot_x as f64, cursor.hotspot_y as f64);
NSCursor::new(&image, hotspot)
}
}
impl DefaultId for NSCursor {

View file

@ -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<Self> {
unsafe { msg_send_id![Self::alloc(), initWithData: data] }
}
pub fn init_with_size(size: NSSize) -> Id<Self> {
unsafe { msg_send_id![Self::alloc(), initWithSize: size] }
}
pub fn add_representation(&self, representation: &NSBitmapImageRep) {
unsafe { msg_send![self, addRepresentation: representation] }
}
}
);

View file

@ -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;

View file

@ -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;

View file

@ -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 {

View file

@ -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)]

View file

@ -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(

View file

@ -0,0 +1,351 @@
use std::{
cell::RefCell,
ops::Deref,
rc::{Rc, Weak},
};
use crate::cursor::{BadImage, CursorImage};
use cursor_icon::CursorIcon;
use wasm_bindgen::{closure::Closure, JsCast};
use wasm_bindgen_futures::JsFuture;
use web_sys::{
Blob, Document, HtmlCanvasElement, ImageBitmap, ImageBitmapOptions,
ImageBitmapRenderingContext, ImageData, PremultiplyAlpha, Url, Window,
};
use super::backend::Style;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum WebCustomCursor {
Image(CursorImage),
Url {
url: String,
hotspot_x: u16,
hotspot_y: u16,
},
}
impl WebCustomCursor {
pub fn from_rgba(
rgba: Vec<u8>,
width: u16,
height: u16,
hotspot_x: u16,
hotspot_y: u16,
) -> Result<Self, BadImage> {
Ok(Self::Image(CursorImage::from_rgba(
rgba, width, height, hotspot_x, hotspot_y,
)?))
}
pub(super) fn build(
&self,
window: &Window,
document: &Document,
style: &Style,
previous: SelectedCursor,
) -> SelectedCursor {
let previous = previous.into();
match self {
WebCustomCursor::Image(image) => SelectedCursor::Image(CursorImageState::from_image(
window,
document.clone(),
style.clone(),
image,
previous,
)),
WebCustomCursor::Url {
url,
hotspot_x,
hotspot_y,
} => {
let value = previous.style_with_url(url, *hotspot_x, *hotspot_y);
style.set("cursor", &value);
SelectedCursor::Url {
style: value,
previous,
url: url.clone(),
hotspot_x: *hotspot_x,
hotspot_y: *hotspot_y,
}
}
}
}
}
#[derive(Debug)]
pub enum SelectedCursor {
Named(CursorIcon),
Url {
style: String,
previous: Previous,
url: String,
hotspot_x: u16,
hotspot_y: u16,
},
Image(Rc<RefCell<Option<CursorImageState>>>),
}
impl Default for SelectedCursor {
fn default() -> Self {
Self::Named(Default::default())
}
}
impl SelectedCursor {
pub fn set_style(&self, style: &Style) {
let value = match self {
SelectedCursor::Named(icon) => icon.name(),
SelectedCursor::Url { style, .. } => style,
SelectedCursor::Image(image) => {
let image = image.borrow();
let value = match image.deref().as_ref().unwrap() {
CursorImageState::Loading { previous, .. } => previous.style(),
CursorImageState::Failed(previous) => previous.style(),
CursorImageState::Ready { style, .. } => style,
};
return style.set("cursor", value);
}
};
style.set("cursor", value);
}
}
#[derive(Debug)]
pub enum Previous {
Named(CursorIcon),
Url {
style: String,
url: String,
hotspot_x: u16,
hotspot_y: u16,
},
Image {
style: String,
image: WebCursorImage,
},
}
impl Previous {
fn style(&self) -> &str {
match self {
Previous::Named(icon) => icon.name(),
Previous::Url { style: url, .. } => url,
Previous::Image { style, .. } => style,
}
}
fn style_with_url(&self, new_url: &str, new_hotspot_x: u16, new_hotspot_y: u16) -> String {
match self {
Previous::Named(icon) => format!("url({new_url}) {new_hotspot_x} {new_hotspot_y}, {}", icon.name()),
Previous::Url {
url,
hotspot_x,
hotspot_y,
..
}
| Previous::Image {
image:
WebCursorImage {
data_url: url,
hotspot_x,
hotspot_y,
..
},
..
} => format!(
"url({new_url}) {new_hotspot_x} {new_hotspot_y}, url({url}) {hotspot_x} {hotspot_y}, auto",
),
}
}
}
impl From<SelectedCursor> for Previous {
fn from(value: SelectedCursor) -> Self {
match value {
SelectedCursor::Named(icon) => Self::Named(icon),
SelectedCursor::Url {
style,
url,
hotspot_x,
hotspot_y,
..
} => Self::Url {
style,
url,
hotspot_x,
hotspot_y,
},
SelectedCursor::Image(image) => {
match Rc::try_unwrap(image).unwrap().into_inner().unwrap() {
CursorImageState::Loading { previous, .. } => previous,
CursorImageState::Failed(previous) => previous,
CursorImageState::Ready {
style,
image: current,
..
} => Self::Image {
style,
image: current,
},
}
}
}
}
}
#[derive(Debug)]
pub enum CursorImageState {
Loading {
style: Style,
previous: Previous,
hotspot_x: u16,
hotspot_y: u16,
},
Failed(Previous),
Ready {
style: String,
image: WebCursorImage,
previous: Previous,
},
}
impl CursorImageState {
fn from_image(
window: &Window,
document: Document,
style: Style,
image: &CursorImage,
previous: Previous,
) -> Rc<RefCell<Option<Self>>> {
// Can't create array directly when backed by SharedArrayBuffer.
// Adapted from https://github.com/rust-windowing/softbuffer/blob/ab7688e2ed2e2eca51b3c4e1863a5bd7fe85800e/src/web.rs#L196-L223
#[cfg(target_feature = "atomics")]
let image_data = {
use js_sys::{Uint8Array, Uint8ClampedArray};
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
#[wasm_bindgen]
extern "C" {
#[wasm_bindgen(js_namespace = ImageData)]
type ImageDataExt;
#[wasm_bindgen(catch, constructor, js_class = ImageData)]
fn new(array: Uint8ClampedArray, sw: u32) -> Result<ImageDataExt, JsValue>;
}
let array = Uint8Array::new_with_length(image.rgba.len() as u32);
array.copy_from(&image.rgba);
let array = Uint8ClampedArray::new(&array);
ImageDataExt::new(array, image.width as u32)
.map(JsValue::from)
.map(ImageData::unchecked_from_js)
.unwrap()
};
#[cfg(not(target_feature = "atomics"))]
let image_data = ImageData::new_with_u8_clamped_array(
wasm_bindgen::Clamped(&image.rgba),
image.width as u32,
)
.unwrap();
let mut options = ImageBitmapOptions::new();
options.premultiply_alpha(PremultiplyAlpha::None);
let bitmap = JsFuture::from(
window
.create_image_bitmap_with_image_data_and_image_bitmap_options(&image_data, &options)
.unwrap(),
);
let state = Rc::new(RefCell::new(Some(Self::Loading {
style,
previous,
hotspot_x: image.hotspot_x,
hotspot_y: image.hotspot_y,
})));
wasm_bindgen_futures::spawn_local({
let weak = Rc::downgrade(&state);
let CursorImage { width, height, .. } = *image;
async move {
if weak.strong_count() == 0 {
return;
}
let bitmap: ImageBitmap = bitmap.await.unwrap().unchecked_into();
if weak.strong_count() == 0 {
return;
}
let canvas: HtmlCanvasElement =
document.create_element("canvas").unwrap().unchecked_into();
#[allow(clippy::disallowed_methods)]
canvas.set_width(width as u32);
#[allow(clippy::disallowed_methods)]
canvas.set_height(height as u32);
let context: ImageBitmapRenderingContext = canvas
.get_context("bitmaprenderer")
.unwrap()
.unwrap()
.unchecked_into();
context.transfer_from_image_bitmap(&bitmap);
thread_local! {
static CURRENT_STATE: RefCell<Option<Weak<RefCell<Option<CursorImageState>>>>> = 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<dyn Fn(Option<Blob>)> = Closure::new(|blob| {
CURRENT_STATE.with(|weak| {
let Some(state) = weak.borrow_mut().take().and_then(|weak| weak.upgrade()) else {
return;
};
let mut state = state.borrow_mut();
// Extract old state.
let CursorImageState::Loading { style, previous, hotspot_x, hotspot_y, .. } = state.take().unwrap() else {
unreachable!("found invalid state")
};
let Some(blob) = blob else {
*state = Some(CursorImageState::Failed(previous));
return;
};
let data_url = Url::create_object_url_with_blob(&blob).unwrap();
let value = previous.style_with_url(&data_url, hotspot_x, hotspot_y);
style.set("cursor", &value);
*state = Some(
CursorImageState::Ready {
style: value,
image: WebCursorImage{ data_url, hotspot_x, hotspot_y },
previous,
});
});
});
}
CURRENT_STATE.with(|state| *state.borrow_mut() = Some(weak));
CALLBACK
.with(|callback| canvas.to_blob(callback.as_ref().unchecked_ref()).unwrap());
}
});
state
}
}
#[derive(Debug)]
pub struct WebCursorImage {
data_url: String,
hotspot_x: u16,
hotspot_y: u16,
}
impl Drop for WebCursorImage {
fn drop(&mut self) {
Url::revoke_object_url(&self.data_url).unwrap();
}
}

View file

@ -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;

View file

@ -55,7 +55,7 @@ pub struct Common {
fullscreen_handler: Rc<FullscreenHandler>,
}
#[derive(Clone)]
#[derive(Clone, Debug)]
pub struct Style {
read: CssStyleDeclaration,
write: CssStyleDeclaration,

View file

@ -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;

View file

@ -1,3 +1,4 @@
use crate::cursor::CustomCursor;
use crate::dpi::{PhysicalPosition, PhysicalSize, Position, Size};
use crate::error::{ExternalError, NotSupportedError, OsError as RootOE};
use crate::icon::Icon;
@ -7,10 +8,10 @@ use crate::window::{
};
use crate::SendSyncWrapper;
use web_sys::HtmlCanvasElement;
use super::cursor::SelectedCursor;
use super::r#async::Dispatcher;
use super::{backend, monitor::MonitorHandle, EventLoopWindowTarget, Fullscreen};
use web_sys::HtmlCanvasElement;
use std::cell::RefCell;
use std::collections::VecDeque;
@ -24,7 +25,7 @@ pub struct Inner {
id: WindowId,
pub window: web_sys::Window,
canvas: Rc<RefCell<backend::Canvas>>,
previous_pointer: RefCell<&'static str>,
selected_cursor: RefCell<SelectedCursor>,
destroy_fn: Option<Box<dyn FnOnce()>>,
}
@ -53,7 +54,7 @@ impl Window {
id,
window: window.clone(),
canvas,
previous_pointer: RefCell::new("auto"),
selected_cursor: Default::default(),
destroy_fn: Some(destroy_fn),
};
@ -195,10 +196,22 @@ impl Inner {
#[inline]
pub fn set_cursor_icon(&self, cursor: CursorIcon) {
*self.previous_pointer.borrow_mut() = cursor.name();
*self.selected_cursor.borrow_mut() = SelectedCursor::Named(cursor);
self.canvas.borrow().style().set("cursor", cursor.name());
}
#[inline]
pub fn set_custom_cursor(&self, cursor: CustomCursor) {
let canvas = self.canvas.borrow();
let new_cursor = cursor.inner.build(
canvas.window(),
canvas.document(),
canvas.style(),
self.selected_cursor.take(),
);
*self.selected_cursor.borrow_mut() = new_cursor;
}
#[inline]
pub fn set_cursor_position(&self, _position: Position) -> Result<(), ExternalError> {
Err(ExternalError::NotSupported(NotSupportedError::new()))
@ -225,10 +238,9 @@ impl Inner {
if !visible {
self.canvas.borrow().style().set("cursor", "none");
} else {
self.canvas
self.selected_cursor
.borrow()
.style()
.set("cursor", &self.previous_pointer.borrow());
.set_style(self.canvas.borrow().style());
}
}

View file

@ -101,7 +101,7 @@ use runner::{EventLoopRunner, EventLoopRunnerShared};
use self::runner::RunnerState;
use super::window::set_skip_taskbar;
use super::{window::set_skip_taskbar, SelectedCursor};
type GetPointerFrameInfoHistory = unsafe extern "system" fn(
pointerId: u32,
@ -2011,16 +2011,21 @@ unsafe fn public_window_callback_inner<T: 'static>(
// `WM_MOUSEMOVE` seems to come after `WM_SETCURSOR` for a given cursor movement.
let in_client_area = super::loword(lparam as u32) as u32 == HTCLIENT;
if in_client_area {
Some(window_state.mouse.cursor)
Some(window_state.mouse.selected_cursor.clone())
} else {
None
}
};
match set_cursor_to {
Some(cursor) => {
let cursor = unsafe { LoadCursorW(0, util::to_windows_cursor(cursor)) };
unsafe { SetCursor(cursor) };
Some(selected_cursor) => {
let hcursor = match selected_cursor {
SelectedCursor::Named(cursor_icon) => unsafe {
LoadCursorW(0, util::to_windows_cursor(cursor_icon))
},
SelectedCursor::Custom(cursor) => cursor.as_raw_handle(),
};
unsafe { SetCursor(hcursor) };
result = ProcResult::Value(0);
}
None => result = ProcResult::DefWindowProc(wparam),

View file

@ -1,18 +1,23 @@
use std::{fmt, io, mem, path::Path, sync::Arc};
use std::{ffi::c_void, fmt, io, mem, path::Path, sync::Arc};
use cursor_icon::CursorIcon;
use windows_sys::{
core::PCWSTR,
Win32::{
Foundation::HWND,
Graphics::Gdi::{
CreateBitmap, CreateCompatibleBitmap, DeleteObject, GetDC, ReleaseDC, SetBitmapBits,
},
UI::WindowsAndMessaging::{
CreateIcon, DestroyIcon, LoadImageW, SendMessageW, HICON, ICON_BIG, ICON_SMALL,
IMAGE_ICON, LR_DEFAULTSIZE, LR_LOADFROMFILE, WM_SETICON,
CreateIcon, CreateIconIndirect, DestroyCursor, DestroyIcon, LoadImageW, SendMessageW,
HCURSOR, HICON, ICONINFO, ICON_BIG, ICON_SMALL, IMAGE_ICON, LR_DEFAULTSIZE,
LR_LOADFROMFILE, WM_SETICON,
},
},
};
use crate::dpi::PhysicalSize;
use crate::icon::*;
use crate::{cursor::CursorImage, dpi::PhysicalSize};
use super::util;
@ -160,3 +165,92 @@ pub fn unset_for_window(hwnd: HWND, icon_type: IconType) {
SendMessageW(hwnd, WM_SETICON, icon_type as usize, 0);
}
}
#[derive(Debug, Clone)]
pub enum SelectedCursor {
Named(CursorIcon),
Custom(WinCursor),
}
impl Default for SelectedCursor {
fn default() -> Self {
Self::Named(Default::default())
}
}
#[derive(Clone, Debug)]
pub struct WinCursor {
inner: Arc<RaiiCursor>,
}
impl WinCursor {
pub fn as_raw_handle(&self) -> HICON {
self.inner.handle
}
fn from_handle(handle: HCURSOR) -> Self {
Self {
inner: Arc::new(RaiiCursor { handle }),
}
}
pub fn new(image: &CursorImage) -> Result<Self, io::Error> {
let mut bgra = image.rgba.clone();
bgra.chunks_exact_mut(4).for_each(|chunk| chunk.swap(0, 2));
let w = image.width as i32;
let h = image.height as i32;
unsafe {
let hdc_screen = GetDC(0);
if hdc_screen == 0 {
return Err(io::Error::last_os_error());
}
let hbm_color = CreateCompatibleBitmap(hdc_screen, w, h);
ReleaseDC(0, hdc_screen);
if hbm_color == 0 {
return Err(io::Error::last_os_error());
}
if SetBitmapBits(hbm_color, bgra.len() as u32, bgra.as_ptr() as *const c_void) == 0 {
DeleteObject(hbm_color);
return Err(io::Error::last_os_error());
};
// Mask created according to https://learn.microsoft.com/en-us/windows/win32/api/wingdi/nf-wingdi-createbitmap#parameters
let mask_bits: Vec<u8> = vec![0xff; ((((w + 15) >> 4) << 1) * h) as usize];
let hbm_mask = CreateBitmap(w, h, 1, 1, mask_bits.as_ptr() as *const _);
if hbm_mask == 0 {
DeleteObject(hbm_color);
return Err(io::Error::last_os_error());
}
let icon_info = ICONINFO {
fIcon: 0,
xHotspot: image.hotspot_x as u32,
yHotspot: image.hotspot_y as u32,
hbmMask: hbm_mask,
hbmColor: hbm_color,
};
let handle = CreateIconIndirect(&icon_info as *const _);
DeleteObject(hbm_color);
DeleteObject(hbm_mask);
if handle == 0 {
return Err(io::Error::last_os_error());
}
Ok(Self::from_handle(handle))
}
}
}
#[derive(Debug)]
struct RaiiCursor {
handle: HCURSOR,
}
impl Drop for RaiiCursor {
fn drop(&mut self) {
unsafe { DestroyCursor(self.handle) };
}
}

View file

@ -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;

View file

@ -55,6 +55,7 @@ use windows_sys::Win32::{
};
use crate::{
cursor::CustomCursor,
dpi::{PhysicalPosition, PhysicalSize, Position, Size},
error::{ExternalError, NotSupportedError, OsError as RootOsError},
icon::Icon,
@ -66,13 +67,13 @@ use crate::{
dpi::{dpi_to_scale_factor, enable_non_client_dpi_scaling, hwnd_dpi},
drop_handler::FileDropHandler,
event_loop::{self, EventLoopWindowTarget, DESTROY_MSG_ID},
icon::{self, IconType},
icon::{self, IconType, WinCursor},
ime::ImeContext,
keyboard::KeyEventBuilder,
monitor::{self, MonitorHandle},
util,
window_state::{CursorFlags, SavedWindow, WindowFlags, WindowState},
Fullscreen, PlatformSpecificWindowBuilderAttributes, WindowId,
Fullscreen, PlatformSpecificWindowBuilderAttributes, SelectedCursor, WindowId,
},
window::{
CursorGrabMode, CursorIcon, ImePurpose, ResizeDirection, Theme, UserAttentionType,
@ -396,13 +397,28 @@ impl Window {
#[inline]
pub fn set_cursor_icon(&self, cursor: CursorIcon) {
self.window_state_lock().mouse.cursor = cursor;
self.window_state_lock().mouse.selected_cursor = SelectedCursor::Named(cursor);
self.thread_executor.execute_in_thread(move || unsafe {
let cursor = LoadCursorW(0, util::to_windows_cursor(cursor));
SetCursor(cursor);
});
}
#[inline]
pub fn set_custom_cursor(&self, cursor: CustomCursor) {
let new_cursor = match WinCursor::new(&cursor.inner) {
Ok(cursor) => cursor,
Err(err) => {
warn!("Failed to create custom cursor: {err}");
return;
}
};
self.window_state_lock().mouse.selected_cursor = SelectedCursor::Custom(new_cursor.clone());
self.thread_executor.execute_in_thread(move || unsafe {
SetCursor(new_cursor.as_raw_handle());
});
}
#[inline]
pub fn set_cursor_grab(&self, mode: CursorGrabMode) -> Result<(), ExternalError> {
let confine = match mode {

View file

@ -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<PhysicalPosition<f64>>,
@ -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,

View file

@ -9,6 +9,7 @@ use crate::{
platform_impl, SendSyncWrapper,
};
pub use crate::cursor::{BadImage, CustomCursor, MAX_CURSOR_SIZE};
pub use crate::icon::{BadIcon, Icon};
#[doc(inline)]
@ -1336,6 +1337,7 @@ impl Window {
/// Cursor functions.
impl Window {
/// Modifies the cursor icon of the window.
/// Overwrites cursors set in [`Window::set_custom_cursor`].
///
/// ## Platform-specific
///
@ -1346,6 +1348,19 @@ impl Window {
.maybe_queue_on_main(move |w| w.set_cursor_icon(cursor))
}
/// Modifies the cursor icon of the window with a custom cursor.
/// Overwrites cursors set in [`Window::set_cursor_icon`].
///
/// ## Platform-specific
///
/// - **iOS / Android / Orbital:** Unsupported.
#[inline]
pub fn set_custom_cursor(&self, cursor: &CustomCursor) {
let cursor = cursor.clone();
self.window
.maybe_queue_on_main(move |w| w.set_custom_cursor(cursor))
}
/// Changes the position of the cursor in window coordinates.
///
/// ```no_run

View file

@ -28,3 +28,8 @@ fn ids_send() {
needs_send::<winit::event::DeviceId>();
needs_send::<winit::monitor::MonitorHandle>();
}
#[test]
fn custom_cursor_send() {
needs_send::<winit::window::CustomCursor>();
}

View file

@ -11,3 +11,8 @@ fn window_sync() {
fn window_builder_sync() {
needs_sync::<winit::window::WindowBuilder>();
}
#[test]
fn custom_cursor_sync() {
needs_sync::<winit::window::CustomCursor>();
}