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

commit52b9d38cc42f30e461e6a71c2c6f67418eac7ed9
parentaae9f5938b
authorLucas Galante <[email protected]>
date2026-08-21 18:58
feat: re-tile Tiled windows when the grid geometry changes

retile_for_grid_change sweeps every mapped Tiled window through
cells::remap_block after a cell-size/gap/inset change, writing the
remapped virtual position and box so the next arrange configures each
window to the same BLOCK of squares on the new grid. Wired into both
change paths: the layout IPC arm (the config-value live-edit bridge)
and reload_config. Windows under an active seat op are skipped; the
saved floating spot follows the remap like move-window's does. No-op
when the grid params are unchanged.

Shadow-verified both paths: a 2-column tiled window kept B-2:C-2
through a live grid_cell_height 512->256 (1032x504 -> 1032x248), and a
3x2 block kept A-2:C-1 through a config reload changing 600x512 ->
512x256 (1824x1032 -> 1560x520).

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 549710d..b76062f 100644
--- a/src/server/window_manager.rs
+++ b/src/server/window_manager.rs
@@ -4073,6 +4073,7 @@ impl WindowManager {
                 if parts.len() < 3 { return "error: missing layout key or value\n".to_string(); }
                 let key = parts[1];
                 let val = parts[2];
+                let old_sp = self.layout.snap_params();
                 match key {
                     "desktop_gap_color" => {
                         self.layout.desktop_gap_color = val.to_string();
@@ -4139,6 +4140,7 @@ impl WindowManager {
                     "side_panel_border_gap" | "pinned_border_gap" | "overlay_border_gap" => { if let Ok(v) = val.parse::<i32>() { self.layout.overlay_border_gap = v; } }
                     _ => return format!("error: unknown layout key: {}\n", key),
                 }
+                self.retile_for_grid_change(old_sp);
                 self.dirty_windowing();
                 "ok\n".to_string()
             }
@@ -4454,8 +4456,60 @@ impl WindowManager {
         }
     }
 
+    /// 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
+    /// keeping its old pixel box and later spanning whatever new cells that
+    /// box happens to touch. A window under an active seat op is left
+    /// alone — the op owns its geometry until release. No-op when the
+    /// geometry is unchanged.
+    pub unsafe fn retile_for_grid_change(&mut self, old: crate::policy::snap::SnapParams) {
+        let new = self.layout.snap_params();
+        if old.cell_w == new.cell_w
+            && old.cell_h == new.cell_h
+            && old.gap_width == new.gap_width
+            && old.cell_inset == new.cell_inset
+        {
+            return;
+        }
+        let op_win = self
+            .first_seat()
+            .and_then(|s| (*s).op.as_ref().map(|op| op.window_ptr))
+            .unwrap_or(std::ptr::null_mut());
+        let wins: Vec<*mut crate::window::Window> = self.windows.iter().copied().collect();
+        for w in wins {
+            if w.is_null() || (*w).closed || w == op_win {
+                continue;
+            }
+            if !matches!((*w).state, crate::window::WindowState::Mapped) {
+                continue;
+            }
+            if self.get_mode_for_window(w) != crate::tiling::TilingMode::Tiled {
+                continue;
+            }
+            let (nx, ny, nw, nh) = crate::policy::cells::remap_block(
+                (*w).virtual_x,
+                (*w).virtual_y,
+                (*w).box_geom.width as f64,
+                (*w).box_geom.height as f64,
+                &old,
+                &new,
+            );
+            (*w).virtual_x = nx;
+            (*w).virtual_y = ny;
+            (*w).box_geom.width = nw.round() as i32;
+            (*w).box_geom.height = nh.round() as i32;
+            // The saved floating spot follows like move-window: a later
+            // Tiled -> Floating exit restores at the remapped square rather
+            // than yanking the window back across the resized grid.
+            (*w).saved_floating_virtual_x = nx;
+            (*w).saved_floating_virtual_y = ny;
+        }
+    }
+
     pub unsafe fn reload_config(&mut self) -> Result<(), String> {
         if let Some(path) = crate::config::default_config_path() {
+            let old_sp = self.layout.snap_params();
             let old_pids = std::mem::take(&mut self.startup_pids);
             match crate::config::parse_config(&path, self) {
                 Ok(()) => {
@@ -4479,6 +4533,7 @@ impl WindowManager {
                         link = (*link).next;
                     }
 
+                    self.retile_for_grid_change(old_sp);
                     self.dirty_windowing();
 
                     // Process old PIDs