Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Custom cursor images for all desktop platforms #3218

Merged
merged 35 commits into from
Dec 16, 2023
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
6f7f7c3
Implement custom cursor images for all desktop platforms
eero-lehtinen Nov 17, 2023
9da54a8
Web: general improvements
daxpedda Nov 18, 2023
c3267b9
Web: generate a cached data URL
daxpedda Nov 18, 2023
28b7a61
Handle windows custom cursor creation errors better
eero-lehtinen Nov 20, 2023
c031a2d
Simplify wayland custom cursor creation
eero-lehtinen Nov 20, 2023
a0579b7
Fix documentation error
eero-lehtinen Nov 21, 2023
c86503a
macOS: work around clippy false positive
eero-lehtinen Nov 21, 2023
a813832
Web: change Rc usage to be 1.65 compatible
eero-lehtinen Nov 21, 2023
c96738b
Fix BadImage Error implementation
eero-lehtinen Nov 21, 2023
f68d731
Make custom cursors example keyboard layout agnostic
eero-lehtinen Nov 22, 2023
fda16f4
Windows: Cursor creation improvements
eero-lehtinen Nov 22, 2023
7304d5d
Wayland: Improve cursor allocation & fix bugs
eero-lehtinen Nov 22, 2023
bb5db5b
X11: fix import
eero-lehtinen Nov 22, 2023
6412a01
Wayland: remove useless comment
eero-lehtinen Nov 23, 2023
2c43519
Windows: fix cursor destruction
eero-lehtinen Nov 23, 2023
5c04cd3
Update custom cursors documentation
eero-lehtinen Nov 23, 2023
d2e9f41
Add missing custom cursor documentation
eero-lehtinen Nov 24, 2023
7357049
Fix possible overflow in cursor size checks
eero-lehtinen Nov 24, 2023
de364fe
Allow cursors to be created with impl Into<Vec<u8>>
eero-lehtinen Nov 24, 2023
d03b041
X11: Destroy custom cursor on drop
eero-lehtinen Nov 24, 2023
2245b5e
Add documentation about maximum cursor sizes
eero-lehtinen Nov 24, 2023
3b56af4
Update custom cursors documentation
eero-lehtinen Nov 24, 2023
51fce13
Update custom cursor from_url docs
eero-lehtinen Nov 24, 2023
fd1bff3
Ensure that way too large custom cursors can't be created
eero-lehtinen Nov 25, 2023
b6be372
Update changelog and features
eero-lehtinen Nov 25, 2023
134e94d
Add feature description
eero-lehtinen Nov 25, 2023
a4ff890
Web: don't drop previous cursor until new cursor is applied
daxpedda Nov 27, 2023
d2565b9
Web: simplify state machine
daxpedda Nov 27, 2023
43e39ff
Web: retain previous cursor as a fallback
daxpedda Nov 27, 2023
3bea0ab
Web: add failed state in case of `toBlob()` failure
daxpedda Nov 27, 2023
0678780
Web: retain previous cursor as a fallback for URLs as well
daxpedda Nov 27, 2023
d374d60
Windows: fix cursor mutex scope
eero-lehtinen Nov 27, 2023
64c4826
Windows: keep cursor handle alive when setting in another thread
eero-lehtinen Nov 27, 2023
d337015
Fixup style issues
kchibisov Dec 16, 2023
951a012
Update src/cursor.rs
kchibisov Dec 16, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ version = "0.3.64"
features = [
'AbortController',
'AbortSignal',
'Blob',
'console',
'CssStyleDeclaration',
'Document',
Expand All @@ -190,6 +191,10 @@ features = [
'FocusEvent',
'HtmlCanvasElement',
'HtmlElement',
'ImageBitmap',
'ImageBitmapOptions',
'ImageBitmapRenderingContext',
'ImageData',
'IntersectionObserver',
'IntersectionObserverEntry',
'KeyboardEvent',
Expand All @@ -199,14 +204,16 @@ features = [
'Node',
'PageTransitionEvent',
'PointerEvent',
'PremultiplyAlpha',
'ResizeObserver',
'ResizeObserverBoxOptions',
'ResizeObserverEntry',
'ResizeObserverOptions',
'ResizeObserverSize',
'VisibilityState',
'Window',
'WheelEvent'
'WheelEvent',
'Url',
]

[target.'cfg(target_family = "wasm")'.dependencies]
Expand Down
2 changes: 2 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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** |
Expand Down
92 changes: 92 additions & 0 deletions examples/custom_cursors.rs
Original file line number Diff line number Diff line change
@@ -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();
}
_ => {}
})
}
Binary file added examples/data/cross.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added examples/data/cross2.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
198 changes: 198 additions & 0 deletions src/cursor.rs
Original file line number Diff line number Diff line change
@@ -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 {
daxpedda marked this conversation as resolved.
Show resolved Hide resolved
pub(crate) inner: Arc<PlatformCustomCursor>,
eero-lehtinen marked this conversation as resolved.
Show resolved Hide resolved
}

impl CustomCursor {
/// Creates a new cursor from an rgba buffer.
///
/// ## Platform-specific
///
/// - **Web:** Setting cursor could be delayed due to use of data URLs, which are async by
/// nature.
kchibisov marked this conversation as resolved.
Show resolved Hide resolved
pub fn from_rgba(
notgull marked this conversation as resolved.
Show resolved Hide resolved
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 },
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consideration (that you may have already talked about, if so, I would like to see the reasoning be stated in a code comment):

softbuffer uses u32 instead of u8 in its buffer, exactly so that errors like this are entirely avoided.

Copy link
Contributor Author

@eero-lehtinen eero-lehtinen Nov 21, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It doesn't have reasoning other than that I copied it from src/icon.rs link.

I'm fine with using u32, but what about endianess? Currently it is using big endian RGBA pixels, which work directly only in web and macos. Windows, X11 and Wayland want little endian ARGB pixels and need extra processing currently.

But in general I like u8s more because they represent the pixel values more directly. I would expect most users would need to use bitshifts or something to convert their real color values into u32s.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@eero-lehtinen endianess is explicitly stated in the underlying pixel formats. On Wayland it's always little endian, regardless of used arch, maybe the same on X11.

I'd suggest to follow softbuffer approach to buffers here, since it's known to work and using u32 avoids a lot of errors.

The users need to decode images anyway, and they need to ensure that they have alpha properly in them, like they can't just pass data as is.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not particularly convinced that softbuffer has done the right thing here. Even though endianess seems to be defined for Wayland it isn't clear in other backends/OS's (AFAIK). I wrote up some of my thoughts here: rust-windowing/softbuffer#109.

I'm strongly in favor of leaving it as is, as the same API is already used in Icon. We can change the API in a follow-up issue/PR where we can have that discussion and change both and not leave them differently.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You don't have endianess when you have u32 at all, if you convert u8 to u32 you have it, but not with just u32 when the platform expects u32 as well. Wayland defines it because it uses u8, some platforms use bgra, you have the same bgra on both little and big endian platforms.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[..], but not with just u32 when the platform expects u32 as well.

That's good to know!

Copy link
Contributor Author

@eero-lehtinen eero-lehtinen Nov 22, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to leave it as is. I already made it and it already works :D.

The change would be annoying, because x11 is the only one that accepts u32, so all others would need to be manually converted to u8. It would remove this single user facing error but not much else, I think. But I'll leave this up to you to choose.

/// 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 {
eero-lehtinen marked this conversation as resolved.
Show resolved Hide resolved
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)
}
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading
Loading