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

commit0ff132bb6fca9a5ec65de1de1f9031c2e9d3e7f1
parenta5b8a03517
authorLucas Galante <[email protected]>
date2026-09-16 11:55
fix(status): a still click in adjust mode is a click, not a snap

Two holes in the adjust-position grab. Every left press on a segment
became a segment drag and every release snapped to the edge nearest the
release point, so a click that never travelled re-homed a top-edge
segment to top-center. And an expanded segment (menu open) was grabbed
like any other, which swallowed the press its own "Done" row needed —
the one control that ends the mode from the bar was unreachable while
the mode was on.

The grab still starts on press, for immediate drag feedback; on release
a travel under 6px ends it without snapping and replays press+release
to the segment as the click it was. An expanded segment is not grabbed
at all (is_expanded_status_segment, factored out of
any_expanded_status_segment).

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

 src/server/cursor.rs         | 36 ++++++++++++++++++++++++++++++++++--
 src/server/window_manager.rs | 36 ++++++++++++++++++++----------------
 2 files changed, 54 insertions(+), 18 deletions(-)

diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index b26be6c..3cf5b6e 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -1396,7 +1396,11 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
                     }
                 }
             }
-            if !clicked_status.is_null() {
+            // An EXPANDED segment (menu open) is never grabbed: its rows are
+            // clicked, and a grab here would swallow the press the "Done"
+            // row needs to leave adjust mode — the one control that ends the
+            // mode from the bar would be unreachable while it is on.
+            if !clicked_status.is_null() && !(*server).wm.is_expanded_status_segment(clicked_status) {
                 (*server).wm.stop_panning_animation();
                 let cursor_x = (*cursor.wlr_cursor).x;
                 let cursor_y = (*cursor.wlr_cursor).y;
@@ -1960,9 +1964,37 @@ unsafe extern "C" fn handle_button(listener: *mut ffi::wl_listener, data: *mut s
                 && (*event).button == 0x110
             {
                 let win = op.window_ptr;
+                let app_id = (*win).get_app_id_string().unwrap_or_default();
+
+                // A press that never travelled is a CLICK, not a drag: end
+                // the grab without snapping — snapping classifies the
+                // release point alone, so a still click on a top-edge
+                // segment away from the corners re-homed it to top-center
+                // — and replay press+release to the segment, which never
+                // saw the press. The grab is taken on press so drag
+                // feedback is immediate; this is where the two are told
+                // apart.
+                const STATUS_CLICK_TRAVEL: f64 = 6.0;
+                let travel = (lx - op.start_x as f64).hypot(ly - op.start_y as f64);
+                if travel < STATUS_CLICK_TRAVEL {
+                    log::info!("[StatusRelease] click (travel {:.1}px) on app_id={} — replayed, not snapped", travel, app_id);
+                    seat.op_end();
+                    cursor.pressed.remove(&(*event).button);
+                    let time = (*event).time_msec;
+                    // Re-evaluate pointer focus onto the surface under the
+                    // pointer (nothing was notified during the grab), then
+                    // deliver the click.
+                    cursor.passthrough(time);
+                    ffi::wlr_seat_pointer_notify_button(seat.wlr_seat, time, (*event).button, ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_PRESSED);
+                    ffi::wlr_seat_pointer_notify_frame(seat.wlr_seat);
+                    ffi::wlr_seat_pointer_notify_button(seat.wlr_seat, time, (*event).button, ffi::wl_pointer_button_state_WL_POINTER_BUTTON_STATE_RELEASED);
+                    ffi::wlr_seat_pointer_notify_frame(seat.wlr_seat);
+                    (*server).wm.dirty_windowing();
+                    return;
+                }
+
                 let mut closest_edge = crate::window::StatusEdge::TopLeft;
                 let mut min_dist = f64::MAX;
-                let app_id = (*win).get_app_id_string().unwrap_or_default();
                 log::info!("[StatusRelease] Released status window: app_id={}, lx={}, ly={}", app_id, lx, ly);
                 
                 let outputs_list = &mut (*server).om.outputs as *mut ffi::wl_list as *mut WlList;
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index d019ba4..37e6da0 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -3794,23 +3794,27 @@ impl WindowManager {
     /// dismiss (cursor.rs) and the Escape dismiss (keyboard_group.rs) so the
     /// two triggers can never disagree about what counts as open.
     pub unsafe fn any_expanded_status_segment(&self, except: *mut crate::window::Window) -> bool {
+        self.windows.iter().any(|&w| w != except && self.is_expanded_status_segment(w))
+    }
+
+    /// A mapped status segment thicker than the bar — one whose in-surface
+    /// menu is open. The thickness IS the signal: the bar grows its own
+    /// surface into the menu and shrinks it back on close.
+    pub unsafe fn is_expanded_status_segment(&self, w: *mut crate::window::Window) -> bool {
         let bar_h = self.layout.bar_height;
-        self.windows.iter().any(|&w| {
-            !w.is_null()
-                && !(*w).closed
-                && w != except
-                && (*w).is_status_bar()
-                && matches!((*w).state, crate::window::WindowState::Mapped)
-                && {
-                    let bg = (*w).box_geom;
-                    let thickness = match (*w).status_edge {
-                        crate::policy::arrange::StatusEdge::Left
-                        | crate::policy::arrange::StatusEdge::Right => bg.width,
-                        _ => bg.height,
-                    };
-                    thickness > bar_h
-                }
-        })
+        !w.is_null()
+            && !(*w).closed
+            && (*w).is_status_bar()
+            && matches!((*w).state, crate::window::WindowState::Mapped)
+            && {
+                let bg = (*w).box_geom;
+                let thickness = match (*w).status_edge {
+                    crate::policy::arrange::StatusEdge::Left
+                    | crate::policy::arrange::StatusEdge::Right => bg.width,
+                    _ => bg.height,
+                };
+                thickness > bar_h
+            }
     }
 
     pub unsafe fn update_status(&self) {