cloud storage client
git clone https://git.lucas.co/cce-cloud.git
fix: a press on the scrollbar or a clipped sliver no longer selects a row
`FuzzelWidget::on_event` gated the row math on `scroll_box.hit()`, which spans the
whole region including the scrollbar strip, and then clamped the computed index only
against `filtered_items.len()`. Two consequences, both of which emit a selection the
user did not make — and in Dmenu/switcher mode a press commits and closes the popup,
so the wrong item goes straight to stdout:
- Pressing the scrollbar selected whatever row sat at that y. (The strip is also the
only thing there: `ScrollRegion::press`/`release`/`cursor_moved` are never called in
this crate, so the scrollbar cannot actually be dragged — a separate gap, left as is.)
- `get_draw_y` requires a row to be FULLY inside the viewport, but the click math
accepted any y in the region. Wheel steps are 24px against a 25px item height, so
after any scroll a 1-24px band at the viewport edge was blank but clickable and
resolved to the partially-clipped row behind it.
Both gates now use what the paint uses: reject `hit_scrollbar`, and require
`get_draw_y(...).is_some()` for the row that was hit — the same predicate the paint
loop virtualizes on, so only a row actually drawn is selectable.
Live-verified in --dmenu with 100 items (against the restarted daemon, since a bare
invocation is a client of the old one): clicking the scrollbar printed "item-5" and
closed; it now prints nothing and stays open, while a normal row click still commits
"item-5".
Co-Authored-By: Claude Opus 5 <[email protected]>
src/main.rs | 16 ++++++++++++++--
src/scroll_region.rs | 2 +-
2 files changed, 15 insertions(+), 3 deletions(-)
diff --git a/src/main.rs b/src/main.rs
index c1e6662..999562d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -657,10 +657,22 @@ impl cce_ui::widget::Input for FuzzelWidget {
} = event
{
let item_h = 25.0;
- if self.scroll_box.hit(*px, *py) {
+ // `hit()` spans the whole region, scrollbar strip included, and the row math
+ // below accepts any y inside it — so without these two gates a press on the
+ // scrollbar, or on the partially-clipped sliver at the viewport edge, resolved
+ // to a row. In Dmenu/switcher mode a press commits and closes, so that emitted
+ // an item the user never clicked (and, on the sliver, never even saw).
+ if self.scroll_box.hit(*px, *py) && !self.scroll_box.hit_scrollbar(*px, *py) {
let click_virtual_y = *py - self.scroll_box.viewport_y + self.scroll_box.scroll_y;
let clicked_idx = (click_virtual_y / item_h).floor() as usize;
- if clicked_idx < self.filtered_items.len() {
+ // Same predicate the paint loop virtualizes on, so only a row actually
+ // drawn this frame is selectable.
+ if clicked_idx < self.filtered_items.len()
+ && self
+ .scroll_box
+ .get_draw_y(clicked_idx as f32 * item_h, item_h)
+ .is_some()
+ {
self.selected = clicked_idx;
return true;
}
diff --git a/src/scroll_region.rs b/src/scroll_region.rs
index 8126331..6b61c04 100644
--- a/src/scroll_region.rs
+++ b/src/scroll_region.rs
@@ -125,7 +125,7 @@ impl ScrollRegion {
(sb_x, track_y, sb_w, track_h, thumb_y, thumb_h)
}
- fn hit_scrollbar(&self, px: f32, py: f32) -> bool {
+ pub fn hit_scrollbar(&self, px: f32, py: f32) -> bool {
if self.content_h <= self.viewport_h {
return false;
}