mirror of
https://github.com/emilk/egui.git
synced 2026-09-01 06:10:06 -04:00
Add Context::set_cursor_image for OS-level custom cursors (#8155)
## What Adds a way for apps to push an RGBA bitmap as the OS cursor — the missing companion to `Context::set_cursor_icon`. The integration translates it into a real `winit::CustomCursor`, so the cursor is drawn by the compositor and can extend past the egui window edge like any native cursor. ## Why Apps with custom-shaped windows (Winamp-style skins, themed launchers, kiosk apps) currently have no clean way to display a custom cursor: - `CursorIcon` is limited to the standard system enum. - Painting the cursor sprite via `egui::Painter` works inside the canvas but gets clipped at the window edge — the bottom/right of the cursor disappears the moment the pointer is near the boundary, and there's no way to render onto the desktop area exposed by a transparent/region-shaped window. `winit` 0.30+ already supports `CustomCursor::from_rgba` + `ActiveEventLoop::create_custom_cursor`, but `egui-winit` doesn't surface it. This PR exposes it through egui. ### Visual demonstration Driving use case: a Winamp WSZ skin player ([all3f0r1/oneamp](https://github.com/all3f0r1/oneamp)) with a transparent + region-shaped window where the skin ships its own `.cur` files. The bottom-right corner of the playlist exposes the resize cursor — notice how it gets clipped at the window edge in the painter-based approach. | Before (cursor painted via `egui::Painter`) | After (cursor pushed via `set_cursor_image`) | | --- | --- | |  |  | ## API ```rust // new in egui::data::output pub struct CustomCursorImage { pub rgba: std::sync::Arc<[u8]>, pub size: [u16; 2], // matches winit's u16 to avoid lossy casts pub hotspot: [u16; 2], } // new field on PlatformOutput (skipped from serde — ephemeral) pub cursor_image: Option<CustomCursorImage>, // new method on Context ctx.set_cursor_image(Some(image)); // overrides cursor_icon for this frame ctx.set_cursor_image(None); // revert to cursor_icon ``` `Arc<[u8]>` is intentional: the integration dedupes by `Arc::as_ptr`, so reusing the same Arc across frames means the bitmap is only uploaded to the OS once per skin, not once per frame. ## Integration changes - `egui_winit::State::handle_platform_output_with_event_loop(window, Option<&ActiveEventLoop>, ...)` is a new method that threads the active event loop so it can call `event_loop.create_custom_cursor(...)`. - The legacy `handle_platform_output(window, ...)` delegates with `None` and silently drops `cursor_image`. **No existing callers break.** - The icon and bitmap paths are unified in a private `apply_cursor`. The no-flicker dedupe of the old `set_cursor_icon` is preserved on both paths. - If `CustomCursor::from_rgba` rejects the bitmap (bad dimensions, hotspot OOB, etc.), we log a warning and fall back to the icon path. - eframe's wgpu + glow integrations thread `&ActiveEventLoop` through `run_ui_and_paint` (glow already had it; wgpu needed one extra parameter) and call the new method. - Immediate viewports keep the old path because they're invoked from a `Context` callback that doesn't have an event loop reference. Custom cursors are a no-op in immediate viewports — acceptable since they're a niche path. ## Fallback semantics | backend / context | what happens | |--------------------------------|-------------------------------| | eframe wgpu/glow main viewport | bitmap displayed via OS | | eframe immediate viewport | falls back to `cursor_icon` | | eframe web | falls back to `cursor_icon` | | custom integrations not opted in | falls back to `cursor_icon` | | `from_rgba` returns `BadImage` | warning + falls back to icon | ## Verification - `cargo fmt --all -- --check` ✅ - `cargo clippy -p egui -p egui-winit -p eframe --all-targets --all-features -- -D warnings` ✅ - `cargo doc --lib --no-deps -p egui -p egui-winit -p eframe --all-features` ✅ - `cargo check -p egui --no-default-features --features serde` ✅ (validates the `serde(skip)` on `cursor_image`) - Interactive validation on Linux/Wayland with the OneAmp WSZ skin player — see screenshots above. I haven't run the full snapshot test suite (`scripts/check.sh`) because we're on Linux and the snapshots are macOS-rendered — happy to run it if you'd like. ## Notes Drafted per the contributing guide ("open a draft PR, you may get helpful feedback early"). Open to design feedback on: 1. Whether `CustomCursorImage` should live in `egui::viewport` rather than `egui::data::output`. 2. Whether the legacy `handle_platform_output` should grow `event_loop` directly (breaking) instead of getting a sibling method (non-breaking, what I did). 3. Whether to also wire it through eframe-web (probably not — `wasm-bindgen-cursor` would need its own path). --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -679,7 +679,7 @@ impl GlowWinitRunning<'_> {
|
|||||||
let gl_surface = viewport.gl_surface.as_ref().unwrap();
|
let gl_surface = viewport.gl_surface.as_ref().unwrap();
|
||||||
let egui_winit = viewport.egui_winit.as_mut().unwrap();
|
let egui_winit = viewport.egui_winit.as_mut().unwrap();
|
||||||
|
|
||||||
egui_winit.handle_platform_output(&window, platform_output);
|
egui_winit.handle_platform_output_with_event_loop(&window, event_loop, platform_output);
|
||||||
|
|
||||||
if is_visible {
|
if is_visible {
|
||||||
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
|
let clipped_primitives = integration.egui_ctx.tessellate(shapes, pixels_per_point);
|
||||||
|
|||||||
@@ -409,7 +409,7 @@ impl WinitApp for WgpuWinitApp<'_> {
|
|||||||
self.initialized_all_windows(event_loop);
|
self.initialized_all_windows(event_loop);
|
||||||
|
|
||||||
if let Some(running) = &mut self.running {
|
if let Some(running) = &mut self.running {
|
||||||
running.run_ui_and_paint(window_id)
|
running.run_ui_and_paint(window_id, event_loop)
|
||||||
} else {
|
} else {
|
||||||
Ok(EventResult::Wait)
|
Ok(EventResult::Wait)
|
||||||
}
|
}
|
||||||
@@ -569,7 +569,11 @@ impl WgpuWinitRunning<'_> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// This is called both for the root viewport, and all deferred viewports
|
/// This is called both for the root viewport, and all deferred viewports
|
||||||
fn run_ui_and_paint(&mut self, window_id: WindowId) -> Result<EventResult> {
|
fn run_ui_and_paint(
|
||||||
|
&mut self,
|
||||||
|
window_id: WindowId,
|
||||||
|
event_loop: &ActiveEventLoop,
|
||||||
|
) -> Result<EventResult> {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
let Some(viewport_id) = self
|
let Some(viewport_id) = self
|
||||||
@@ -710,7 +714,7 @@ impl WgpuWinitRunning<'_> {
|
|||||||
return Ok(EventResult::Wait);
|
return Ok(EventResult::Wait);
|
||||||
};
|
};
|
||||||
|
|
||||||
egui_winit.handle_platform_output(window, platform_output);
|
egui_winit.handle_platform_output_with_event_loop(window, event_loop, platform_output);
|
||||||
|
|
||||||
let vsync_secs = if is_visible {
|
let vsync_secs = if is_visible {
|
||||||
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
|
let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point);
|
||||||
|
|||||||
@@ -368,7 +368,8 @@ impl AppRunner {
|
|||||||
let egui::PlatformOutput {
|
let egui::PlatformOutput {
|
||||||
commands,
|
commands,
|
||||||
cursor_icon,
|
cursor_icon,
|
||||||
events: _, // already handled
|
cursor_image: _, // TODO(alextournai): support custom bitmap cursors on the web (via CSS `url(...)`)
|
||||||
|
events: _, // already handled
|
||||||
mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569
|
mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569
|
||||||
ime,
|
ime,
|
||||||
accesskit_update: _, // not currently implemented
|
accesskit_update: _, // not currently implemented
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ use winit::{
|
|||||||
dpi::{PhysicalPosition, PhysicalSize},
|
dpi::{PhysicalPosition, PhysicalSize},
|
||||||
event::ElementState,
|
event::ElementState,
|
||||||
event_loop::ActiveEventLoop,
|
event_loop::ActiveEventLoop,
|
||||||
window::{CursorGrabMode, Window, WindowButtons, WindowLevel},
|
window::{CursorGrabMode, CustomCursor, Window, WindowButtons, WindowLevel},
|
||||||
};
|
};
|
||||||
|
|
||||||
pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 {
|
pub fn screen_size_in_pixels(window: &Window) -> egui::Vec2 {
|
||||||
@@ -88,6 +88,14 @@ pub struct State {
|
|||||||
any_pointer_button_down: bool,
|
any_pointer_button_down: bool,
|
||||||
current_cursor_icon: Option<egui::CursorIcon>,
|
current_cursor_icon: Option<egui::CursorIcon>,
|
||||||
|
|
||||||
|
/// Cached `CustomCursor` for the last RGBA bitmap pushed through
|
||||||
|
/// `PlatformOutput::cursor_image`. We dedupe by `Arc::as_ptr` so the
|
||||||
|
/// integration only re-uploads the bitmap to the OS when the app
|
||||||
|
/// switches sprite, not every frame the cursor moves. `usize` is the
|
||||||
|
/// raw pointer of the source `Arc<[u8]>` — opaque, only used as a
|
||||||
|
/// cache key.
|
||||||
|
current_custom_cursor: Option<(usize, CustomCursor)>,
|
||||||
|
|
||||||
clipboard: clipboard::Clipboard,
|
clipboard: clipboard::Clipboard,
|
||||||
|
|
||||||
/// If `true`, mouse inputs will be treated as touches.
|
/// If `true`, mouse inputs will be treated as touches.
|
||||||
@@ -141,6 +149,7 @@ impl State {
|
|||||||
pointer_pos_in_points: None,
|
pointer_pos_in_points: None,
|
||||||
any_pointer_button_down: false,
|
any_pointer_button_down: false,
|
||||||
current_cursor_icon: None,
|
current_cursor_icon: None,
|
||||||
|
current_custom_cursor: None,
|
||||||
|
|
||||||
clipboard: clipboard::Clipboard::new(
|
clipboard: clipboard::Clipboard::new(
|
||||||
display_target.display_handle().ok().map(|h| h.as_raw()),
|
display_target.display_handle().ok().map(|h| h.as_raw()),
|
||||||
@@ -1020,12 +1029,38 @@ impl State {
|
|||||||
&mut self,
|
&mut self,
|
||||||
window: &Window,
|
window: &Window,
|
||||||
platform_output: egui::PlatformOutput,
|
platform_output: egui::PlatformOutput,
|
||||||
|
) {
|
||||||
|
self.handle_platform_output_inner(window, None, platform_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Same as [`Self::handle_platform_output`] but threads the
|
||||||
|
/// `ActiveEventLoop` so we can register a `winit::CustomCursor` from
|
||||||
|
/// `PlatformOutput::cursor_image`. Integration paths that don't have
|
||||||
|
/// access to the event loop (e.g. immediate viewports) should call
|
||||||
|
/// [`Self::handle_platform_output`] instead — any custom cursor
|
||||||
|
/// request is silently dropped there and the standard `cursor_icon`
|
||||||
|
/// path still runs.
|
||||||
|
pub fn handle_platform_output_with_event_loop(
|
||||||
|
&mut self,
|
||||||
|
window: &Window,
|
||||||
|
event_loop: &ActiveEventLoop,
|
||||||
|
platform_output: egui::PlatformOutput,
|
||||||
|
) {
|
||||||
|
self.handle_platform_output_inner(window, Some(event_loop), platform_output);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn handle_platform_output_inner(
|
||||||
|
&mut self,
|
||||||
|
window: &Window,
|
||||||
|
event_loop: Option<&ActiveEventLoop>,
|
||||||
|
platform_output: egui::PlatformOutput,
|
||||||
) {
|
) {
|
||||||
profiling::function_scope!();
|
profiling::function_scope!();
|
||||||
|
|
||||||
let egui::PlatformOutput {
|
let egui::PlatformOutput {
|
||||||
commands,
|
commands,
|
||||||
cursor_icon,
|
cursor_icon,
|
||||||
|
cursor_image,
|
||||||
events: _, // handled elsewhere
|
events: _, // handled elsewhere
|
||||||
mutable_text_under_cursor: _, // only used in eframe web
|
mutable_text_under_cursor: _, // only used in eframe web
|
||||||
ime,
|
ime,
|
||||||
@@ -1048,7 +1083,7 @@ impl State {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_cursor_icon(window, cursor_icon);
|
self.apply_cursor(window, event_loop, cursor_icon, cursor_image.as_ref());
|
||||||
|
|
||||||
let allow_ime = ime.is_some();
|
let allow_ime = ime.is_some();
|
||||||
let is_toggling_ime = self.allow_ime != allow_ime;
|
let is_toggling_ime = self.allow_ime != allow_ime;
|
||||||
@@ -1111,26 +1146,92 @@ impl State {
|
|||||||
let _ = accesskit_update;
|
let _ = accesskit_update;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn set_cursor_icon(&mut self, window: &Window, cursor_icon: egui::CursorIcon) {
|
/// Apply either a bitmap cursor (preferred when both `cursor_image`
|
||||||
|
/// and `event_loop` are `Some`) or the standard `cursor_icon` to the
|
||||||
|
/// window. Mirrors the no-flicker dedupe the old `set_cursor_icon`
|
||||||
|
/// did, on the appropriate cache key for whichever path is active.
|
||||||
|
fn apply_cursor(
|
||||||
|
&mut self,
|
||||||
|
window: &Window,
|
||||||
|
event_loop: Option<&ActiveEventLoop>,
|
||||||
|
cursor_icon: egui::CursorIcon,
|
||||||
|
cursor_image: Option<&egui::CustomCursorImage>,
|
||||||
|
) {
|
||||||
|
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
|
||||||
|
if !is_pointer_in_window {
|
||||||
|
// Drop both caches so the cursor gets re-applied (and the
|
||||||
|
// bitmap re-checked for staleness) once the pointer comes
|
||||||
|
// back. Same contract the old `set_cursor_icon` followed.
|
||||||
|
self.current_cursor_icon = None;
|
||||||
|
self.current_custom_cursor = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bitmap cursor wins over CursorIcon when both are present and we
|
||||||
|
// have an event loop to register it with. Otherwise the bitmap is
|
||||||
|
// dropped and we fall through to the icon path — this is the
|
||||||
|
// documented fallback for integrations that didn't opt in.
|
||||||
|
if let (Some(image), Some(event_loop)) = (cursor_image, event_loop) {
|
||||||
|
let key = std::sync::Arc::as_ptr(&image.rgba).cast::<u8>() as usize;
|
||||||
|
let cached = self
|
||||||
|
.current_custom_cursor
|
||||||
|
.as_ref()
|
||||||
|
.filter(|(k, _)| *k == key)
|
||||||
|
.map(|(_, c)| c.clone());
|
||||||
|
|
||||||
|
let custom = match cached {
|
||||||
|
Some(c) => c,
|
||||||
|
None => match winit::window::CustomCursor::from_rgba(
|
||||||
|
image.rgba.to_vec(),
|
||||||
|
image.size[0],
|
||||||
|
image.size[1],
|
||||||
|
image.hotspot[0],
|
||||||
|
image.hotspot[1],
|
||||||
|
) {
|
||||||
|
Ok(source) => {
|
||||||
|
let c = event_loop.create_custom_cursor(source);
|
||||||
|
self.current_custom_cursor = Some((key, c.clone()));
|
||||||
|
c
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
log::warn!(
|
||||||
|
"egui-winit: invalid cursor bitmap, falling back to cursor_icon: {err:?}"
|
||||||
|
);
|
||||||
|
self.current_custom_cursor = None;
|
||||||
|
self.set_cursor_icon_inner(window, cursor_icon);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
window.set_cursor_visible(true);
|
||||||
|
window.set_cursor(custom);
|
||||||
|
// Resync `current_cursor_icon` so the next icon-only path
|
||||||
|
// notices a real change rather than dedupe-skipping it.
|
||||||
|
self.current_cursor_icon = None;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.current_custom_cursor = None;
|
||||||
|
self.set_cursor_icon_inner(window, cursor_icon);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Icon-only path, factored out so `apply_cursor` can fall back to it
|
||||||
|
/// when the bitmap path bails. Preserves the original dedupe.
|
||||||
|
fn set_cursor_icon_inner(&mut self, window: &Window, cursor_icon: egui::CursorIcon) {
|
||||||
if self.current_cursor_icon == Some(cursor_icon) {
|
if self.current_cursor_icon == Some(cursor_icon) {
|
||||||
// Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
|
// Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing.
|
||||||
// On other platforms: just early-out to save CPU.
|
// On other platforms: just early-out to save CPU.
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let is_pointer_in_window = self.pointer_pos_in_points.is_some();
|
self.current_cursor_icon = Some(cursor_icon);
|
||||||
if is_pointer_in_window {
|
|
||||||
self.current_cursor_icon = Some(cursor_icon);
|
|
||||||
|
|
||||||
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
|
if let Some(winit_cursor_icon) = translate_cursor(cursor_icon) {
|
||||||
window.set_cursor_visible(true);
|
window.set_cursor_visible(true);
|
||||||
window.set_cursor(winit_cursor_icon);
|
window.set_cursor(winit_cursor_icon);
|
||||||
} else {
|
|
||||||
window.set_cursor_visible(false);
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// Remember to set the cursor again once the cursor returns to the screen:
|
window.set_cursor_visible(false);
|
||||||
self.current_cursor_icon = None;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1533,6 +1533,19 @@ impl Context {
|
|||||||
self.output_mut(|o| o.cursor_icon = cursor_icon);
|
self.output_mut(|o| o.cursor_icon = cursor_icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Request that the integration display this RGBA bitmap as the OS
|
||||||
|
/// cursor for the next frame, instead of the standard `cursor_icon`.
|
||||||
|
/// Backends that don't support custom cursors (web, eframe with
|
||||||
|
/// non-winit integrations) silently fall back to the icon.
|
||||||
|
///
|
||||||
|
/// Pass `None` to clear and revert to `cursor_icon` selection.
|
||||||
|
///
|
||||||
|
/// The integration is expected to dedupe by `Arc` pointer identity,
|
||||||
|
/// so reusing the same `Arc<[u8]>` across frames is cheap.
|
||||||
|
pub fn set_cursor_image(&self, image: Option<crate::CustomCursorImage>) {
|
||||||
|
self.output_mut(|o| o.cursor_image = image);
|
||||||
|
}
|
||||||
|
|
||||||
/// Add a command to [`PlatformOutput::commands`],
|
/// Add a command to [`PlatformOutput::commands`],
|
||||||
/// for the integration to execute at the end of the frame.
|
/// for the integration to execute at the end of the frame.
|
||||||
pub fn send_cmd(&self, cmd: crate::OutputCommand) {
|
pub fn send_cmd(&self, cmd: crate::OutputCommand) {
|
||||||
|
|||||||
@@ -116,6 +116,16 @@ pub struct PlatformOutput {
|
|||||||
/// Set the cursor to this icon.
|
/// Set the cursor to this icon.
|
||||||
pub cursor_icon: CursorIcon,
|
pub cursor_icon: CursorIcon,
|
||||||
|
|
||||||
|
/// If set, the integration should display this RGBA image as the OS
|
||||||
|
/// cursor (via e.g. `winit::window::CustomCursor`) instead of the
|
||||||
|
/// standard `cursor_icon`. Set per frame; integrations that don't
|
||||||
|
/// support custom cursors fall back to `cursor_icon`.
|
||||||
|
///
|
||||||
|
/// Skipped from serde because the bitmap is ephemeral and shouldn't
|
||||||
|
/// roundtrip through persisted state.
|
||||||
|
#[cfg_attr(feature = "serde", serde(skip))]
|
||||||
|
pub cursor_image: Option<CustomCursorImage>,
|
||||||
|
|
||||||
/// Events that may be useful to e.g. a screen reader.
|
/// Events that may be useful to e.g. a screen reader.
|
||||||
pub events: Vec<OutputEvent>,
|
pub events: Vec<OutputEvent>,
|
||||||
|
|
||||||
@@ -177,6 +187,7 @@ impl PlatformOutput {
|
|||||||
let Self {
|
let Self {
|
||||||
mut commands,
|
mut commands,
|
||||||
cursor_icon,
|
cursor_icon,
|
||||||
|
cursor_image,
|
||||||
mut events,
|
mut events,
|
||||||
mutable_text_under_cursor,
|
mutable_text_under_cursor,
|
||||||
ime,
|
ime,
|
||||||
@@ -187,6 +198,7 @@ impl PlatformOutput {
|
|||||||
|
|
||||||
self.commands.append(&mut commands);
|
self.commands.append(&mut commands);
|
||||||
self.cursor_icon = cursor_icon;
|
self.cursor_icon = cursor_icon;
|
||||||
|
self.cursor_image = cursor_image;
|
||||||
self.events.append(&mut events);
|
self.events.append(&mut events);
|
||||||
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
self.mutable_text_under_cursor = mutable_text_under_cursor;
|
||||||
self.ime = ime.or(self.ime);
|
self.ime = ime.or(self.ime);
|
||||||
@@ -198,10 +210,12 @@ impl PlatformOutput {
|
|||||||
self.accesskit_update = accesskit_update;
|
self.accesskit_update = accesskit_update;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Take everything ephemeral (everything except `cursor_icon` currently)
|
/// Take everything ephemeral (everything except `cursor_icon` and
|
||||||
|
/// `cursor_image` currently)
|
||||||
pub fn take(&mut self) -> Self {
|
pub fn take(&mut self) -> Self {
|
||||||
let taken = std::mem::take(self);
|
let taken = std::mem::take(self);
|
||||||
self.cursor_icon = taken.cursor_icon; // everything else is ephemeral
|
self.cursor_icon = taken.cursor_icon; // sticky between frames
|
||||||
|
self.cursor_image = taken.cursor_image.clone(); // sticky between frames
|
||||||
taken
|
taken
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -261,6 +275,39 @@ pub enum UserAttentionType {
|
|||||||
Reset,
|
Reset,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A bitmap cursor pushed to the integration via [`PlatformOutput::cursor_image`].
|
||||||
|
///
|
||||||
|
/// The integration is expected to upload this to the OS as a real cursor
|
||||||
|
/// (so the image is not clipped by the egui window — what `egui::Painter`
|
||||||
|
/// drawn cursors suffer from). Backends that don't support it should fall
|
||||||
|
/// back to [`PlatformOutput::cursor_icon`].
|
||||||
|
///
|
||||||
|
/// `rgba` is straight (non-premultiplied) RGBA — same encoding as
|
||||||
|
/// `winit::window::CustomCursor::from_rgba`. The buffer length must be
|
||||||
|
/// exactly `size[0] * size[1] * 4` bytes. `size` and `hotspot` use
|
||||||
|
/// `u16` to match winit's native types and avoid a lossy cast in the
|
||||||
|
/// integration layer.
|
||||||
|
///
|
||||||
|
/// `Arc<[u8]>` is used so integrations can dedupe / cache by pointer
|
||||||
|
/// identity (`Arc::ptr_eq`) and avoid re-uploading the same bitmap to
|
||||||
|
/// the OS every frame.
|
||||||
|
#[derive(Clone, PartialEq, Eq)]
|
||||||
|
pub struct CustomCursorImage {
|
||||||
|
pub rgba: std::sync::Arc<[u8]>,
|
||||||
|
pub size: [u16; 2],
|
||||||
|
pub hotspot: [u16; 2],
|
||||||
|
}
|
||||||
|
|
||||||
|
impl std::fmt::Debug for CustomCursorImage {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
f.debug_struct("CustomCursorImage")
|
||||||
|
.field("size", &self.size)
|
||||||
|
.field("hotspot", &self.hotspot)
|
||||||
|
.field("rgba_len", &self.rgba.len())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A mouse cursor icon.
|
/// A mouse cursor icon.
|
||||||
///
|
///
|
||||||
/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.
|
/// egui emits a [`CursorIcon`] in [`PlatformOutput`] each frame as a request to the integration.
|
||||||
|
|||||||
@@ -467,8 +467,8 @@ pub use self::{
|
|||||||
Key, UserData,
|
Key, UserData,
|
||||||
input::*,
|
input::*,
|
||||||
output::{
|
output::{
|
||||||
self, CursorIcon, FullOutput, OpenUrl, OutputCommand, PlatformOutput,
|
self, CursorIcon, CustomCursorImage, FullOutput, OpenUrl, OutputCommand,
|
||||||
UserAttentionType, WidgetInfo,
|
PlatformOutput, UserAttentionType, WidgetInfo,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
drag_and_drop::DragAndDrop,
|
drag_and_drop::DragAndDrop,
|
||||||
|
|||||||
Reference in New Issue
Block a user