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

commit50c76aecf5ec0885fe961b9b471312949f0ba7b4
parent82c7a3002e
authorLucas Galante <[email protected]>
date2026-07-28 20:35
fix: overlay UI is transparent to the WM's focused-window concept

Logging out through the desktop context menu (a cce-cloud Overlay
instance) made the MENU the focused window at save_state time, so no
real window carried the focused flag into the next session — the other
half of the login focus mess. Overlay-mode windows (and cce-cloud by
app_id) still take seat focus for input while open, but the WM now
looks through them: focused_window() falls back through focus_history
to the most recent real window (fixing saved state, arrange focus
styling, viewport actions, and the stream's `focused` query in one
place), record_focus never admits overlay UI into the history,
refocus-after-close skips it, the alt-tab ring excludes it, and
ActionCtx.focused resolves through it so menu-triggered actions act on
the real window underneath.

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

 src/server/window.rs         |  9 ++++++++
 src/server/window_manager.rs | 50 ++++++++++++++++++++++++++++++++++++++------
 2 files changed, 53 insertions(+), 6 deletions(-)

diff --git a/src/server/window.rs b/src/server/window.rs
index 2e8e9bc..6664ef1 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -843,6 +843,15 @@ impl Window {
         }
     }
 
+    /// Overlay-mode UI (cce-cloud menus and the like): takes keyboard input
+    /// while open, but is invisible to the window manager's notion of "the
+    /// focused window" — persistence, camera follow, arrange focus styling
+    /// and refocus rules all look through it to the real window underneath.
+    pub unsafe fn is_overlay_ui(&self) -> bool {
+        self.tiling_mode == crate::tiling::TilingMode::Overlay
+            || self.get_app_id_string().as_deref() == Some("cce-cloud")
+    }
+
     pub unsafe fn try_restore(&mut self) {
         if self.restored {
             return;
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index bd75991..9cff2dd 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -870,10 +870,13 @@ impl WindowManager {
                     }
                 }
             }
-            if let crate::seat::Focus::Window(fw) = (*seat).focused {
-                if !fw.is_null() && !(*fw).closed {
-                    focused = Some(WindowId((*fw).ref_key));
-                }
+        }
+        // The WM's effective focus (overlay UI looked through): actions
+        // triggered from a menu act on the real window underneath.
+        {
+            let fw = self.focused_window();
+            if !fw.is_null() && !(*fw).closed {
+                focused = Some(WindowId((*fw).ref_key));
             }
         }
 
@@ -908,7 +911,10 @@ impl WindowManager {
                 && visible
                 && resolved_mode != crate::tiling::TilingMode::Popup
                 && resolved_mode != crate::tiling::TilingMode::Overlay;
-            let focus_cyclable = rendered.contains(&(w as usize)) && !(*w).minimized && !is_status;
+            let focus_cyclable = rendered.contains(&(w as usize))
+                && !(*w).minimized
+                && !is_status
+                && !(*w).is_overlay_ui();
             windows.push(ActionWindow {
                 id: WindowId((*w).ref_key),
                 app_id,
@@ -1887,16 +1893,40 @@ impl WindowManager {
         }
     }
 
+    /// The window manager's notion of the focused window. Overlay UI
+    /// (cce-cloud menus) holds SEAT focus while open so it gets input, but
+    /// is transparent here: this falls through to the most recently focused
+    /// real window, so opening a menu never changes what "the focused
+    /// window" is — for saved state, arrange styling, viewport actions, or
+    /// the stream's `focused` query. (Logging out via the desktop menu used
+    /// to save the MENU as the session's focused window, poisoning the next
+    /// restore.)
     pub unsafe fn focused_window(&self) -> *mut crate::window::Window {
         let seats_list = &(*self.server).input_manager.seats as *const ffi::wl_list as *const WlList as *mut WlList;
         let mut curr_seat = (*seats_list).next;
         while curr_seat != seats_list {
             let seat = crate::container_of!(curr_seat, crate::seat::Seat, link);
             if let crate::seat::Focus::Window(w) = (*seat).focused {
-                return w;
+                if !w.is_null() && !(*w).is_overlay_ui() {
+                    return w;
+                }
+                break;
             }
             curr_seat = (*curr_seat).next;
         }
+        // Seat focus is on overlay UI (or nothing): the effective focused
+        // window is the most recent real one still on screen.
+        for &w in self.focus_history.iter() {
+            if !w.is_null()
+                && !(*w).closed
+                && matches!((*w).state, crate::window::WindowState::Mapped)
+                && !(*w).is_overlay_ui()
+                && !(*w).is_status_bar()
+                && !(*w).is_wallpaper()
+            {
+                return w;
+            }
+        }
         std::ptr::null_mut()
     }
 
@@ -1946,6 +1976,11 @@ impl WindowManager {
         if window.is_null() {
             return;
         }
+        // Overlay UI never enters the history: it takes input while open but
+        // must not displace the real window as "most recently focused".
+        if (*window).is_overlay_ui() {
+            return;
+        }
         self.focus_history.retain(|&w| w != window);
         self.focus_history.insert(0, window);
     }
@@ -1963,6 +1998,9 @@ impl WindowManager {
             if (*w).closed || (*w).minimized || !matches!((*w).state, crate::window::WindowState::Mapped) {
                 return false;
             }
+            if (*w).is_overlay_ui() {
+                return false;
+            }
             let app_id = (*w).get_app_id_string();
             let is_status_bar = app_id.as_deref().map_or(false, |id| id.starts_with("cce-status"));
             let is_wallpaper = app_id.as_deref() == Some("cce-wallpaper");