window management library
git clone https://git.lucas.co/cce-window-manager.git
feat: background module — desktop grid drawing plan; real GridSpec
api::GridSpec becomes the desktop grid as actually configured (gap and
cell colors, cell size, gap width, corner radius, fade inset + mode —
replacing the stale line-based sketch), and background::grid_frame
derives one frame's drawing plan from spec + camera + output extent:
the modulo tree shift that makes the grid infinite (floor-matched to
window coordinate truncation, minus one period of overhang), the
density fade that scales cell alpha and then kills the lattice below a
15px period, the backdrop extent (viewport + one period so pan never
exposes an edge), and the zoom-scaled cell lattice with its safety
caps (1000 cols/rows, 20000 cells) and the 45%-of-cell fade-inset cap.
Scene rects, the reuse pool, and scenefx's fade-inset wire encoding
stay mechanism-side.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/api.rs | 43 +++++++++--
src/background.rs | 209 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
src/lib.rs | 3 +
3 files changed, 249 insertions(+), 6 deletions(-)
diff --git a/src/api.rs b/src/api.rs
index 2416b25..5d9ee0c 100644
--- a/src/api.rs
+++ b/src/api.rs
@@ -219,20 +219,51 @@ pub struct ShadowSpec {
pub blur_sigma: f32,
}
-/// What the background layer shows. Absorbs cce-wallpaper (Solid) and the
-/// grid_tree drawing in `output.rs` (Grid).
+/// What the background layer shows. The desktop is normally Grid; Solid is
+/// the degenerate single-color background.
#[derive(Debug, Clone, PartialEq)]
pub enum BackgroundSpec {
Solid(Rgba),
Grid(GridSpec),
}
+/// The desktop grid, as configured (unzoomed virtual units). The per-frame
+/// geometry — pan/zoom offsets, density fade, cell counts — is derived by
+/// `background::grid_frame`.
#[derive(Debug, Clone, PartialEq)]
pub struct GridSpec {
- pub background: Rgba,
- pub line_color: Rgba,
- pub cell_size: i32,
- pub line_width: i32,
+ /// Backdrop color behind and between the cells (premultiplied).
+ pub gap_color: Rgba,
+ pub cell_color: Rgba,
+ pub cell_size: f64,
+ pub gap_width: f64,
+ pub cell_corner_radius: i32,
+ /// Cells fade inward by this many virtual px.
+ pub cell_fade_inset: i32,
+ pub fade_mode: GridFadeMode,
+}
+
+/// Shape of a cell's inward edge fade.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum GridFadeMode {
+ Linear,
+ Smoothstep,
+ Quadratic,
+ Cosine,
+ Gaussian,
+}
+
+impl GridFadeMode {
+ /// Config-string names; anything unrecognized is Linear.
+ pub fn from_name(name: &str) -> Self {
+ match name {
+ "smoothstep" => GridFadeMode::Smoothstep,
+ "quadratic" => GridFadeMode::Quadratic,
+ "cosine" => GridFadeMode::Cosine,
+ "gaussian" => GridFadeMode::Gaussian,
+ _ => GridFadeMode::Linear,
+ }
+ }
}
/// Everything `Policy::action` may consult, captured by the mechanism at
diff --git a/src/background.rs b/src/background.rs
new file mode 100644
index 0000000..e47cfe4
--- /dev/null
+++ b/src/background.rs
@@ -0,0 +1,209 @@
+// Background drawing policy: the per-frame geometry of the infinite desktop
+// grid. `grid_frame` maps a GridSpec + camera + output extent to a concrete
+// drawing plan — where the grid tree sits (the modulo shift that makes the
+// grid infinite), how big the backdrop is, and whether/how the cells draw
+// (density fade, safety caps, zoom-scaled extents). The mechanism owns the
+// scene rects, the reuse pool, and scenefx's fade-inset wire encoding.
+
+use crate::api::{GridSpec, Rgba};
+use crate::camera::Camera;
+
+/// A camera zoom as the background math consumes it: NaN and non-positive
+/// values fall back to 1.
+pub fn sanitized_zoom(zoom: f64) -> f64 {
+ if zoom.is_nan() || zoom <= 0.0 { 1.0 } else { zoom }
+}
+
+/// One frame's grid drawing plan, in output-local px unless noted.
+#[derive(Debug, Clone, PartialEq)]
+pub struct GridFrame {
+ /// Grid-tree translation in layout px (includes the output's own
+ /// offset): the pan shift folded modulo one period, minus one period so
+ /// the tree always overhangs the top-left edge. None when the zoomed
+ /// period rounds to zero — the tree keeps its last position.
+ pub tree_pos: Option<(i32, i32)>,
+ /// The zoomed grid period (cell + gap), rounded to px.
+ pub period_px: i32,
+ /// Backdrop (gap color) extent: viewport plus one period, so pan shifts
+ /// never expose the edge.
+ pub backdrop_w: i32,
+ pub backdrop_h: i32,
+ /// None when the cells are fully density-faded, the period degenerates,
+ /// or the cell count exceeds the safety caps — backdrop only.
+ pub cells: Option<GridCells>,
+}
+
+/// The repeated cell lattice: draw a cell at (col * period_px,
+/// row * period_px) for col in 0..=cols, row in 0..=rows, tree-local.
+#[derive(Debug, Clone, PartialEq)]
+pub struct GridCells {
+ pub cell_px: i32,
+ pub cols: i32,
+ pub rows: i32,
+ /// Cell color with the density fade already applied to alpha.
+ pub color: Rgba,
+ pub corner_radius_px: i32,
+ /// Zoom-scaled fade inset, capped at 45% of the cell so cells can't
+ /// blur out entirely when far zoomed out; 0 disables the fade.
+ pub fade_inset_px: i32,
+}
+
+pub fn grid_frame(
+ spec: &GridSpec,
+ cam: Camera,
+ viewport_w: i32,
+ viewport_h: i32,
+ output_x: i32,
+ output_y: i32,
+) -> GridFrame {
+ let zoom = sanitized_zoom(cam.zoom);
+ let cell_size = spec.cell_size.max(5.0);
+ let gap = spec.gap_width.max(0.0);
+ let period = cell_size + gap;
+
+ // Fade the cells out as they shrink (period under 30 screen px) to
+ // prevent visual noise and pathological cell counts 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 period_px = period_pixels.round() as i32;
+
+ // Modulo shift for infinite scrolling; .floor() matches the truncation
+ // direction of window coordinates.
+ let tree_pos = if period_px > 0 {
+ let origin_x = ((-cam.pan_x) * zoom).floor() as i32;
+ let origin_y = ((-cam.pan_y) * zoom).floor() as i32;
+ Some((
+ output_x + origin_x.rem_euclid(period_px) - period_px,
+ output_y + origin_y.rem_euclid(period_px) - period_px,
+ ))
+ } else {
+ None
+ };
+
+ let cells = if period_px > 0 && density_fade > 0.0 {
+ let cols = (viewport_w as f64 / period_px as f64).ceil() as i32 + 1;
+ let rows = (viewport_h as f64 / period_px as f64).ceil() as i32 + 1;
+ let cell_px = (cell_size * zoom).round() as i32;
+ if cols > 0 && rows > 0 && cols <= 1000 && rows <= 1000 && cols * rows <= 20000 && cell_px > 0 {
+ let mut color = spec.cell_color;
+ color.0[3] *= density_fade as f32;
+ let max_inset = (cell_px as f64 * 0.45).floor() as i32;
+ let fade_inset_px = ((spec.cell_fade_inset as f64 * zoom).round() as i32)
+ .min(max_inset)
+ .max(0);
+ Some(GridCells {
+ cell_px,
+ cols,
+ rows,
+ color,
+ corner_radius_px: (spec.cell_corner_radius as f64 * zoom).round() as i32,
+ fade_inset_px,
+ })
+ } else {
+ None
+ }
+ } else {
+ None
+ };
+
+ GridFrame {
+ tree_pos,
+ period_px,
+ backdrop_w: viewport_w + period_px,
+ backdrop_h: viewport_h + period_px,
+ cells,
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+ use crate::api::GridFadeMode;
+
+ const VW: i32 = 1920;
+ const VH: i32 = 1080;
+
+ fn spec() -> GridSpec {
+ GridSpec {
+ gap_color: Rgba([0.0, 0.0, 0.0, 1.0]),
+ cell_color: Rgba([0.05, 0.05, 0.05, 0.6]),
+ cell_size: 100.0,
+ gap_width: 10.0,
+ cell_corner_radius: 8,
+ cell_fade_inset: 4,
+ fade_mode: GridFadeMode::Linear,
+ }
+ }
+
+ fn cam(pan_x: f64, pan_y: f64, zoom: f64) -> Camera {
+ Camera { pan_x, pan_y, zoom }
+ }
+
+ #[test]
+ fn unpanned_grid_overhangs_one_period() {
+ let f = grid_frame(&spec(), cam(0.0, 0.0, 1.0), VW, VH, 100, 50);
+ assert_eq!(f.period_px, 110);
+ assert_eq!(f.tree_pos, Some((100 - 110, 50 - 110)));
+ assert_eq!((f.backdrop_w, f.backdrop_h), (VW + 110, VH + 110));
+ let cells = f.cells.unwrap();
+ // ceil(1920/110)+1 = 19, ceil(1080/110)+1 = 11.
+ assert_eq!((cells.cols, cells.rows), (19, 11));
+ assert_eq!(cells.cell_px, 100);
+ assert_eq!(cells.fade_inset_px, 4);
+ assert_eq!(cells.corner_radius_px, 8);
+ // Full density: color untouched.
+ assert_eq!(cells.color, Rgba([0.05, 0.05, 0.05, 0.6]));
+ }
+
+ #[test]
+ fn pan_folds_modulo_one_period() {
+ // Pan 30 right: origin -30, rem_euclid(110) = 80, minus a period.
+ let f = grid_frame(&spec(), cam(30.0, 0.0, 1.0), VW, VH, 0, 0);
+ assert_eq!(f.tree_pos, Some((-30, -110)));
+ // A full period of pan lands back where it started.
+ let g = grid_frame(&spec(), cam(140.0, 0.0, 1.0), VW, VH, 0, 0);
+ assert_eq!(g.tree_pos, Some((-30, -110)));
+ }
+
+ #[test]
+ fn density_fade_scales_alpha_then_kills_cells() {
+ // Zoom 0.2: period 110 * 0.2 = 22 px — mid-ramp (22-15)/15.
+ let f = grid_frame(&spec(), cam(0.0, 0.0, 0.2), VW, VH, 0, 0);
+ let cells = f.cells.unwrap();
+ assert!((cells.color.0[3] - 0.6 * (7.0 / 15.0) as f32).abs() < 1e-6);
+ // Zoom 0.1: period 11 px — fully faded, backdrop only.
+ let f = grid_frame(&spec(), cam(0.0, 0.0, 0.1), VW, VH, 0, 0);
+ assert!(f.cells.is_none());
+ assert_eq!(f.period_px, 11);
+ assert!(f.tree_pos.is_some());
+ }
+
+ #[test]
+ fn fade_inset_caps_at_45_percent_of_cell() {
+ let mut s = spec();
+ s.cell_fade_inset = 60;
+ let f = grid_frame(&s, cam(0.0, 0.0, 1.0), VW, VH, 0, 0);
+ assert_eq!(f.cells.unwrap().fade_inset_px, 45);
+ }
+
+ #[test]
+ fn degenerate_zoom_and_period_are_safe() {
+ // NaN zoom falls back to 1.
+ let f = grid_frame(&spec(), cam(0.0, 0.0, f64::NAN), VW, VH, 0, 0);
+ assert_eq!(f.period_px, 110);
+ assert!(f.cells.is_some());
+ // A period that rounds to zero: no tree move, no cells, backdrop
+ // stays viewport-sized.
+ let f = grid_frame(&spec(), cam(0.0, 0.0, 0.001), VW, VH, 0, 0);
+ assert_eq!(f.period_px, 0);
+ assert!(f.tree_pos.is_none());
+ assert!(f.cells.is_none());
+ assert_eq!((f.backdrop_w, f.backdrop_h), (VW, VH));
+ }
+}
diff --git a/src/lib.rs b/src/lib.rs
index b30e328..c605a63 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -18,6 +18,8 @@
// - `bindings`: keybinding vocabulary — action names, chord grammar,
// `BindingTable`, stock defaults. The compositor feeds it plain data
// parsed from `input.kdl`; keysym name→code lookup stays mechanism-side.
+// - `background`: per-frame desktop-grid geometry (`grid_frame`) —
+// modulo tree shift, density fade, cell lattice with safety caps.
// - `camera`: viewport pan/zoom math — keyed/wheel zoom about an anchor,
// centering, overview fit, focus-follow visibility.
// - `focus`: directional focus selection (which window is "up/left/…"
@@ -35,6 +37,7 @@
pub mod actions;
pub mod api;
pub mod arrange;
+pub mod background;
pub mod bindings;
pub mod camera;
pub mod focus;