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

commit22ccfd37e33f8e57c32d58621144d9506fbb3a58
parentfa1146b1a5
authorLucas Galante <[email protected]>
date2026-09-04 12:16
fix(overview): keep the launcher focused when it opens in overview

Super+d in overview mapped cce-cloud, focused it, and lost it again in the
same second: the launcher's first configure schedules the idle pointer
refresh, which runs passthrough, and in overview passthrough hover-focuses
whatever world window the stationary pointer rests on. cce-cloud closes on
keyboard leave, so the launcher vanished the instant it appeared. The next
super+d inside the 5s reconnect grace then hit take_recent_vanish — the
self-dismissal had been recorded as an unbidden vanish — and mapped with
focus blocked, so typing went nowhere until a click.

- cursor: overview hover-to-focus skips while chrome holds the keyboard.
- window: a Popup/Overlay unmapping without a requested close is not a
  vanish — chrome closes itself as part of being used.
- seat: focus_is_chrome() shared by the key path (was inline) and hover.

Verified headless (cce-shadow, --bin target/release/cce-fx): launcher maps
focused under a stationary pointer, survives hover across world windows,
and a relaunch 2s after a click-away focuses.

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

 src/server/cursor.rs         |  9 ++++++++-
 src/server/keyboard_group.rs | 30 ++++--------------------------
 src/server/seat.rs           | 26 ++++++++++++++++++++++++++
 src/server/window.rs         | 14 ++++++++++++--
 4 files changed, 50 insertions(+), 29 deletions(-)

diff --git a/src/server/cursor.rs b/src/server/cursor.rs
index d1d008c..c57ee0a 100644
--- a/src/server/cursor.rs
+++ b/src/server/cursor.rs
@@ -831,8 +831,15 @@ impl Cursor {
                 // must not move the camera, only the click and keyboard paths
                 // may. (Zoom was never at stake — focus_follow_pan pans at the
                 // current zoom — but a partially visible window would still
-                // get dragged on-screen mid-hover.)
+                // get dragged on-screen mid-hover.) And never while chrome
+                // holds the keyboard: the cce-cloud launcher closes itself on
+                // keyboard leave, and this path runs not only on real motion
+                // but on the idle pointer refresh a commit schedules — the
+                // launcher's own first configure — so a stationary pointer
+                // resting on a world window was refocusing that window and
+                // dismissing the launcher the instant it mapped.
                 if !hovered_toplevel.is_null()
+                    && !(*self.seat).focus_is_chrome()
                     && (*self.seat).focused
                         != crate::seat::Focus::Window(hovered_toplevel)
                 {
diff --git a/src/server/keyboard_group.rs b/src/server/keyboard_group.rs
index 0accf92..9b36f04 100644
--- a/src/server/keyboard_group.rs
+++ b/src/server/keyboard_group.rs
@@ -459,32 +459,10 @@ unsafe extern "C" fn handle_group_key(listener: *mut ffi::wl_listener, data: *mu
             }
         }
         KeyConsumer::Focus => {
-            let is_overlay_mode = if let crate::seat::Focus::Window(fw) = (*group.seat).focused {
-                if !fw.is_null() {
-                    // Overlay AND Popup: desktop chrome (docks, the
-                    // cce-cloud launcher) stays keyboard-interactive in
-                    // overview — only world windows (spatial thumbnails)
-                    // have their presses eaten.
-                    (*fw).tiling_mode == crate::tiling::TilingMode::Overlay
-                        || (*fw).tiling_mode == crate::tiling::TilingMode::Popup
-                } else {
-                    false
-                }
-            } else if let crate::seat::Focus::LayerSurface(focused_layer) = (*group.seat).focused {
-                if !focused_layer.is_null() {
-                    let wlr_layer_surface = crate::ffi::wlr_layer_surface_v1_try_from_wlr_surface(focused_layer);
-                    if !wlr_layer_surface.is_null() && !(*wlr_layer_surface).namespace.is_null() {
-                        let ns = std::ffi::CStr::from_ptr((*wlr_layer_surface).namespace).to_string_lossy();
-                        ns.starts_with("cce-cloud")
-                    } else {
-                        false
-                    }
-                } else {
-                    false
-                }
-            } else {
-                false
-            };
+            // Overlay AND Popup: desktop chrome (docks, the cce-cloud
+            // launcher) stays keyboard-interactive in overview — only world
+            // windows (spatial thumbnails) have their presses eaten.
+            let is_overlay_mode = (*group.seat).focus_is_chrome();
 
             if (*(*group.seat).server).wm.mode != crate::window_manager::WindowManagerMode::Overview
                 || is_overlay_mode
diff --git a/src/server/seat.rs b/src/server/seat.rs
index 8bde122..c45b004 100644
--- a/src/server/seat.rs
+++ b/src/server/seat.rs
@@ -902,6 +902,32 @@ impl Seat {
     }
 
 
+    /// Is this seat's keyboard focus desktop chrome — a Popup/Overlay window
+    /// (the cce-cloud launcher, a dock) or a cce-cloud layer surface (a
+    /// context menu)? Chrome stays keyboard-interactive in overview and is
+    /// dismissed by using it (Escape, a pick, a click-away), so the
+    /// overview-mode key and hover paths consult this before treating the
+    /// focus as a world window's: keys are delivered rather than eaten, and
+    /// hover-to-focus leaves the ring where it is instead of pulling the
+    /// keyboard out from under the launcher.
+    pub unsafe fn focus_is_chrome(&self) -> bool {
+        match self.focused {
+            Focus::Window(w) if !w.is_null() => matches!(
+                (*w).tiling_mode,
+                crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Overlay
+            ),
+            Focus::LayerSurface(s) if !s.is_null() => {
+                let wlr_layer_surface = ffi::wlr_layer_surface_v1_try_from_wlr_surface(s);
+                !wlr_layer_surface.is_null()
+                    && !(*wlr_layer_surface).namespace.is_null()
+                    && std::ffi::CStr::from_ptr((*wlr_layer_surface).namespace)
+                        .to_string_lossy()
+                        .starts_with("cce-cloud")
+            }
+            _ => false,
+        }
+    }
+
     /// Focus-follow: pan the camera to a focused Floating/Maximized window —
     /// centering when it is mostly hidden, nudging a clipped edge into view
     /// otherwise. Fullscreen is pinned to an output and popups/overlays are
diff --git a/src/server/window.rs b/src/server/window.rs
index 35220c5..0785f31 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -1769,8 +1769,18 @@ impl Window {
         }
         // Nobody asked this window to go: either its program exited on its
         // own or — the case this feeds — its Wayland connection broke and
-        // cce-ui is about to rebuild the surface on a fresh one.
-        if !self.close_requested {
+        // cce-ui is about to rebuild the surface on a fresh one. Chrome is
+        // the exception: a Popup (the cce-cloud launcher) or an Overlay dock
+        // closes itself as part of being used — Escape, a pick, a click-away,
+        // a keyboard leave — and the next super+d inside the grace is a
+        // deliberate relaunch that must focus, not a crashed client
+        // reconnecting. Counting it left the reopened launcher unfocused.
+        if !self.close_requested
+            && !matches!(
+                self.tiling_mode,
+                crate::tiling::TilingMode::Popup | crate::tiling::TilingMode::Overlay
+            )
+        {
             if let Some(app_id) = self.get_app_id_string() {
                 (*self.server).wm.note_vanished(app_id);
             }