Merge pull request #5748 from Combiler01/fix/single-page-zoom-scroll-lock-5713

fix(ui): prevent wheel lockup at edge in non-continuous view (#5713)
This commit is contained in:
Dylan M. Taylor
2026-11-02 22:59:04 -05:00
committed by GitHub
3 changed files with 106 additions and 1 deletions
+6 -1
View File
@@ -279,7 +279,11 @@ impl DocView {
let Some(forward) = self.wheel.feed(unit, dy, phase, now, can_turn) else { return false };
let before = self.current;
self.step_page(forward);
self.current != before
if self.current == before {
self.wheel.unturn();
return false;
}
true
}
/// One wheel event in a non-continuous view whose page is taller than the window, where the
@@ -299,6 +303,7 @@ impl DocView {
let before = self.current;
self.step_page(forward);
if self.current == before {
self.wheel.unturn();
return false;
}
if !forward {
+8
View File
@@ -73,6 +73,11 @@ impl WheelPager {
self.accum = 0.0;
}
/// Reset any turned state when a turn did not happen (e.g. at document boundary).
pub(crate) fn unturn(&mut self) {
self.turned = None;
}
/// Whether the current gesture already turned a page: the rest of it must not pan the page
/// just shown away from its top (or bottom).
pub(crate) fn gesture_turned(&self) -> bool {
@@ -144,6 +149,9 @@ impl WheelPager {
};
if notch {
if !can_turn {
// A notch is a gesture of its own, so one that only pans ends the gesture a
// previous notch turned; otherwise `gesture_turned` keeps swallowing the wheel (#5713).
self.turned = None;
return None;
}
// Every notch turns a page; a smooth tail of the same gesture won't add another.
+92
View File
@@ -520,6 +520,98 @@ fn single_page_swipe_that_reaches_the_edge_does_not_turn_but_a_fresh_one_does()
);
}
#[test]
fn single_page_wheel_at_last_page_bottom_edge_can_scroll_back_up() {
let mut h = harness_stepping(1.0 / 60.0, &[("layout", "single"), ("zoom", "200")]);
h.state_mut().views[0].go_to_page(2); // Go to last page of PAGES (3 pages)
h.run_steps(20);
let at = h.state().views[0].viewport_rect().center();
h.hover_at(at);
h.run_steps(4);
// Scroll to the bottom edge of page 3
let mut notches = 0;
let mut last = rect(&h, 2).expect("page 3 on screen").min.y;
let mut at_bottom = false;
while notches < 80 {
h.event(wheel(MouseWheelUnit::Line, -1.0, TouchPhase::Move));
h.run_steps(12);
notches += 1;
if let Some(r) = rect(&h, 2) {
if (r.min.y - last).abs() < 0.01 {
at_bottom = true;
break;
}
last = r.min.y;
}
}
assert!(at_bottom, "reached bottom edge of last page");
assert_eq!(h.state().views[0].current, 2, "remains on page 3");
// Push the wheel further against the bottom edge (cannot turn because it's the last page)
for _ in 0..5 {
h.event(wheel(MouseWheelUnit::Line, -1.0, TouchPhase::Move));
h.run_steps(12);
}
let bottom_y = rect(&h, 2).expect("page 3 on screen").min.y;
// Roll the wheel back up: the page must move away from the bottom edge (#5713)
h.event(wheel(MouseWheelUnit::Line, 1.0, TouchPhase::Move));
h.run_steps(12);
let after_up_y = rect(&h, 2).expect("page 3 on screen").min.y;
assert!(after_up_y > bottom_y, "scrolling up moves the page away from the bottom edge: {after_up_y} > {bottom_y}");
}
/// One notch of a mouse wheel that macOS reports in points (a Logitech mouse sends about 14).
const POINT_NOTCH: f32 = 14.0;
#[test]
fn single_page_point_notches_pan_the_page_a_notch_turned_to() {
let mut h = harness_stepping(1.0 / 60.0, &[("layout", "single"), ("zoom", "200")]);
let at = h.state().views[0].viewport_rect().center();
h.hover_at(at);
h.run_steps(20);
for _ in 0..120 {
h.event(wheel(MouseWheelUnit::Point, -POINT_NOTCH, TouchPhase::Move));
h.run_steps(12);
if h.state().views[0].current == 1 {
break;
}
}
assert_eq!(h.state().views[0].current, 1, "notches pan page 1, then turn to page 2");
h.run_steps(20);
let top = rect(&h, 1).expect("page 2 on screen").min.y;
for _ in 0..3 {
h.event(wheel(MouseWheelUnit::Point, -POINT_NOTCH, TouchPhase::Move));
h.run_steps(12);
}
let after = rect(&h, 1).expect("page 2 on screen").min.y;
assert!(after < top, "notches pan page 2 down after the turn (#5713): {after} < {top}");
}
#[test]
fn single_page_point_notches_scroll_back_up_from_the_last_page_bottom() {
let mut h = harness_stepping(1.0 / 60.0, &[("layout", "single"), ("zoom", "200")]);
h.state_mut().views[0].go_to_page(2);
h.run_steps(20);
let at = h.state().views[0].viewport_rect().center();
h.hover_at(at);
h.run_steps(4);
// Past the bottom: the extra notches push against the last page's edge.
for _ in 0..200 {
h.event(wheel(MouseWheelUnit::Point, -POINT_NOTCH, TouchPhase::Move));
h.run_steps(12);
}
assert_eq!(h.state().views[0].current, 2, "stays on the last page");
let bottom = rect(&h, 2).expect("page 3 on screen").min.y;
for _ in 0..3 {
h.event(wheel(MouseWheelUnit::Point, POINT_NOTCH, TouchPhase::Move));
h.run_steps(12);
}
let after = rect(&h, 2).expect("page 3 on screen").min.y;
assert!(after > bottom + 1.0, "notches scroll back up from the last page's bottom (#5713): {after} > {bottom}");
}
#[test]
fn stepping_past_either_end_leaves_the_view_alone() {
// Paging on past the first or last page must not jump the page back to its top.