GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
fix(config): another program's keys never become the toolkit's
The flatten maps the config paths the toolkit reads to its own keys. An
unmapped path was cut down: the compositor's `layout` and `transparency`
blocks to their bare keys, anything else past its first segment. So the
compositor's `layout { grid_gap 18 }`, its window-tiling gap, arrived as the
toolkit's `grid_gap`, and any block's key could stand in for a toolkit one.
An unmapped path now keeps its whole name (`layout.grid_gap`); mapped paths
and flat top-level keys are unchanged. In the live config and every per-app
override, layout.grid_gap was the only path that reached a key the toolkit
reads this way. another_programs_keys_do_not_become_the_toolkits.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
CLAUDE.md | 9 +++++++--
src/layout/mod.rs | 20 ++++++++++++++++++++
src/layout/registry.rs | 21 ++++++++-------------
3 files changed, 35 insertions(+), 15 deletions(-)
diff --git a/CLAUDE.md b/CLAUDE.md
index dfea012..68b0e29 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -1809,9 +1809,14 @@ cce-system-interface) to confirm behavior, not just the test suite.
first use; a `(mm)` length never reached a slot. Do not add a slot for a config key.
One thing it changed on screen: those first-use scans ran after an app's own setter and
overwrote it, so cce-system-interface's `set_grid_gap(root_plate_gap())` lost to the
- config's `layout { grid_gap 18 }` — the compositor's window-tiling gap, which reaches the
- toolkit because the flatten strips `layout.` off keys it does not map. Its multi-column
+ config's `layout { grid_gap 18 }` — the compositor's window-tiling gap. Its multi-column
pages now stand their sections the root gap apart, as the app asks (kept by choice).
+ **And another program's key never becomes the toolkit's**: the flatten maps the paths
+ the toolkit reads to its keys and leaves every other path whole (`layout.grid_gap`).
+ Until the same day it cut an unmapped path down — the compositor's `layout` and
+ `transparency` blocks to their bare keys, anything else past its first segment — which
+ is how the tiling gap arrived as `grid_gap`. A new key the toolkit reads from a nested
+ block needs its mapping; `another_programs_keys_do_not_become_the_toolkits`.
- `bridge.rs` — the flat-host render bridge: `RenderTarget`, `PopoverCollector`,
`render_widget`, `render_popovers`, the carve types that cross it.
- `section.rs` — a settings page's sections: `PageFlow` places them (a masonry of
diff --git a/src/layout/mod.rs b/src/layout/mod.rs
index 055d8d7..5496bde 100644
--- a/src/layout/mod.rs
+++ b/src/layout/mod.rs
@@ -2147,6 +2147,26 @@ mod tests {
assert_eq!(textbox_height(), len.to_px(), "a length in mm is honoured");
}
+ /// Another program's config keeps its own name in the registry: the compositor's
+ /// `layout { grid_gap 18 }` (its window-tiling gap) is `layout.grid_gap`, never the
+ /// toolkit's `grid_gap`, which the settings app's section flow reads. A path the
+ /// toolkit maps still lands on its key, and a flat top-level key stays flat.
+ #[test]
+ fn another_programs_keys_do_not_become_the_toolkits() {
+ let val = crate::config::parse_kdl_to_json(
+ "layout {\n grid_gap (i64)18\n gap (i64)48\n}\ntransparency {\n plate_opacity (f64)0.5\n}\nwindow_manager {\n control_relief (bool)true\n}\nstyle {\n control {\n button height=(i64)30\n }\n}\nplate_corner_radius (i64)14\n",
+ );
+ let mut flat = String::new();
+ super::flatten_json_to_flat_props(&val, "", &mut flat);
+ let keys: Vec<&str> = flat.lines().filter_map(|l| l.split('=').next()).map(str::trim).collect();
+ for leaked in ["grid_gap", "gap", "plate_opacity"] {
+ assert!(!keys.contains(&leaked), "{leaked} leaked into the toolkit's keys: {keys:?}");
+ }
+ for kept in ["layout.grid_gap", "layout.gap", "transparency.plate_opacity", "control_relief", "button_height", "plate_corner_radius"] {
+ assert!(keys.contains(&kept), "{kept} missing: {keys:?}");
+ }
+ }
+
#[test]
fn test_column_gap() {
// A legacy key: set (by config or setter) it is honoured; unset it
diff --git a/src/layout/registry.rs b/src/layout/registry.rs
index 1ec8af1..d9abbac 100644
--- a/src/layout/registry.rs
+++ b/src/layout/registry.rs
@@ -416,19 +416,14 @@ pub(super) fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str,
"style.surface.plate.border_thickness" => "plate_border_thickness",
"input.touchpad.natural_scroll" => "touchpad_natural_scroll",
- other => {
- if let Some(rest) = other.strip_prefix("layout.") {
- rest
- } else if let Some(rest) = other.strip_prefix("transparency.") {
- rest
- } else {
- if let Some(idx) = other.find('.') {
- &other[idx + 1..]
- } else {
- other
- }
- }
- }
+ // Anything else keeps its whole path. A key the toolkit reads is mapped above
+ // (or is a flat top-level key, which has no path to strip); the rest belong
+ // to other programs. Until 2026-10-08 an unmapped path was cut down — the
+ // `layout.` and `transparency.` blocks (the compositor's own) to their bare
+ // keys, anything else past its first segment — so the compositor's
+ // `layout { grid_gap 18 }`, its window-tiling gap, arrived as the toolkit's
+ // `grid_gap`, and any block's key could stand in for a toolkit one.
+ other => other,
};
if let Some(s) = val.as_str() {