Merge PR #2587: M3: single-page view turns pages without a blank frame

This commit is contained in:
aiongg
2026-10-16 14:02:07 +00:00
2 changed files with 60 additions and 6 deletions
+28 -6
View File
@@ -2410,9 +2410,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
// When not scrolling continuously, only one row is shown at a time: the current page, or the
// current two-page spread (with the cover rule). These indices drive paging, which pages are
// drawn, and the content height (#752). Empty for a document with no pages.
let row: Vec<usize> = if rects.is_empty() {
Vec::new()
} else {
let shown_row = |view: &DocView| -> Vec<usize> {
if rects.is_empty() {
return Vec::new();
}
let last = rects.len() - 1;
let cur = view.current.min(last);
match view.layout {
@@ -2424,6 +2425,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
}
}
};
let row = shown_row(view);
let middle_gesture = view.auto_scroll.blocks_input();
// Not scrolling: when the whole page or spread fits, the wheel would do nothing, so it turns
// pages instead; zoomed in far enough to pan, it pans, and at the edge it turns the page on
@@ -2524,6 +2526,9 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
if view.fling.gliding() {
ui.ctx().request_repaint();
}
// The wheel may have turned the page above: draw the row it turned to. Measured from the old
// row, the jump to the new page scrolled both out of view, a blank frame on every turn forward.
let row = shown_row(view);
let visible_pages: Vec<usize> = if view.continuous { (0..rects.len()).collect() } else { row.clone() };
let (y_shift, content_h) = if view.continuous {
(0.0, rects.iter().map(|r| r.bottom()).reduce(f32::max).map(|bottom| bottom + MARGIN).unwrap_or(0.0))
@@ -2563,9 +2568,6 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
let r = rects[page.min(rects.len() - 1)];
let point = pos2(r.left() + fx * r.width(), r.top() - y_shift + fy * r.height());
scroll = scroll.scroll_offset(vec2((point.x - rel.x).max(0.0), (point.y - rel.y).max(0.0)));
} else if edge_turned {
// The row above was laid out for the page before the turn: place the new one next frame.
ui.ctx().request_repaint();
} else if let Some((page, frac)) = view.goto.take() {
let page = page.min(rects.len() - 1);
let r = rects[page];
@@ -3612,6 +3614,26 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
view.last_scroll = scroll_offset;
let pan_held = now_settle < view.pan_hold_until;
let (mut wanted, visible_now) = out.inner;
// Single-page and two-page views draw only the current row, so the next one was first asked
// for on the turn and showed blank paper until it rendered. Prefetch the rows either side,
// as continuous scrolling does for the pages just off screen.
if !view.continuous
&& let (Some(&first), Some(&last)) = (visible_pages.first(), visible_pages.last())
{
let n = if view.layout == PageLayout::Single { 1 } else { 2 };
let max_side = ui.ctx().input(|inp| inp.max_texture_side) as f32;
for i in (last.saturating_add(1)..=last.saturating_add(n)).chain(first.saturating_sub(n)..first) {
let Some(p) = info.pages.get(i).filter(|_| !view.errors.contains_key(&i)) else { continue };
// As drawn: the whole page at this scale, or the backdrop of a tiled page.
let side = p.width.max(p.height).max(1.0);
wanted.push(if side * scale > TILE_THRESHOLD.min(max_side) {
let bs = BASE_SIDE.min(max_side) / side;
(i, bs, scale_tag(bs), None)
} else {
(i, scale, tag, None)
});
}
}
let cur = view.current;
wanted.sort_by_key(|w| (!visible_now.contains(&w.0), w.0.abs_diff(cur), w.3.is_some()));
// Tiles of far-away pages are useless: free them. Tiles of an earlier zoom stay for a page
+32
View File
@@ -185,6 +185,38 @@ fn single_page_layout_shows_one_page_at_a_time() {
assert!(rect(&h, 0).is_none() && rect(&h, 1).is_some(), "only page 2");
}
#[test]
fn single_page_view_prefetches_the_next_page() {
// Only the shown page is drawn, but the next one renders ahead of the turn: otherwise
// turning forward shows blank paper until it arrives.
let mut h = harness(&[("layout", "single"), ("zoom", "50")]);
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(10);
while h.state().render_pending() {
assert!(std::time::Instant::now() < deadline, "renders settle");
h.run_steps(2);
std::thread::sleep(std::time::Duration::from_millis(5));
}
assert!(rect(&h, 1).is_none(), "page 2 is not shown");
assert!(h.state().views[0].raster_memory().page_count >= 2, "page 2 is rendered ahead of the turn");
}
#[test]
fn single_page_wheel_turn_draws_the_new_page_at_once() {
// The frame that turns the page draws the new one. Its jump used to be measured from the
// old page's place, which scrolled both out of view: a blank frame on every turn forward.
let mut h = harness_stepping(1.0 / 60.0, &[("layout", "single")]);
h.key_press_modifiers(Modifiers::COMMAND, Key::Num0); // fit page
h.run_steps(4);
let vp = h.state().views[0].viewport_rect();
h.hover_at(vp.center());
h.run_steps(2);
h.event(wheel(MouseWheelUnit::Line, -1.0, TouchPhase::Move));
h.step();
assert_eq!(h.state().views[0].current, 1, "one notch turns one page");
let r = rect(&h, 1).expect("page 2 drawn in the frame that turns to it");
assert!(r.min.y >= vp.min.y && r.max.y <= vp.max.y, "page 2 fits on screen: {r:?} in {vp:?}");
}
#[test]
fn single_page_wheel_pans_instead_when_zoomed_in() {
// The zoomed-in guard: with room to pan, the wheel pans and must not turn pages.