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

commita06a8be4e10cdf472739d5c66c623a28171963cc
parent220169d8ba
authorLucas Galante <[email protected]>
date2026-08-22 13:17
fix: let a view-centered modal be a Utility window, and center it at its real size

`try_center_on_view` forced Floating unconditionally. `set_utility` arrives
before map and every Utility gate reads `tiling_mode` RAW, so a modal that
declared itself a utility window had the mode silently stripped at map — back
to resizable, geometry saved, and a stale size restored over it next time.
cce-authenticator is the first caller to want both.

Utility is now exempt from the mode forcing ONLY. Like `try_hint_placement`,
this function owns the window's POSITION and never its size, and a Utility
window already satisfies everything the forcing is for: it always floats, never
tiles, and both the grid snap and the overview displacement skip it.

That exposed a second bug, latent until now. The centering runs in `map()`,
but a self-sizing window's geometry does not arrive until a commit — so
`mapped_size_hint` returned its 400x400 floor and the modal was centered as if
it were that size, landing 120px right and 20px high for a 640x360 prompt.
With `try_restore` skipped for Utility this went from a first-launch-only
wrinkle to every launch. The centering is split into `apply_view_centering`
and latched with `pending_view_center`, which the self-sizing commit branches
consume once the real box_geom lands. One-shot: a later commit, or a user
dragging the window, must not snap it back to the middle. The latch is set for
Utility only — the mode whose size arrives late, and the only one the commit
path can ever consume it for.

Shadow-verified against cce-authenticator: mode=Utility, two centering passes
(400x400 -> 440,160 then the real 640x360 -> 320,180, exactly centered on
1280x720), a right-border drag MOVES it (x 320 -> 396) with size unchanged and
no re-classing, and state.json comes back with no entry for it.

 src/server/window.rs       | 54 ++++++++++++++++++++++++++++++++++++++++++++--
 src/server/xdg_toplevel.rs |  6 ++++++
 2 files changed, 58 insertions(+), 2 deletions(-)

