Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
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);