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

commitbb1e038b408253668d2a751f3144da682eaebea8
parentf3b37f9d4c
authorLucas Galante <[email protected]>
date2026-07-01 13:32
Implement dynamic scrollbar layering (behind on idle, on top on scroll) and tree opacity/blur configurations

 src/layout.rs                    | 78 ++++++++++++++++++++++++++++++++++++
 src/widget/container/treelist.rs | 86 +++++++++++++++++++++++++++++++---------
 2 files changed, 146 insertions(+), 18 deletions(-)

diff --git a/src/layout.rs b/src/layout.rs
index 40fdb40..ac2fc71 100644
--- a/src/layout.rs
+++ b/src/layout.rs
@@ -125,6 +125,8 @@ fn flatten_json_to_flat_props(val: &serde_json::Value, prefix: &str, flat_props:
                 "style.overlay.border_gap" => "overlay_border_gap",
                 "style.editor.last_page" | "style.data.editor.last_page" => "last_page",
                 "style.data.tree.corner_radius" => "tree_corner_radius",
+                "style.data.tree.opacity" => "tree_opacity",
+                "style.data.tree.blur" => "tree_blur",
                 "style.surface.desktop.gap_color" => "desktop_gap_color",
                 "style.surface.desktop.cell_color" => "desktop_cell_color",
                 "style.surface.desktop.gap_width" => "desktop_gap_width",
@@ -212,6 +214,8 @@ static PAGINATOR_TAB_PADDING_X: RwLock<f32> = RwLock::new(10.0);
 static BUTTON_PADDING: RwLock<f32> = RwLock::new(14.0);
 static BUTTON_STRIP_SPACING: RwLock<f32> = RwLock::new(8.0);
 static SCROLLBAR_WIDTH: RwLock<f32> = RwLock::new(4.0);
+static TREE_OPACITY: RwLock<f32> = RwLock::new(1.0);
+static TREE_BLUR: RwLock<f32> = RwLock::new(0.0);
 
 static PLATE_PADDING: RwLock<f32> = RwLock::new(20.0);
 static DROPDOWN_HEIGHT: RwLock<f32> = RwLock::new(44.0);
@@ -384,6 +388,24 @@ pub fn reload_config() {
                     }
                 }
             }
+            if let Some(rest) = trimmed.strip_prefix("tree_opacity") {
+                let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+                let val_str = rest.trim_end_matches('"').trim();
+                if let Ok(val) = val_str.parse::<f32>() {
+                    if let Ok(mut lock) = TREE_OPACITY.write() {
+                        *lock = val;
+                    }
+                }
+            }
+            if let Some(rest) = trimmed.strip_prefix("tree_blur") {
+                let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+                let val_str = rest.trim_end_matches('"').trim();
+                if let Ok(val) = val_str.parse::<f32>() {
+                    if let Ok(mut lock) = TREE_BLUR.write() {
+                        *lock = val;
+                    }
+                }
+            }
             if let Some(rest) = trimmed.strip_prefix("spinbox_height") {
                 let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
                 let val_str = rest.trim_end_matches('"').trim();
@@ -2305,6 +2327,62 @@ pub fn set_scrollbar_width(width: f32) {
     }
 }
 
