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

commit6400a4e07e6e274b861d13b2c5e93a572a5ff818
parenta324b58ec1
authorLucas Galante <[email protected]>
date2026-08-18 14:07
lines: line_relief accepts a (relief) value — a full custom material

The key's three states: absent = the DE-wide relief material, integer =
lip width (0 = no lip), and a (relief) string = width + depth + wall
profile of its own (cce_ui::relief_spec::ReliefSpec), edited in place
with cce-relief --key style.surface.desktop.line_relief. The client
installs the spec's material process-wide — it draws nothing but the
grid, so process-global IS per-feature — behind a change detector, and
rolls back to the registry's material if the key reverts. Explicit
widths clamp to the half-rail; the material default keeps the tighter
backplate clamp.

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

 CLAUDE.md   |  10 ++++--
 src/main.rs | 105 +++++++++++++++++++++++++++++++++++++++++++++++++++---------
 2 files changed, 98 insertions(+), 17 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 98b43dc..b04b401 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -22,8 +22,14 @@ 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. `style.surface.desktop.line_relief` overrides
-the lip width in logical px for the grid alone (0 = no lip; unset =
-follow the DE-wide relief material) — both renderers honor it. It reads the same `style.surface.desktop.*` /
+the lip for the grid alone: a plain integer is the lip width in logical
+px (0 = no lip; unset = follow the DE-wide relief material), and a
+`(relief)` value carries a full custom material — width, depth, and wall
+profile (`cce_ui::relief_spec::ReliefSpec`), installed process-wide by
+this client (it draws nothing else) and edited in place with
+`cce-relief --key style.surface.desktop.line_relief`. The compositor
+fallback honors the integer form and a `(relief)` value's width (its
+scenefx chamfer has no custom profile to install). 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).
diff --git a/src/main.rs b/src/main.rs
index 002eb75..db4705a 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -36,6 +36,25 @@ struct Patch {
 
 struct GridApp {
     patch: Option<Patch>,
+    /// The raw `(relief)` string currently installed process-wide (depth +
+    /// wall profile LUT) — a change detector, so the registry is only
+    /// touched when the config value actually changes.
+    applied_relief: Option<String>,
+    /// The DE-wide `bevel_depth` captured before the first spec override,
+    /// restored if the key later reverts to a plain width or is removed.
+    base_depth: Option<f32>,
+}
+
+/// The `line_relief` key's three states — see [`Style::line_relief`].
+enum LineRelief {
+    /// Key absent: follow the DE-wide relief material.
+    Material,
+    /// Plain integer: lip width in virtual units, 0 = no lip.
+    Width(f64),
+    /// A `(relief)` value: its own width/depth/profile, editable in place
+    /// with `cce-relief --key style.surface.desktop.line_relief`. The raw
+    /// string rides along as the change detector.
+    Spec(String, cce_ui::relief_spec::ReliefSpec),
 }
 
 /// Style knobs, re-read per frame from the shared config (cheap: cce-ui
@@ -47,10 +66,10 @@ struct Style {
     corner_radius: f64,
     gap_color: [f32; 4],
     cell_color: [f32; 4],
-    /// Grid-line lip width in virtual units; None = follow the DE-wide
-    /// relief material (`layout::bevel_width` clamped to the rail), 0 = no
-    /// lip. Negative config values mean unset.
-    line_relief: Option<f64>,
+    /// Grid-line lip material: a plain integer width (0 = no lip), a full
+    /// `(relief)` value, or absent = the DE-wide material. Negative integers
+    /// mean unset.
+    line_relief: LineRelief,
 }
 
 fn style() -> Style {
@@ -74,9 +93,17 @@ fn style() -> Style {
             get_color("/style/surface/desktop/cell_color")
                 .unwrap_or([0.0, 0.0, 0.0, 1.0]),
         ),
-        line_relief: match get_i64("/style/surface/desktop/line_relief", -1) {
-            v if v < 0 => None,
-            v => Some(v as f64),
+        line_relief: match cce_ui::config::get_string("/style/surface/desktop/line_relief") {
+            // A string value is a (relief) spec; an unparseable one reads
+            // as unset rather than as some accidental width.
+            Some(s) => match cce_ui::relief_spec::ReliefSpec::parse(&s) {
+                Some(spec) => LineRelief::Spec(s, spec),
+                None => LineRelief::Material,
+            },
+            None => match get_i64("/style/surface/desktop/line_relief", -1) {
+                v if v < 0 => LineRelief::Material,
+                v => LineRelief::Width(v as f64),
+            },
         },
     }
 }
@@ -86,7 +113,47 @@ fn style() -> Style {
 const MAX_CELLS: usize = 8192;
 
 impl GridApp {
-    fn paint(&self, pc: &mut PaintCtx, size: LogicalSize) {
+    /// Install (or roll back) the process-wide material a `(relief)` value
+    /// carries — depth into the style registry, profile into the wall LUT.
+    /// This app draws nothing but the grid, so process-global IS
+    /// per-feature; a change detector keeps it idempotent per frame.
+    fn sync_relief_material(&mut self, line_relief: &LineRelief) {
+        match line_relief {
+            LineRelief::Spec(raw, spec) => {
+                if self.applied_relief.as_deref() == Some(raw.as_str()) {
+                    return;
+                }
+                if self.base_depth.is_none() {
+                    self.base_depth = Some(cce_ui::layout::bevel_depth());
+                }
+                if let Some(d) = spec.depth {
+                    if let Ok(mut reg) = cce_ui::layout::get_style_registry().write() {
+                        reg.set_float("bevel_depth", d);
+                    }
+                }
+                cce_ui::layout::install_wall_profile_spec(spec.profile.as_deref());
+                self.applied_relief = Some(raw.clone());
+            }
+            _ if self.applied_relief.is_some() => {
+                // The key reverted to a plain width or vanished: back to
+                // the DE-wide material the registry still carries.
+                if let Some(d) = self.base_depth.take() {
+                    if let Ok(mut reg) = cce_ui::layout::get_style_registry().write() {
+                        reg.set_float("bevel_depth", d);
+                    }
+                }
+                let global = cce_ui::layout::get_style_registry()
+                    .read()
+                    .ok()
+                    .and_then(|reg| reg.get_string("bevel_profile_spec"));
+                cce_ui::layout::install_wall_profile_spec(global.as_deref());
+                self.applied_relief = None;
+            }
+            _ => {}
+        }
+    }
+
+    fn paint(&mut self, pc: &mut PaintCtx, size: LogicalSize) {
         let Some(p) = self.patch else { return };
         if p.scale <= 0.0 {
             return;
@@ -127,12 +194,20 @@ impl GridApp {
         // read far heavier than any plate edge in the toolkit.) Rings stay
         // inside their own half-rail, so neighbors never overlap; the outer
         // radius offsets by the roll to stay concentric with the cell arc.
-        // style.surface.desktop.line_relief overrides the roll width
-        // outright (0 = no lip) without touching the DE-wide material.
-        let roll = st
-            .line_relief
-            .unwrap_or_else(|| (cce_ui::layout::bevel_width() as f64).min(st.gap_width * 0.25))
-            .max(0.0)
+        // style.surface.desktop.line_relief overrides the roll: a plain
+        // width (0 = no lip), or a full (relief) value carrying its own
+        // width/depth/profile. Explicit widths clamp to the half-rail (the
+        // rings' geometric budget); the material default keeps the tighter
+        // backplate clamp.
+        self.sync_relief_material(&st.line_relief);
+        let roll = match &st.line_relief {
+            LineRelief::Material => {
+                (cce_ui::layout::bevel_width() as f64).min(st.gap_width * 0.25)
+            }
+            LineRelief::Width(w) => w.min(st.gap_width / 2.0),
+            LineRelief::Spec(_, spec) => (spec.width as f64).min(st.gap_width / 2.0),
+        }
+        .max(0.0)
             * s;
         let lip = roll >= 0.5;
         let ring_radius = radius + roll as f32;
@@ -185,7 +260,7 @@ impl Application for GridApp {
         _qh: &QueueHandle<EngineState<Self>>,
         _sender: calloop::channel::Sender<Self::Message>,
     ) -> Self {
-        Self { patch: None }
+        Self { patch: None, applied_relief: None, base_depth: None }
     }
 
     fn settings(&self) -> WindowSettings {