Wayland compositor (wlroots)
git clone https://git.lucas.co/cce-compositor.git
fix: closed windows keep their squares across a grid change
Changing the desktop grid re-tiled every MAPPED Tiled window correctly
(retile_for_grid_change), but the restore queue and last_window_states
held raw pixels: a window closed before the change and reopened after —
or restored across a compositor restart — landed misaligned on the new
grid, touched extra cells, and the tiled snap grew it by a cell per axis.
Today's gap change (16 -> 8) did exactly that to cce-files and Chrome.
save_state now records the grid (policy SavedGrid) alongside the
geometry; load_state remaps every Tiled entry onto its block of squares
when the file's grid differs from the session's, before placeholders or
restores consume the pixels. The same remap (remap_saved_entries, the
stateful sibling of retile_for_grid_change) runs at both grid-changing
paths — config reload and ccectl layout — so entries queued mid-session
stay square-true too. Pre-field state files carry no grid and restore
as-is.
Verified headless: a state file recording a 3-column tiled terminal under
the gap-16 grid restores under gap 8 at exactly 3 columns (w=1544,
vx=-2596, cell -E1:-C1) where the unfixed path produced 4 (w=2064).
Co-Authored-By: Claude Fable 5 <[email protected]>
src/server/window_manager.rs | 55 ++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 55 insertions(+)
diff --git a/src/server/window_manager.rs b/src/server/window_manager.rs
index 18edc1a..93eea1e 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -482,6 +482,17 @@ impl WindowManager {
self.set_mode(if (state.desk_zoom - 1.0).abs() > 0.001 { WindowManagerMode::Overview } else { WindowManagerMode::Normal });
self.restore_queue = state.windows;
self.last_window_states = state.last_window_states;
+ // Geometry in the file was measured under the grid it
+ // records; if this session's grid differs, put every Tiled
+ // entry back on ITS SQUARES now, before placeholders and
+ // restores consume the pixels. A pre-field file (grid: None)
+ // has nothing to remap from and loads as-is.
+ if let Some(g) = state.grid {
+ let current = self.layout.snap_params();
+ if !g.matches(¤t) {
+ self.remap_saved_entries(&g.to_params(¤t));
+ }
+ }
self.has_restored_focused_window = self.restore_queue.iter().any(|w| w.focused);
self.restored_focused_window_mapped = false;
log::info!(
@@ -820,6 +831,11 @@ impl WindowManager {
desk_zoom: self.desk_zoom,
windows: saved_wins,
last_window_states: self.last_window_states.clone(),
+ // The grid these geometries were measured under, so a later
+ // session under a different grid can keep each Tiled entry on
+ // its squares (remap_saved_entries) instead of re-deriving the
+ // span from stale pixels.
+ grid: Some(crate::policy::state::SavedGrid::from_params(&self.layout.snap_params())),
};
if let Ok(json_str) = serde_json::to_string_pretty(&state) {
@@ -4320,6 +4336,7 @@ impl WindowManager {
_ => return format!("error: unknown layout key: {}\n", key),
}
self.retile_for_grid_change(old_sp);
+ self.remap_saved_entries(&old_sp);
self.dirty_windowing();
"ok\n".to_string()
}
@@ -4660,6 +4677,40 @@ impl WindowManager {
}
}
+ /// Re-tile the SAVED Tiled entries (restore queue + last-window
+ /// states) from `old` grid params onto the current grid — the
+ /// stateful sibling of `retile_for_grid_change`, which can only reach
+ /// mapped windows. Without this, a window closed under one grid and
+ /// reopened under another restores misaligned pixels, touches extra
+ /// cells, and the tiled snap grows it by a cell.
+ pub unsafe fn remap_saved_entries(&mut self, old: &crate::policy::snap::SnapParams) {
+ let new = self.layout.snap_params();
+ for entry in self
+ .restore_queue
+ .iter_mut()
+ .chain(self.last_window_states.iter_mut())
+ {
+ if entry.tiling_mode != crate::tiling::TilingMode::Tiled {
+ continue;
+ }
+ if entry.width == 0 || entry.height == 0 {
+ continue;
+ }
+ let (nx, ny, nw, nh) = crate::policy::cells::remap_block(
+ entry.virtual_x,
+ entry.virtual_y,
+ entry.width as f64,
+ entry.height as f64,
+ old,
+ &new,
+ );
+ entry.virtual_x = nx;
+ entry.virtual_y = ny;
+ entry.width = nw.round() as u32;
+ entry.height = nh.round() as u32;
+ }
+ }
+
/// Re-tile every Tiled window after a grid-geometry change (cell
/// sizes, gap, or fade inset): each window keeps its BLOCK of squares
/// (`cells::remap_block`), so it resizes with the grid instead of
@@ -4738,6 +4789,10 @@ impl WindowManager {
}
self.retile_for_grid_change(old_sp);
+ // The saved entries hold geometry from before the
+ // reload too — closed windows must reopen on their
+ // squares, not their stale pixels.
+ self.remap_saved_entries(&old_sp);
self.dirty_windowing();
// Process old PIDs