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