git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

commit5d7036195b289ac08ea4d8fd78c578f0b63252e1
parent4923f2f1cd
authorLucas Galante <lsgalante12@gmail.com>
date2026-09-28 14:39
refactor: the relief's shader toggle is style.surface.relief.shader

`window_manager.bevel_shader` — the A/B switch between shader2d's SDF-lit
edge and the legacy banded shading — was the last relief key spelled in
the block the relief was born in. It lives with the relief now
(`relief shader=(bool)false` is the old look), and the old spelling is
retired like the block's other bevel keys: emits no flat line (the
fall-through would land it on the live registry key), reported by path
with the retired surface keys. cce-relief's Save carries a value it
finds across to `relief.shader` rather than dropping it, since the
toggle is not one of the editor's knobs.

A `(bool)` works in the new spelling: it flattens to the string "false",
which `shader_on` reads beside a number; the old float read saw only
`0`. Tests: the toggle flattens to `bevel_shader = false` from a bool,
the reading rule, and the retired spellings reaching no registry key.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

 CLAUDE.md             |  7 ++++---
 src/bin/cce-relief.rs | 17 ++++++++++++++++-
 src/color.rs          | 15 ++++++++-------
 src/layout.rs         | 51 +++++++++++++++++++++++++++++++++++++++++++--------
 4 files changed, 71 insertions(+), 19 deletions(-)

diff --git a/CLAUDE.md b/CLAUDE.md
index 9c1800c..ac93514 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -551,9 +551,10 @@ style {
                 finish light=(f64)0.15 spec=(f64)0.4 shininess=(f64)24.0 curvature=(f64)0.2
             }
         }
