git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit595d61ad02adb648b58e4341985382d4982ff904
parent972f35e44b
authorLucas Galante <[email protected]>
date2026-08-01 23:41
fix: don't clear hover on pointer Leave while a button is held

Mid-drag the compositor may unfocus the surface (pointer past the input
region), and the synthetic off-screen move corrupted the drag — a ramp
key snapped to the graph corner. Track held buttons and gate the Leave
hover-clear on none held.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/backend/window_runner.rs | 21 +++++++++++++++++----
 1 file changed, 17 insertions(+), 4 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index 8e8198e..b3cd5bc 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2690,6 +2690,11 @@ pub struct EngineState<A: Application> {
     /// Serial of the most recent pointer press, kept for
     /// [`Application::take_window_action`] move/resize grabs.
     pub last_press_serial: Option<u32>,
+    /// Mouse buttons currently held (tracked Press/Release pairs). Gates the
+    /// Leave hover-clear: mid-drag the compositor may unfocus the surface
+    /// (pointer past the input region), and the synthetic off-screen move
+    /// would corrupt the drag (a ramp key snapped to the graph corner).
+    pub buttons_down: u32,
     /// This frame's display-list text, shaped and held here so the glyphon `TextArea`s built
     /// in the render pass can borrow the buffers (Phase 6 —
     /// [`Application::display_list_text`]).
@@ -3375,10 +3380,15 @@ impl<A: Application> PointerHandler for EngineState<A> {
                 }
                 PointerEventKind::Leave { .. } => {
                     self.current_cursor_icon = None;
-                    let mut rebuild = false;
-                    self.inner.as_mut().unwrap().handle_pointer_move(LogicalPosition::new(-10000.0, -10000.0), &mut rebuild);
-                    if rebuild {
-                        self.redraw = true;
+                    // Clear hover with an off-screen move — but not while a
+                    // button is held: a drag in progress must not see the
+                    // sentinel position as cursor motion.
+                    if self.buttons_down == 0 {
+                        let mut rebuild = false;
+                        self.inner.as_mut().unwrap().handle_pointer_move(LogicalPosition::new(-10000.0, -10000.0), &mut rebuild);
+                        if rebuild {
+                            self.redraw = true;
+                        }
                     }
                 }
                 PointerEventKind::Motion { .. } => {
@@ -3405,6 +3415,7 @@ impl<A: Application> PointerHandler for EngineState<A> {
                         _ => continue,
                     };
                     self.last_press_serial = Some(*serial);
+                    self.buttons_down = self.buttons_down.saturating_add(1);
 
                     // Client-Side Decorations (CSD) Drag & Resize Handling
                     let is_status_bar = self.inner.as_ref().unwrap().settings().app_id.starts_with("cce-status");
@@ -3494,6 +3505,7 @@ impl<A: Application> PointerHandler for EngineState<A> {
                         274 => MouseButton::Middle,
                         _ => continue,
                     };
+                    self.buttons_down = self.buttons_down.saturating_sub(1);
                     let mut rebuild = false;
                     if let Some(msg) = self.inner.as_mut().unwrap().handle_mouse_input(btn, ElementState::Released, LogicalPosition::new(lx, ly), &mut rebuild) {
                         let mut update_rebuild = false;
@@ -3964,6 +3976,7 @@ pub fn run<A: Application>() {
         last_pinch_scale: 1.0,
         cursor_pos: (0.0, 0.0),
         last_press_serial: None,
+        buttons_down: 0,
         dl_text_items: Vec::new(),
     };