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

commit7f3de8f8f5ee8193535ad8f74391bc2680505e78
parent52b9d38cc4
authorLucas Galante <[email protected]>
date2026-08-21 19:21
feat: open cce-authenticator centered on the current view, not where it last sat

A window's remembered position is the wrong answer for a session modal on a panning desktop: the camera has almost always moved since, so the dialog maps off-view and the prompt reads as never having appeared — which for the polkit agent means the privileged action silently times out while the user stares at an unchanged screen.

try_center_on_view runs last in map(), after try_restore and try_hint_placement, so the remembered SIZE is still used and only the position is overridden (the same split try_hint_placement documents), and a session modal's placement beats a stale place-next hint. It also forces Floating + mode_locked and clears minimized: a restored Tiled mode would re-snap the window onto a grid cell and undo the centering, and a restored minimized would hide the prompt outright. It sets hint_placed, so the spawn viewport pan is skipped — the window is already centered in view and panning to it would drag the desktop out from under the user for a dialog about to close again.

The camera math lives in the policy crate (cce-window-manager@6397b46, camera::centered_window_origin); the app_id predicate is hardcoded beside the compositor's other DE-internal window classes (cce-status*/cce-wallpaper/cce-grid in try_restore, cce-notifier/cce-cloud in get_mode_for_window) because the config's per-app rules assign a tiling mode, not a placement. Also factors the duplicated first-enabled-output lookup out of try_hint_placement and gives it a size hint that falls back to the client's committed toplevel geometry, so a first-ever launch with no saved size still centers on the real size.

Verified in a headless shadow session running this build, seeded with a hostile remembered state (Tiled + minimized at virtual 9000,9000): the window opened Floating, unminimized and screen-centered, and followed the camera across three different pans — virtual (320,160), (1375,704), (848,432), screen (320,160) every time in a 1280x720 output.

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

 src/server/window.rs | 139 ++++++++++++++++++++++++++++++++++++++++-----------
 1 file changed, 111 insertions(+), 28 deletions(-)

