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

commit5426835f785ad36dff66b8d1d46a217a6fc9341b
parent1805a6ee6a
authorLucas Galante <[email protected]>
date2026-06-29 14:24
Group configuration styling properties into nested nodes (Option C)

 src/color.rs  |  62 ++++++++++++++-------------
 src/config.rs | 133 ++++++++++++++++++++++++++++++++++++++++++++++++++++++----
 src/layout.rs | 129 +++++++++++++++++++++++++++++++++++++-------------------
 3 files changed, 243 insertions(+), 81 deletions(-)

diff --git a/src/color.rs b/src/color.rs
index 4539bc0..83fa3b7 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -86,28 +86,44 @@ fn read_config() -> Option<String> {
     None
 }
 
-fn parse_and_set_colors(content: &str) {
-    let val: serde_json::Value = match serde_json::from_str(content) {
-        Ok(v) => v,
-        Err(e) => {
-            log::error!("JSON parse error: {}", e);
-            return;
+fn get_config_val<'a>(val: &'a serde_json::Value, pointer: &str) -> Option<&'a serde_json::Value> {
+    if let Some(v) = val.pointer(pointer) {
+        return Some(v);
+    }
+    let parts: Vec<&str> = pointer.trim_start_matches('/').split('/').collect();
+    if parts.len() == 2 {
+        let section = parts[0];
+        let key = parts[1];
+        let (target_sec, target_node, target_prop) = crate::config::map_legacy_key(key, section);
+        if let Some(sec_val) = val.get(&target_sec) {
+            if let Some(node_val) = sec_val.get(&target_node) {
+                if let Some(prop_name) = target_prop {
+                    return node_val.get(&prop_name);
+                } else {
+                    return Some(node_val);
+                }
+            }
         }
-    };
+    }
+    None
+}
 
