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

commita19edaa3b98843482d701acc7c699304c97b81c9
parent653389ec52
authorLucas Galante <[email protected]>
date2026-09-11 15:15
feat(stacking): the focused window's menus clear the floating plane

Floating windows stack in front of tiled ones (653389e), and a window's xdg
popups ride in its own popup_tree just above it — so a menu opened in a
TILED app was cut off wherever a floating window lay over it. Inkscape's
File menu, with a floating editor across its lower half, lost every item
below the halfway point: "New from", "Open Rec", "Save a Co", sliced
mid-word.

A menu is transient and belongs to whatever the user is working in, which
is the focused window by definition. So that one window's popup tree rides
above every window in layers.wm, either plane. Only its popup tree moves,
never its own tree, so the plane rule itself is untouched: close the menu
and the floating window is in front again.

Called from two places, because neither alone is enough. The reorder pass
alone would lose it: opening a menu changes nothing the order hash can see,
so it schedules no transaction at all and the raise would wait for some
unrelated restack. Popup creation alone would lose it too: the next restack
drops the popup back onto its own window. So handle_new_popup does it once
and the pass maintains it.

raise_to_top returns early when the node is already on top, so the repeat
costs nothing and damages nothing, and an empty or disabled popup tree
raises harmlessly — this never has to know whether a menu is really open.

Verified headless with a tiled Inkscape under a floating cce-text-editor:
the File menu now draws in full over it, and hovering an item at
coordinates that lie over the floating window highlights that item — scene
order is what wlr_scene_node_at reads, so input follows. Worth knowing for
anyone reproducing it: no cce-ui app uses xdg popups (their menus are
in-surface) and X11 menus are override-redirect, in a layer above
everything, so this is reachable only from native Wayland GTK/Qt apps.

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

 src/server/window_manager.rs | 44 ++++++++++++++++++++++++++++++++++++++++++++
 src/server/xdg_toplevel.rs   |  6 ++++++
 2 files changed, 50 insertions(+)

diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 610100b..d7946de 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -2525,6 +2525,7 @@ impl WindowManager {
         // keeps its own `overlay_behavior` rule and stays out of this.
         if reorder {
             let wm_layer = (*self.server).scene.layers.wm;
+            let mut focused_popups: *mut Window = std::ptr::null_mut();
             curr = (*render_list).next;
             while curr != render_list {
                 let next = (*curr).next;
@@ -2544,9 +2545,15 @@ impl WindowManager {
                             (*window).tree as *mut _,
                         );
                     }
+                    // Last, so an open menu clears the plane it was just
+                    // stacked behind (`raise_focused_popups`).
+                    focused_popups = if (*window).is_seat_focused() { window } else { focused_popups };
                 }
                 curr = next;
             }
+            if !focused_popups.is_null() {
+                self.raise_focused_popups(focused_popups);
+            }
         }
 
         // The traveling light_source segment crosses over its sibling
@@ -3981,6 +3988,43 @@ impl WindowManager {
         }
     }
 
+    /// Keep a window's open menus clear of the floating plane.
+    ///
+    /// Floating windows stack in front of tiled ones (the reorder pass), and
+    /// a window's xdg popups ride in its own `popup_tree` just above it — so
+    /// a menu opened in a TILED app was covered by any floating window over
+    /// it. A menu is transient and belongs to whatever the user is working
+    /// in, which is the focused window by definition, so that one window's
+    /// popup tree rides above every window in layers.wm, either plane.
+    ///
+    /// Only the focused window, and only while it is in layers.wm: a
+    /// fullscreen/popup/status window is in a layer of its own, where the
+    /// raise would reorder that layer's members instead. An empty or
+    /// disabled popup tree raises harmlessly (nothing to draw), so this does
+    /// not need to know whether a menu is actually open —
+    /// `wlr_scene_node_raise_to_top` returns early when the node is already
+    /// on top, so a repeat costs nothing and damages nothing.
+    ///
+    /// Called from two places, because neither alone is enough: the reorder
+    /// pass (a restack would otherwise drop the popup back to its window),
+    /// and popup creation (opening a menu changes nothing the order hash can
+    /// see, so it schedules no transaction at all).
+    pub unsafe fn raise_focused_popups(&mut self, window: *mut Window) {
+        if window.is_null() || (*window).popup_tree.is_null() {
+            return;
+        }
+        if !(*window).is_seat_focused() {
+            return;
+        }
+        let wm_layer = (*self.server).scene.layers.wm;
+        if wm_layer.is_null()
+            || ffi::river_scene_node_get_parent((*window).popup_tree as *mut _) != wm_layer
+        {
+            return;
+        }
+        ffi::wlr_scene_node_raise_to_top((*window).popup_tree as *mut _);
+    }
+
     pub unsafe fn raise_window(&mut self, window: *mut Window) {
         if window.is_null() {
             return;
diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index 78b7740..5b55c05 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -515,7 +515,13 @@ unsafe extern "C" fn handle_new_popup(listener: *mut ffi::wl_listener, data: *mu
     ) {
         log::error!("Failed to create popup: {}", e);
         ffi::wl_resource_post_no_memory((*wlr_xdg_popup).resource);
+        return;
     }
+    // Opening a menu changes nothing the reorder pass's order hash can see,
+    // so it schedules no transaction: without this raise a tiled window's
+    // menu would stay under the floating plane until some unrelated restack
+    // came along. The pass re-applies it from then on.
+    (*(*window).server).wm.raise_focused_popups(window);
 }
 
 unsafe extern "C" fn handle_ack_configure(