graphic design tool
git clone https://git.lucas.co/cce-designer.git
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();