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

commite2dbdb60b203f904989e8b3e7ec5254f910a3587
parent6151bda576
authorLucas Galante <[email protected]>
date2026-09-08 10:43
fix(restore): never restore or save state for a transient window

A dialog shares its app_id with the main window it hangs off, so the
app_id-only third pass of the saved-state matchers — kept so a relaunched
main window whose title changed still finds its geometry — handed every
dialog the MAIN window's geometry: Houdini's Preferences opened at the full
1856x1141 of its session, hkey's "Redeem Result" at the administrator's
size. Restore fired on app_id notify, before the dialog even had a title.

`try_restore` now bails for any window with a parent (xdg parent or X11
WM_TRANSIENT_FOR, via the existing `get_parent`), next to the Utility
bail. The save pass skips transients to match: saved, a dialog carried its
parent's cmdline and a session restore would have spawned the whole app a
second time to place a dialog that no longer existed.

Verified headless against a GTK4 client run under Xwayland: the old binary
restored its transient at the main window's 400x300, this one leaves it at
its own 300x200 and logs no restore.

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

 src/server/window.rs         | 13 +++++++++++++
 src/server/window_manager.rs |  7 +++++++
 2 files changed, 20 insertions(+)

diff --git a/src/server/window.rs b/src/server/window.rs
index 4605c3f..b9ebe36 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -1035,6 +1035,19 @@ impl Window {
         if self.tiling_mode == crate::tiling::TilingMode::Utility {
             return;
         }
+        // A transient — an xdg toplevel with a parent, or an X11 window with
+        // WM_TRANSIENT_FOR — is a dialog of the window it hangs off, and is
+        // never what a saved entry describes. It shares its app_id with the
+        // main window, so the app_id-only third pass of the state matchers
+        // (kept for a relaunched main window whose title has changed) would
+        // hand it the MAIN window's geometry: Houdini's Preferences opened at
+        // the full 1856x1141 of the session it belongs to, and hkey's
+        // "Redeem Result" at the administrator's size. The save pass skips
+        // transients for the same reason, so there is nothing of their own to
+        // restore either; they size themselves.
+        if !self.get_parent().is_null() {
+            return;
+        }
         let app_id_str = self.get_app_id_string().unwrap_or_default();
         if app_id_str.is_empty()
             || app_id_str.starts_with("cce-status")
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index f7d7455..2a1d383 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -766,6 +766,13 @@ impl WindowManager {
             if (*w).tiling_mode == crate::tiling::TilingMode::Utility {
                 continue;
             }
+            // A transient (dialog) belongs to its parent's process: saved, it
+            // would carry that process's cmdline and a session restore would
+            // spawn the whole app a second time just to place a dialog that
+            // no longer exists. `try_restore` skips transients to match.
+            if !(*w).get_parent().is_null() {
+                continue;
+            }
 
             let app_id = (*w).get_app_id_string().unwrap_or_default();
             if app_id.is_empty() {