-        relief light=(f64)0.15 width=(f64)9.3 spec=(f64)0.4 shininess=(f64)24.0 curvature=(f64)0.2 {
+        relief light=(f64)0.15 width=(f64)9.3 spec=(f64)0.4 shininess=(f64)24.0 curvature=(f64)0.2 shader=(bool)true {
             // light: the strength, NOT a length; width: the ONE run of every roll and wall;
-            // spec / shininess / curvature: the DE finish beyond its strength
+            // spec / shininess / curvature: the DE finish beyond its strength;
+            // shader: false = the legacy banded edge shading, for A/B comparison
             wall height=(mm)0.3 profile="smooth;…"   // a carve's side (buttons, wells, rows): height = drop, profile = ramp spec
             edge height=4.0    profile="smooth;…"   // a plate's perimeter roll: height = rise (unset = quarter-round of width)
         }
@@ -567,7 +568,7 @@ Spellings that are NOT current, and what the loader does with each:
 | Spelling | Status |
 |---|---|
 | `param.color` (+ top-level `plate_opacity`) | alias of `plate.pane.color`, multiplied by the opacity line |
-| `plate.blur` / `.radius` / `.backdrop_compression` / `.refraction`, `frost.backdrop_compression`, `plate.bevel_width`, `relief.depth`, a material's `finish depth=`, `relief.height` / `.profile` / `.edge_height` / `.edge_profile`, `window_manager.bevel_depth` / `.bevel_width` | RETIRED: reported by path (`color::retired_surface_keys`), not read; cce-relief seeds from each once and its Save writes the current spelling and removes the old |
+| `plate.blur` / `.radius` / `.backdrop_compression` / `.refraction`, `frost.backdrop_compression`, `plate.bevel_width`, `relief.depth`, a material's `finish depth=`, `relief.height` / `.profile` / `.edge_height` / `.edge_profile`, `window_manager.bevel_depth` / `.bevel_width` / `.bevel_shader` | RETIRED: reported by path (`color::retired_surface_keys`), not read; cce-relief seeds from each once and its Save writes the current spelling and removes the old (the shader toggle it carries across as `relief.shader`) |
 | `relief.wall.knobs` / `edge.knobs`, `profile_knobs` / `edge_knobs` | not style: cce-relief's own state (`~/.config/cce/cce-relief/state.kdl`); read once as a seed, removed on its next Save |
 
 Where each rule is argued, by its lead-in: **Frost is one block** and
diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
index 676540d..0600861 100644
--- a/src/bin/cce-relief.rs
+++ b/src/bin/cce-relief.rs
@@ -1354,7 +1354,22 @@ impl BevelPopup {
             & ["bevel_depth", "bevel_width"]
                 .iter()
                 .all(|k| cce_ui::config::remove_config_value(&p, &format!("window_manager.{k}")));
-        let ok = ok & migrated;
+        // The shader toggle is not one of this editor's knobs, but its old
+        // home is one of the retired keys a save cleans up: carry the value
+        // across to `relief.shader` rather than drop it.
+        let shader_moved = match std::fs::read_to_string(&p)
+            .ok()
+            .map(|c| cce_ui::config::parse_kdl_to_json(&c))
+            .and_then(|v| v.pointer("/window_manager/bevel_shader").cloned())
+        {
+            Some(v) => {
+                let on = v.as_f64().map_or(v.as_bool().unwrap_or(true), |f| f != 0.0);
+                w("style.surface.relief.shader", if on { "true" } else { "false" })
+                    & cce_ui::config::remove_config_value(&p, "window_manager.bevel_shader")
+            }
+            None => true,
+        };
+        let ok = ok & migrated & shader_moved;
         self.status = if ok {
             println!("saved {p}");
             "Saved — apps pick the material up on start.".to_string()
diff --git a/src/color.rs b/src/color.rs
index 0ba6e9d..5f4109d 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -683,7 +683,7 @@ fn parse_and_set_colors(content: &str) {
     let retired = retired_surface_keys(&val);
     if !retired.is_empty() {
         log::warn!(
-            "retired style.surface keys in config: {} — the frost is `plate {{ frost radius= compression= refraction= }}` (a material's `frost` child spells `compression`), every roll's width is `relief width=`, the light strength is `relief light=` (a material's `finish light=`), and the relief's geometry is `relief {{ wall height= profile= ; edge height= profile= }}` (the window_manager bevel_* spellings are the same relief keys)",
+            "retired style.surface keys in config: {} — the frost is `plate {{ frost radius= compression= refraction= }}` (a material's `frost` child spells `compression`), every roll's width is `relief width=`, the light strength is `relief light=` (a material's `finish light=`), and the relief's geometry is `relief {{ wall height= profile= ; edge height= profile= }}` (the window_manager bevel_* spellings are the same relief keys, and its bevel_shader is `relief shader=`)",
             retired.join(", ")
         );
     }
@@ -778,10 +778,10 @@ fn parse_and_set_colors(content: &str) {
 /// the relief's flat geometry keys `height` / `profile` (the wall's, now
 /// `wall.height` / `wall.profile`) and `edge_height` / `edge_profile` (the
 /// edge's, now `edge.height` / `edge.profile`); and `window_manager.bevel_depth`
-/// / `bevel_width`, the block the relief keys were born in before
-/// `style.surface.relief` existed (the compositor never read them). Each
-/// was an alias for part of 2026-09-28 and is not read now. Empty for a
-/// clean config.
+/// / `bevel_width` / `bevel_shader`, the block the relief keys were born in
+/// before `style.surface.relief` existed (the compositor never read them;
+/// the shader toggle is `relief.shader` now). Each was an alias for part
+/// of 2026-09-28 and is not read now. Empty for a clean config.
 pub fn retired_surface_keys(val: &serde_json::Value) -> Vec<String> {
     let mut found = Vec::new();
     for k in ["blur", "radius", "backdrop_compression", "refraction", "bevel_width"] {
@@ -797,7 +797,7 @@ pub fn retired_surface_keys(val: &serde_json::Value) -> Vec<String> {
             found.push(format!("style.surface.relief.{k}"));
         }
     }
-    for k in ["bevel_depth", "bevel_width"] {
+    for k in ["bevel_depth", "bevel_width", "bevel_shader"] {
         if val.pointer(&format!("/window_manager/{k}")).is_some() {
             found.push(format!("window_manager.{k}"));
         }
@@ -2274,7 +2274,7 @@ mod tests {
                        "bevel_width": 12.0, "frost": { "backdrop_compression": 0.2 } },
             "relief": { "depth": 0.15, "height": 1.0, "profile": "a", "edge_height": 2.0, "edge_profile": "b" },
             "material": { "glass": { "frost": { "backdrop_compression": 0.6 }, "finish": { "depth": 0.2 } } }
-        } }, "window_manager": { "bevel_depth": 0.15, "bevel_width": 9.3 } });
+        } }, "window_manager": { "bevel_depth": 0.15, "bevel_width": 9.3, "bevel_shader": 0 } });
         assert_eq!(retired_surface_keys(&old), vec![
             "style.surface.plate.blur",
             "style.surface.plate.radius",
@@ -2289,6 +2289,7 @@ mod tests {
             "style.surface.relief.edge_profile",
             "window_manager.bevel_depth",
             "window_manager.bevel_width",
+            "window_manager.bevel_shader",
             "style.surface.material.glass.frost.backdrop_compression",
             "style.surface.material.glass.finish.depth",
         ]);
diff --git a/src/layout.rs b/src/layout.rs
index 37f26b0..952fc29 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -194,7 +194,7 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
             // `bevel_depth` registry key by accident, alias or not. The
             // `style.surface.*` retirements need no guard — stripped, they
             // become `surface.relief.depth` and the like, which nothing reads.
-            if matches!(prefix, "window_manager.bevel_depth" | "window_manager.bevel_width") {
+            if matches!(prefix, "window_manager.bevel_depth" | "window_manager.bevel_width" | "window_manager.bevel_shader") {
                 return;
             }
             let flat_key = match prefix {
@@ -286,7 +286,11 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
                 "style.container.section.depth" => "section_depth",
                 "style.surface.param.backdrop_compression" => "param_compression",
                 "style.surface.param.label_layout" => "param_label_layout",
-                "window_manager.bevel_shader" => "bevel_shader",
+                // The relief's shader toggle lives with the relief
+                // (`relief shader=(bool)false` is the legacy banded look);
+                // `window_manager.bevel_shader`, its old home, is retired
+                // and reported like the block's other bevel keys.
+                "style.surface.relief.shader" => "bevel_shader",
                 "window_manager.control_relief" => "control_relief",
                 "window_manager.corner_shape" => "corner_shape",
                 "style.control.ramp.height" => "ramp_height",
@@ -1851,12 +1855,27 @@ pub fn corner_span_factor_for(n: f32) -> f32 {
 
 /// Whether the relief primitives (see `scene::paint::Prim`) render through
 /// shader2d's per-pixel SDF-lit branch (the default) or the legacy banded vertex
-/// shading. `bevel_shader 0` in config flips back to the old look for A/B
-/// comparison — the key keeps the bevel name because it selects how the shared
-/// lit EDGE is computed, not which shapes exist.
+/// shading. `style.surface.relief.shader=(bool)false` (or `0`) flips back to
+/// the old look for A/B comparison — the registry key keeps the bevel name
+/// because it selects how the shared lit EDGE is computed, not which shapes
+/// exist. Until 2026-09-28 the config spelling was `window_manager.bevel_shader`
+/// (retired, reported), and only a NUMBER worked: a `(bool)` flattens to the
+/// string "false", which the float read never saw.
 pub fn bevel_shader() -> bool {
     lazy_init_style_registry();
-    get_style_registry().read().unwrap().get_float("bevel_shader").map(|v| v != 0.0).unwrap_or(true)
+    let reg = get_style_registry().read().unwrap();
+    shader_on(reg.get_float("bevel_shader"), reg.get_string("bevel_shader").as_deref())
+}
+
+/// The shader toggle's reading of its registry slot: a number is on unless
+/// zero, a string is on unless it says `false` / `off` / `no`, and an unset
+/// slot is on.
+fn shader_on(float: Option<f32>, string: Option<&str>) -> bool {
+    match (float, string) {
+        (Some(v), _) => v != 0.0,
+        (None, Some(s)) => !matches!(s.trim().to_ascii_lowercase().as_str(), "false" | "off" | "no" | "0"),
+        (None, None) => true,
+    }
 }
 
 /// How wide a rolled edge is, in logical px — the distance over which a plate's perimeter
@@ -6980,11 +6999,11 @@ mod tests {
                 "height": 2.0, "edge_height": 3.0, "profile": "a", "edge_profile": "b",
                 "wall": { "knobs": "1,1,1" }, "edge_knobs": "2,2,2"
             } } },
-            "window_manager": { "bevel_depth": 0.3, "bevel_width": 5.0 }
+            "window_manager": { "bevel_depth": 0.3, "bevel_width": 5.0, "bevel_shader": 0 }
         });
         let mut flat = String::new();
         flatten_json_to_flat_props(&old, "", &mut flat);
