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

commitb678fb813cd1f1c5947a1e1332e7eea1de5a8781
parent3db4ef04d5
authorLucas Galante <[email protected]>
date2026-09-02 12:58
fix(focus): parented cce-cloud popups return focus to their parent on unmap

The cce-cloud exemption in handle_layer_surface_unmap (added for the bare
launcher, then widened to starts_with for status submenus) also caught
parented popups like cce-designer's add-node palette — closing one left
the seat focused on nothing, so the parent app lost keyboard focus until
the user clicked back into it.

A namespace of "cce-cloud:<app-id>" now looks up the mapped window with
that app id and focuses it directly, with suppress_focus_pan so
dismissing chrome never moves the camera. The bare launcher keeps its
no-refocus behavior (its selection is about to map its own window), and
cce-status parents keep it too (the bar is never a keyboard target).

Verified in shadow sessions: old binary leaves focused=false on the
parent after popup close; this build restores focused=true.

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

 src/server/layer_shell.rs | 34 +++++++++++++++++++++++++++++++++-
 1 file changed, 33 insertions(+), 1 deletion(-)

diff --git a/src/server/layer_shell.rs b/src/server/layer_shell.rs
index b2980db..4b414c4 100644
--- a/src/server/layer_shell.rs
+++ b/src/server/layer_shell.rs
@@ -549,14 +549,46 @@ unsafe extern "C" fn handle_layer_surface_unmap(listener: *mut ffi::wl_listener,
             if let crate::seat::Focus::LayerSurface(surface) = (*seat).focused {
                 if surface == (*wlr_layer_surface).surface {
                     (*seat).focus(crate::seat::Focus::None);
+                    // cce-cloud surfaces skip the focus_next fallback: the bare
+                    // launcher is about to be replaced by whatever it spawned,
+                    // and refocusing the old window first would fight the new
+                    // map. But a PARENTED popup ("cce-cloud:<app-id>", e.g. the
+                    // designer's add-node palette) is chrome OF that app —
+                    // closing it must hand the keyboard straight back to its
+                    // parent, not leave the seat focused on nothing.
                     let mut is_cce_cloud = false;
+                    let mut cloud_parent: Option<String> = None;
                     if !(*wlr_layer_surface).namespace.is_null() {
                         let ns = std::ffi::CStr::from_ptr((*wlr_layer_surface).namespace).to_string_lossy();
                         if ns.starts_with("cce-cloud") {
                             is_cce_cloud = true;
+                            cloud_parent = ns.strip_prefix("cce-cloud:").map(str::to_string);
                         }
                     }
-                    if !is_cce_cloud {
+                    if let Some(parent_app_id) = cloud_parent {
+                        // Status modules parent their submenus too; the bar is
+                        // never a keyboard-focus target, so those keep the old
+                        // leave-it-unfocused behavior.
+                        if !parent_app_id.starts_with("cce-status") {
+                            for &win_ptr in (*server).wm.windows.iter() {
+                                if win_ptr.is_null()
+                                    || (*win_ptr).closed
+                                    || (*win_ptr).minimized
+                                    || !matches!((*win_ptr).state, crate::window::WindowState::Mapped)
+                                {
+                                    continue;
+                                }
+                                if (*win_ptr).get_app_id_string().as_deref() == Some(parent_app_id.as_str()) {
+                                    // Dismissing chrome, not switching windows:
+                                    // the camera stays where the user left it.
+                                    (*seat).suppress_focus_pan = true;
+                                    (*seat).focus(crate::seat::Focus::Window(win_ptr));
+                                    (*seat).suppress_focus_pan = false;
+                                    break;
+                                }
+                            }
+                        }
+                    } else if !is_cce_cloud {
                         (*server).wm.focus_next_visible_window(seat);
                     }
                 }