-    if let Some(opacity) = val.pointer("/layout/menubar_opacity").and_then(|v| v.as_f64()) {
+fn parse_and_set_colors(content: &str) {
+    let val = crate::config::parse_kdl_to_json(content);
+
+    if let Some(opacity) = get_config_val(&val, "/layout/menubar_opacity").and_then(|v| v.as_f64()) {
         if let Ok(mut lock) = OPACITY.write() {
             *lock = Some(opacity as f32);
         }
     }
 
-    if let Some(w_opacity) = val.pointer("/surfaces/backplate_opacity").or_else(|| val.pointer("/surfaces/window_opacity")).and_then(|v| v.as_f64()) {
+    if let Some(w_opacity) = get_config_val(&val, "/surfaces/backplate_opacity").or_else(|| get_config_val(&val, "/surfaces/window_opacity")).and_then(|v| v.as_f64()) {
         if let Ok(mut lock) = BACKPLATE_OPACITY.write() {
             *lock = Some(w_opacity as f32);
         }
     }
 
-    if let Some(radius) = val.pointer("/surfaces/backplate_corner_radius").or_else(|| val.pointer("/surfaces/window_corner_radius")).and_then(|v| v.as_f64()) {
+    if let Some(radius) = get_config_val(&val, "/surfaces/backplate_corner_radius").or_else(|| get_config_val(&val, "/surfaces/window_corner_radius")).and_then(|v| v.as_f64()) {
         if let Ok(mut lock) = BACKPLATE_CORNER_RADIUS.write() {
             *lock = radius as f32;
         }
@@ -145,7 +161,7 @@ fn parse_and_set_colors(content: &str) {
     };
 
     let get_color = |pointer: &str| -> Option<[f32; 4]> {
-        val.pointer(pointer).and_then(|v| v.as_str()).and_then(parse_hex)
+        get_config_val(&val, pointer).and_then(|v| v.as_str()).and_then(parse_hex)
     };
 
     if let Some(c) = get_color("/surfaces/backplate_color").or_else(|| get_color("/surfaces/window_color")).or_else(|| get_color("/layout/page_low_color")) {
@@ -232,11 +248,7 @@ fn load_colors_once() {
 }
 
 pub fn reload_colors(content: &str) {
-    if serde_json::from_str::<serde_json::Value>(content).is_ok() {
-        parse_and_set_colors(content);
-    } else if let Some(raw) = read_config() {
-        parse_and_set_colors(&raw);
-    }
+    parse_and_set_colors(content);
 }
 
 pub fn page_low_color() -> [f32; 4] {
@@ -531,10 +543,7 @@ pub fn active_window_mode() -> String {
         Ok(c) => c,
         Err(_) => return "floating".to_string(),
     };
-    let val: serde_json::Value = match serde_json::from_str(&content) {
-        Ok(v) => v,
-        Err(_) => return "floating".to_string(),
-    };
+    let val = crate::config::parse_kdl_to_json(&content);
 
     if let Some(mode_rules) = val.get("mode_rule").and_then(|r| r.as_array()) {
         for rule in mode_rules {
@@ -574,10 +583,7 @@ pub fn active_backplate_opacity() -> f32 {
         Ok(c) => c,
         Err(_) => return 0.9,
     };
-    let val: serde_json::Value = match serde_json::from_str(&content) {
-        Ok(v) => v,
-        Err(_) => return 0.9,
-    };
+    let val = crate::config::parse_kdl_to_json(&content);
 
     let key = match mode.as_str() {
         "fullscreen" => "fullscreen_backplate_opacity",
@@ -589,13 +595,13 @@ pub fn active_backplate_opacity() -> f32 {
         _ => "window_backplate_opacity",
     };
 
-    if let Some(opacity) = val.pointer(&format!("/layout/{}", key)).and_then(|v| v.as_f64()) {
+    if let Some(opacity) = get_config_val(&val, &format!("/layout/{}", key)).and_then(|v| v.as_f64()) {
         return opacity as f32;
     }
 
     // Modern surfaces fallback:
-    if let Some(opacity) = val.pointer("/surfaces/backplate_opacity")
-        .or_else(|| val.pointer("/surfaces/window_opacity"))
+    if let Some(opacity) = get_config_val(&val, "/surfaces/backplate_opacity")
+        .or_else(|| get_config_val(&val, "/surfaces/window_opacity"))
         .and_then(|v| v.as_f64()) {
         return opacity as f32;
     }
@@ -611,7 +617,7 @@ pub fn active_backplate_opacity() -> f32 {
         _ => "window_opacity",
     };
 
-    if let Some(opacity) = val.pointer(&format!("/layout/{}", legacy_key)).and_then(|v| v.as_f64()) {
+    if let Some(opacity) = get_config_val(&val, &format!("/layout/{}", legacy_key)).and_then(|v| v.as_f64()) {
         return opacity as f32;
     }
 
diff --git a/src/config.rs b/src/config.rs
index 3744676..6fa9306 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -133,11 +133,82 @@ pub fn update_json_in_memory(val_obj: &mut Value, key: &str, value: &str, defaul
     updated
 }
 
+pub fn map_legacy_key(key: &str, section: &str) -> (String, String, Option<String>) {
+    let parts: Vec<&str> = key.split('.').collect();
+    if parts.len() == 3 {
+        return (parts[0].to_string(), parts[1].to_string(), Some(parts[2].to_string()));
+    } else if parts.len() == 2 {
+        return (parts[0].to_string(), parts[1].to_string(), None);
+    }
+    
+    if section == "layout" {
+        match key {
+            "border_color" => ("style".to_string(), "border".to_string(), Some("color".to_string())),
+            "border_width" => ("style".to_string(), "border".to_string(), Some("width".to_string())),
+            "border_blur" => ("style".to_string(), "border".to_string(), Some("blur".to_string())),
+            "border_font_size" => ("style".to_string(), "border".to_string(), Some("font_size".to_string())),
+            "fullscreen_border_width" => ("style".to_string(), "border".to_string(), Some("fullscreen_border_width".to_string())),
+            "cascade_border_width" => ("style".to_string(), "border".to_string(), Some("cascade_border_width".to_string())),
+            "grid_border_width" => ("style".to_string(), "border".to_string(), Some("grid_border_width".to_string())),
+            "floating_border_width" => ("style".to_string(), "border".to_string(), Some("floating_border_width".to_string())),
+            
+            "background_color" => ("style".to_string(), "background".to_string(), Some("color".to_string())),
+            
+            "button_font" => ("style".to_string(), "button".to_string(), Some("font".to_string())),
+            "button_padding" => ("style".to_string(), "button".to_string(), Some("padding".to_string())),
+            
+            "button_strip_font" => ("style".to_string(), "button_strip".to_string(), Some("font".to_string())),
+            "button_strip_spacing" => ("style".to_string(), "button_strip".to_string(), Some("spacing".to_string())),
+            
+            "dropdown_height" => ("style".to_string(), "dropdown".to_string(), Some("height".to_string())),
+            "font_selector_height" => ("style".to_string(), "font_selector".to_string(), Some("height".to_string())),
+            "label_font" => ("style".to_string(), "label".to_string(), Some("font".to_string())),
+            
+            "slider_height" => ("style".to_string(), "slider".to_string(), Some("height".to_string())),
+            "slider_corner_radius" => ("style".to_string(), "slider".to_string(), Some("corner_radius".to_string())),
+            
+            "spinbox_height" => ("style".to_string(), "spinbox".to_string(), Some("height".to_string())),
+            "textbox_height" => ("style".to_string(), "textbox".to_string(), Some("height".to_string())),
+            
+            "toggle_font" => ("style".to_string(), "toggle".to_string(), Some("font".to_string())),
+            "toggle_height" => ("style".to_string(), "toggle".to_string(), Some("height".to_string())),
+            "toggle_border_width" => ("style".to_string(), "toggle".to_string(), Some("border_width".to_string())),
+            "toggle_disabled_color" => ("style".to_string(), "toggle".to_string(), Some("disabled_color".to_string())),
+            
+            "status_normal_color" => ("style".to_string(), "status".to_string(), Some("normal_color".to_string())),
+            "status_box_opacity" => ("style".to_string(), "status".to_string(), Some("box_opacity".to_string())),
+            
+            "primary_highlight_color" => ("style".to_string(), "highlight".to_string(), Some("primary".to_string())),
+            
+            "window_opacity" => ("style".to_string(), "window".to_string(), Some("opacity".to_string())),
+            "floating_backplate_opacity" => ("style".to_string(), "window".to_string(), Some("floating_backplate_opacity".to_string())),
+            "window_blur" => ("style".to_string(), "window".to_string(), Some("blur".to_string())),
+            "page_opacity" => ("style".to_string(), "window".to_string(), Some("page_opacity".to_string())),
+            "page_margin" => ("style".to_string(), "window".to_string(), Some("page_margin".to_string())),
+            "plate_padding" => ("style".to_string(), "window".to_string(), Some("plate_padding".to_string())),
+            "transition_duration" => ("style".to_string(), "window".to_string(), Some("transition_duration".to_string())),
+            
+            "overlay_behavior" => ("style".to_string(), "overlay".to_string(), Some("behavior".to_string())),
+            "overlay_width" => ("style".to_string(), "overlay".to_string(), Some("width".to_string())),
+            "overlay_position" => ("style".to_string(), "overlay".to_string(), Some("position".to_string())),
+            "overlay_border_gap" => ("style".to_string(), "overlay".to_string(), Some("border_gap".to_string())),
+            
+            "last_page" => ("style".to_string(), "editor".to_string(), Some("last_page".to_string())),
+            
+            _ => (section.to_string(), key.to_string(), None),
+        }
+    } else {
+        (section.to_string(), key.to_string(), None)
+    }
+}
+
 pub fn update_kdl_in_memory(doc: &mut kdl::KdlDocument, key: &str, value: &str, default_section: &str) -> bool {
-    let section_node = if let Some(node) = doc.nodes_mut().iter_mut().find(|n| n.name().value() == default_section) {
+    let (target_section, target_node, target_prop) = map_legacy_key(key, default_section);
+    
+    let section_node = if let Some(node) = doc.nodes_mut().iter_mut().find(|n| n.name().value() == target_section) {
         node
     } else {
-        if let Ok(new_node) = format!("{}\n", default_section).parse::<kdl::KdlNode>() {
+        if let Ok(new_node) = format!("{}\n", target_section).parse::<kdl::KdlNode>() {
             doc.nodes_mut().push(new_node);
             doc.nodes_mut().last_mut().unwrap()
         } else {
@@ -147,10 +218,10 @@ pub fn update_kdl_in_memory(doc: &mut kdl::KdlDocument, key: &str, value: &str,
 
     let children = section_node.ensure_children();
 
-    let child_node = if let Some(child) = children.nodes_mut().iter_mut().find(|n| n.name().value() == key) {
+    let child_node = if let Some(child) = children.nodes_mut().iter_mut().find(|n| n.name().value() == target_node) {
         child
     } else {
-        if let Ok(new_child) = format!("{}\n", key).parse::<kdl::KdlNode>() {
+        if let Ok(new_child) = format!("{}\n", target_node).parse::<kdl::KdlNode>() {
             children.nodes_mut().push(new_child);
             children.nodes_mut().last_mut().unwrap()
         } else {
@@ -175,12 +246,35 @@ pub fn update_kdl_in_memory(doc: &mut kdl::KdlDocument, key: &str, value: &str,
         (kdl::KdlValue::String(s), None)
     };
 
-    child_node.entries_mut().clear();
-    let mut entry = kdl::KdlEntry::new(kdl_val);
-    if let Some(ty) = kdl_ty {
-        entry.set_ty(ty);
+    if let Some(prop_name) = target_prop {
+        let mut found = false;
+        for entry in child_node.entries_mut() {
+            if let Some(id) = entry.name() {
+                if id.value() == prop_name {
+                    *entry = kdl::KdlEntry::new_prop(prop_name.clone(), kdl_val.clone());
+                    if let Some(ty) = kdl_ty {
+                        entry.set_ty(ty);
+                    }
+                    found = true;
+                    break;
+                }
+            }
+        }
+        if !found {
+            let mut entry = kdl::KdlEntry::new_prop(prop_name, kdl_val);
+            if let Some(ty) = kdl_ty {
+                entry.set_ty(ty);
+            }
+            child_node.entries_mut().push(entry);
+        }
+    } else {
+        child_node.entries_mut().clear();
+        let mut entry = kdl::KdlEntry::new(kdl_val);
+        if let Some(ty) = kdl_ty {
+            entry.set_ty(ty);
+        }
+        child_node.entries_mut().push(entry);
     }
-    child_node.entries_mut().push(entry);
 
     true
 }
@@ -251,3 +345,24 @@ pub fn write_config_value(path: &str, key: &str, value: &str, default_section: &
     }
     false
 }
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+    
+    #[test]
+    fn test_nested_parsing() {
+        let content = "style {\n    status box_opacity=(f64)0.75\n}\n";
+        let val = parse_kdl_to_json(content);
+        println!("val = {:?}", val);
+        let (sec, node, prop) = map_legacy_key("status_box_opacity", "layout");
+        assert_eq!(sec, "style");
+        assert_eq!(node, "status");
+        assert_eq!(prop, Some("box_opacity".to_string()));
+        
+        let sec_val = val.get(&sec).unwrap();
+        let node_val = sec_val.get(&node).unwrap();
+        let prop_val = node_val.get(prop.as_ref().unwrap()).unwrap();
+        assert_eq!(prop_val.as_f64().unwrap(), 0.75);
+    }
+}
diff --git a/src/layout.rs b/src/layout.rs
index c989b30..6666a2f 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -2,55 +2,96 @@ use crate::widget::Element;
 use crate::context::UiContext;
 use std::sync::RwLock;
 
-fn read_config() -> Option<String> {
-    let path = crate::config::get_config_path();
-    if let Ok(content) = std::fs::read_to_string(&path) {
-        if let Ok(val) = serde_json::from_str::<serde_json::Value>(&content) {
-                let mut toml_like = String::new();
-                if let Some(layout) = val.get("layout").and_then(|l| l.as_object()) {
-                    for (k, v) in layout {
-                        if let Some(s) = v.as_str() {
-                            toml_like.push_str(&format!("{} = \"{}\"\n", k, s));
-                        } else if let Some(b) = v.as_bool() {
-                            toml_like.push_str(&format!("{} = {}\n", k, b));
-                        } else if let Some(n) = v.as_f64() {
-                            toml_like.push_str(&format!("{} = {}\n", k, n));
-                        } else if let Some(n) = v.as_i64() {
-                            toml_like.push_str(&format!("{} = {}\n", k, n));
-                        }
-                    }
-                }
-                if let Some(notifications) = val.get("notifications").and_then(|n| n.as_object()) {
-                    toml_like.push_str("[notifications]\n");
-                    for (k, v) in notifications {
-                        if let Some(s) = v.as_str() {
-                            toml_like.push_str(&format!("{} = \"{}\"\n", k, s));
-                        } else if let Some(b) = v.as_bool() {
-                            toml_like.push_str(&format!("{} = {}\n", k, b));
-                        } else if let Some(n) = v.as_f64() {
-                            toml_like.push_str(&format!("{} = {}\n", k, n));
-                        } else if let Some(n) = v.as_i64() {
-                            toml_like.push_str(&format!("{} = {}\n", k, n));
-                        }
-                    }
-                }
-                if let Some(transparency) = val.get("transparency").and_then(|t| t.as_object()) {
-                    toml_like.push_str("[transparency]\n");
-                    for (k, v) in transparency {
-                        if let Some(s) = v.as_str() {
-                            toml_like.push_str(&format!("{} = \"{}\"\n", k, s));
-                        } else if let Some(b) = v.as_bool() {
-                            toml_like.push_str(&format!("{} = {}\n", k, b));
-                        } else if let Some(n) = v.as_f64() {
-                            toml_like.push_str(&format!("{} = {}\n", k, n));
-                        } else if let Some(n) = v.as_i64() {
-                            toml_like.push_str(&format!("{} = {}\n", k, n));
+fn flatten_map_json(val: &serde_json::Value, prefix: &str, toml_like: &mut String) {
+    match val {
+        serde_json::Value::Object(map) => {
+            for (k, v) in map {
+                let next_prefix = if prefix.is_empty() {
+                    k.clone()
+                } else {
+                    format!("{}.{}", prefix, k)
+                };
+                flatten_map_json(v, &next_prefix, toml_like);
+            }
+        }
+        _ => {
+            let flat_key = match prefix {
+                "style.border.color" => "border_color",
+                "style.border.width" => "border_width",
+                "style.border.blur" => "border_blur",
+                "style.border.font_size" => "border_font_size",
+                "style.border.fullscreen_border_width" => "fullscreen_border_width",
+                "style.border.cascade_border_width" => "cascade_border_width",
+                "style.border.grid_border_width" => "grid_border_width",
+                "style.border.floating_border_width" => "floating_border_width",
+                "style.background.color" => "background_color",
+                "style.button.font" => "button_font",
+                "style.button.padding" => "button_padding",
+                "style.button_strip.font" => "button_strip_font",
+                "style.button_strip.spacing" => "button_strip_spacing",
+                "style.dropdown.height" => "dropdown_height",
+                "style.font_selector.height" => "font_selector_height",
+                "style.label.font" => "label_font",
+                "style.slider.height" => "slider_height",
+                "style.slider.corner_radius" => "slider_corner_radius",
+                "style.spinbox.height" => "spinbox_height",
+                "style.textbox.height" => "textbox_height",
+                "style.toggle.font" => "toggle_font",
+                "style.toggle.height" => "toggle_height",
+                "style.toggle.border_width" => "toggle_border_width",
+                "style.toggle.disabled_color" => "toggle_disabled_color",
+                "style.status.normal_color" => "status_normal_color",
+                "style.status.box_opacity" => "status_box_opacity",
+                "style.highlight.primary" => "primary_highlight_color",
+                "style.window.opacity" => "window_opacity",
+                "style.window.floating_backplate_opacity" => "floating_backplate_opacity",
+                "style.window.blur" => "window_blur",
+                "style.window.page_opacity" => "page_opacity",
+                "style.window.page_margin" => "page_margin",
+                "style.window.plate_padding" => "plate_padding",
+                "style.window.transition_duration" => "transition_duration",
+                "style.overlay.behavior" => "overlay_behavior",
+                "style.overlay.width" => "overlay_width",
+                "style.overlay.position" => "overlay_position",
+                "style.overlay.border_gap" => "overlay_border_gap",
+                "style.editor.last_page" => "last_page",
+                
+                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
                         }
                     }
                 }
-                return Some(toml_like);
+            };
+            
+            if let Some(s) = val.as_str() {
+                toml_like.push_str(&format!("{} = \"{}\"\n", flat_key, s));
+            } else if let Some(b) = val.as_bool() {
+                toml_like.push_str(&format!("{} = {}\n", flat_key, b));
+            } else if let Some(n) = val.as_f64() {
+                toml_like.push_str(&format!("{} = {}\n", flat_key, n));
+            } else if let Some(n) = val.as_i64() {
+                toml_like.push_str(&format!("{} = {}\n", flat_key, n));
+            }
         }
     }
+}
+
+fn read_config() -> Option<String> {
+    let path = crate::config::get_config_path();
+    if let Ok(content) = std::fs::read_to_string(&path) {
+        let val = crate::config::parse_kdl_to_json(&content);
+        let mut toml_like = String::new();
+        flatten_map_json(&val, "", &mut toml_like);
+        return Some(toml_like);
+    }
     None
 }