GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
refactor: retire the relief's flat geometry keys; no alias precedence is left
`relief.height` / `.profile` (the wall's) and `relief.edge_height` /
`.edge_profile` (the edge's) were aliases of the `wall` / `edge` node
spellings since this morning's rename. An alias that keeps working is a
second spelling forever, so the loader no longer reads them: their
flatten arms go, and a config still carrying one is reported by path
with the other retired surface keys. That was the last pair
`prefer_relief_spellings` had to decide between, so the pass goes too —
every legacy spelling of the relief is retired, a file is what it says,
and a comment where the pass stood says why it existed. cce-relief still
reads a flat key once as a seed, and its Save writes the node spelling
and removes the flat one, as before.
The precedence test folds into the flatten test: every retired spelling
reaches NO registry key, and a file carrying both spellings of a key is
what its current one says with nothing in between.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
CLAUDE.md | 27 +++++++-----
src/bin/cce-relief.rs | 5 ++-
src/color.rs | 31 ++++++++-----
src/layout.rs | 120 +++++++++++++++-----------------------------------
4 files changed, 74 insertions(+), 109 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index 83e0934..980026b 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -567,10 +567,8 @@ Spellings that are NOT current, and what the loader does with each:
| Spelling | Status |
|---|---|
| `window_manager.bevel_depth` / `.bevel_width` | compat aliases of `relief.light` / `.width` — the compositor's old block, a different family |
-| `relief.height` / `.profile`, `relief.edge_height` / `.edge_profile` | aliases of `wall.*` / `edge.*`; the node wins when both are present |
-| `window_manager.bevel_width` | alias of `relief.width` |
| `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=` | RETIRED: reported by path (`color::retired_surface_keys`), not read; cce-relief seeds from `depth` once and writes `light` |
+| `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` | 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 |
| `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
@@ -578,10 +576,12 @@ Where each rule is argued, by its lead-in: **Frost is one block** and
editor's knobs are not a style key** and **There is one roll width** under
"Units" (the geometry is unit-aware, which is why they sit there); the
`menu` block under "The context menu draws in its own popup surface".
-`prefer_relief_spellings` in `layout.rs` is the one place the alias
-precedence is decided, `color::retired_surface_keys` the one place a retired
-key is named (`retired_surface_keys`), and cce-relief's Save is the migration for all of it: it
-writes the current spellings and removes every superseded one it finds.
+There is no alias precedence to decide any more — every legacy spelling of
+the relief is retired, so a file is what it says — and
+`color::retired_surface_keys` is the one place a retired key is named;
+cce-relief's Save is the migration for all of it: it seeds from a retired
+key once, writes the current spelling and removes every superseded one it
+finds.
## The standard app — root plate, rungs, and the spacing ladder
@@ -770,11 +770,14 @@ quadrant for an edge). The registry keys never moved — `bevel_height`
downstream of the registry knows. Until 2026-09-28 the keys were flat on the
node with the wall UNNAMED (`height`, `profile`) and the edge prefixed
(`edge_height`, `edge_profile`), which read as one shape with an "edge"
-variant rather than two shapes; those spellings survive as aliases in
-`layout.rs`'s flatten table, and `prefer_relief_spellings` drops a flat one
-whenever its node spelling is present, so a file carrying both is decided by
-the node and not by line order (`the_current_spelling_of_a_relief_key_wins_over_the_legacy_one`).
-`cce-relief`'s Save writes the node spellings and REMOVES the flat ones
+variant rather than two shapes; those spellings were aliases for the rest
+of that day and are RETIRED — not read, reported by path with the other
+retired surface keys, reaching no registry key (the flatten test asserts
+it). While they were aliases a `prefer_relief_spellings` pass dropped a flat
+one whenever its node spelling was present, because two spellings of one
+registry key were otherwise decided by line order; with nothing left to
+prefer, the pass is gone. `cce-relief` seeds from a flat key once, and its
+Save writes the node spellings and REMOVES the flat ones
(`config::remove_config_value`), so a file migrates the first time it is
saved; `wall` and `edge` are `PROP_NODES` members so their keys land as
properties.
diff --git a/src/bin/cce-relief.rs b/src/bin/cce-relief.rs
index 043c4b5..3976f54 100644
--- a/src/bin/cce-relief.rs
+++ b/src/bin/cce-relief.rs
@@ -1417,8 +1417,9 @@ impl Application for BevelPopup {
// A key of one of the relief's two SHAPES: `relief { wall k=… }` first,
// then the flat legacy spelling a file saved before 2026-09-28 still
// carries (`height` / `profile` / `profile_knobs` for the wall,
- // `edge_*` for the edge) — the same precedence the registry load
- // applies through `layout::prefer_relief_spellings`.
+ // `edge_*` for the edge). The loader does not read those any more
+ // (retired, reported); this is the one-time seed, and Save writes
+ // the node spelling and removes the flat one.
let rel_shape = |node: &str, k: &str, legacy: &str| {
let r = target_relief.as_ref()?;
r.get(node).and_then(|n| n.get(k)).or_else(|| r.get(legacy)).cloned()
diff --git a/src/color.rs b/src/color.rs
index 43cf279..3fe9f8d 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=`, and the light strength is `relief light=` (a material's `finish light=`)",
+ "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= }}`",
retired.join(", ")
);
}
@@ -774,9 +774,11 @@ fn parse_and_set_colors(content: &str) {
/// material's), whose one name is `compression`; `plate.bevel_width`, the
/// pane roll's former width of its own (every roll is `relief.width`); and
/// `relief.depth` with `depth` inside any material's `finish` child, the
-/// light strength's former name (`light`, since it is not a length). Each
-/// was an alias for part of 2026-09-28 and is not read now. Empty for a
-/// clean config.
+/// light strength's former name (`light`, since it is not a length); and
+/// 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`). 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"] {
@@ -787,8 +789,10 @@ pub fn retired_surface_keys(val: &serde_json::Value) -> Vec<String> {
if val.pointer("/style/surface/plate/frost/backdrop_compression").is_some() {
found.push("style.surface.plate.frost.backdrop_compression".to_string());
}
- if val.pointer("/style/surface/relief/depth").is_some() {
- found.push("style.surface.relief.depth".to_string());
+ for k in ["depth", "height", "profile", "edge_height", "edge_profile"] {
+ if val.pointer(&format!("/style/surface/relief/{k}")).is_some() {
+ found.push(format!("style.surface.relief.{k}"));
+ }
}
if let Some(mats) = val.pointer("/style/surface/material").and_then(|v| v.as_object()) {
for (name, node) in mats {
@@ -2245,21 +2249,22 @@ mod tests {
}
/// The four flat frost keys, the `backdrop_compression` spelling inside
- /// a `frost` child, `plate.bevel_width` and `depth` (on the relief, or
- /// in a material's `finish`) are reported by path; the block itself and
- /// a material's `compression` / `light` are not.
+ /// a `frost` child, `plate.bevel_width`, `depth` (on the relief, or in a
+ /// material's `finish`) and the relief's flat geometry keys are
+ /// reported by path; the blocks themselves and a material's
+ /// `compression` / `light` are not.
#[test]
fn retired_surface_keys_are_named_by_path_and_the_block_is_not() {
let clean: serde_json::Value = serde_json::json!({ "style": { "surface": {
"plate": { "frost": { "radius": 5.5, "compression": 0.0, "refraction": 0.0 }, "color": "#6c6c7bf2" },
- "relief": { "light": 0.15 },
+ "relief": { "light": 0.15, "wall": { "height": 1.0, "profile": "a" }, "edge": { "height": 2.0, "profile": "b" } },
"material": { "glass": { "frost": { "compression": 0.6 }, "finish": { "light": 0.2 } } }
} } });
assert!(retired_surface_keys(&clean).is_empty());
let old: serde_json::Value = serde_json::json!({ "style": { "surface": {
"plate": { "blur": true, "radius": 1.5, "backdrop_compression": 0.85, "refraction": 0.3,
"bevel_width": 12.0, "frost": { "backdrop_compression": 0.2 } },
- "relief": { "depth": 0.15 },
+ "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 } } }
} } });
assert_eq!(retired_surface_keys(&old), vec![
@@ -2270,6 +2275,10 @@ mod tests {
"style.surface.plate.bevel_width",
"style.surface.plate.frost.backdrop_compression",
"style.surface.relief.depth",
+ "style.surface.relief.height",
+ "style.surface.relief.profile",
+ "style.surface.relief.edge_height",
+ "style.surface.relief.edge_profile",
"style.surface.material.glass.frost.backdrop_compression",
"style.surface.material.glass.finish.depth",
]);
diff --git a/src/layout.rs b/src/layout.rs
index 59ef75b..8d8a807 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -259,25 +259,21 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
// the carve's drop, the roll's rise; unset = follow the
// width) and `profile` (the ramp spec of its curve, written
// by cce-relief, installed by reload_config). The registry
- // keys keep their old names. The flat spellings on the
- // second line of each pair are the pre-rename aliases
- // (`height` / `profile` were the wall's, `edge_*` the
- // edge's); `prefer_relief_spellings` drops a flat one
- // whenever its node spelling is present, so a config
- // carrying both is decided by the node, not by line order.
- // cce-relief's slider knobs are NOT a style key any more
+ // keys keep their old names. The pre-rename flat spellings
+ // (`height` / `profile` were the wall's, `edge_height` /
+ // `edge_profile` the edge's) were aliases for the rest of
+ // that day and are RETIRED: not read, reported by path
+ // (`color::retired_surface_keys`), removed by cce-relief's
+ // Save, which also reads them once as a seed. cce-relief's
+ // slider knobs are NOT a style key any more
// (`relief.wall.knobs` / `edge.knobs`, before that
// `profile_knobs` / `edge_knobs`): editor state, kept in
// that app's own state.kdl, and read off a config only by
// the editor itself, as a one-time seed.
- "style.surface.relief.wall.height"
- | "style.surface.relief.height" => "bevel_height",
- "style.surface.relief.edge.height"
- | "style.surface.relief.edge_height" => "roll_height",
- "style.surface.relief.wall.profile"
- | "style.surface.relief.profile" => "bevel_profile_spec",
- "style.surface.relief.edge.profile"
- | "style.surface.relief.edge_profile" => "roll_profile_spec",
+ "style.surface.relief.wall.height" => "bevel_height",
+ "style.surface.relief.edge.height" => "roll_height",
+ "style.surface.relief.wall.profile" => "bevel_profile_spec",
+ "style.surface.relief.edge.profile" => "roll_profile_spec",
"style.container.section.depth" => "section_depth",
"style.surface.param.backdrop_compression" => "param_compression",
"style.surface.param.label_layout" => "param_label_layout",
@@ -398,38 +394,19 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
}
}
-/// The relief's current spellings win over its legacy ones: the `wall` /
-/// `edge` node spellings over the flat geometry keys. Each pair flattens to
-/// ONE registry key (`relief.wall.height` and `relief.height` are both
-/// `bevel_height`), and `reload_config` loads the flat lines in the order
-/// the JSON hands them out, so a config carrying both spellings — a file
-/// cce-relief has saved once under the new names while an older line
-/// survives, or a per-app override written in the other spelling — would
-/// otherwise be decided by which line came first. Run on the parsed config
-/// before it is flattened. (`depth` versus `light` was a pair here for the
-/// rest of 2026-09-28; `depth` is retired now and never flattens at all.)
-pub(crate) fn prefer_relief_spellings(val: &mut serde_json::Value) {
- const NODE_PAIRS: &[(&str, &str, &str)] = &[
- ("height", "wall", "height"),
- ("profile", "wall", "profile"),
- ("edge_height", "edge", "height"),
- ("edge_profile", "edge", "profile"),
- ];
- let Some(relief) = val.pointer_mut("/style/surface/relief").and_then(|v| v.as_object_mut()) else {
- return;
- };
- for (flat, node, key) in NODE_PAIRS {
- if relief.get(*node).and_then(|n| n.get(*key)).is_some() {
- relief.remove(*flat);
- }
- }
-}
+// There is no alias-precedence pass any more. For the rest of 2026-09-28
+// `prefer_relief_spellings` ran here, dropping a relief key's legacy
+// spelling whenever its current one was present (`height` under
+// `wall.height`, `depth` under `light`), because two spellings of one
+// registry key would otherwise be decided by the order the JSON handed
+// the lines out. Every legacy spelling of the relief is retired now — not
+// read at all, reported by `color::retired_surface_keys` — so there is
+// nothing left to prefer, and a config is what it says.
fn read_config() -> Option<String> {
let path = crate::config::get_config_path();
if let Ok(content) = std::fs::read_to_string(&path) {
- let mut val = crate::config::parse_kdl_to_json(&content);
- prefer_relief_spellings(&mut val);
+ let val = crate::config::parse_kdl_to_json(&content);
let mut flat_props = String::new();
flatten_json_to_flat_props(&val, "", &mut flat_props);
return Some(flat_props);
@@ -440,8 +417,7 @@ fn read_config() -> Option<String> {
pub fn read_config_value(target_key: &str) -> Option<String> {
let path = crate::config::get_config_path();
if let Ok(content) = std::fs::read_to_string(&path) {
- let mut val = crate::config::parse_kdl_to_json(&content);
- prefer_relief_spellings(&mut val);
+ let val = crate::config::parse_kdl_to_json(&content);
let mut flat_props = String::new();
flatten_json_to_flat_props(&val, "", &mut flat_props);
for line in flat_props.lines() {
@@ -6985,60 +6961,36 @@ mod tests {
assert_eq!(value("roll_height").as_deref(), Some("4"), "{flat}");
assert_eq!(value("bevel_profile_spec").as_deref(), Some("smooth;0:0,1:1"), "{flat}");
assert_eq!(value("roll_profile_spec").as_deref(), Some("smooth;0:0,1:0.9"), "{flat}");
- // And the flat legacy spellings still land on the same keys.
+ // The retired spellings — the flat geometry keys, `depth`, and the
+ // knob keys that are editor state — reach NO registry key: a config
+ // that says only these draws the defaults, and the load-time report
+ // (`color::retired_surface_keys`) is what says why.
let old: serde_json::Value = serde_json::json!({
"style": { "surface": { "relief": {
+ "depth": 0.3,
"height": 2.0, "edge_height": 3.0, "profile": "a", "edge_profile": "b",
"wall": { "knobs": "1,1,1" }, "edge_knobs": "2,2,2"
} } }
});
let mut flat = String::new();
flatten_json_to_flat_props(&old, "", &mut flat);
- for k in ["bevel_height", "roll_height", "bevel_profile_spec", "roll_profile_spec"] {
- assert!(flat.lines().any(|l| l.starts_with(&format!("{k} = "))), "{k} missing: {flat}");
+ for k in ["bevel_depth", "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}");
}
- // The knobs are editor state, not style: no registry key of their own.
- assert!(!flat.contains("profile_knobs"), "{flat}");
- }
-
- /// A config carrying both spellings of one relief key is decided by the
- /// current spelling — the node one for the geometry — whichever line
- /// the file wrote first: the flat pass loads in JSON order, and
- /// `prefer_relief_spellings` is what makes the order irrelevant. A key
- /// with no current spelling beside it is left alone. The retired
- /// `depth` is not a pair any more: it never reaches a registry key.
- #[test]
- fn the_current_spelling_of_a_relief_key_wins_over_the_legacy_one() {
- let retired: serde_json::Value = serde_json::json!({
- "style": { "surface": { "relief": { "depth": 0.3 } } }
- });
- let mut flat = String::new();
- flatten_json_to_flat_props(&retired, "", &mut flat);
- assert!(!flat.contains("bevel_depth"), "a retired `depth` flattens to no registry key: {flat}");
- let mut val: serde_json::Value = serde_json::json!({
+ // And a file carrying BOTH spellings is what its current one says,
+ // with no precedence pass in between — the retired one is not read.
+ let both: serde_json::Value = serde_json::json!({
"style": { "surface": { "relief": {
- "light": 0.15,
+ "light": 0.15, "depth": 0.9,
"height": 2.0, "wall": { "height": 5.0 },
- "edge_profile": "old", "edge": { "profile": "new" },
- "profile": "flat-only"
+ "edge_profile": "old", "edge": { "profile": "new" }
} } }
});
- prefer_relief_spellings(&mut val);
- let relief = val.pointer("/style/surface/relief").unwrap();
- assert!(relief.get("height").is_none(), "the flat wall height yields: {relief}");
- assert!(relief.get("edge_profile").is_none(), "the flat edge profile yields: {relief}");
- assert_eq!(relief.pointer("/wall/height").and_then(|v| v.as_f64()), Some(5.0));
- assert_eq!(relief.pointer("/edge/profile").and_then(|v| v.as_str()), Some("new"));
- assert_eq!(relief.get("profile").and_then(|v| v.as_str()), Some("flat-only"), "no node spelling: kept");
- assert_eq!(relief.get("light").and_then(|v| v.as_f64()), Some(0.15));
let mut flat = String::new();
- flatten_json_to_flat_props(&val, "", &mut flat);
+ flatten_json_to_flat_props(&both, "", &mut flat);
assert_eq!(flat.lines().filter(|l| l.starts_with("bevel_height = ")).count(), 1, "{flat}");
- assert!(flat.contains("bevel_height = 5"), "{flat}");
- // A config with no relief block at all is untouched.
- let mut none: serde_json::Value = serde_json::json!({ "style": {} });
- prefer_relief_spellings(&mut none);
- assert_eq!(none, serde_json::json!({ "style": {} }));
+ assert!(flat.contains("bevel_height = 5") && flat.contains("bevel_depth = 0.15"), "{flat}");
+ assert!(flat.contains("roll_profile_spec = \"new\""), "{flat}");
}
/// The control rung's key flattens to `control_corner_radius`, beside a