git.lucas.co / cce-cloud
cloud storage client
git clone https://git.lucas.co/cce-cloud.git

commit27ef121dd328d267fd84290bffb7e789df73fab1
parent74f9ad4272
authorLucas Galante <[email protected]>
date2026-09-01 12:49
launcher list: designer raise/sink scrollbar (first visible bar)

The item list adopts the toolkit ScrollRegion's sink-behind mode. The
bar was previously never drawn at all — grabbable but invisible — so
this is its first visible incarnation: sunk it rides dimly under the
list's translucent bg, a wheel scroll or a keyboard-nav auto-snap
(notify_scrolled) raises it over the rows, and it sinks back 0.7s after
the last activity. The selection chip no longer reserves width for it,
since a raised bar rides over the rows by design.

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

 src/main.rs | 39 +++++++++++++++++++++++++++++++++++----
 1 file changed, 35 insertions(+), 4 deletions(-)

diff --git a/src/main.rs b/src/main.rs
index 56a8350..78d78f8 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -630,7 +630,11 @@ impl FuzzelWidget {
             selected: 0,
             icons: std::collections::HashMap::new(),
             icon_gutter: 0.0,
-            scroll_box: ScrollRegion::new(22.0, 0.0),
+            // Designer raise/sink treatment: the bar idles sunk under the
+            // list's translucent bg (dimly visible through it) and raises over
+            // the rows on scroll. The region already sits inset from the popup
+            // edge, so the stock 4px edge inset reads right here.
+            scroll_box: ScrollRegion::new(22.0, 0.0).with_sink_behind(true),
         })
     }
 
@@ -694,6 +698,7 @@ impl FuzzelWidget {
         }
 
         let virtual_selected_y = self.selected as f32 * item_h;
+        let old_scroll = self.scroll_box.scroll_y;
         if virtual_selected_y + item_h > self.scroll_box.scroll_y + viewport_h {
             self.scroll_box.scroll_y = virtual_selected_y + item_h - viewport_h;
         } else if virtual_selected_y < self.scroll_box.scroll_y {
@@ -702,6 +707,11 @@ impl FuzzelWidget {
 
         let max_scroll = (content_h - viewport_h).max(0.0);
         self.scroll_box.scroll_y = self.scroll_box.scroll_y.clamp(0.0, max_scroll);
+        // Keyboard navigation scrolls the list without touching the wheel
+        // path — raise the sink-behind bar for it too.
+        if (self.scroll_box.scroll_y - old_scroll).abs() > 0.01 {
+            self.scroll_box.notify_scrolled();
+        }
     }
 }
 
@@ -767,13 +777,15 @@ impl cce_ui::widget::Paint for FuzzelWidget {
             if !self.filtered_items.is_empty() {
                 let virtual_selected_y = self.selected as f32 * item_h;
                 if let Some(draw_y) = self.scroll_box.get_draw_y(virtual_selected_y, item_h) {
-                    let scrollbar_w = if self.scroll_box.content_h > self.scroll_box.viewport_h { 10.0 } else { 0.0 };
                     // A raised beveled chip, not a flat tint: the selection reads
-                    // as sitting proud of the list the way focused panes do.
+                    // as sitting proud of the list the way focused panes do. No
+                    // width reserved for the scrollbar anymore — the sink-behind
+                    // bar idles under the list bg and rides OVER the rows while
+                    // raised, so the chip keeps its full width either way.
                     let sel = Rect {
                         x: self.x + pad + 2.0,
                         y: draw_y,
-                        width: self.w - pad * 2.0 - 4.0 - scrollbar_w,
+                        width: self.w - pad * 2.0 - 4.0,
                         height: item_h - 2.0,
                     };
                     let depth = cce_ui::color::plate_bevel_width().min(sel.height * 0.2);
@@ -800,6 +812,15 @@ impl cce_ui::widget::Paint for FuzzelWidget {
             }
         });
 
+        // The raised scrollbar rides over the rows while a scroll holds it up
+        // (the sunk layer went under the list bg inside push_quads). The bar
+        // was previously never drawn at all — grabbable but invisible.
+        let mut bar = Vec::new();
+        self.scroll_box.push_scrollbar_quads(&mut bar);
+        for (qx, qy, qw, qh, qc) in bar {
+            ctx.quad(Rect { x: qx, y: qy, width: qw, height: qh }, qc);
+        }
+
         // Prompt/query line and the empty-state notice — outside the list clip.
         for l in self.own_labels() {
             ctx.text(l.text, l.x, l.y, l.font_size, l.color);
@@ -2791,6 +2812,11 @@ fn run_standalone() {
                     app.redraw = true;
                 }
             }
+            // Raise/sink upkeep for the list scrollbar (true while the
+            // post-scroll hold runs or on the depth flip).
+            if state.fuzzel.scroll_box.tick(dt) {
+                app.redraw = true;
+            }
         }
 
         if app.redraw {
@@ -3300,6 +3326,11 @@ fn run_daemon(socket_path: &str) {
                         app.redraw = true;
                     }
                 }
+                // Raise/sink upkeep for the list scrollbar (true while the
+                // post-scroll hold runs or on the depth flip).
+                if st.fuzzel.scroll_box.tick(dt) {
+                    app.redraw = true;
+                }
             }
 
             if app.redraw {