GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
feat: config-persisted relief profiles
style.surface.relief.profile / edge_profile carry the custom wall and
plate-roll curves as ramp specs. reload_config parses and installs them
after the style registry loads, so every process starts with the styled
walls — not just the editor that wrote them. The identity-smooth spec is
the analytic sentinel (cce-designer's Edge Profile convention): for the
wall curve it coincides with the analytic smoothstep anyway, for the
roll it must not read as a straight chamfer. relief joins PROP_NODES so
config writes land as properties on the existing relief node instead of
growing duplicate child nodes beside depth=/width=.
Co-Authored-By: Claude Fable 5 <[email protected]>
src/config.rs | 26 +++++++++++++++++++++++-
src/layout.rs | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 90 insertions(+), 1 deletion(-)
diff --git a/src/config.rs b/src/config.rs
index 814ee97..5bb7ec6 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -308,7 +308,7 @@ const PROP_NODES: &[&str] = &[
"gestures", "key_bindings", "pointer_bind", "gesture_bind",
"button", "button_strip", "dropdown", "toggle", "spinbox", "slider", "font_selector",
"status", "overlay", "backplate", "desktop", "list", "section", "textbox", "multiline", "editor", "tree",
- "menubar", "statusbar", "node"
+ "menubar", "statusbar", "node", "relief"
];
fn get_or_create_node_mut<'a>(doc: &'a mut kdl::KdlDocument, path: &[&str]) -> Option<&'a mut kdl::KdlNode> {
@@ -702,6 +702,30 @@ mod tests {
assert_eq!(prop_val.as_f64().unwrap(), 0.75);
}
+ #[test]
+ fn relief_keys_write_as_properties_and_round_trip() {
+ // `relief` is a PROP_NODES member: style.surface.relief.* must land as
+ // properties on the existing relief node (the config.kdl shape), not
+ // as duplicate child nodes shadowing the depth=/width= properties.
+ let content = "style {\n surface {\n relief depth=(f64)0.15 width=(f64)9.3\n }\n}\n";
+ let mut doc = content.parse::<kdl::KdlDocument>().unwrap();
+ let spec = "smooth;0.000:0.500,0.400:1.000,1.000:0.000";
+ assert!(update_kdl_in_memory(&mut doc, "style.surface.relief.profile", spec, "style"));
+ assert!(update_kdl_in_memory(&mut doc, "style.surface.relief.depth", "0.3", "style"));
+ let out = doc.to_string();
+ // Still one relief node, no child block grown under it.
+ assert_eq!(out.matches("relief").count(), 1, "out: {out}");
+ assert!(!out.contains("relief {"), "out: {out}");
+
+ // The reload path reads through parse_kdl_to_json: the new property
+ // must surface at the same dotted path the style registry maps.
+ let val = parse_kdl_to_json(&out);
+ let relief = val.get("style").unwrap().get("surface").unwrap().get("relief").unwrap();
+ assert_eq!(relief.get("profile").unwrap().as_str().unwrap(), spec);
+ assert_eq!(relief.get("depth").unwrap().as_f64().unwrap(), 0.3);
+ assert_eq!(relief.get("width").unwrap().as_f64().unwrap(), 9.3);
+ }
+
#[test]
fn test_get_kdl_type_annotation() {
let content = "input {\n accel_profile (\"menu:flat,adaptive,none,custom\")\"flat\"\n touchpad {\n gestures pinch=(bool)true\n }\n}\n";
diff --git a/src/layout.rs b/src/layout.rs
index 6879af9..2d22ee5 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -119,6 +119,10 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
// spelling survives only as a compat alias).
"style.surface.relief.depth" | "window_manager.bevel_depth" => "bevel_depth",
"style.surface.relief.width" | "window_manager.bevel_width" => "bevel_width",
+ // Ramp-spec strings for the custom wall/roll profiles
+ // (written by cce-bevel, installed by reload_config).
+ "style.surface.relief.profile" => "bevel_profile_spec",
+ "style.surface.relief.edge_profile" => "roll_profile_spec",
"style.container.section.depth" => "section_depth",
"window_manager.bevel_shader" => "bevel_shader",
"window_manager.control_relief" => "control_relief",
@@ -1108,9 +1112,44 @@ pub fn reload_config() {
if let Ok(raw_kdl) = std::fs::read_to_string(crate::config::get_config_path()) {
crate::color::reload_colors(&raw_kdl);
}
+ // The configured relief profiles, applied last so every process (not
+ // just the editor that wrote them) starts with the styled walls.
+ apply_relief_profile_config();
}
}
+/// The untouched editor curve — the "analytic" sentinel in the config'd
+/// profile specs (cce-designer's Edge Profile convention). For the wall curve
+/// identity-smooth IS the analytic smoothstep, so skipping it changes
+/// nothing; for the roll it would be a straight chamfer, not the analytic
+/// superellipse quadrant, so it must read as "no custom profile".
+pub const RELIEF_PROFILE_IDENTITY_SPEC: &str = "smooth;0.000:0.000,1.000:1.000";
+
+/// Parse-and-install the relief profiles config carries as ramp specs
+/// (`style.surface.relief.profile` / `edge_profile` → the style registry's
+/// `bevel_profile_spec` / `roll_profile_spec`). Absent, identity, or
+/// unparseable specs clear back to the analytic profiles.
+fn apply_relief_profile_config() {
+ let (wall, edge) = {
+ let reg = get_style_registry().read().unwrap();
+ (reg.get_string("bevel_profile_spec"), reg.get_string("roll_profile_spec"))
+ };
+ match parse_relief_profile_spec(wall.as_deref()) {
+ Some((keys, smooth)) => set_bevel_profile_keys(&keys, smooth),
+ None => clear_bevel_profile(),
+ }
+ match parse_relief_profile_spec(edge.as_deref()) {
+ Some((keys, smooth)) => set_roll_profile_keys(&keys, smooth),
+ None => clear_roll_profile(),
+ }
+}
+
+/// A config'd profile spec → installable keys. `None` (falling back to the
+/// analytic profile) for absent, identity-sentinel, or unparseable specs.
+fn parse_relief_profile_spec(spec: Option<&str>) -> Option<(Vec<(f32, f32)>, bool)> {
+ spec.filter(|s| *s != RELIEF_PROFILE_IDENTITY_SPEC).and_then(crate::widget::parse_ramp_spec)
+}
+
fn mod_rest(rest: &str) -> &str {
let rest = rest.trim();
if rest.starts_with('"') && rest.ends_with('"') && rest.len() >= 2 {
@@ -5932,5 +5971,31 @@ mod tests {
assert!(crate::layout::bevel_profile_slopes().is_none());
assert!(crate::layout::bevel_profile_generation() > gen);
}
+
+ #[test]
+ fn relief_profile_specs_parse_with_identity_sentinel() {
+ // Absent and identity-smooth mean "analytic" — nothing to install.
+ assert!(crate::layout::parse_relief_profile_spec(None).is_none());
+ assert!(crate::layout::parse_relief_profile_spec(Some(
+ crate::layout::RELIEF_PROFILE_IDENTITY_SPEC
+ ))
+ .is_none());
+ // Garbage falls back to analytic instead of poisoning the walls.
+ assert!(crate::layout::parse_relief_profile_spec(Some("not a spec")).is_none());
+ // A real curve installs: keys and line type round-trip.
+ let (keys, smooth) = crate::layout::parse_relief_profile_spec(Some(
+ "linear;0.000:0.200,0.500:1.000,1.000:0.800",
+ ))
+ .expect("custom spec parses");
+ assert!(!smooth);
+ assert_eq!(keys.len(), 3);
+ assert!((keys[1].0 - 0.5).abs() < 0.001 && (keys[1].1 - 1.0).abs() < 0.001);
+ // Identity under a LINEAR line type is a real profile (a straight
+ // chamfer), not the sentinel — only the smooth spelling is analytic.
+ assert!(crate::layout::parse_relief_profile_spec(Some(
+ "linear;0.000:0.000,1.000:1.000"
+ ))
+ .is_some());
+ }
}