Compare commits
2 Commits
eb1756df3f
...
4bb7970234
| Author | SHA1 | Date |
|---|---|---|
|
|
4bb7970234 | |
|
|
f5477e30e5 |
|
|
@ -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<EventResult> {
|
||||
fn run_ui_and_paint(
|
||||
&mut self,
|
||||
event_loop: &ActiveEventLoop,
|
||||
window_id: WindowId,
|
||||
) -> Result<EventResult> {
|
||||
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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -85,6 +85,15 @@ pub struct State {
|
|||
any_pointer_button_down: bool,
|
||||
current_cursor_icon: Option<egui::CursorIcon>,
|
||||
|
||||
/// 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<Option<egui::CursorImage>>,
|
||||
/// `id` of the custom cursor currently set on the window, so we only rebuild it on change.
|
||||
current_cursor_image_id: Option<u64>,
|
||||
|
||||
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,11 +895,13 @@ impl State {
|
|||
let contents = contents.replace("\r\n", "\n");
|
||||
if !contents.is_empty() {
|
||||
self.egui_input.events.push(egui::Event::Paste(contents));
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Clipboard is empty or unavailable — fall through to push a
|
||||
// Key event so the application can handle paste from its own cache.
|
||||
}
|
||||
}
|
||||
|
||||
self.egui_input.events.push(egui::Event::Key {
|
||||
key: active_key,
|
||||
|
|
@ -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 {
|
|||
}
|
||||
}
|
||||
|
||||
// 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.
|
||||
|
|
|
|||
|
|
@ -515,6 +515,13 @@ impl SubMenu {
|
|||
content(ui)
|
||||
});
|
||||
|
||||
if let Some(_) = &popup_response {
|
||||
// Keep the submenu alive so `from_id`'s staleness check doesn't close it.
|
||||
// Without this, menus whose content has no further SubMenuButtons never update
|
||||
// `last_visible_pass`, causing the parent to reset `open_item` after ~2 frames.
|
||||
MenuState::mark_shown(ui.ctx(), id);
|
||||
}
|
||||
|
||||
if let Some(popup_response) = &popup_response {
|
||||
// If no child sub menu is open means we must be the deepest child sub menu.
|
||||
let is_deepest_submenu = MenuState::is_deepest_open_sub_menu(ui.ctx(), id);
|
||||
|
|
|
|||
|
|
@ -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<crate::CursorImage>) {
|
||||
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) {
|
||||
|
|
|
|||
|
|
@ -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<Vec<u8>>,
|
||||
|
||||
/// 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<CursorImage>,
|
||||
|
||||
/// Events that may be useful to e.g. a screen reader.
|
||||
pub events: Vec<OutputEvent>,
|
||||
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Reference in New Issue