A precision touchpad sends pixel-precise `MouseWheel` events. On Windows they arrive with no `TouchPhase::Start`, so egui never takes its no-smoothing "in touch" path and eases any delta of 8 px or more over several frames, leaving the page a frame behind the finger. The page view now applies precise (Point-unit) wheel input itself, 1:1, through the same un-eased scroll path as keyboard and middle-button auto-scroll, and suppresses egui's eased copy for a short window so its internal easing tail neither doubles nor trails the motion. A coarse mouse wheel (Line/Page units) keeps egui's smoothing, which is pleasant for chunky notches, and single-page paging still owns the vertical axis. Regression test: one 200 px precise swipe scrolls the full 200 px at once (an eased path would ramp there over ~0.1 s) and leaves no drift. Co-authored-by: Justin John <babyjjbabyjj549@gmail.com> Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
7a1e0bce86
commit
3cd619e937
|
|
@ -306,6 +306,12 @@ pub struct DocView {
|
|||
shown: bool,
|
||||
/// Turns wheel input into page turns in single-page view.
|
||||
wheel: crate::wheel_pager::WheelPager,
|
||||
/// Until this time, apply precise (touchpad) wheel input 1:1 and suppress egui's eased copy.
|
||||
/// A precision touchpad sends pixel-precise `MouseWheel` events; on Windows they arrive with
|
||||
/// no `TouchPhase::Start`, so egui eases deltas of 8 px or more and the page lags the finger
|
||||
/// (#759). The window outlasts a single frame so egui's internal easing tail is suppressed as
|
||||
/// it decays, instead of adding on top of the 1:1 motion.
|
||||
precise_scroll_until: f64,
|
||||
pub(crate) auto_scroll: crate::autoscroll::AutoScroll,
|
||||
/// Pages selected in the organize grid or the Pages panel (0-based). Empty means "the current page".
|
||||
pub selected: BTreeSet<usize>,
|
||||
|
|
@ -503,6 +509,7 @@ impl DocView {
|
|||
zoom_anchor: None,
|
||||
shown: false,
|
||||
wheel: Default::default(),
|
||||
precise_scroll_until: 0.0,
|
||||
auto_scroll: Default::default(),
|
||||
selected: BTreeSet::new(),
|
||||
select_anchor: None,
|
||||
|
|
@ -1772,10 +1779,12 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
|||
// instead; zoomed in far enough to pan, it pans. Touch drags are untouched: with nothing
|
||||
// to pan, touch users turn pages with the rail buttons, the page box and the arrow keys.
|
||||
// Runs before `visible_pages` so the frame that turns the page draws it.
|
||||
let mut single_paging = false;
|
||||
if view.layout == PageLayout::Single {
|
||||
// Within a point, so layout rounding can't stop a fitting page from turning.
|
||||
let fits = rects.get(view.current.min(rects.len().saturating_sub(1))).is_some_and(|r| r.height() + 2.0 * MARGIN <= avail.height() + 1.0);
|
||||
let can_turn = fits && !middle_gesture && unobstructed && ui.rect_contains_pointer(avail);
|
||||
single_paging = can_turn;
|
||||
// Every wheel event goes to the pager, so it follows each trackpad touch to its end
|
||||
// even while the page can't turn.
|
||||
ui.input(|i| {
|
||||
|
|
@ -1793,6 +1802,46 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
|||
ui.input_mut(|i| i.smooth_scroll_delta.y = 0.0);
|
||||
}
|
||||
}
|
||||
// Touchpad scrolling 1:1 (#759). A precision touchpad sends pixel-precise `MouseWheel`
|
||||
// events; on Windows they carry no `TouchPhase::Start`, so egui never takes its no-smoothing
|
||||
// path and eases deltas of 8 px or more, leaving the page a frame behind the finger. Apply
|
||||
// the precise delta ourselves this frame through the un-animated scroll path, and suppress
|
||||
// egui's eased copy while it decays so it neither doubles nor trails the motion. A coarse
|
||||
// mouse wheel keeps egui's easing (pleasant for chunky notches).
|
||||
let now = ui.input(|i| i.time);
|
||||
let mut precise_scroll = Vec2::ZERO;
|
||||
if ui.rect_contains_pointer(avail) {
|
||||
let mut coarse_wheel = false;
|
||||
ui.input(|i| {
|
||||
for e in &i.events {
|
||||
if let egui::Event::MouseWheel { unit, delta, modifiers, .. } = e {
|
||||
if modifiers.command || modifiers.ctrl {
|
||||
continue; // Zooming, not scrolling.
|
||||
}
|
||||
match unit {
|
||||
// Shift scrolls sideways, matching egui's own wheel handling.
|
||||
egui::MouseWheelUnit::Point if modifiers.shift => precise_scroll.x += delta.x + delta.y,
|
||||
egui::MouseWheelUnit::Point => precise_scroll += *delta,
|
||||
egui::MouseWheelUnit::Line | egui::MouseWheelUnit::Page => coarse_wheel = true,
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
if coarse_wheel {
|
||||
view.precise_scroll_until = 0.0;
|
||||
} else if precise_scroll != Vec2::ZERO {
|
||||
view.precise_scroll_until = now + 0.25;
|
||||
}
|
||||
}
|
||||
// Paging owns the vertical axis in single-page view; leave it to the pager.
|
||||
if single_paging {
|
||||
precise_scroll.y = 0.0;
|
||||
}
|
||||
if now < view.precise_scroll_until {
|
||||
ui.input_mut(|i| i.smooth_scroll_delta = Vec2::ZERO);
|
||||
} else {
|
||||
precise_scroll = Vec2::ZERO;
|
||||
}
|
||||
let visible_pages: Vec<usize> = match view.layout {
|
||||
PageLayout::Single => vec![view.current.min(rects.len() - 1)],
|
||||
_ => (0..rects.len()).collect(),
|
||||
|
|
@ -1922,8 +1971,9 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
|||
// Only interactions pause; the document must keep its original colours.
|
||||
ui.set_opacity(opacity);
|
||||
}
|
||||
// Middle-button auto-scroll and the keyboard (positive y moves the content down).
|
||||
let delta = auto_delta - vec2(0.0, key_scroll);
|
||||
// Middle-button auto-scroll, the keyboard and precise touchpad input (#759); positive y
|
||||
// moves the content down. All three scroll without easing, unlike egui's wheel default.
|
||||
let delta = auto_delta - vec2(0.0, key_scroll) + precise_scroll;
|
||||
if delta != Vec2::ZERO {
|
||||
ui.scroll_with_delta_animation(delta, egui::style::ScrollAnimation::none());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -214,6 +214,30 @@ fn wheel_turns_one_page_per_notch_and_per_trackpad_swipe() {
|
|||
assert_eq!(h.state().views[0].current, 1, "one swipe back turns one page back");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn precise_touchpad_wheel_scrolls_one_to_one() {
|
||||
// A precision touchpad's pixel delta scrolls the page by that amount in the same frame,
|
||||
// instead of egui easing it over several frames so the page lags the finger (#759). A single
|
||||
// 200 px precise event is well over egui's 8 px smoothing threshold, so without the fix only
|
||||
// a fraction would land on the first frame.
|
||||
let mut h = harness_stepping(1.0 / 60.0, &[("layout", "continuous"), ("zoom", "100")]);
|
||||
let at = h.state().views[0].viewport_rect().center();
|
||||
h.hover_at(at);
|
||||
h.run_steps(2);
|
||||
let before = rect(&h, 0).expect("page 1 on screen").min.y;
|
||||
h.event(wheel(MouseWheelUnit::Point, -200.0, TouchPhase::Move));
|
||||
// A few frames for the event to reach the ScrollArea and the new offset to be drawn. The
|
||||
// page jumps the full 200 px at once; an eased path would ramp there over ~0.1 s (6 frames).
|
||||
h.run_steps(4);
|
||||
let moved = before - rect(&h, 0).expect("page 1 still laid out").min.y;
|
||||
assert!(moved > 190.0, "a 200 px precise swipe scrolls ~200 px at once, not an eased fraction: moved {moved}");
|
||||
// And it stops with the finger: idle frames must not drift on a leftover easing tail.
|
||||
let settled = rect(&h, 0).unwrap().min.y;
|
||||
h.run_steps(20);
|
||||
let drift = settled - rect(&h, 0).unwrap().min.y;
|
||||
assert!(drift.abs() < 2.0, "no easing tail after the swipe: drifted {drift}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn page_display_commands_switch_layouts() {
|
||||
let mut h = harness(&[]);
|
||||
|
|
|
|||
Loading…
Reference in New Issue