-        for k in ["bevel_depth", "bevel_width", "bevel_height", "roll_height", "bevel_profile_spec", "roll_profile_spec", "profile_knobs"] {
+        for k in ["bevel_depth", "bevel_width", "bevel_shader", "bevel_height", "roll_height", "bevel_profile_spec", "roll_profile_spec", "profile_knobs"] {
             assert!(!flat.lines().any(|l| l.starts_with(&format!("{k} = "))), "{k} landed from a retired spelling: {flat}");
         }
         // And a file carrying BOTH spellings is what its current one says,
@@ -7003,6 +7022,22 @@ mod tests {
         assert!(flat.contains("roll_profile_spec = \"new\""), "{flat}");
     }
 
+    /// The relief's shader toggle is `style.surface.relief.shader`, and a
+    /// `(bool)` works there: it flattens to the string "false", which the
+    /// getter reads (a number was the only thing the old float read saw).
+    #[test]
+    fn the_shader_toggle_lives_with_the_relief_and_takes_a_bool() {
+        let val: serde_json::Value = serde_json::json!({
+            "style": { "surface": { "relief": { "shader": false } } }
+        });
+        let mut flat = String::new();
+        flatten_json_to_flat_props(&val, "", &mut flat);
+        assert!(flat.lines().any(|l| l == "bevel_shader = false"), "{flat}");
+        assert!(shader_on(None, None), "unset: the SDF branch");
+        assert!(shader_on(Some(1.0), None) && !shader_on(Some(0.0), Some("true")), "a number decides when present");
+        assert!(!shader_on(None, Some("false")) && !shader_on(None, Some("off")) && shader_on(None, Some("true")));
+    }
+
     /// The control rung's key flattens to `control_corner_radius`, beside a
     /// widget's own override — the config shape `control corner_radius=8 { button corner_radius=10 }`.
     #[test]