+pub fn tree_opacity() -> f32 {
+    use std::sync::Once;
+    static INIT: Once = Once::new();
+    INIT.call_once(|| {
+        if let Some(content) = read_config() {
+            for line in content.lines() {
+                let trimmed = line.trim();
+                if let Some(rest) = trimmed.strip_prefix("tree_opacity") {
+                    let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+                    let val_str = rest.trim_end_matches('"').trim();
+                    if let Ok(val) = val_str.parse::<f32>() {
+                        if let Ok(mut lock) = TREE_OPACITY.write() {
+                            *lock = val;
+                        }
+                    }
+                }
+            }
+        }
+    });
+    *TREE_OPACITY.read().unwrap()
+}
+
+pub fn set_tree_opacity(opacity: f32) {
+    if let Ok(mut lock) = TREE_OPACITY.write() {
+        *lock = opacity;
+    }
+}
+
+pub fn tree_blur() -> f32 {
+    use std::sync::Once;
+    static INIT: Once = Once::new();
+    INIT.call_once(|| {
+        if let Some(content) = read_config() {
+            for line in content.lines() {
+                let trimmed = line.trim();
+                if let Some(rest) = trimmed.strip_prefix("tree_blur") {
+                    let rest = rest.trim_start_matches(|c: char| c == ' ' || c == '=' || c == '"');
+                    let val_str = rest.trim_end_matches('"').trim();
+                    if let Ok(val) = val_str.parse::<f32>() {
+                        if let Ok(mut lock) = TREE_BLUR.write() {
+                            *lock = val;
+                        }
+                    }
+                }
+            }
+        }
+    });
+    *TREE_BLUR.read().unwrap()
+}
+
+pub fn set_tree_blur(blur: f32) {
+    if let Ok(mut lock) = TREE_BLUR.write() {
+        *lock = blur;
+    }
+}
+
 pub fn set_spinbox_button_padding(padding: f32) {
     if let Ok(mut lock) = SPINBOX_BUTTON_PADDING.write() {
         *lock = padding;
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index 43060ce..f90df4b 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -129,6 +129,8 @@ pub struct TreeList {
     pub right_clicked_section: Option<String>,
     pub parent: Option<*mut (dyn Element + 'static)>,
     pub children: Vec<*mut (dyn Element + 'static)>,
+    pub last_scroll_y: f32,
+    pub scrollbar_activity_timer: f32,
 }
 
 impl TreeList {
@@ -146,6 +148,8 @@ impl TreeList {
             right_clicked_section: None,
             parent: None,
             children: Vec::new(),
+            last_scroll_y: 0.0,
+            scrollbar_activity_timer: 0.0,
         }
     }
 
@@ -183,6 +187,15 @@ impl TreeList {
     pub fn take_clicked_item(&mut self) -> Option<TreeElement> {
         self.clicked_item.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;
+            ctx.register_tick_receiver(self.base.id());
+            self.last_scroll_y = self.scroll_box.scroll_y;
+            self.mark_dirty(ctx);
+        }
+    }
 }
 
 impl Element for TreeList {
@@ -201,6 +214,7 @@ impl Element for TreeList {
         
         let content_h = self.items.len() as f32 * self.item_height;
         self.scroll_box.update_bounds(content_h, y, h);
+        self.last_scroll_y = self.scroll_box.scroll_y;
     }
 
     fn color(&self) -> [f32; 4] {
@@ -240,6 +254,7 @@ impl Element for TreeList {
 
     fn mouse_input(&mut self, button: MouseButton, state: ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
         let mut changed = self.scroll_box.mouse_input(button, state, px, py, ctx);
+        self.check_scroll_activity(ctx);
 
         let list_left = self.scroll_box.base.x;
         let list_width = self.scroll_box.base.w;
@@ -361,19 +376,32 @@ impl Element for TreeList {
     }
 
     fn mouse_wheel(&mut self, delta: &MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
-        self.scroll_box.mouse_wheel(delta, px, py, ctx)
+        let changed = self.scroll_box.mouse_wheel(delta, px, py, ctx);
+        self.check_scroll_activity(ctx);
+        changed
+    }
+
+    fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
+        if self.scrollbar_activity_timer > 0.0 {
+            self.scrollbar_activity_timer = (self.scrollbar_activity_timer - dt).max(0.0);
+            if self.scrollbar_activity_timer <= 0.0 {
+                ctx.unregister_tick_receiver(self.base.id());
+            }
+            true
+        } else {
+            false
+        }
     }
 
     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
-        let mut quads = self.scroll_box.extra_quads();
         let (r1, r2, r3, r4) = self.rounded_corners();
         let has_rounded = r1 || r2 || r3 || r4;
         if has_rounded {
-            if !quads.is_empty() {
-                quads.remove(0);
-            }
+            return Vec::new();
         }
 
+        let mut quads = self.scroll_box.extra_quads();
+
         let list_left = self.scroll_box.base.x;
         let list_width = self.scroll_box.base.w;
         let list_top = self.scroll_box.viewport_y;
@@ -600,12 +628,37 @@ impl Element for TreeList {
         let radius = self.corner_radius();
         let (x, y, w, h) = self.rect();
         
+        let opacity = crate::layout::tree_opacity();
+        let apply_opacity = |mut c: [f32; 4]| -> [f32; 4] {
+            c[3] *= opacity;
+            c
+        };
+
         // 1. Draw container border and background
         if let Some((border_color, thickness)) = self.solid_border() {
-            quads.push((x, y, w, h, radius, border_color, (r1, r2, r3, r4)));
-            quads.push((x + thickness, y + thickness, w - 2.0 * thickness, h - 2.0 * thickness, radius - thickness, crate::color::tree_background_color(), (r1, r2, r3, r4)));
+            quads.push((x, y, w, h, radius, apply_opacity(border_color), (r1, r2, r3, r4)));
+            quads.push((x + thickness, y + thickness, w - 2.0 * thickness, h - 2.0 * thickness, radius - thickness, apply_opacity(crate::color::tree_background_color()), (r1, r2, r3, r4)));
         } else {
-            quads.push((x, y, w, h, radius, crate::color::tree_background_color(), (r1, r2, r3, r4)));
+            quads.push((x, y, w, h, radius, apply_opacity(crate::color::tree_background_color()), (r1, r2, r3, r4)));
+        }
+
+        // Helper to collect scrollbar quads
+        let get_scrollbar_quads = || {
+            let mut sb_quads = Vec::new();
+            let scroll_quads = self.scroll_box.extra_quads();
+            if scroll_quads.len() > 1 {
+                for q in &scroll_quads[1..] {
+                    sb_quads.push((q.0, q.1, q.2, q.3, 0.0, q.4, (false, false, false, false)));
+                }
+            }
+            sb_quads
+        };
+
+        let show_on_top = self.scrollbar_activity_timer > 0.0;
+
+        // If NOT on top, draw scrollbar first (behind items)
+        if !show_on_top {
+            quads.extend(get_scrollbar_quads());
         }
 
         // 2. Draw items (row backgrounds, separator lines, color previews)
@@ -646,12 +699,12 @@ impl Element for TreeList {
                 }
             };
             
-            quads.push((list_left + 1.0, draw_y, list_width - 9.0, draw_h, 0.0, bg_color, (false, false, false, false)));
+            quads.push((list_left + 1.0, draw_y, list_width - 9.0, draw_h, 0.0, apply_opacity(bg_color), (false, false, false, false)));
             
             if let TreeElement::Leaf { ref val, original_idx, .. } = item {
                 let separator_color = crate::color::tree_separator_color();
-                quads.push((list_left + 180.0, draw_y, 1.0, draw_h, 0.0, separator_color, (false, false, false, false)));
-                quads.push((list_left + 235.0, draw_y, 1.0, draw_h, 0.0, separator_color, (false, false, false, false)));
+                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)));
 
                 if Some(*original_idx) != self.selected_key_idx {
                     if let serde_json::Value::String(s) = val {
@@ -672,16 +725,13 @@ impl Element for TreeList {
             }
 
             if row_y + self.item_height <= list_bottom {
-                quads.push((list_left + 1.0, row_y + self.item_height - 1.0, list_width - 9.0, 1.0, 0.0, [0.13, 0.13, 0.17, 1.0], (false, false, false, false)));
+                quads.push((list_left + 1.0, row_y + self.item_height - 1.0, list_width - 9.0, 1.0, 0.0, apply_opacity([0.13, 0.13, 0.17, 1.0]), (false, false, false, false)));
             }
         }
 
-        // 3. Draw scrollbar track and thumb
-        let scroll_quads = self.scroll_box.extra_quads();
-        if scroll_quads.len() > 1 {
-            for q in &scroll_quads[1..] {
-                quads.push((q.0, q.1, q.2, q.3, 0.0, q.4, (false, false, false, false)));
-            }
+        // If on top, draw scrollbar last
+        if show_on_top {
+            quads.extend(get_scrollbar_quads());
         }
 
         for &child_ptr in &self.children(ctx) {