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

commit67ea356b85f82f5df882d8b1339da3230a3d4cef
parent267529e293
authorLucas Galante <[email protected]>
date2026-09-01 12:49
scroll-region: self-contained sink-behind layering + notify_scrolled

Make sink-behind adoption nearly free for existing hosts by folding the
two-layer emission into the existing entry points:

- push_quads lays a sunk bar UNDER the translucent bg fill (alpha 0.3),
  so it shows through dimly; push_scrollbar_quads becomes the raised
  layer only. A host's existing bg -> rows -> scrollbar order needs no
  change to adopt.
- push_prims does the same for framed regions: sunk bar between the
  border and the bg fill, raised bar on top.
- notify_scrolled() raises the bar for hosts that scroll the region
  programmatically (selection auto-snap, jump to a search hit).

Non-sink regions emit exactly what they did before.

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

 src/widget/scroll_region.rs | 66 +++++++++++++++++++++++++++++++++++++++++++--
 1 file changed, 64 insertions(+), 2 deletions(-)

diff --git a/src/widget/scroll_region.rs b/src/widget/scroll_region.rs
index 5dc8a0c..f003594 100644
--- a/src/widget/scroll_region.rs
+++ b/src/widget/scroll_region.rs
@@ -470,6 +470,13 @@ impl ScrollRegion {
         }
     }
 
+    /// A host scrolled the region programmatically (selection auto-snap, jump
+    /// to a search hit): raise a sink-behind bar the same way a wheel scroll
+    /// does. No-op for regions without [`Self::sink_behind`].
+    pub fn notify_scrolled(&mut self) {
+        self.raise();
+    }
+
     /// Whether the scrollbar currently draws in front of the content and takes
     /// input. Always true for a region without [`Self::sink_behind`].
     pub fn scrollbar_raised(&self) -> bool {
@@ -552,6 +559,13 @@ impl ScrollRegion {
             };
             let all = (true, true, true, true);
             pc.rect_with_radius_corners(border_color, self.x, self.y, self.w, self.h, radius, all);
+            // A sunk sink-behind bar draws here, UNDER the translucent bg fill
+            // (list_bg_color's alpha is 0.3): it shows through dimly, sunk into
+            // the list plate — the designer parameter-pane look, self-contained
+            // for framed regions.
+            if self.sink_behind && !self.activity.raised() {
+                self.push_scrollbar_prims(pc);
+            }
             pc.rect_with_radius_corners(
                 crate::color::list_bg_color(),
                 self.x + 1.0,
@@ -562,8 +576,10 @@ impl ScrollRegion {
                 all,
             );
         }
-        // A sunk sink-behind bar is NOT drawn here: the host paints it under
-        // its plate via `push_scrollbar_prims` so it shows through dimly.
+        // Raised (or plain always-on): the bar rides on top. A FRAMELESS
+        // sink-behind region draws no sunk layer here — its rows sit directly
+        // on the host's plate, so the host owns the under-plate emission via
+        // `push_scrollbar_prims`.
         if self.scrollbar_raised() {
             self.push_scrollbar_prims(pc);
         }
@@ -592,12 +608,28 @@ impl ScrollRegion {
     /// split into [`Self::push_scrollbar_quads`] so the host can emit it AFTER
     /// the rows — drawn together, the rows paint over the thumb and it peeks
     /// through the inter-row gaps as dotted segments.
+    ///
+    /// For a sink-behind region, a sunk bar is emitted here FIRST, under the
+    /// translucent bg fill (alpha 0.3), so it shows through dimly — and
+    /// [`Self::push_scrollbar_quads`] goes quiet. The host's existing
+    /// bg → rows → scrollbar order needs no change to adopt the treatment.
     pub fn push_quads(&self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>) {
+        if self.sink_behind && !self.activity.raised() {
+            self.push_scrollbar_quads_always(quads);
+        }
         quads.push((self.x, self.y, self.w, self.h, crate::color::list_bg_color()));
     }
 
     /// Scrollbar track + thumb when the content overflows; emit after the rows.
+    /// For a sink-behind region this is the RAISED layer only — while sunk the
+    /// bar was already emitted under the bg by [`Self::push_quads`].
     pub fn push_scrollbar_quads(&self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>) {
+        if self.scrollbar_raised() {
+            self.push_scrollbar_quads_always(quads);
+        }
+    }
+
+    fn push_scrollbar_quads_always(&self, quads: &mut Vec<(f32, f32, f32, f32, [f32; 4])>) {
         if self.content_h > self.viewport_h {
             let (sb_x, track_y, sb_w, track_h, thumb_y, thumb_h) = self.scrollbar_geom();
             quads.push((sb_x, track_y, sb_w, track_h, crate::color::scrollbar_track_color()));
@@ -783,6 +815,36 @@ mod tests {
         assert!(!r.scrollbar_raised());
     }
 
+    #[test]
+    fn tuple_emission_layers_by_raised_state() {
+        let mut r = region().with_sink_behind(true);
+        r.update_bounds(10, 20.0, 100.0);
+        // Sunk: bar quads come UNDER the bg (push_quads emits bar then bg, the
+        // raised-layer call is quiet).
+        let mut under = Vec::new();
+        r.push_quads(&mut under);
+        assert_eq!(under.len(), 3); // track + thumb + bg
+        assert_eq!(under[2].2, 200.0); // last quad is the full-width bg fill
+        let mut over = Vec::new();
+        r.push_scrollbar_quads(&mut over);
+        assert!(over.is_empty());
+        // Raised: bg alone below, bar above.
+        r.wheel(&MouseScrollDelta::LineDelta(0.0, -1.0), 50.0, 50.0);
+        let mut under = Vec::new();
+        r.push_quads(&mut under);
+        assert_eq!(under.len(), 1);
+        let mut over = Vec::new();
+        r.push_scrollbar_quads(&mut over);
+        assert_eq!(over.len(), 2);
+        // A non-sink region keeps the legacy shape: bg alone, bar always.
+        let mut plain = region();
+        plain.update_bounds(10, 20.0, 100.0);
+        let (mut under, mut over) = (Vec::new(), Vec::new());
+        plain.push_quads(&mut under);
+        plain.push_scrollbar_quads(&mut over);
+        assert_eq!((under.len(), over.len()), (1, 2));
+    }
+
     #[test]
     fn non_sink_regions_are_unchanged() {
         let mut r = region();