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

commita15e994ae04047cd63969aceb73ab8d5f496794a
parent30cafa76dc
authorLucas Galante <[email protected]>
date2026-09-14 13:15
fix(restore): keep a floating window remembered beside the tiled desk

`try_restore` recalled every off-view floating window into the restored
view. cce-data-editor parked left of the first tiled column (x -8461,
column at -7380) was moved to the view centre every login because the
camera had been saved two screens to the right — see cce.log 2026-09-14,
"Recalling off-view floating window into view".

New `WindowManager::tiled_desk_bounds`: the union of the session's Tiled
entries still in `restore_queue` and every live Tiled window, skipping
minimized ones and the status/wallpaper/grid layers. Passed to
`policy::camera::recalled_origin`, which now leaves a window within one
viewport of that box where it was. The recall log line prints the box.

Verified in a headless shadow with a seeded state.json: a floating
cce-data-editor at x -1100 beside a tiled cce-terminal at 0, camera at 0,
restored at -1100; a floating cce-color-editor at -6000 was still recalled.

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

 CLAUDE.md                    |  8 +++++++-
 src/server/window.rs         | 13 +++++++++++--
 src/server/window_manager.rs | 45 ++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 63 insertions(+), 3 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index aec6b71..3857ff7 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -491,7 +491,13 @@ A restored **floating** window is recalled into the current view
 when its remembered position would show less than a quarter of it: the
 camera at restore is wherever the session left it, and a floating window a
 screen away from that is lost, not remembered. Tiled windows stay where the
-grid has them.
+grid has them. **Except on the tiled desk**: a floating window within one
+viewport of the tiled windows' bounding box (`tiled_desk_bounds` — the
+session's Tiled entries still queued plus the live Tiled windows) keeps its
+remembered spot however far the camera is, since the columns beside it are
+what the user pans along (cce-data-editor parked left of the first column
+came back mid-view every login before 2026-09-14). The recall is for a
+window with no tiled neighbour within a screen.
 
 ### IPC & status sockets
 
diff --git a/src/server/window.rs b/src/server/window.rs
index a58de84..eab7681 100644
--- a/src/server/window.rs
+++ b/src/server/window.rs
@@ -1207,6 +1207,13 @@ impl Window {
             // viewport above the desk every login, at the cell its previous
             // incarnation had been saved in, with nothing on screen to say
             // it existed. Tiled windows are the grid's and stay put.
+            //
+            // Unless it is on the tiled desk: a window within a viewport of
+            // the tiled windows' bounding box (`tiled_desk_bounds`, the
+            // session's tiled entries still to restore plus the tiled
+            // windows already up) is placed beside content the user pans
+            // along, and stays where it was put — cce-data-editor parked
+            // left of the first column came back mid-view every login.
             if self.tiling_mode == crate::tiling::TilingMode::Floating && !self.minimized {
                 let (_, _, vp_w, vp_h) = self.first_enabled_output_box();
                 let wm = &(*self.server).wm;
@@ -1215,6 +1222,7 @@ impl Window {
                     pan_y: wm.desk_pan_y,
                     zoom: wm.desk_zoom,
                 };
+                let desk = wm.tiled_desk_bounds();
                 if let Some((nx, ny)) = crate::policy::camera::recalled_origin(
                     self.virtual_x,
                     self.virtual_y,
@@ -1223,10 +1231,11 @@ impl Window {
                     cam,
                     vp_w,
                     vp_h,
+                    desk,
                 ) {
                     log::info!(
-                        "Recalling off-view floating window into view: app_id={} remembered=({:.0},{:.0}) -> ({:.0},{:.0})",
-                        app_id_str, self.virtual_x, self.virtual_y, nx, ny
+                        "Recalling off-view floating window into view: app_id={} remembered=({:.0},{:.0}) -> ({:.0},{:.0}) (tiled desk: {:?})",
+                        app_id_str, self.virtual_x, self.virtual_y, nx, ny, desk
                     );
                     self.virtual_x = nx;
                     self.virtual_y = ny;
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 2be9c84..886ca43 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -684,6 +684,51 @@ impl WindowManager {
         }
     }
 
+    /// Bounding box of the TILED desk, virtual units: the union of the
+    /// session's Tiled entries still waiting in `restore_queue` and every
+    /// live Tiled window (mapped, or restored and about to map). `None` when
+    /// there is no tiled window at all. Feeds `recalled_origin`'s on-desk
+    /// exemption in `try_restore`, so a floating window remembered beside
+    /// the tiled columns is not recalled into the view like a lost one.
+    /// Minimized entries are skipped — a minimized window is nowhere on the
+    /// desk to be beside.
+    pub unsafe fn tiled_desk_bounds(&self) -> Option<(f64, f64, f64, f64)> {
+        let mut bounds: Option<(f64, f64, f64, f64)> = None;
+        let mut extend = |x: f64, y: f64, w: f64, h: f64| {
+            if w <= 0.0 || h <= 0.0 {
+                return;
+            }
+            let (min_x, min_y, max_x, max_y) =
+                bounds.unwrap_or((f64::MAX, f64::MAX, f64::MIN, f64::MIN));
+            bounds = Some((min_x.min(x), min_y.min(y), max_x.max(x + w), max_y.max(y + h)));
+        };
+        for e in &self.restore_queue {
+            if e.tiling_mode == crate::tiling::TilingMode::Tiled && !e.minimized {
+                extend(e.virtual_x, e.virtual_y, e.width as f64, e.height as f64);
+            }
+        }
+        for &w in self.windows.iter() {
+            if w.is_null()
+                || (*w).closed
+                || matches!((*w).state, crate::window::WindowState::Closing | crate::window::WindowState::Init)
+                || (*w).tiling_mode != crate::tiling::TilingMode::Tiled
+                || (*w).minimized
+                || (*w).is_status_bar()
+                || (*w).is_wallpaper()
+                || (*w).is_grid()
+            {
+                continue;
+            }
+            extend(
+                (*w).virtual_x,
+                (*w).virtual_y,
+                (*w).box_geom.width as f64,
+                (*w).box_geom.height as f64,
+            );
+        }
+        bounds
+    }
+
     /// Dim frames at every restored window's saved geometry, shown from
     /// login until the real window maps (or a timeout sweeps the leftovers):
     /// the desk isn't a void while slow programs load, and the saved camera