diff --git a/src/server/window.rs b/src/server/window.rs
index 9d9f475..11c7cf0 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -386,6 +386,12 @@ pub struct Window {
     /// view-centered session modal. Either way it suppresses the spawn
     /// viewport pan — the window is already where the user is looking.
     pub hint_placed: bool,
+    /// A view-centering that ran before the window's real size was known and
+    /// must be redone once it lands. Only self-sizing modals set it: their
+    /// geometry arrives on a commit, well after `map()`, so the centering at
+    /// map sees `mapped_size_hint`'s fallback and misses by half the
+    /// difference between that and the truth.
+    pub pending_view_center: bool,
     /// True only when the restored geometry came out of the startup restore queue
     /// (`state.json`'s window list). A window reopened later in the session matches
     /// `last_window_states` instead and leaves this false, so it still counts as a
@@ -640,6 +646,7 @@ impl Window {
             is_new: true,
             restored: false,
             hint_placed: false,
+            pending_view_center: false,
             session_restored: false,
             restored_focused: false,
             closed: false,
@@ -1151,10 +1158,41 @@ impl Window {
         // the centering) and a restored `minimized` would hide the prompt
         // outright. `mode_locked` is the "explicit beats heuristic" latch, so
         // the arrange pass cannot geometrically re-promote it either.
-        self.tiling_mode = crate::tiling::TilingMode::Floating;
+        //
+        // Utility is exempt from the mode forcing ONLY — like
+        // `try_hint_placement`, this owns the window's POSITION, never its
+        // size. A Utility window already satisfies everything the forcing is
+        // for: it always floats, never tiles, and both the grid snap and the
+        // overview displacement skip it. Overwriting the field would silently
+        // strip the mode — `set_utility` arrives before map, and every Utility
+        // gate reads `tiling_mode` RAW — leaving the modal resizable, its
+        // geometry saved, and a stale size restored over it next time.
+        if self.tiling_mode != crate::tiling::TilingMode::Utility {
+            self.tiling_mode = crate::tiling::TilingMode::Floating;
+        }
         self.mode_locked = true;
         self.minimized = false;
 
+        // A self-sizing modal has not committed its geometry yet, so
+        // `mapped_size_hint` here is still the 400x400 floor — centering
+        // against that misses by half the difference from the real size (a
+        // 640x360 prompt landed 120px right and 20px high). Center anyway so
+        // the first frame is not wildly off, and latch a redo for the commit
+        // that brings the truth.
+        //
+        // Utility only, because it is the only mode whose size arrives after
+        // map — and so the only one the commit path will ever consume this
+        // for. A Floating modal either has restored geometry (`try_restore`
+        // filled `box_geom` before we got here) or is being given a size by
+        // the arrange pass rather than reporting one.
+        self.pending_view_center = self.tiling_mode == crate::tiling::TilingMode::Utility
+            && (self.box_geom.width <= 0 || self.box_geom.height <= 0);
+        self.apply_view_centering();
+    }
+
+    /// The centering itself, split out so the self-sizing commit path can redo
+    /// it once the client's real size lands.
+    unsafe fn apply_view_centering(&mut self) {
         let (_, _, vp_w, vp_h) = self.first_enabled_output_box();
         let wm = &(*self.server).wm;
         let zoom = wm.desk_zoom.max(0.01);
@@ -1167,10 +1205,22 @@ impl Window {
         self.hint_placed = true;
         log::info!(
             "view-centered modal: app_id={} size=({:.0}x{:.0}) zoom={:.2} virtual=({:.1},{:.1})",
-            app_id, w, h, zoom, self.virtual_x, self.virtual_y
+            self.get_app_id_string().unwrap_or_default(),
+            w, h, zoom, self.virtual_x, self.virtual_y
         );
     }
 
+    /// Redo a latched view-centering now that a self-sizing modal's real
+    /// geometry has arrived. One-shot: a later commit (or a user dragging the
+    /// window) must not snap it back to the middle.
+    pub unsafe fn take_pending_view_center(&mut self) {
+        if !self.pending_view_center || self.box_geom.width <= 0 || self.box_geom.height <= 0 {
+            return;
+        }
+        self.pending_view_center = false;
+        self.apply_view_centering();
+    }
+
     pub unsafe fn map(&mut self) -> Result<(), &'static str> {
         log::debug!("window '{:?}' mapped", self.get_title());
         if self.get_app_id_string().map_or(false, |id| id.starts_with("cce-status")) {
diff --git a/src/server/xdg_toplevel.rs b/src/server/xdg_toplevel.rs
index c433766..f922056 100644
--- a/src/server/xdg_toplevel.rs
+++ b/src/server/xdg_toplevel.rs
@@ -468,6 +468,9 @@ unsafe extern "C" fn handle_map(listener: *mut ffi::wl_listener, _data: *mut std
     if is_self_sized {
         (*(*toplevel).window).box_geom.width = new_geometry.width;
         (*(*toplevel).window).box_geom.height = new_geometry.height;
+        // A view-centered modal that is ALSO self-sizing was centered at map
+        // against a size it had not committed yet; redo it now that it has.
+        (*(*toplevel).window).take_pending_view_center();
         (*(*(*toplevel).window).server).wm.dirty_windowing();
     }
 }
@@ -524,6 +527,9 @@ unsafe extern "C" fn handle_ack_configure(
     if is_self_sized {
         (*(*toplevel).window).box_geom.width = new_geometry.width;
         (*(*toplevel).window).box_geom.height = new_geometry.height;
+        // A view-centered modal that is ALSO self-sizing was centered at map
+        // against a size it had not committed yet; redo it now that it has.
+        (*(*toplevel).window).take_pending_view_center();
         (*(*(*toplevel).window).server).wm.dirty_windowing();
     }
 }