diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index c6c715c8..df4b065e 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -392,7 +392,7 @@ impl WinitApp for WgpuWinitApp<'_> { self.initialized_all_windows(event_loop); if let Some(running) = &mut self.running { - running.run_ui_and_paint(window_id) + running.run_ui_and_paint(event_loop, window_id) } else { Ok(EventResult::Wait) } @@ -540,7 +540,11 @@ impl WgpuWinitRunning<'_> { } /// This is called both for the root viewport, and all deferred viewports - fn run_ui_and_paint(&mut self, window_id: WindowId) -> Result { + fn run_ui_and_paint( + &mut self, + event_loop: &ActiveEventLoop, + window_id: WindowId, + ) -> Result { profiling::function_scope!(); let Some(viewport_id) = self @@ -675,6 +679,9 @@ impl WgpuWinitRunning<'_> { }; egui_winit.handle_platform_output(window, platform_output); + // Custom cursor images can only be created from an `ActiveEventLoop`, so they're applied + // here rather than inside `handle_platform_output`. + egui_winit.apply_pending_cursor_image(event_loop, window); let clipped_primitives = egui_ctx.tessellate(shapes, pixels_per_point); diff --git a/crates/eframe/src/web/app_runner.rs b/crates/eframe/src/web/app_runner.rs index 4a97235a..0534184e 100644 --- a/crates/eframe/src/web/app_runner.rs +++ b/crates/eframe/src/web/app_runner.rs @@ -321,6 +321,7 @@ impl AppRunner { let egui::PlatformOutput { commands, cursor_icon, + cursor_image: _, // TODO: web could express this as a CSS `cursor: url(...)` events: _, // already handled mutable_text_under_cursor: _, // TODO(#4569): https://github.com/emilk/egui/issues/4569 ime, diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index b180b244..f23b0364 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -85,6 +85,15 @@ pub struct State { any_pointer_button_down: bool, current_cursor_icon: Option, + /// Custom cursor image requested by the app this frame, not yet applied. + /// + /// Applying it needs an `ActiveEventLoop` (that's where winit creates custom cursors), which + /// `handle_platform_output` doesn't have — so we stash it and let the integration call + /// [`State::apply_pending_cursor_image`] from somewhere that does. + pending_cursor_image: Option>, + /// `id` of the custom cursor currently set on the window, so we only rebuild it on change. + current_cursor_image_id: Option, + clipboard: clipboard::Clipboard, /// If `true`, mouse inputs will be treated as touches. @@ -141,6 +150,8 @@ impl State { pointer_pos_in_points: None, any_pointer_button_down: false, current_cursor_icon: None, + pending_cursor_image: None, + current_cursor_image_id: None, clipboard: clipboard::Clipboard::new( display_target.display_handle().ok().map(|h| h.as_raw()), @@ -884,9 +895,11 @@ impl State { let contents = contents.replace("\r\n", "\n"); if !contents.is_empty() { self.egui_input.events.push(egui::Event::Paste(contents)); + return; } } - return; + // Clipboard is empty or unavailable — fall through to push a + // Key event so the application can handle paste from its own cache. } } @@ -941,6 +954,7 @@ impl State { let egui::PlatformOutput { commands, cursor_icon, + cursor_image, events: _, // handled elsewhere mutable_text_under_cursor: _, // only used in eframe web ime, @@ -964,7 +978,18 @@ impl State { } } - self.set_cursor_icon(window, cursor_icon); + // A custom cursor image, when set, *is* the cursor — don't also apply the icon, or we'd + // fight with it every frame. Stash it for `apply_pending_cursor_image`, which runs where + // an `ActiveEventLoop` is in scope. + if cursor_image.is_some() { + self.pending_cursor_image = Some(cursor_image); + } else { + if self.current_cursor_image_id.is_some() { + // Custom cursor was just cleared — go back to a normal icon. + self.pending_cursor_image = Some(None); + } + self.set_cursor_icon(window, cursor_icon); + } // When IME is disabled as a workaround for buggy systems, don't enable IME at all. // This ensures the system doesn't try to intercept keyboard input. @@ -1013,6 +1038,56 @@ impl State { } } + /// Apply the custom cursor image requested by the last [`Self::handle_platform_output`]. + /// + /// Must be called from somewhere with an `ActiveEventLoop` — winit only creates custom cursors + /// there. Cheap to call every frame: it's a no-op unless the image actually changed. + pub fn apply_pending_cursor_image( + &mut self, + event_loop: &winit::event_loop::ActiveEventLoop, + window: &Window, + ) { + let Some(pending) = self.pending_cursor_image.take() else { + return; + }; + + match pending { + Some(image) => { + if self.current_cursor_image_id == Some(image.id) { + return; // Already set — rebuilding it every frame would be wasteful. + } + + let source = winit::window::CustomCursor::from_rgba( + image.rgba.as_ref().clone(), + image.size.0, + image.size.1, + image.hotspot.0, + image.hotspot.1, + ); + + match source { + Ok(source) => { + let cursor = event_loop.create_custom_cursor(source); + window.set_cursor_visible(true); + window.set_cursor(winit::window::Cursor::Custom(cursor)); + self.current_cursor_image_id = Some(image.id); + // Force the next icon change to be re-applied, since the window is no + // longer showing whatever icon `current_cursor_icon` claims. + self.current_cursor_icon = None; + } + Err(err) => { + log::warn!("Failed to build custom cursor: {err}"); + self.current_cursor_image_id = None; + } + } + } + None => { + self.current_cursor_image_id = None; + self.current_cursor_icon = None; // Force the icon to be re-applied. + } + } + } + fn set_cursor_icon(&mut self, window: &Window, cursor_icon: egui::CursorIcon) { if self.current_cursor_icon == Some(cursor_icon) { // Prevent flickering near frame boundary when Windows OS tries to control cursor icon for window resizing. diff --git a/crates/egui/src/context.rs b/crates/egui/src/context.rs index 5a868dd7..ccd6617e 100644 --- a/crates/egui/src/context.rs +++ b/crates/egui/src/context.rs @@ -1514,6 +1514,15 @@ impl Context { self.output_mut(|o| o.cursor_icon = cursor_icon); } + /// Replace the mouse cursor with a custom image for this frame. + /// + /// This becomes the real OS cursor, so it is composited by the window system and does not lag + /// behind the pointer the way a cursor painted into the egui scene does. Pass `None` to go + /// back to the icon set by [`Self::set_cursor_icon`]. + pub fn set_cursor_image(&self, cursor_image: Option) { + self.output_mut(|o| o.cursor_image = cursor_image); + } + /// Add a command to [`PlatformOutput::commands`], /// for the integration to execute at the end of the frame. pub fn send_cmd(&self, cmd: crate::OutputCommand) { diff --git a/crates/egui/src/data/output.rs b/crates/egui/src/data/output.rs index deec5162..ec439c98 100644 --- a/crates/egui/src/data/output.rs +++ b/crates/egui/src/data/output.rs @@ -66,6 +66,34 @@ impl FullOutput { } } +/// A custom mouse cursor image, handed to the windowing backend to become the real OS cursor. +/// +/// Because the OS composites the cursor, this has none of the one-frame lag you get from painting +/// a cursor into the egui scene. +#[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct CursorImage { + /// Identifies this image. The backend recreates the OS cursor only when this changes, so the + /// pixels don't have to be compared (or even hashed) every frame. + pub id: u64, + + /// Premultiplied-alpha RGBA8, `size.0 * size.1 * 4` bytes, row-major. + pub rgba: std::sync::Arc>, + + /// Width and height in physical pixels. + pub size: (u16, u16), + + /// The click point, in physical pixels from the image's top-left. + pub hotspot: (u16, u16), +} + +impl PartialEq for CursorImage { + fn eq(&self, other: &Self) -> bool { + // `id` identifies the pixels; comparing the buffers themselves would be pointless work. + self.id == other.id && self.size == other.size && self.hotspot == other.hotspot + } +} + /// Information about text being edited. /// /// Useful for IME. @@ -113,6 +141,15 @@ pub struct PlatformOutput { /// Set the cursor to this icon. pub cursor_icon: CursorIcon, + /// Replace the mouse cursor with a custom image. + /// + /// Unlike drawing a cursor into the egui scene, this becomes the real OS cursor, so it is + /// composited by the window system and does not lag a frame behind the pointer. + /// + /// Takes precedence over [`Self::cursor_icon`] when set. Backends that do not support custom + /// cursors ignore this and fall back to `cursor_icon`. + pub cursor_image: Option, + /// Events that may be useful to e.g. a screen reader. pub events: Vec, @@ -172,6 +209,7 @@ impl PlatformOutput { let Self { mut commands, cursor_icon, + cursor_image, mut events, mutable_text_under_cursor, ime, @@ -183,6 +221,7 @@ impl PlatformOutput { self.commands.append(&mut commands); self.cursor_icon = cursor_icon; + self.cursor_image = cursor_image; self.events.append(&mut events); self.mutable_text_under_cursor = mutable_text_under_cursor; self.ime = ime.or(self.ime); diff --git a/crates/egui/src/lib.rs b/crates/egui/src/lib.rs index 960480b2..a83858a4 100644 --- a/crates/egui/src/lib.rs +++ b/crates/egui/src/lib.rs @@ -488,7 +488,7 @@ pub use self::{ Key, UserData, input::*, output::{ - self, CursorIcon, FullOutput, OpenUrl, OutputCommand, PlatformOutput, + self, CursorIcon, CursorImage, FullOutput, OpenUrl, OutputCommand, PlatformOutput, UserAttentionType, WidgetInfo, }, },