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

commit861cc11dfb987efe731b365a97aa7ec6cc4ae74b
parent74739d1b47
authorLucas Galante <[email protected]>
date2026-07-28 13:01
refactor: draw_grid renders from BackgroundSpec via policy grid_frame

Layout::background_spec() builds the declarative spec (always Grid
today; a Solid arm now exists and draws a single viewport rect), and
draw_grid consumes policy::background::grid_frame's per-frame plan —
tree shift, backdrop extent, density-faded cell lattice — instead of
computing geometry inline. The redraw cache collapses to viewport +
sanitized zoom + the whole spec (PartialEq), replacing nine per-field
caches; the write-only last_grid_pan_x/y and grid_is_low_res fields
(the latter always computed false) are dropped. The rect pool, scene
nodes, and the scenefx fade-inset wire encoding (inset*1000 + mode)
stay here.

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

 src/server/config.rs |  16 ++++
 src/server/output.rs | 259 ++++++++++++++++++++-------------------------------
 2 files changed, 119 insertions(+), 156 deletions(-)

diff --git a/src/server/config.rs b/src/server/config.rs
index e3a8b85..e8f3d91 100644
--- a/src/server/config.rs
+++ b/src/server/config.rs
@@ -87,6 +87,22 @@ pub struct Layout {
 }
 
 impl Layout {
+    /// The desktop background as the policy crate's declarative spec. The
+    /// desktop is always the grid; per-frame geometry comes from
+    /// `policy::background::grid_frame`.
+    pub fn background_spec(&self) -> crate::policy::api::BackgroundSpec {
+        use crate::policy::api::{BackgroundSpec, GridFadeMode, GridSpec, Rgba};
+        BackgroundSpec::Grid(GridSpec {
+            gap_color: Rgba(parse_hex_color_rgba(&self.desktop_gap_color)),
+            cell_color: Rgba(self.desktop_cell_color),
+            cell_size: self.desktop_grid_scale,
+            gap_width: self.desktop_gap_width as f64,
+            cell_corner_radius: self.desktop_cell_corner_radius,
+            cell_fade_inset: self.desktop_cell_fade_inset as i32,
+            fade_mode: GridFadeMode::from_name(&self.desktop_grid_fade_mode),
+        })
+    }
+
     /// Snap parameters for interactive ops. A zero threshold (snap
     /// disabled) makes every snap function a no-op.
     pub fn snap_params(&self) -> crate::policy::snap::SnapParams {
diff --git a/src/server/output.rs b/src/server/output.rs
index f6e28c9..d2bc180 100644
--- a/src/server/output.rs
+++ b/src/server/output.rs
@@ -161,17 +161,10 @@ pub struct Output {
     pub last_grid_viewport_w: i32,
     pub last_grid_viewport_h: i32,
     pub last_grid_zoom: f64,
-    pub last_grid_pan_x: f64,
-    pub last_grid_pan_y: f64,
-    pub last_grid_spacing: f64,
-    pub last_grid_gap_width: i32,
-    pub last_grid_cell_color: [f32; 4],
-    pub last_grid_cell_corner_radius: i32,
-    pub last_grid_cell_fade_inset: i64,
-    pub last_grid_fade_mode: String,
-    pub last_grid_gap_color: String,
-    pub last_grid_gap_color_rgba: [f32; 4],
-    pub grid_is_low_res: bool,
+    /// The spec the pool was last drawn from; a change (or viewport/zoom
+    /// change) forces a redraw. Pan alone never redraws — it only moves the
+    /// grid tree.
+    pub last_grid_spec: Option<crate::policy::api::BackgroundSpec>,
     pub grid_rect_pool: Vec<*mut ffi::wlr_scene_rect>,
     pub grid_force_redraw_frames: u8,
 
@@ -424,17 +417,7 @@ impl Output {
             last_grid_viewport_w: 0,
             last_grid_viewport_h: 0,
             last_grid_zoom: 0.0,
-            last_grid_pan_x: 0.0,
-            last_grid_pan_y: 0.0,
-            last_grid_spacing: 0.0,
-            last_grid_gap_width: 0,
-            last_grid_cell_color: [0.0, 0.0, 0.0, 0.0],
-            last_grid_cell_corner_radius: 0,
-            last_grid_cell_fade_inset: 0,
-            last_grid_fade_mode: String::new(),
-            last_grid_gap_color: String::new(),
-            last_grid_gap_color_rgba: [0.0, 0.0, 0.0, 0.0],
-            grid_is_low_res: false,
+            last_grid_spec: None,
             grid_rect_pool: Vec::new(),
             grid_force_redraw_frames: 0,
             destroy: std::mem::zeroed(),
@@ -673,6 +656,12 @@ impl Output {
         }
     }
 
+    /// Draw the desktop background from the policy crate's declarative
+    /// spec: `Layout::background_spec()` says WHAT to show, and (for the
+    /// grid) `policy::background::grid_frame` derives this frame's geometry
+    /// — tree shift, backdrop extent, cell lattice, density fade. This side
+    /// keeps the scene nodes, the rect reuse pool, and scenefx's fade-inset
+    /// wire encoding.
     pub unsafe fn draw_grid(&mut self) {
         if self.grid_tree.is_null() {
             return;
@@ -687,165 +676,123 @@ impl Output {
         ffi::wlr_scene_node_raise_to_top(self.grid_tree as *mut ffi::wlr_scene_node);
 
         let (viewport_w, viewport_h) = self.current.dimensions();
-        let zoom = if wm.desk_zoom.is_nan() || wm.desk_zoom <= 0.0 {
-            1.0
-        } else {
-            wm.desk_zoom
-        };
-
-        let cell_size = wm.layout.desktop_grid_scale.max(5.0);
-        let gap_size = (wm.layout.desktop_gap_width as f64).max(0.0);
-        let period = cell_size + gap_size;
-
-        // Smoothly fade out grid cells when they get too small (period * zoom < 30px)
-        // to prevent extreme visual noise, layout popping, and massive CPU allocation stutters when zooming.
-        let period_pixels = period * zoom;
-        let density_fade = if period_pixels < 15.0 {
-            0.0
-        } else if period_pixels < 30.0 {
-            (period_pixels - 15.0) / 15.0
-        } else {
-            1.0
-        };
+        let spec = wm.layout.background_spec();
+        let zoom = crate::policy::background::sanitized_zoom(wm.desk_zoom);
 
-        let mut cell_color: [f32; 4] = wm.layout.desktop_cell_color;
-        cell_color[3] *= density_fade as f32;
-        let cell_corner_radius = wm.layout.desktop_cell_corner_radius;
-        let cell_fade_inset = wm.layout.desktop_cell_fade_inset;
-        let fade_mode_str = &wm.layout.desktop_grid_fade_mode;
-        let gap_color = &wm.layout.desktop_gap_color;
-
-        // Detect layout or viewport changes to request a redraw.
+        // Spec/viewport/zoom changes force a redraw of the rect pool; pan
+        // alone only moves the grid tree.
         let structure_changed = self.last_grid_viewport_w != viewport_w
             || self.last_grid_viewport_h != viewport_h
             || self.last_grid_zoom != zoom
-            || self.last_grid_spacing != cell_size
-            || self.last_grid_gap_width != gap_size as i32
-            || self.last_grid_cell_color != cell_color
-            || self.last_grid_cell_corner_radius != cell_corner_radius
-            || self.last_grid_cell_fade_inset != cell_fade_inset
-            || &self.last_grid_fade_mode != fade_mode_str
-            || &self.last_grid_gap_color != gap_color;
-
+            || self.last_grid_spec.as_ref() != Some(&spec);
         if structure_changed {
             self.grid_force_redraw_frames = 3;
         }
-
-        // Modulo shift calculations for virtual grid infinite scrolling.
-        // Use .floor() to match window coordinates truncation direction.
-        let origin_x = ((-wm.desk_pan_x) * zoom).floor() as i32;
-        let origin_y = ((-wm.desk_pan_y) * zoom).floor() as i32;
-
-        let period_pixels_i = period_pixels.round() as i32;
-        if period_pixels_i > 0 {
-            let dest_x = self.sent.x + origin_x.rem_euclid(period_pixels_i) - period_pixels_i;
-            let dest_y = self.sent.y + origin_y.rem_euclid(period_pixels_i) - period_pixels_i;
-
-            // Apply shift translation to root grid tree node.
-            ffi::river_scene_node_set_position_if_changed(
-                self.grid_tree as *mut ffi::wlr_scene_node,
-                dest_x,
-                dest_y,
-            );
-        }
-
-        if self.grid_force_redraw_frames > 0 {
+        let force = self.grid_force_redraw_frames > 0;
+        if force {
             self.grid_force_redraw_frames -= 1;
-
             self.last_grid_viewport_w = viewport_w;
             self.last_grid_viewport_h = viewport_h;
             self.last_grid_zoom = zoom;
-            self.last_grid_spacing = cell_size;
-            self.last_grid_gap_width = gap_size as i32;
-            self.last_grid_cell_color = cell_color;
-            self.last_grid_cell_corner_radius = cell_corner_radius;
-            self.last_grid_cell_fade_inset = cell_fade_inset;
-            self.last_grid_fade_mode = fade_mode_str.clone();
-            self.last_grid_gap_color = gap_color.clone();
-            self.last_grid_gap_color_rgba = crate::config::parse_hex_color_rgba(&self.last_grid_gap_color);
-            self.grid_is_low_res = self.last_grid_zoom != zoom;
-
-            let grid_tree = self.grid_tree;
-            let pool = &mut self.grid_rect_pool;
-            let mut pool_idx = 0;
-
-            // Helper closure to manage/reuse the pool of wlr_scene_rect elements.
-            let mut get_rect = |w: i32, h: i32, color_ptr: *const f32, x: i32, y: i32, corner_r: i32, fade_i: i32| -> *mut ffi::wlr_scene_rect {
-                let rect = if pool_idx < pool.len() {
-                    let node = pool[pool_idx];
-                    ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, true);
-                    ffi::wlr_scene_rect_set_size(node, w, h);
-                    ffi::wlr_scene_rect_set_color(node, color_ptr);
-                    node
-                } else {
-                    let node = ffi::wlr_scene_rect_create(grid_tree, w, h, color_ptr);
-                    if !node.is_null() {
-                        pool.push(node);
-                    }
-                    node
-                };
+            self.last_grid_spec = Some(spec.clone());
+        }
+
+        let grid_tree = self.grid_tree;
+        let pool = &mut self.grid_rect_pool;
+        let mut pool_idx = 0;
 
-                if !rect.is_null() {
-                    ffi::wlr_scene_node_set_position(rect as *mut ffi::wlr_scene_node, x, y);
-                    ffi::river_scene_rect_set_corner_radius(rect, corner_r);
-                    ffi::wlr_scene_rect_set_fade_inset(rect, fade_i);
+        // Helper closure to manage/reuse the pool of wlr_scene_rect elements.
+        let mut get_rect = |w: i32, h: i32, color_ptr: *const f32, x: i32, y: i32, corner_r: i32, fade_i: i32| -> *mut ffi::wlr_scene_rect {
+            let rect = if pool_idx < pool.len() {
+                let node = pool[pool_idx];
+                ffi::wlr_scene_node_set_enabled(node as *mut ffi::wlr_scene_node, true);
+                ffi::wlr_scene_rect_set_size(node, w, h);
+                ffi::wlr_scene_rect_set_color(node, color_ptr);
+                node
+            } else {
+                let node = ffi::wlr_scene_rect_create(grid_tree, w, h, color_ptr);
+                if !node.is_null() {
+                    pool.push(node);
                 }
-                pool_idx += 1;
-                rect
+                node
             };
 
-            // 1. Draw base/background rect using the gap color.
-            // Sized larger by one period to prevent flickering at edges during pan shifts.
-            let bg_w = viewport_w + period_pixels_i;
-            let bg_h = viewport_h + period_pixels_i;
-            get_rect(bg_w, bg_h, self.last_grid_gap_color_rgba.as_ptr(), 0, 0, 0, 0);
-
-            // 2. Draw grid cells.
-            let cols = (viewport_w as f64 / period_pixels_i as f64).ceil() as i32 + 1;
-            let rows = (viewport_h as f64 / period_pixels_i as f64).ceil() as i32 + 1;
-
-            if density_fade > 0.0 && cols > 0 && rows > 0 && cols <= 1000 && rows <= 1000 && cols * rows <= 20000 {
-                let rw = (cell_size * zoom).round() as i32;
-                let rh = (cell_size * zoom).round() as i32;
-                let scaled_corner_radius = (cell_corner_radius as f64 * zoom).round() as i32;
-                let fade_mode = match fade_mode_str.as_str() {
-                    "smoothstep" => 1,
-                    "quadratic" => 2,
-                    "cosine" => 3,
-                    "gaussian" => 4,
-                    _ => 0, // "linear"
-                };
-                
-                // Cap the active fade inset at 45% of the cell size to prevent cells from completely blurring out at high zoom out levels
-                let max_inset = (rw as f64 * 0.45).floor() as i32;
-                let inset_pixels = (cell_fade_inset as f64 * zoom).round() as i32;
-                let active_inset = inset_pixels.min(max_inset).max(0);
-
-                let inset_scaled = if active_inset > 0 {
-                    (active_inset * 1000) + fade_mode
-                } else {
-                    0
-                };
-
-                if rw > 0 && rh > 0 {
-                    for col in 0..=cols {
-                        let rel_x = col * period_pixels_i;
-                        for row in 0..=rows {
-                            let rel_y = row * period_pixels_i;
-                            get_rect(rw, rh, cell_color.as_ptr(), rel_x, rel_y, scaled_corner_radius, inset_scaled);
+            if !rect.is_null() {
+                ffi::wlr_scene_node_set_position(rect as *mut ffi::wlr_scene_node, x, y);
+                ffi::river_scene_rect_set_corner_radius(rect, corner_r);
+                ffi::wlr_scene_rect_set_fade_inset(rect, fade_i);
+            }
+            pool_idx += 1;
+            rect
+        };
+
+        match &spec {
+            crate::policy::api::BackgroundSpec::Grid(grid) => {
+                let frame = crate::policy::background::grid_frame(
+                    grid,
+                    wm.camera(),
+                    viewport_w,
+                    viewport_h,
+                    self.sent.x,
+                    self.sent.y,
+                );
+
+                if let Some((x, y)) = frame.tree_pos {
+                    ffi::river_scene_node_set_position_if_changed(
+                        grid_tree as *mut ffi::wlr_scene_node,
+                        x,
+                        y,
+                    );
+                }
+
+                if force {
+                    // Backdrop in the gap color, then the cell lattice.
+                    get_rect(frame.backdrop_w, frame.backdrop_h, grid.gap_color.0.as_ptr(), 0, 0, 0, 0);
+
+                    if let Some(cells) = &frame.cells {
+                        // scenefx fade-inset wire encoding: inset px * 1000
+                        // + fade-mode index; 0 disables the fade.
+                        use crate::policy::api::GridFadeMode;
+                        let fade_mode = match grid.fade_mode {
+                            GridFadeMode::Linear => 0,
+                            GridFadeMode::Smoothstep => 1,
+                            GridFadeMode::Quadratic => 2,
+                            GridFadeMode::Cosine => 3,
+                            GridFadeMode::Gaussian => 4,
+                        };
+                        let inset_scaled = if cells.fade_inset_px > 0 {
+                            cells.fade_inset_px * 1000 + fade_mode
+                        } else {
+                            0
+                        };
+                        for col in 0..=cells.cols {
+                            let rel_x = col * frame.period_px;
+                            for row in 0..=cells.rows {
+                                let rel_y = row * frame.period_px;
+                                get_rect(cells.cell_px, cells.cell_px, cells.color.0.as_ptr(), rel_x, rel_y, cells.corner_radius_px, inset_scaled);
+                            }
                         }
                     }
                 }
             }
+            crate::policy::api::BackgroundSpec::Solid(color) => {
+                ffi::river_scene_node_set_position_if_changed(
+                    grid_tree as *mut ffi::wlr_scene_node,
+                    self.sent.x,
+                    self.sent.y,
+                );
+                if force {
+                    get_rect(viewport_w, viewport_h, color.0.as_ptr(), 0, 0, 0, 0);
+                }
+            }
+        }
 
+        if force {
             // Disable unused rects in the pool to release GPU/scene resources.
             for i in pool_idx..pool.len() {
                 ffi::wlr_scene_node_set_enabled(pool[i] as *mut ffi::wlr_scene_node, false);
             }
         }
-
-        self.last_grid_pan_x = wm.desk_pan_x;
-        self.last_grid_pan_y = wm.desk_pan_y;
     }
 }