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

commit5d83c37d295151d4c08877a49e5a7ffe6355bd4a
parente131a0c40a
authorLucas Galante <[email protected]>
date2026-09-04 12:49
fix(overview): key and gesture toggles exit onto the focused window, not the pointer

`Action::Overview` is the cursor-driven toggle: its exit lands on the
hovered window, else on the virtual point under the pointer. Only some of
its sources are actually the pointer. For the rest the pointer is wherever
it was last left, and the exit landed on empty desktop.

- A key binding carries no pointer position, so a key-bound toggle now
  makes the same substitution the control socket made in e131a0c: the
  keyed exit (focused window) when in overview, the enter otherwise.
  `overview_action_for_socket` is renamed `overview_action_pointerless`
  to say so.
- A swipe or pinch IS pointer-located, so exiting onto the hovered window
  stays; but with nothing under the pointer the toggle used to zoom onto
  the bare desktop there. `overview_action_for_gesture` keeps the
  cursor-driven toggle when a window is hovered and falls back to the
  keyed exit when none is.

The background click and the click-release exits are untouched: a click
on the desktop in overview is a deliberate "go here", and a click on a
window already lands on it.

Verified the socket path in a headless shadow after the refactor (an
800x600 window at (100,100) comes back centred at (240,60)); key and
gesture sources cannot be injected headless, so those are by inspection —
they run the same two helpers.

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

 src/server/cursor.rs         | 14 ++++++++++++++
 src/server/keyboard_group.rs | 14 ++++++++++++--
 src/server/window_manager.rs | 33 ++++++++++++++++++++++++++++-----
 3 files changed, 54 insertions(+), 7 deletions(-)

diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index 36a9958..85d7a97 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -2594,6 +2594,13 @@ unsafe extern "C" fn handle_swipe_update(listener: *mut ffi::wl_listener, data:
             }
         }
 
+        // The overview toggle lands on the hovered window, else on the
+        // FOCUSED one — never on the empty desktop under the pointer.
+        let matched_action = if matched_action == crate::config::Action::Overview {
+            (*seat.server).wm.overview_action_for_gesture()
+        } else {
+            matched_action
+        };
         (*seat.server).wm.execute_action(&matched_action, matched_command.as_deref());
 
         let pointer_gestures = (*seat.server).input_manager.pointer_gestures;
@@ -2786,6 +2793,13 @@ unsafe extern "C" fn handle_pinch_update(listener: *mut ffi::wl_listener, data:
 
     if matched_action != crate::config::Action::None {
         cursor.gesture_triggered = true;
+        // The overview toggle lands on the hovered window, else on the
+        // FOCUSED one — never on the empty desktop under the pointer.
+        let matched_action = if matched_action == crate::config::Action::Overview {
+            (*seat.server).wm.overview_action_for_gesture()
+        } else {
+            matched_action
+        };
         (*seat.server).wm.execute_action(&matched_action, matched_command.as_deref());
 
         let pointer_gestures = (*seat.server).input_manager.pointer_gestures;
diff --git a/src/server/keyboard_group.rs b/src/server/keyboard_group.rs
index 9b36f04..830874e 100644
--- a/src/server/keyboard_group.rs
+++ b/src/server/keyboard_group.rs
@@ -432,8 +432,18 @@ unsafe extern "C" fn handle_group_key(listener: *mut ffi::wl_listener, data: *mu
         KeyConsumer::Builtin => {}
         KeyConsumer::CceBinding(kb) => {
             if (*event).state == ffi::wl_keyboard_key_state_WL_KEYBOARD_KEY_STATE_PRESSED {
-                log::info!("executing CCE monolithic action: {:?}", kb.action);
-                (*(*group.seat).server).wm.execute_action(&kb.action, kb.command.as_deref());
+                let wm = &mut (*(*group.seat).server).wm;
+                // A key carries no pointer position: the overview toggle's
+                // exit must land on the focused window, not on whatever the
+                // pointer was left hovering (or the empty desktop under it).
+                // Same substitution the control socket makes.
+                let action = if kb.action == crate::config::Action::Overview {
+                    wm.overview_action_pointerless()
+                } else {
+                    kb.action
+                };
+                log::info!("executing CCE monolithic action: {:?}", action);
+                wm.execute_action(&action, kb.command.as_deref());
             }
         }
         KeyConsumer::Binding(binding) => {
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 7c535f1..ffd1758 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -3627,10 +3627,16 @@ impl WindowManager {
         self.keep_status_bar_on_top();
     }
 
-    /// The overview action a socket command stands for: the keyed
-    /// (focused-window) exit when in overview, else the enter. See the
-    /// "overview" arm of the control handler for why not the toggle.
-    fn overview_action_for_socket(&self) -> crate::config::Action {
+    /// The overview action a POINTER-LESS toggle stands for — a key press
+    /// or a socket command: the keyed (focused-window) exit when in
+    /// overview, else the enter. `Action::Overview` itself is the
+    /// cursor-driven toggle, whose exit lands on the hovered window or
+    /// else on the virtual point under the pointer; that is right for the
+    /// sources that ARE the pointer (the background click, the click
+    /// release, the gesture binding, a pointer-button binding) and wrong
+    /// for the ones that are not, where the pointer is wherever it was
+    /// last left. See the "overview" arm of the control handler.
+    pub fn overview_action_pointerless(&self) -> crate::config::Action {
         if self.mode == WindowManagerMode::Overview {
             crate::config::Action::OverviewExit
         } else {
@@ -3638,6 +3644,23 @@ impl WindowManager {
         }
     }
 
+    /// The overview action a GESTURE toggle stands for. A swipe or pinch
+    /// is pointer-located, so exiting onto the hovered window is the
+    /// point — but with nothing under the pointer the cursor-driven exit
+    /// lands on the empty desktop there, and the pointer was not aimed at
+    /// anything: the focused window is what the user was working in, so
+    /// the keyed exit takes over. Enter is the toggle's own.
+    pub unsafe fn overview_action_for_gesture(&mut self) -> crate::config::Action {
+        if self.mode != WindowManagerMode::Overview {
+            return crate::config::Action::OverviewEnter;
+        }
+        if self.build_action_ctx().hovered.is_some() {
+            crate::config::Action::Overview
+        } else {
+            crate::config::Action::OverviewExit
+        }
+    }
+
     pub unsafe fn execute_action(&mut self, action: &crate::config::Action, command: Option<&str>) {
         use crate::config::Action;
         self.stop_panning_animation();
@@ -4120,7 +4143,7 @@ impl WindowManager {
             // nothing is focused; the enter half is identical to the
             // toggle's.
             "overview" | "expose" => {
-                self.execute_action(&self.overview_action_for_socket(), None);
+                self.execute_action(&self.overview_action_pointerless(), None);
                 "ok\n".to_string()
             }
             "wm-mode" => {