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

commit686c57652043fe07384d94ed656edb56bbdb747c
parent9f59fb2436
authorLucas Galante <[email protected]>
date2026-07-03 16:03
feat: support app-specific configuration files override

 src/config.rs                    | 44 +++++++++++++++++++++--
 src/layout.rs                    | 77 ++++++++++++++++++++++++++++++++++++++++
 src/widget/container/treelist.rs | 71 ++++++++++++++++++++++++++++--------
 3 files changed, 176 insertions(+), 16 deletions(-)

diff --git a/src/config.rs b/src/config.rs
index 3e299f9..b7846b1 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -197,12 +197,52 @@ fn kdl_to_json(doc: &kdl::KdlDocument) -> serde_json::Value {
     serde_json::Value::Object(map)
 }
 
+pub fn get_app_name() -> Option<String> {
+    std::env::current_exe()
+        .ok()
+        .and_then(|p| {
+            p.file_name()
+                .and_then(|s| s.to_str().map(|ss| ss.to_string()))
+        })
+}
+
+pub fn get_app_config_path(app_name: &str) -> std::path::PathBuf {
+    get_config_path().parent().unwrap().join(app_name).join("config.kdl")
+}
+
+fn merge_json(a: &mut serde_json::Value, b: &serde_json::Value) {
+    match (a, b) {
+        (serde_json::Value::Object(a_map), serde_json::Value::Object(b_map)) => {
+            for (k, v) in b_map {
+                if !v.is_null() {
+                    merge_json(a_map.entry(k.clone()).or_insert(serde_json::Value::Null), v);
+                }
+            }
+        }
+        (a_val, b_val) => {
+            *a_val = b_val.clone();
+        }
+    }
+}
+
 pub fn parse_kdl_to_json(content: &str) -> serde_json::Value {
-    if let Ok(doc) = content.parse::<kdl::KdlDocument>() {
+    let mut main_val = if let Ok(doc) = content.parse::<kdl::KdlDocument>() {
         kdl_to_json(&doc)
     } else {
         serde_json::json!({})
+    };
+
+    if let Some(app_name) = get_app_name() {
+        let app_path = get_app_config_path(&app_name);
+        if let Ok(override_content) = std::fs::read_to_string(&app_path) {
+            if let Ok(override_doc) = override_content.parse::<kdl::KdlDocument>() {
+                let override_val = kdl_to_json(&override_doc);
+                merge_json(&mut main_val, &override_val);
+            }
+        }
     }
+
+    main_val
 }
 
 pub fn update_json_in_memory(val_obj: &mut Value, key: &str, value: &str, default_section: &str) -> bool {
@@ -339,7 +379,7 @@ pub fn update_kdl_in_memory(doc: &mut kdl::KdlDocument, key: &str, value: &str,
     };
 
     if let Some(ref ext_ty) = existing_ty {
-        if ext_ty.starts_with("menu:") {
+        if ext_ty.starts_with("menu:") || ext_ty == "button" || ext_ty.starts_with("button:") {
             kdl_ty = Some(ext_ty.clone());
         }
     }
diff --git a/src/layout.rs b/src/layout.rs
index 86f3060..b4b2804 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -116,6 +116,7 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
                 "style.data.tree.corner_radius" => "tree_corner_radius",
                 "style.data.tree.opacity" => "tree_opacity",
                 "style.data.tree.blur" => "tree_blur",
+                "style.data.tree.font" => "tree_font",
                 "style.surface.desktop.gap_color" => "desktop_gap_color",
                 "style.surface.desktop.cell_color" => "desktop_cell_color",
                 "style.surface.desktop.gap_width" => "desktop_gap_width",
@@ -269,6 +270,8 @@ static SLIDER_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 static PLATE_CORNER_RADIUS: RwLock<f32> = RwLock::new(12.0);
 static LIST_FONT: RwLock<String> = RwLock::new(String::new());
 static LIST_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
+static TREE_FONT: RwLock<String> = RwLock::new(String::new());
+static TREE_FONT_CACHED: RwLock<Option<(String, f32)>> = RwLock::new(None);
 static LIST_JUSTIFICATION: RwLock<u8> = RwLock::new(0);
 static PLATE_OPACITY: RwLock<f32> = RwLock::new(1.0);
 static PAGE_OPACITY: RwLock<f32> = RwLock::new(1.0);
@@ -311,6 +314,7 @@ pub fn reload_config() {
         let mut spinbox_font_changed = false;
         let mut slider_font_changed = false;
         let mut list_font_changed = false;
+        let mut tree_font_changed = false;
         for line in content.lines() {
             let trimmed = line.trim();
             if let Some(eq_idx) = trimmed.find('=') {
@@ -919,6 +923,21 @@ pub fn reload_config() {
                     list_font_changed = true;
                 }
             }
+            if let Some(rest) = trimmed.strip_prefix("tree_font") {
+                let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
+                let rest = mod_rest(rest);
+                let font = rest.trim().to_string();
+                let mut changed = false;
+                if let Ok(mut lock) = TREE_FONT.write() {
+                    if *lock != font {
+                        *lock = font;
+                        changed = true;
+                    }
+                }
+                if changed {
+                    tree_font_changed = true;
+                }
+            }
             if let Some(rest) = trimmed.strip_prefix("list_justification") {
                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
                 let val_str = rest.trim_end_matches('"').trim();
@@ -984,6 +1003,11 @@ pub fn reload_config() {
                 *lock = None;
             }
         }
+        if tree_font_changed {
+            if let Ok(mut lock) = TREE_FONT_CACHED.write() {
+                *lock = None;
+            }
+        }
         crate::color::reload_colors(&content);
     }
 }
@@ -1960,6 +1984,59 @@ pub fn set_list_font(font: &str) {
     }
 }
 
+// Tree Font
+pub fn tree_font() -> String {
+    use std::sync::Once;
+    static INIT: Once = Once::new();
+    INIT.call_once(|| {
+        let mut font = "Outfit".to_string();
+        if let Some(content) = read_config() {
+            for line in content.lines() {
+                let trimmed = line.trim();
+                if let Some(rest) = trimmed.strip_prefix("tree_font") {
+                    let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=');
+                    let rest = mod_rest(rest);
+                    font = rest.trim().to_string();
+                }
+            }
+        }
+        if let Ok(mut lock) = TREE_FONT.write() {
+            *lock = font;
+        }
+    });
+    let lock = TREE_FONT.read().unwrap();
+    if lock.is_empty() {
+        "Outfit".to_string()
+    } else {
+        lock.clone()
+    }
+}
+
+pub fn tree_font_parsed() -> (String, f32) {
+    if let Ok(lock) = TREE_FONT_CACHED.read() {
+        if let Some(ref val) = *lock {
+            return val.clone();
+        }
+    }
+    let font_str = tree_font();
+    let parsed = parse_font_string(&font_str);
+    let size = parsed.1.unwrap_or(12.0);
+    let val = (parsed.0, size);
+    if let Ok(mut lock) = TREE_FONT_CACHED.write() {
+        *lock = Some(val.clone());
+    }
+    val
+}
+
+pub fn set_tree_font(font: &str) {
+    if let Ok(mut lock) = TREE_FONT.write() {
+        *lock = font.to_string();
+    }
+    if let Ok(mut lock) = TREE_FONT_CACHED.write() {
+        *lock = None;
+    }
+}
+
 // List Justification
 pub fn list_justification() -> u8 {
     use std::sync::Once;
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index 9d33e68..5a4d789 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -339,6 +339,10 @@ impl Element for TreeList {
         true
     }
 
+    fn widget_font(&self) -> Option<String> {
+        Some(crate::layout::tree_font())
+    }
+
     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
         self.base.x = x;
         self.base.y = y;
@@ -814,6 +818,9 @@ impl Element for TreeList {
             ]
         };
 
+        let (_, tree_font_size) = crate::layout::tree_font_parsed();
+        let header_font_size = (tree_font_size - 1.0).max(8.0);
+
         let mut labels = Vec::new();
         let list_left = self.scroll_box.base.x;
         let list_top = self.scroll_box.viewport_y;
@@ -827,21 +834,21 @@ impl Element for TreeList {
             text: "Key".to_string(),
             x: list_left + 8.0,
             y: self.base.y + offset_y + 6.0,
-            font_size: 11.0,
+            font_size: header_font_size,
             color: [200, 200, 210],
         });
         labels.push(TextLabel {
             text: "Type".to_string(),
             x: list_left + 180.0 + 8.0,
             y: self.base.y + offset_y + 6.0,
-            font_size: 11.0,
+            font_size: header_font_size,
             color: [200, 200, 210],
         });
         labels.push(TextLabel {
             text: "Value".to_string(),
             x: list_left + 235.0 + 8.0,
             y: self.base.y + offset_y + 6.0,
-            font_size: 11.0,
+            font_size: header_font_size,
             color: [200, 200, 210],
         });
 
@@ -859,7 +866,7 @@ impl Element for TreeList {
                             text: display_text,
                             x: list_left + 8.0 + *indent as f32 * 12.0,
                             y: row_y + 6.0,
-                            font_size: 12.0,
+                            font_size: tree_font_size,
                             color: f32_to_rgb(crate::color::tree_section_text_color()),
                         });
                     }
