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