GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git
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;