@@ -883,7 +890,7 @@ impl Element for TreeList {
                             text: name.clone(),
                             x: list_left + 8.0 + *indent as f32 * 12.0,
                             y: row_y + 6.0,
-                            font_size: 12.0,
+                            font_size: tree_font_size,
                             color,
                         });
                     }
@@ -919,6 +926,8 @@ impl Element for TreeList {
                     if let Some(Some(ref anno)) = self.annotations.get(*original_idx) {
                         if anno.starts_with("menu:") {
                             display_ty = Some("menu".to_string());
+                        } else if anno == "button" || anno.starts_with("button:") {
+                            display_ty = Some("button".to_string());
                         } else {
                             display_ty = Some(anno.clone());
                         }
@@ -934,12 +943,19 @@ impl Element for TreeList {
                             text: ty_text,
                             x: list_left + 190.0,
                             y: row_y + 6.0,
-                            font_size: 12.0,
+                            font_size: tree_font_size,
                             color: f32_to_rgb(crate::color::tree_type_text_color()),
                         });
                     }
 
                     if Some(*original_idx) != self.selected_key_idx {
+                        let mut is_button = false;
+                        if let Some(Some(ref anno)) = self.annotations.get(*original_idx) {
+                            if anno == "button" || anno.starts_with("button:") {
+                                is_button = true;
+                            }
+                        }
+
                         let is_color = if let serde_json::Value::String(s) = val {
                             s.starts_with('#')
                         } else {
@@ -952,13 +968,23 @@ impl Element for TreeList {
                             list_left + 245.0
                         };
 
-                        labels.push(TextLabel {
-                            text: display_val,
-                            x: label_x,
-                            y: row_y + 6.0,
-                            font_size: 12.0,
-                            color: f32_to_rgb(crate::color::tree_value_text_color()),
-                        });
+                        if is_button {
+                            labels.push(TextLabel {
+                                text: display_val,
+                                x: list_left + 245.0 + 8.0,
+                                y: row_y + 6.0,
+                                font_size: tree_font_size,
+                                color: [240, 240, 245],
+                            });
+                        } else {
+                            labels.push(TextLabel {
+                                text: display_val,
+                                x: label_x,
+                                y: row_y + 6.0,
+                                font_size: tree_font_size,
+                                color: f32_to_rgb(crate::color::tree_value_text_color()),
+                            });
+                        }
                     }
                 }
             }
