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