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

commit1355cb903056179b7babd1faecfa385a109c9609
parent3c91c9c1f0
authorLucas Galante <[email protected]>
date2026-07-03 10:22
Fix status bar modules cursor resize hover logic, add color tests, improve dropdown rendering, and treelist editing

 src/backend/window_runner.rs     |   4 +-
 src/color.rs                     |  19 +++
 src/config.rs                    |   2 +-
 src/widget/container/treelist.rs | 243 ++++++++++++++++++++++++++++++++++++---
 src/widget/input/dropdown.rs     |  35 ++++++
 5 files changed, 285 insertions(+), 18 deletions(-)

diff --git a/src/backend/window_runner.rs b/src/backend/window_runner.rs
index ccd7db2..a71d7ca 100644
--- a/src/backend/window_runner.rs
+++ b/src/backend/window_runner.rs
@@ -2025,7 +2025,7 @@ impl<A: Application> PointerHandler for EngineState<A> {
             
             match &event.kind {
                 PointerEventKind::Enter { .. } => {
-                    let is_status_bar = self.inner.settings().app_id == "cce-status";
+                    let is_status_bar = self.inner.settings().app_id.starts_with("cce-status");
                     let mut cursor_icon = CursorIcon::Default;
                     if !is_status_bar {
                         let border = 8.0f32;
@@ -2071,7 +2071,7 @@ impl<A: Application> PointerHandler for EngineState<A> {
                         self.redraw = true;
                     }
 
-                    let is_status_bar = self.inner.settings().app_id == "cce-status";
+                    let is_status_bar = self.inner.settings().app_id.starts_with("cce-status");
                     let mut cursor_icon = CursorIcon::Default;
                     if !is_status_bar {
                         let border = 8.0f32;
diff --git a/src/color.rs b/src/color.rs
index 85c70f7..9f18c86 100644
--- a/src/color.rs
+++ b/src/color.rs
@@ -1085,3 +1085,22 @@ pub fn set_backplate_statusbar_blur(b: bool) {
     if let Ok(mut lock) = BACKPLATE_STATUSBAR_BLUR.write() { *lock = b; }
 }
 
+#[cfg(test)]
+mod color_tests {
+    use super::*;
+
+    #[test]
+    fn test_print_active_config() {
+        let path = crate::config::get_config_path();
+        println!("ACTIVE CONFIG PATH: {:?}", path);
+        if let Ok(content) = std::fs::read_to_string(&path) {
+            println!("FILE READ OK! Length: {}", content.len());
+            let val = crate::config::parse_kdl_to_json(&content);
+            println!("PARSED JSON POINTER: {:?}", val.pointer("/style/control/dropdown/color"));
+        } else {
+            println!("FILE READ FAILED!");
+        }
+        println!("DROPDOWN COLOR GETTER: {:?}", dropdown_background_color());
+    }
+}
+
diff --git a/src/config.rs b/src/config.rs
index f7273d6..3e299f9 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -614,7 +614,7 @@ mod tests {
         assert_eq!(crate::color::backplate_menubar_blur(), true);
         
         let dd_color = crate::color::dropdown_background_color();
-        assert!(dd_color[0] > 0.0);
+        assert!((dd_color[0] - crate::color::srgb_to_linear(8.0 / 255.0)).abs() < 0.0001);
         
         let placeholder_color = crate::color::textbox_placeholder_text_color();
         assert_eq!(placeholder_color, [0x60, 0x60, 0x6a]);
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index af5d387..9d33e68 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -179,6 +179,10 @@ pub struct TreeList {
     pub last_scroll_y: f32,
     pub scrollbar_activity_timer: f32,
     pub deleted_key_path: Option<String>,
+    pub edit_box: TextBox,
+    pub editing_key_idx: Option<usize>,
+    pub double_click_timer: Option<(std::time::Instant, usize)>,
+    pub rename_request: Option<(String, String)>,
 }
 
 impl TreeList {
@@ -203,6 +207,10 @@ impl TreeList {
             last_scroll_y: 0.0,
             scrollbar_activity_timer: 0.0,
             deleted_key_path: None,
+            edit_box: TextBox::new(String::new()).with_multiline(false).with_draw_bg_border(true),
+            editing_key_idx: None,
+            double_click_timer: None,
+            rename_request: None,
         }
     }
 
@@ -255,6 +263,10 @@ impl TreeList {
         self.deleted_key_path.take()
     }
 
+    pub fn take_rename_request(&mut self) -> Option<(String, String)> {
+        self.rename_request.take()
+    }
+
     pub fn check_scroll_activity(&mut self, ctx: &mut UiContext) {
         if (self.scroll_box.scroll_y - self.last_scroll_y).abs() > 0.01 {
             self.scrollbar_activity_timer = 1.0;
@@ -376,9 +388,27 @@ impl Element for TreeList {
     fn prepare_text(&mut self, fs: &mut glyphon::FontSystem) {
         self.search_box.prepare_text(fs);
         self.scroll_box.prepare_text(fs);
+        if self.editing_key_idx.is_some() {
+            self.edit_box.prepare_text(fs);
+        }
     }
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
+        if self.editing_key_idx.is_some() {
+            if button == MouseButton::Left && state == ElementState::Pressed {
+                let (ex, ey, ew, eh) = self.edit_box.rect();
+                if px >= ex && px <= ex + ew && py >= ey && py <= ey + eh {
+                    if self.edit_box.mouse_input(button, state, px, py, ctx) {
+                        return true;
+                    }
+                } else {
+                    ctx.clear_focus();
+                    return true;
+                }
+            }
+            return false;
+        }
+
         let mut changed = self.scroll_box.mouse_input(button, state, px, py, ctx);
         if self.search_box.mouse_input(button, state, px, py, ctx) {
             ctx.set_focused(&mut self.search_box);
@@ -400,6 +430,49 @@ impl Element for TreeList {
                 let row_idx = (relative_y / self.item_height) as usize;
                 if row_idx < self.items.len() {
                     let item = self.items[row_idx].clone();
+                    
+                    let mut is_double = false;
+                    let now = std::time::Instant::now();
+                    if let Some((prev_time, prev_row)) = self.double_click_timer {
+                        if prev_row == row_idx && now.duration_since(prev_time).as_millis() < 300 {
+                            is_double = true;
+                        }
+                    }
+                    self.double_click_timer = Some((now, row_idx));
+
+                    if is_double {
+                        let (_path_to_edit, relative_name) = match &item {
+                            TreeElement::Section { path, name, .. } => {
+                                if self.collapsed_sections.contains(path) {
+                                    self.collapsed_sections.remove(path);
+                                } else {
+                                    self.collapsed_sections.insert(path.clone());
+                                }
+                                self.rebuild_tree();
+                                (path.clone(), name.clone())
+                            }
+                            TreeElement::Leaf { path, name, .. } => (path.clone(), name.clone()),
+                        };
+                        self.editing_key_idx = Some(row_idx);
+                        self.edit_box = TextBox::new(relative_name).with_multiline(false).with_draw_bg_border(true);
+                        self.edit_box.editing = true;
+                        self.edit_box.cursor_idx = self.edit_box.text.chars().count();
+                        self.edit_box.select_anchor = Some(0);
+                        
+                        let self_ptr = self as *mut Self;
+                        let self_id = self.base.id();
+                        unsafe {
+                            let eb_ptr = &mut (*self_ptr).edit_box as *mut TextBox as *mut (dyn Element + 'static);
+                            let eb_id = (*self_ptr).edit_box.base().unwrap().id();
+                            ctx.register_widget(eb_id, eb_ptr);
+                            ctx.link_ids(self_id, eb_id);
+                            (*eb_ptr).set_parent(Some(self_ptr), ctx);
+                        }
+                        
+                        ctx.set_focused(&mut self.edit_box);
+                        return true;
+                    }
+
                     match item {
                         TreeElement::Section { ref path, .. } => {
                             if self.collapsed_sections.contains(path) {
@@ -549,6 +622,45 @@ impl Element for TreeList {
             self.mark_dirty(ctx);
             changed = true;
         }
+        
+        if self.editing_key_idx.is_some() {
+            if self.edit_box.tick(dt, ctx) {
+                changed = true;
+            }
+            if let Some(row_idx) = self.editing_key_idx {
+                if row_idx < self.items.len() {
+                    let list_left = self.scroll_box.base.x;
+                    let list_top = self.scroll_box.viewport_y;
+                    let row_y = list_top + row_idx as f32 * self.item_height - self.scroll_box.scroll_y;
+                    let box_x = list_left + 5.0;
+                    let box_y = row_y + 2.0;
+                    self.edit_box.set_rect(box_x, box_y, 170.0, 24.0);
+                }
+            }
+            if !self.edit_box.editing {
+                let row_idx = self.editing_key_idx.unwrap();
+                if row_idx < self.items.len() {
+                    let (old_path, relative_name) = match &self.items[row_idx] {
+                        TreeElement::Section { path, name, .. } => (path.clone(), name.clone()),
+                        TreeElement::Leaf { path, name, .. } => (path.clone(), name.clone()),
+                    };
+                    let new_name = self.edit_box.text.trim().to_string();
+                    if !new_name.is_empty() && new_name != relative_name {
+                        let new_path = if let Some(pos) = old_path.rfind('.') {
+                            format!("{}.{}", &old_path[..pos], new_name)
+                        } else {
+                            new_name
+                        };
+                        self.rename_request = Some((old_path, new_path));
+                    }
+                }
+                self.editing_key_idx = None;
+                ctx.set_focused(self);
+                self.mark_dirty(ctx);
+                changed = true;
+            }
+        }
+
         if (self.scroll_box.scroll_y - self.last_scroll_y).abs() > 0.01 {
             self.last_scroll_y = self.scroll_box.scroll_y;
             self.scrollbar_activity_timer = 1.0;
@@ -562,6 +674,18 @@ impl Element for TreeList {
         changed
     }
 
+    fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
+        if self.editing_key_idx.is_some() {
+            if self.edit_box.keyboard_input(event, ctx) {
+                return true;
+            }
+        }
+        if self.search_box.keyboard_input(event, ctx) {
+            return true;
+        }
+        false
+    }
+
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
         let (r1, r2, r3, r4) = self.rounded_corners();
         let has_rounded = r1 || r2 || r3 || r4;
@@ -647,6 +771,24 @@ impl Element for TreeList {
                                 let preview_draw_y = preview_y.max(list_top);
                                 let preview_draw_h = preview_bottom - preview_draw_y;
                                 if preview_draw_h > 0.0 {
+                                    // Checkerboard pattern
+                                    let grid_size = 8.0;
+                                    quads.push((preview_x, preview_draw_y, 16.0, preview_draw_h, [1.0, 1.0, 1.0, 1.0]));
+                                    let cols = (16.0f32 / grid_size).ceil() as i32;
+                                    let rows = (preview_draw_h as f32 / grid_size).ceil() as i32;
+                                    for r in 0..rows {
+                                        for c in 0..cols {
+                                            if (r + c) % 2 == 1 {
+                                                let qx = preview_x + c as f32 * grid_size;
+                                                let qy = preview_draw_y + r as f32 * grid_size;
+                                                let qw = grid_size.min(preview_x + 16.0 - qx);
+                                                let qh = grid_size.min(preview_draw_y + preview_draw_h - qy);
+                                                if qw > 0.0 && qh > 0.0 {
+                                                    quads.push((qx, qy, qw, qh, [0.8, 0.8, 0.8, 1.0]));
+                                                }
+                                            }
+                                        }
+                                    }
                                     quads.push((preview_x, preview_draw_y, 16.0, preview_draw_h, rgba));
                                 }
                             }
@@ -711,14 +853,16 @@ impl Element for TreeList {
 
             match item {
                 TreeElement::Section { name, indent, collapsed, .. } => {
-                    let display_text = format!("{} {}", if *collapsed { "▶" } else { "▼" }, name);
-                    labels.push(TextLabel {
-                        text: display_text,
-                        x: list_left + 8.0 + *indent as f32 * 12.0,
-                        y: row_y + 6.0,
-                        font_size: 12.0,
-                        color: f32_to_rgb(crate::color::tree_section_text_color()),
-                    });
+                    if self.editing_key_idx != Some(i) {
+                        let display_text = format!("{} {}", if *collapsed { "▶" } else { "▼" }, name);
+                        labels.push(TextLabel {
+                            text: display_text,
+                            x: list_left + 8.0 + *indent as f32 * 12.0,
+                            y: row_y + 6.0,
+                            font_size: 12.0,
+                            color: f32_to_rgb(crate::color::tree_section_text_color()),
+                        });
+                    }
                 }
                 TreeElement::Leaf { name, indent, val, original_idx, .. } => {
                     let val_str = serde_json::to_string(val).unwrap_or_default();
@@ -734,13 +878,15 @@ impl Element for TreeList {
                         f32_to_rgb(crate::color::tree_leaf_text_color())
                     };
 
-                    labels.push(TextLabel {
-                        text: name.clone(),
-                        x: list_left + 8.0 + *indent as f32 * 12.0,
-                        y: row_y + 6.0,
-                        font_size: 12.0,
-                        color,
-                    });
+                    if self.editing_key_idx != Some(i) {
+                        labels.push(TextLabel {
+                            text: name.clone(),
+                            x: list_left + 8.0 + *indent as f32 * 12.0,
+                            y: row_y + 6.0,
+                            font_size: 12.0,
+                            color,
+                        });
+                    }
 
                     let val_ty = match val {
                         serde_json::Value::Bool(_) => Some("bool"),
@@ -893,6 +1039,9 @@ impl Element for TreeList {
         let self_ptr = self as *const Self as *mut Self;
         unsafe {
             list.push(&mut (*self_ptr).search_box as *mut TextBox as *mut (dyn Element + 'static));
+            if (*self_ptr).editing_key_idx.is_some() {
+                list.push(&mut (*self_ptr).edit_box as *mut TextBox as *mut (dyn Element + 'static));
+            }
         }
         list
     }
@@ -1022,6 +1171,24 @@ impl Element for TreeList {
                                 let preview_draw_y = preview_y.max(list_top);
                                 let preview_draw_h = preview_bottom - preview_draw_y;
                                 if preview_draw_h > 0.0 {
+                                    // Checkerboard pattern
+                                    let grid_size = 8.0;
+                                    quads.push((preview_x, preview_draw_y, 16.0, preview_draw_h, 0.0, [1.0, 1.0, 1.0, 1.0], (false, false, false, false)));
+                                    let cols = (16.0f32 / grid_size).ceil() as i32;
+                                    let rows = (preview_draw_h as f32 / grid_size).ceil() as i32;
+                                    for r in 0..rows {
+                                        for c in 0..cols {
+                                            if (r + c) % 2 == 1 {
+                                                let qx = preview_x + c as f32 * grid_size;
+                                                let qy = preview_draw_y + r as f32 * grid_size;
+                                                let qw = grid_size.min(preview_x + 16.0 - qx);
+                                                let qh = grid_size.min(preview_draw_y + preview_draw_h - qy);
+                                                if qw > 0.0 && qh > 0.0 {
+                                                    quads.push((qx, qy, qw, qh, 0.0, [0.8, 0.8, 0.8, 1.0], (false, false, false, false)));
+                                                }
+                                            }
+                                        }
+                                    }
                                     quads.push((preview_x, preview_draw_y, 16.0, preview_draw_h, 0.0, rgba, (false, false, false, false)));
                                 }
                             }
@@ -1354,4 +1521,50 @@ mod tests {
         });
         assert!(has_accel, "Tree should contain 'accel_profile' when matching on value 'flat'!");
     }
+
+    #[test]
+    fn test_treelist_double_click_rename() {
+        let mut ctx = UiContext::new();
+        let mut tree_list = TreeList::new();
+        tree_list.set_rect(0.0, 0.0, 380.0, 500.0);
+        tree_list.set_flat_keys(vec![
+            ("style.control.dropdown.color".to_string(), serde_json::Value::String("#ff00ff".to_string()))
+        ]);
+
+        // 1. Test renaming a section (row 0)
+        let list_top = tree_list.scroll_box.viewport_y;
+        let py0 = list_top + 10.0;
+        tree_list.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py0, &mut ctx);
+        std::thread::sleep(std::time::Duration::from_millis(10));
+        tree_list.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py0, &mut ctx);
+
+        assert!(tree_list.editing_key_idx.is_some());
+        assert_eq!(tree_list.edit_box.text, "style"); // Pre-populated with relative name!
+
+        tree_list.edit_box.text = "theme".to_string();
+        tree_list.edit_box.edit_buffer = "theme".to_string();
+        tree_list.edit_box.editing = false;
+        tree_list.tick(0.016, &mut ctx);
+
+        let req = tree_list.take_rename_request();
+        assert_eq!(req, Some(("style".to_string(), "theme".to_string())));
+
+        // 2. Test renaming a leaf (row 3)
+        tree_list.rebuild_tree();
+        let py3 = list_top + 3.0 * tree_list.item_height + 10.0; // Click row 3 (Leaf "color")
+        tree_list.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py3, &mut ctx);
+        std::thread::sleep(std::time::Duration::from_millis(10));
+        tree_list.mouse_input(MouseButton::Left, ElementState::Pressed, 10.0, py3, &mut ctx);
+
+        assert!(tree_list.editing_key_idx.is_some());
+        assert_eq!(tree_list.edit_box.text, "color"); // Pre-populated with relative name "color"!
+
+        tree_list.edit_box.text = "bg_color".to_string();
+        tree_list.edit_box.edit_buffer = "bg_color".to_string();
+        tree_list.edit_box.editing = false;
+        tree_list.tick(0.016, &mut ctx);
+
+        let req = tree_list.take_rename_request();
+        assert_eq!(req, Some(("style.control.dropdown.color".to_string(), "style.control.dropdown.bg_color".to_string())));
+    }
 }
diff --git a/src/widget/input/dropdown.rs b/src/widget/input/dropdown.rs
index 3920423..ca561b5 100644
--- a/src/widget/input/dropdown.rs
+++ b/src/widget/input/dropdown.rs
@@ -213,6 +213,36 @@ impl Element for Dropdown {
         crate::layout::dropdown_corner_radius()
     }
 
+    fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
+        let mut quads = Vec::new();
+        let (r1, r2, r3, r4) = self.rounded_corners();
+        if r1 || r2 || r3 || r4 {
+            let top = self.base.label_offset();
+            let visual_h = self.base.h - top;
+            let radius = self.corner_radius();
+            
+            let bg_color = colors::dropdown_background_color();
+            let border_color = if self.open {
+                [0.30, 0.50, 0.32, 1.0]
+            } else if self.base.hovered {
+                [0.25, 0.25, 0.35, 1.0]
+            } else {
+                [0.18, 0.18, 0.24, 1.0]
+            };
+
+            // Draw border
+            quads.push((self.base.x, self.base.y + top, self.base.w, visual_h, radius, border_color, (r1, r2, r3, r4)));
+            // Draw background (slightly inset to show border)
+            let inner_radius = (radius - 1.0).max(0.0);
+            quads.push((self.base.x + 1.0, self.base.y + top + 1.0, self.base.w - 2.0, visual_h - 2.0, inner_radius, bg_color, (r1, r2, r3, r4)));
+        }
+        for &child_ptr in &self.children(ctx) {
+            let widget = unsafe { &*child_ptr };
+            quads.extend(widget.all_rounded_quads(ctx));
+        }
+        quads
+    }
+
     fn widget_font(&self) -> Option<String> {
         Some(crate::layout::dropdown_font())
     }
@@ -388,6 +418,11 @@ impl Element for Dropdown {
     }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
+        let (r1, r2, r3, r4) = self.rounded_corners();
+        if r1 || r2 || r3 || r4 {
+            return Vec::new();
+        }
+
         let mut quads = Vec::new();
         let top = self.base.label_offset();
         let visual_h = self.base.h - top;