@@ -1161,8 +1187,25 @@ impl Element for TreeList {
                 quads.push((list_left + 180.0, draw_y, 1.0, draw_h, 0.0, apply_opacity(separator_color), (false, false, false, false)));
                 quads.push((list_left + 235.0, draw_y, 1.0, draw_h, 0.0, apply_opacity(separator_color), (false, false, false, false)));
 
+                let mut is_button = false;
+                if let Some(Some(ref anno)) = self.annotations.get(*original_idx) {
+                    if anno == "button" || anno.starts_with("button:") {
+                        is_button = true;
+                    }
+                }
+
                 if Some(*original_idx) != self.selected_key_idx {
-                    if let serde_json::Value::String(s) = val {
+                    if is_button {
+                        let btn_x = list_left + 245.0;
+                        let btn_y = row_y + 1.0;
+                        let btn_bottom = (row_y + 27.0).min(list_bottom);
+                        let btn_draw_y = btn_y.max(list_top);
+                        let btn_draw_h = btn_bottom - btn_draw_y;
+                        if btn_draw_h > 0.0 {
+                            let btn_bg = [0.10, 0.29, 0.33, 0.65]; // theme button color
+                            quads.push((btn_x, btn_draw_y, 125.0, btn_draw_h, 4.0, apply_opacity(btn_bg), (true, true, true, true)));
+                        }
+                    } else if let serde_json::Value::String(s) = val {
                         if s.starts_with('#') {
                             if let Some(rgba) = parse_hex_f32(s) {
                                 let preview_x = list_left + 245.0;