git.lucas.co / cce-compositor
Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git

commit3d1f2e33c2304941c09a3eea4c3e5f8962c563d5
parentf0ac1d2c86
authorLucas Galante <[email protected]>
date2026-07-28 09:59
feat: implicit pointer grab — held-button motion stays surface-relative

While any button the focused client saw pressed is still held, motion
keeps flowing to that surface relative to its origin at press time,
wherever the pointer goes — so client-side drags (sliders, ramp keys)
track outside the window. Seat focus changes no longer clear pointer
focus mid-grab (a press-then-leave drag lost its target). New FFI
getter river_wlr_seat_get_pointer_focused_surface.

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

 src/server/cursor.rs             | 37 +++++++++++++++++++++++++++++++++++++
 src/server/seat.rs               |  7 ++++++-
 src/server/wlroots_log_wrapper.c |  4 ++++
 wrapper.h                        |  1 +
 4 files changed, 48 insertions(+), 1 deletion(-)

diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 1a70a21..bb6f055 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -29,6 +29,10 @@ pub struct Cursor {
     /// by then (seat op, overview, …) — an orphaned press wedges client-side
     /// input state (widget routers keep a grab armed forever).
     pub notified_pressed: HashSet<u32>,
+    /// Layout origin of the surface the first notified press landed on: the
+    /// implicit grab's frame of reference, so held-button motion stays
+    /// surface-relative wherever the pointer goes (passthrough's grab branch).
+    pub grab_origin: (f64, f64),
 
     pub touch_down_listener: ffi::wl_listener,
     pub touch_motion_listener: ffi::wl_listener,
@@ -85,6 +89,7 @@ impl Default for Cursor {
             touch_points: HashMap::new(),
             pressed: HashMap::new(),
             notified_pressed: HashSet::new(),
+            grab_origin: (0.0, 0.0),
 
             touch_down_listener: unsafe { std::mem::zeroed() },
             touch_motion_listener: unsafe { std::mem::zeroed() },
@@ -498,6 +503,27 @@ impl Cursor {
         let ly = self.y();
         let server = (*self.seat).server;
 
+        // Implicit grab (standard Wayland drag semantics): while any button
+        // the focused client saw pressed is still held, motion keeps flowing
+        // to that surface — relative to its origin at press time — wherever
+        // the pointer goes, so a client-side drag (slider, ramp key) tracks
+        // outside the window. Focus is neither re-evaluated nor cleared until
+        // the last such button releases; wlroots nulls the focused surface if
+        // it is destroyed mid-grab, which falls through to normal dispatch.
+        if !self.notified_pressed.is_empty() {
+            let focused =
+                ffi::river_wlr_seat_get_pointer_focused_surface((*self.seat).wlr_seat);
+            if !focused.is_null() {
+                ffi::wlr_seat_pointer_notify_motion(
+                    (*self.seat).wlr_seat,
+                    time_msec,
+                    lx - self.grab_origin.0,
+                    ly - self.grab_origin.1,
+                );
+                return;
+            }
+        }
+
         if let Some(result) = (*server).scene.at(lx, ly) {
             let lock_state = (*server).lock_manager.state;
             if lock_state != crate::lock_manager::LockState::Unlocked {
@@ -1164,6 +1190,7 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
         cursor.pressed.insert((*event).button, None);
 
         if !should_block_button {
+            let first_grab_button = cursor.notified_pressed.is_empty();
             ffi::wlr_seat_pointer_notify_button(
                 seat.wlr_seat,
                 (*event).time_msec,
@@ -1171,6 +1198,16 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
                 (*event).state,
             );
             cursor.notified_pressed.insert((*event).button);
+            // First grab button: record the pressed surface's layout origin
+            // as the implicit grab's frame of reference (passthrough keeps
+            // motion surface-relative through it while the button is held).
+            if first_grab_button {
+                let glx = cursor.x();
+                let gly = cursor.y();
+                if let Some(result) = (*seat.server).scene.at(glx, gly) {
+                    cursor.grab_origin = (glx - result.sx, gly - result.sy);
+                }
+            }
         }
 
         // If pressed, update focus to window under cursor
diff --git a/src/server/seat.rs b/src/server/seat.rs
index 532f1a0..cc5ec30 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -362,7 +362,12 @@ impl Seat {
             Focus::LayerSurface(_) | Focus::Window(_) | Focus::LockSurface(_) | Focus::OverrideRedirect(_) | Focus::ShellSurface(_) => {
                 ffi::wlr_seat_keyboard_notify_clear_focus(self.wlr_seat);
                 let focused_client = ffi::river_wlr_seat_get_pointer_focused_client(self.wlr_seat);
-                if !focused_client.is_null() {
+                // Keep pointer focus through an active implicit grab (held
+                // client-notified button): clicking a window changes focus,
+                // and dropping pointer focus here orphans the grab until the
+                // next in-surface motion re-enters — a press-then-leave drag
+                // (cursor straight out of the window) lost its target.
+                if !focused_client.is_null() && self.cursor.notified_pressed.is_empty() {
                     ffi::wlr_seat_pointer_notify_clear_focus(self.wlr_seat);
                 }
             }
diff --git a/src/server/wlroots_log_wrapper.c b/src/server/wlroots_log_wrapper.c
index 27ae412..3f408e8 100644
--- a/src/server/wlroots_log_wrapper.c
+++ b/src/server/wlroots_log_wrapper.c
@@ -381,6 +381,10 @@ struct wlr_seat_client *river_wlr_seat_get_pointer_focused_client(struct wlr_sea
 	return seat->pointer_state.focused_client;
 }
 
+struct wlr_surface *river_wlr_seat_get_pointer_focused_surface(struct wlr_seat *seat) {
+	return seat->pointer_state.focused_surface;
+}
+
 struct wl_client *river_wlr_seat_client_get_client(struct wlr_seat_client *client) {
 	return client->client;
 }
diff --git a/wrapper.h b/wrapper.h
index f239a86..4c2d2db 100644
--- a/wrapper.h
+++ b/wrapper.h
@@ -186,6 +186,7 @@ struct wl_signal *river_wlr_seat_get_request_start_drag_signal(struct wlr_seat *
 struct wl_signal *river_wlr_seat_get_start_drag_signal(struct wlr_seat *seat);
 struct wl_signal *river_wlr_seat_get_request_set_primary_selection_signal(struct wlr_seat *seat);
 struct wlr_seat_client *river_wlr_seat_get_pointer_focused_client(struct wlr_seat *seat);
+struct wlr_surface *river_wlr_seat_get_pointer_focused_surface(struct wlr_seat *seat);
 struct wl_client *river_wlr_seat_client_get_client(struct wlr_seat_client *client);
 struct wlr_keyboard *river_wlr_seat_get_keyboard(struct wlr_seat *seat);
 struct wl_global *river_wlr_seat_get_global(struct wlr_seat *seat);