diff --git a/src/server/window.rs b/src/server/window.rs
index 42484e8..9d9f475 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -381,9 +381,10 @@ pub struct Window {
     pub mode_locked: bool,
     pub is_new: bool,
     pub restored: bool,
-    /// Placed at map by a one-shot `place-next` hint (widget-spawned picker
-    /// opening at its control): suppresses the spawn viewport pan — the
-    /// window is already where the user is looking.
+    /// Position was decided at map time rather than by history: a one-shot
+    /// `place-next` hint (widget-spawned picker opening at its control) or a
+    /// 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,
     /// 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
@@ -1032,6 +1033,42 @@ impl Window {
     /// `try_restore` so the remembered SIZE is kept — only the position is
     /// overridden — and marks `hint_placed` so the spawn viewport pan is
     /// skipped (the window is already under the user's pointer).
+    /// Layout box of the first enabled output — `(phys_x, phys_y, width,
+    /// height)`, the viewport every placement decision is measured against.
+    /// Falls back to a 1920x1080 box at the origin before any output is up.
+    unsafe fn first_enabled_output_box(&self) -> (f64, f64, f64, f64) {
+        let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
+        let mut curr_out = (*outputs_list).next;
+        while curr_out != outputs_list {
+            let output = crate::container_of!(curr_out, crate::output::Output, link);
+            if (*output).sent.state == crate::output::OutputStateValue::Enabled {
+                let b = (*output).sent.box_layout();
+                return (b.x as f64, b.y as f64, b.width as f64, b.height as f64);
+            }
+            curr_out = (*curr_out).next;
+        }
+        (0.0, 0.0, 1920.0, 1080.0)
+    }
+
+    /// Best-known window size in VIRTUAL units at map time. `box_geom` is the
+    /// render pass's size and is only filled in once a frame has been drawn
+    /// (or by `try_restore` from the saved geometry), so a first-ever launch
+    /// falls back to the client's committed toplevel geometry.
+    unsafe fn mapped_size_hint(&self) -> (f64, f64) {
+        if self.box_geom.width > 0 && self.box_geom.height > 0 {
+            return (self.box_geom.width as f64, self.box_geom.height as f64);
+        }
+        if let WindowImpl::Toplevel(toplevel) = self.impl_type {
+            if !toplevel.is_null() {
+                let g = (*toplevel).geometry;
+                if g.width > 0 && g.height > 0 {
+                    return (g.width as f64, g.height as f64);
+                }
+            }
+        }
+        (400.0, 400.0)
+    }
+
     unsafe fn try_hint_placement(&mut self) {
         // Utility included: the hint moves only the POSITION, which a utility
         // window does not own — only its size is the client's.
@@ -1049,41 +1086,25 @@ impl Window {
             return;
         };
 
-        // First enabled output's layout box (the center-window fallback).
-        let (mut vp_w, mut vp_h) = (1920.0_f64, 1080.0_f64);
-        let (mut phys_x, mut phys_y) = (0i32, 0i32);
-        let outputs_list = &mut (*self.server).om.outputs as *mut ffi::wl_list as *mut WlList;
-        let mut curr_out = (*outputs_list).next;
-        while curr_out != outputs_list {
-            let output = crate::container_of!(curr_out, crate::output::Output, link);
-            if (*output).sent.state == crate::output::OutputStateValue::Enabled {
-                let wlr_box = (*output).sent.box_layout();
-                vp_w = wlr_box.width as f64;
-                vp_h = wlr_box.height as f64;
-                phys_x = wlr_box.x;
-                phys_y = wlr_box.y;
-                break;
-            }
-            curr_out = (*curr_out).next;
-        }
+        let (phys_x, phys_y, vp_w, vp_h) = self.first_enabled_output_box();
 
         let wm = &(*self.server).wm;
         let zoom = wm.desk_zoom.max(0.01);
-        let w = if self.box_geom.width > 0 { self.box_geom.width as f64 } else { 400.0 } * zoom;
-        let h = if self.box_geom.height > 0 { self.box_geom.height as f64 } else { 400.0 } * zoom;
+        let (vw, vh) = self.mapped_size_hint();
+        let (w, h) = (vw * zoom, vh * zoom);
 
         const OFFSET: f64 = 12.0; // context-menu-style drop below-right of the control
         const MARGIN: f64 = 8.0;
         let sx = (hx + OFFSET)
-            .min(phys_x as f64 + vp_w - w - MARGIN)
-            .max(phys_x as f64 + MARGIN);
+            .min(phys_x + vp_w - w - MARGIN)
+            .max(phys_x + MARGIN);
         let sy = (hy + OFFSET)
-            .min(phys_y as f64 + vp_h - h - MARGIN)
-            .max(phys_y as f64 + MARGIN);
+            .min(phys_y + vp_h - h - MARGIN)
+            .max(phys_y + MARGIN);
 
         // screen = phys + (virtual - desk_pan) * zoom  →  invert for virtual.
-        self.virtual_x = wm.desk_pan_x + (sx - phys_x as f64) / zoom;
-        self.virtual_y = wm.desk_pan_y + (sy - phys_y as f64) / zoom;
+        self.virtual_x = wm.desk_pan_x + (sx - phys_x) / zoom;
+        self.virtual_y = wm.desk_pan_y + (sy - phys_y) / zoom;
         self.hint_placed = true;
         log::info!(
             "place-next hint applied: app_id={} screen=({:.0},{:.0}) virtual=({:.1},{:.1})",
@@ -1091,6 +1112,65 @@ impl Window {
         );
     }
 
+    /// Open a session modal in the middle of what the user is looking at,
+    /// ignoring wherever it last sat.
+    ///
+    /// On a panning desktop a remembered position is actively wrong for these
+    /// windows: the camera has almost always moved since the last time, so
+    /// the window maps somewhere off-view and the prompt reads as never
+    /// having appeared — which for the polkit agent means the privileged
+    /// action silently times out.
+    ///
+    /// Runs after `try_restore`, so the remembered SIZE is still available
+    /// and only the position is overridden — the same split
+    /// `try_hint_placement` uses — and marks `hint_placed` so the spawn
+    /// viewport pan is skipped: the window is already centered in view, and
+    /// panning the camera to it would move the desktop out from under the
+    /// user for a dialog that is about to close again.
+    /// Windows that open centered on the current view rather than wherever
+    /// they last were: DE session modals whose whole job is to interrupt, and
+    /// which the user must be able to answer immediately.
+    ///
+    /// Hardcoded by app_id like the compositor's other DE-internal window
+    /// classes (`cce-status*`/`cce-wallpaper`/`cce-grid` in `try_restore`,
+    /// `cce-notifier`/`cce-cloud` in `get_mode_for_window`). The config's
+    /// per-app window rules assign a tiling MODE, not a placement, so there
+    /// is nothing there to hang this off yet.
+    fn is_view_centered_modal(app_id: &str) -> bool {
+        app_id == "cce-authenticator"
+    }
+
+    unsafe fn try_center_on_view(&mut self) {
+        let app_id = self.get_app_id_string().unwrap_or_default();
+        if !Self::is_view_centered_modal(&app_id) {
+            return;
+        }
+
+        // Whatever history says, a modal has to be visible and free-floating:
+        // a restored Tiled mode would re-snap it onto a grid cell (undoing
+        // 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;
+        self.mode_locked = true;
+        self.minimized = false;
+
+        let (_, _, vp_w, vp_h) = self.first_enabled_output_box();
+        let wm = &(*self.server).wm;
+        let zoom = wm.desk_zoom.max(0.01);
+        let (w, h) = self.mapped_size_hint();
+
+        // Policy owns the camera math; the output's origin cancels out of the
+        // centering, so only the extent is needed per axis.
+        self.virtual_x = crate::policy::camera::centered_window_origin(wm.desk_pan_x, vp_w, zoom, w);
+        self.virtual_y = crate::policy::camera::centered_window_origin(wm.desk_pan_y, vp_h, zoom, h);
+        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
+        );
+    }
+
     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")) {
@@ -1103,6 +1183,9 @@ impl Window {
 
         self.try_restore();
         self.try_hint_placement();
+        // Last: a session modal's placement is not negotiable, so it wins
+        // over both the remembered geometry and any stale place-next hint.
+        self.try_center_on_view();
 
         let surface = self.root_surface();
         if !surface.is_null() {