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

commitc82ce04d5c2aaa24c6fee49f70d656a31a85cf64
parent69d85d30fa
authorLucas Galante <[email protected]>
date2026-08-06 15:20
feat: TreeList scrollbar in the relief bevel style

ScrollBox::paint_scrollbar_relief draws the track as a carved groove
(recess) and the thumb as a raised rounded plate (bevel, configured
thumb color) — the DE highlight/shadow treatment, roll width scaled to
the bar so the default bevel width cannot swallow a 14px thumb. The
TreeList routes its scrollbar through it under control_relief, keeping
the behind-items / on-top-during-activity ordering; the flat-quad view
remains for the legacy path.

Co-Authored-By: Claude Fable 5 <[email protected]>

 src/widget/container/scroll_box.rs | 43 ++++++++++++++++++++++++++++++++++++++
 src/widget/container/treelist.rs   | 19 +++++++++++++----
 2 files changed, 58 insertions(+), 4 deletions(-)

diff --git a/src/widget/container/scroll_box.rs b/src/widget/container/scroll_box.rs
index 410b0f9..91ea477 100644
--- a/src/widget/container/scroll_box.rs
+++ b/src/widget/container/scroll_box.rs
@@ -300,6 +300,49 @@ impl ScrollBox {
         quads
     }
 
+    /// The scrollbar in the DE's relief language: the track a carved groove
+    /// ([`PaintCtx::recess`]), the thumb a raised rounded plate riding in it
+    /// ([`PaintCtx::bevel`] with the configured thumb color) — the
+    /// highlight/shadow bevel treatment. The flat-quad view stays available
+    /// through [`extra_quads`](Self::extra_quads) for legacy paths.
+    pub fn paint_scrollbar_relief(&self, pc: &mut crate::scene::paint::PaintCtx) {
+        if self.content_h <= self.viewport_h {
+            return;
+        }
+        let sb_w = crate::layout::scrollbar_width();
+        let sb_x = self.base.x + self.base.w - sb_w - 4.0;
+        let sb_track_h = self.viewport_h - 8.0;
+        let sb_track_y = self.viewport_y + 4.0;
+
+        let visible_ratio = self.viewport_h / self.content_h;
+        let thumb_h = if sb_track_h <= 20.0 {
+            sb_track_h
+        } else {
+            (sb_track_h * visible_ratio).clamp(20.0, sb_track_h)
+        };
+        let max_scroll = (self.content_h - self.viewport_h).max(0.0);
+        let scroll_ratio = if max_scroll > 0.0 { self.scroll_y / max_scroll } else { 0.0 };
+        let thumb_y = sb_track_y + scroll_ratio * (sb_track_h - thumb_h);
+
+        // Pill radii; the roll width scales to the bar (the DE bevel width
+        // would swallow a 14px-wide thumb whole).
+        let r = sb_w * 0.5;
+        let depth = crate::layout::bevel_width().min(sb_w * 0.35);
+        let radii = (r, r, r, r);
+        use crate::scene::layout::Rect;
+        pc.recess(
+            Rect { x: sb_x, y: sb_track_y, width: sb_w, height: sb_track_h },
+            radii,
+            depth,
+        );
+        pc.bevel(
+            Rect { x: sb_x, y: thumb_y, width: sb_w, height: thumb_h },
+            radii,
+            crate::color::scrollbar_thumb_color(),
+            depth,
+        );
+    }
+
     pub fn keyboard_input(&mut self, event: &KeyEvent, ctx: &mut UiContext) -> bool {
         // Focus never lands on the box itself (post-6av it is not an `WidgetHost`), and its id is
         // never a tree ancestor of the focused widget — like the legacy address walk, this
diff --git a/src/widget/container/treelist.rs b/src/widget/container/treelist.rs
index 345c229..8603e90 100644
--- a/src/widget/container/treelist.rs
+++ b/src/widget/container/treelist.rs
@@ -789,10 +789,17 @@ impl Paint for TreeList {
         };
 
         let show_on_top = self.scrollbar_activity_timer > 0.0;
+        let relief_scrollbar = crate::layout::control_relief();
 
-        // If NOT on top, draw scrollbar first (behind items)
+        // If NOT on top, draw scrollbar first (behind items). Relief style
+        // emits prims directly — they land before the quads flush below, so
+        // the ordering matches the flat path.
         if !show_on_top {
-            quads.extend(get_scrollbar_quads());
+            if relief_scrollbar {
+                self.scroll_box.paint_scrollbar_relief(pc);
+            } else {
+                quads.extend(get_scrollbar_quads());
+            }
         }
 
         // 2. Draw items (row backgrounds, separator lines, color previews)
@@ -915,8 +922,9 @@ impl Paint for TreeList {
             }
         }
 
-        // If on top, draw scrollbar last
-        if show_on_top {
+        // If on top (scroll activity), the scrollbar draws after the rows —
+        // the relief prims are emitted after the quads flush below.
+        if show_on_top && !relief_scrollbar {
             quads.extend(get_scrollbar_quads());
         }
         for (qx, qy, qw, qh, qr, qc, qcorners) in quads {
@@ -926,6 +934,9 @@ impl Paint for TreeList {
                 pc.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
             }
         }
+        if show_on_top && relief_scrollbar {
+            self.scroll_box.paint_scrollbar_relief(pc);
+        }
 
         // Recessed well like a text box or list: the tree floor sits below the
         // pane surface, its wall carved over the bg and row quads above. Focus