GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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]