From 9276778181619be4065715ab03d36ca0b318667d Mon Sep 17 00:00:00 2001 From: Matthew Runo <74583+inktomi@users.noreply.github.com> Date: Mon, 2 Mar 2026 04:00:18 -0800 Subject: [PATCH] Avoid repaints on device mouse motion outside window (#7866) ## Summary - Ignore raw device mouse motion unless the window is focused and the pointer is inside it - Also handles pointers starting down and then moving into or out of the window (drag & drop) - Prevents global mouse motion from triggering continuous repaint loops - Applies to both glow and wgpu backends ## Testing - I ran the check script, nothing seemed to fail --------- Co-authored-by: Emil Ernerfeldt --- crates/eframe/src/native/glow_integration.rs | 15 ++++++++++++--- crates/eframe/src/native/wgpu_integration.rs | 15 ++++++++++++--- crates/egui-winit/src/lib.rs | 18 +++++++++++++++++- crates/egui_kittest/README.md | 4 ++-- 4 files changed, 43 insertions(+), 9 deletions(-) diff --git a/crates/eframe/src/native/glow_integration.rs b/crates/eframe/src/native/glow_integration.rs index 233ce5a49..48557675c 100644 --- a/crates/eframe/src/native/glow_integration.rs +++ b/crates/eframe/src/native/glow_integration.rs @@ -448,12 +448,21 @@ impl WinitApp for GlowWinitApp<'_> { if let Some(viewport) = glutin .focused_viewport .and_then(|viewport| glutin.viewports.get_mut(&viewport)) + && let Some(window) = viewport.window.as_ref() { - if let Some(egui_winit) = viewport.egui_winit.as_mut() { - egui_winit.on_mouse_motion(delta); + if !window.has_focus() + && !viewport + .egui_winit + .as_ref() + .map(|state| state.is_any_pointer_button_down()) + .unwrap_or(false) + { + return Ok(EventResult::Wait); } - if let Some(window) = viewport.window.as_ref() { + if let Some(egui_winit) = viewport.egui_winit.as_mut() + && egui_winit.on_mouse_motion(delta) + { return Ok(EventResult::RepaintNext(window.id())); } } diff --git a/crates/eframe/src/native/wgpu_integration.rs b/crates/eframe/src/native/wgpu_integration.rs index cb634200a..7cfdab148 100644 --- a/crates/eframe/src/native/wgpu_integration.rs +++ b/crates/eframe/src/native/wgpu_integration.rs @@ -454,12 +454,21 @@ impl WinitApp for WgpuWinitApp<'_> { if let Some(viewport) = shared .focused_viewport .and_then(|viewport| shared.viewports.get_mut(&viewport)) + && let Some(window) = viewport.window.as_ref() { - if let Some(egui_winit) = viewport.egui_winit.as_mut() { - egui_winit.on_mouse_motion(delta); + if !window.has_focus() + && !viewport + .egui_winit + .as_ref() + .map(|state| state.is_any_pointer_button_down()) + .unwrap_or(false) + { + return Ok(EventResult::Wait); } - if let Some(window) = viewport.window.as_ref() { + if let Some(egui_winit) = viewport.egui_winit.as_mut() + && egui_winit.on_mouse_motion(delta) + { return Ok(EventResult::RepaintNext(window.id())); } } diff --git a/crates/egui-winit/src/lib.rs b/crates/egui-winit/src/lib.rs index 54059cbd6..c010febd5 100644 --- a/crates/egui-winit/src/lib.rs +++ b/crates/egui-winit/src/lib.rs @@ -640,11 +640,27 @@ impl State { self.has_sent_ime_enabled = false; } - pub fn on_mouse_motion(&mut self, delta: (f64, f64)) { + /// Returns `true` if the event was sent to egui. + pub fn on_mouse_motion(&mut self, delta: (f64, f64)) -> bool { + if !self.is_pointer_in_window() && !self.any_pointer_button_down { + return false; + } + self.egui_input.events.push(egui::Event::MouseMoved(Vec2 { x: delta.0 as f32, y: delta.1 as f32, })); + true + } + + /// Returns `true` when the pointer is currently inside the window. + pub fn is_pointer_in_window(&self) -> bool { + self.pointer_pos_in_points.is_some() + } + + /// Returns `true` if any pointer button is currently held down. + pub fn is_any_pointer_button_down(&self) -> bool { + self.any_pointer_button_down } /// Call this when there is a new [`accesskit::ActionRequest`]. diff --git a/crates/egui_kittest/README.md b/crates/egui_kittest/README.md index 638c61522..7b97f4e1d 100644 --- a/crates/egui_kittest/README.md +++ b/crates/egui_kittest/README.md @@ -97,12 +97,12 @@ You should add the following to your `.gitignore`: * …have a low resolution to avoid growth in repo size * …have a low comparison threshold to avoid the test passing despite unwanted differences (the default threshold should be fine for most usecases!) -### What do do when CI / another computer produces a different image? +### What to do when CI / another computer produces a different image? The default tolerance settings should be fine for almost all gui comparison tests. However, especially when you're using custom rendering, you may observe images difference with different setups leading to unexpected test failures. -First check whether the difference is due to a change in enabled rendering features, potentially due to difference in hardware (/software renderer) capabilitites. +First check whether the difference is due to a change in enabled rendering features, potentially due to difference in hardware (/software renderer) capabilities. Generally you should carefully enforcing the same set of features for all test runs, but this may happen nonetheless. Once you validated that the differences are miniscule and hard to avoid, you can try to _carefully_ adjust the comparison tolerance setting (`SnapshotOptions::threshold`, TODO([#5683](https://github.com/emilk/egui/issues/5683)): as well as number of pixels allowed to differ) for the specific test.