git.lucas.co / cce-grid
desktop grid client
git clone https://git.lucas.co/cce-grid.git

commit7c4a610d64a5e9f4b46149e4a70c614a6a5eee49
parentd0337a6cfb
authorLucas Galante <[email protected]>
date2026-08-18 12:35
lines: raised-grout relief on the rails; cells stay flat

Per-cell Recess rings expanded by the half-gap: the walls occupy exactly
the half-rail around each cell, descending from a crest at the rail
centerline to the cell edge, with the outer radius offset so the wall
stays concentric with the cell arc. Neighboring rings abut at the
centerlines — no overlap, no double-shading — so the rails read as
continuous raised grout while every cell floor stays flat. The cell loop
over-scans by one ring so border cells outside the patch still shade
their half of the boundary rails.

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

 CLAUDE.md   | 18 +++++++++++-------
 src/main.rs | 42 ++++++++++++++++++++++++++++++++----------
 2 files changed, 43 insertions(+), 17 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 2f2da94..2ddcdda 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -14,13 +14,17 @@ its own rect grid as the fallback whenever this client is absent or has not
 latched a patch yet, and its gap-colored backdrop always draws beneath as
 the safety net beyond patch edges.
 
-Rendering is a pure function of (patch, style config): flat rounded cells
-on the gap-colored rail surface — deliberately NO relief, so the grid reads
-as ground under the windows' own bevels (the compositor fallback is flat
-for the same reason: the client latch must not swap the grid's material).
-It reads the same `style.surface.desktop.*` / backplate-radius keys as the
-fallback. Keep it that way — no camera state, no timers, no input (the
-surface is input-transparent compositor-side).
+Rendering is a pure function of (patch, style config): flat rounded cells,
+with the relief on the LINES — per-cell `Recess` rings expanded by the
+half-gap, so the walls occupy exactly the half-rail around each cell
+(crest at the rail centerline, inner edge concentric with the cell arc)
+and neighboring rings abut without double-shading. Cell floors carry NO
+relief (user decision); the rails read as raised grout. The compositor
+fallback draws the same expanded-ring chamfer so the client latch never
+swaps the grid's material. It reads the same `style.surface.desktop.*` /
+backplate-radius keys as the fallback. Keep it that way — no camera
+state, no timers, no input (the surface is input-transparent
+compositor-side).
 
 This directory is its own git repository (gitsite-published, fetch-only
 origin; committing locally is publishing). `cce-grid.service` autostarts it
diff --git a/src/main.rs b/src/main.rs
index 8ca9281..c4b4aa5 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -9,8 +9,9 @@
 //!
 //! The look comes from the same config keys the compositor's fallback grid
 //! reads (`style.surface.desktop.*`, backplate corner radius): flat
-//! rounded cells on the gap-colored rail surface — deliberately unlit, so
-//! the grid reads as ground under the windows' own relief.
+//! rounded cells, with the relief on the LINES — the gap rails read as
+//! raised grout (per-cell half-gap-expanded `Recess` rings that abut at
+//! the rail centerlines), while every cell floor stays flat.
 
 use wayland_client::QueueHandle;
 
@@ -108,11 +109,24 @@ impl GridApp {
         let radius = ((st.corner_radius * s)
             * cce_ui::layout::corner_span_factor() as f64)
             .min(cell_px / 2.0) as f32;
-
-        let col0 = (p.x / period).floor() as i64;
-        let col1 = ((p.x + p.w) / period).ceil() as i64;
-        let row0 = (p.y / period).floor() as i64;
-        let row1 = ((p.y + p.h) / period).ceil() as i64;
+        // The relief lives on the LINES, never the cells: each cell's recess
+        // rect is expanded by the half-gap, so the walls occupy exactly the
+        // half-rail around the cell — descending from a crest at the rail
+        // centerline down to the cell edge. Neighboring recesses abut at the
+        // centerlines (no overlap, no double-shading), so together the rails
+        // read as continuous raised grout while every cell floor stays flat.
+        // The outer radius is the cell arc offset outward by the same
+        // half-gap, so the wall stays concentric with the cell corner.
+        let half_gap = (st.gap_width / 2.0) * s;
+        let ring_radius = radius + half_gap as f32;
+        let ring_depth = (half_gap as f32).max(1.0);
+
+        // One extra ring of cells beyond the patch: a border cell outside the
+        // patch still owns the inner half of the boundary rail's shading.
+        let col0 = (p.x / period).floor() as i64 - 1;
+        let col1 = ((p.x + p.w) / period).ceil() as i64 + 1;
+        let row0 = (p.y / period).floor() as i64 - 1;
+        let row1 = ((p.y + p.h) / period).ceil() as i64 + 1;
         let mut cells = 0usize;
         for col in col0..col1 {
             for row in row0..row1 {
@@ -128,10 +142,18 @@ impl GridApp {
                     width: (len * s) as f32,
                     height: (len * s) as f32,
                 };
-                // A flat cell — no relief: the grid is ground, not furniture,
-                // and a lit well under every window fought the windows' own
-                // bevels.
                 pc.rounded_rect(rect, radius, (true, true, true, true), st.cell_color);
+                let ring = Rect {
+                    x: rect.x - half_gap as f32,
+                    y: rect.y - half_gap as f32,
+                    width: rect.width + 2.0 * half_gap as f32,
+                    height: rect.height + 2.0 * half_gap as f32,
+                };
+                pc.recess(
+                    ring,
+                    (ring_radius, ring_radius, ring_radius, ring_radius),
+                    ring_depth,
+                );
             }
         }
     }