git.lucas.co / cce-designer
graphic design tool
git clone https://git.lucas.co/cce-designer.git

commiteb15a2ece8a00ad471e48d20fd68e6349daff3f1
parent83d0da1d3d
authorLucas Galante <[email protected]>
date2026-09-20 22:28
Dialog: set_page re-clamps the scroll instead of snapping to the selection

layout_dialog records the page on every relayout — off rebuild_positions,
which the frame tick reaches whenever anything animates — and set_page
called scroll_to_selected, so every wheel notch and finger delta was
undone within a frame: the list would not scroll at all. Keeping the
selection in view is the keyboard's job at the sites that move it.
Test: a finger delta and a wheel notch both move the list, and set_page
keeps the offset. Reproduced and re-verified in a shadow with injected
finger scrolls.

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

 src/dialog.rs | 11 +++++++++--
 src/main.rs   | 36 ++++++++++++++++++++++++++++++++++++
 2 files changed, 45 insertions(+), 2 deletions(-)

diff --git a/src/dialog.rs b/src/dialog.rs
index ff3d635..39e0b14 100644
--- a/src/dialog.rs
+++ b/src/dialog.rs
@@ -202,10 +202,17 @@ impl Dialog {
         d
     }
 
-    /// Record how many rows fit, from the laid-out rect.
+    /// Record how many rows fit, from the laid-out rect. Re-clamps the
+    /// offset to the new range and nothing more: this runs on EVERY
+    /// relayout (`layout_dialog`, off `rebuild_positions`, which the frame
+    /// tick reaches whenever anything animates), and snapping to the
+    /// selection here undid every wheel and finger scroll within a frame
+    /// (2026-09-20 — the list "would not scroll at all"). Keeping the
+    /// selection in view is the keyboard's job: `move_selection` and
+    /// `scroll_to_selected` at the call sites that change it.
     pub fn set_page(&mut self, page: usize) {
         self.page = page.max(1);
-        self.scroll_to_selected();
+        self.set_scroll_px(self.scroll_px);
     }
 
     /// How many rows fit — what PageUp/PageDown step by.
diff --git a/src/main.rs b/src/main.rs
index 7a6da9a..d464a17 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -3266,6 +3266,42 @@ mod tests {
     }
 
     #[test]
+    /// The dialog's commands list takes a trackpad (finger-phase pixel
+    /// delta) as well as a wheel notch (2026-09-20: a finger did nothing).
+    #[test]
+    fn dialog_list_scrolls_by_trackpad_and_by_wheel() {
+        use cce_ui::widget::{MouseScrollDelta, Position, ScrollPhase, WidgetHost};
+        use crate::dialog::{Dialog, Row};
+        let mut ctx = cce_ui::context::UiContext::new();
+        let mut d = Dialog::new();
+        d.set_visible(true);
+        WidgetHost::set_rect(&mut d, 0.0, 0.0, 520.0, 420.0);
+        let rows: Vec<Row> = (0..60)
+            .map(|i| Row { id: format!("c{i}"), label: format!("Command {i}"), chord: String::new() })
+            .collect();
+        d.set_rows(rows);
+        d.set_page(10);
+        assert_eq!(d.scroll_px, 0.0);
+
+        cce_ui::widget::scroll_motion::set_scroll_phase(ScrollPhase::Finger);
+        let moved = d.mouse_wheel_ungated(&MouseScrollDelta::PixelDelta(Position { x: 0.0, y: -30.0 }), 100.0, 200.0, &mut ctx);
+        assert!(moved, "a finger delta moves the list");
+        assert!(d.scroll_px > 0.0, "trackpad scrolled the list: {}", d.scroll_px);
+        // The layout re-records the page on every relayout; that must not
+        // snap the list back to the (unscrolled) selection.
+        let scrolled = d.scroll_px;
+        d.set_page(10);
+        assert_eq!(d.scroll_px, scrolled, "set_page keeps the scroll");
+
+        let before = d.scroll_px;
+        cce_ui::widget::scroll_motion::set_scroll_phase(ScrollPhase::Wheel);
+        d.mouse_wheel_ungated(&MouseScrollDelta::LineDelta(0.0, -1.0), 100.0, 200.0, &mut ctx);
+        for _ in 0..30 {
+            WidgetHost::tick(&mut d, 1.0 / 60.0, &mut ctx);
+        }
+        assert!(d.scroll_px > before, "a wheel notch glides the list: {} -> {}", before, d.scroll_px);
+    }
+
     fn test_keyboard_shortcut_system() {
         // Test parsing simple shortcut
         let ctrl_g = Shortcut::parse("Ctrl+g").unwrap();