mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-08 03:54:35 +00:00
Merge pull request #6063 from dotCooCoo/fix/continuous-scroll-prefetch
fix(ui): continuous view renders the pages ahead of the scroll
This commit is contained in:
5 files changed
+327
-13
No files matched your search
@@ -521,6 +521,9 @@ pub struct DocView {
|
||||
/// Pages on screen this frame. Their rasters and tiles are always admitted: the viewport
|
||||
/// bounds them, and the byte and count limits apply to prefetch only.
|
||||
frame_visible: HashSet<usize>,
|
||||
/// Pages whose whole-page raster this frame asks for, on screen or prefetched. An arriving
|
||||
/// raster over the allowance evicts the other cached pages first.
|
||||
frame_demanded: HashSet<usize>,
|
||||
/// Thumbnails that are out of date (still shown until their replacement arrives).
|
||||
stale_thumbs: HashSet<usize>,
|
||||
/// Tiles that are out of date (still shown until their replacement arrives).
|
||||
@@ -712,6 +715,13 @@ pub struct DocView {
|
||||
/// in-flight ones cannot be cancelled mid-operator. Older tiles and the backdrop stay
|
||||
/// stretched meanwhile; fresh tiles go out once the pan settles.
|
||||
pub(crate) last_scroll: Vec2,
|
||||
/// The way the view last moved vertically: 1 down, -1 up, 0 before any move and after the
|
||||
/// content's size changes (a zoom or a layout change). A jump to a page or a search result
|
||||
/// counts as a move. It stays set while the view is still, so the pages prefetched ahead stay
|
||||
/// in the render queue until they arrive.
|
||||
scroll_dir: i8,
|
||||
/// The content's size when `scroll_dir` was last updated.
|
||||
scroll_content: Vec2,
|
||||
/// Side-by-side compare: scroll to here this frame, following the other half.
|
||||
pub(crate) sync_scroll: Option<Vec2>,
|
||||
/// The page widget's id, by which the Hand tool finds its drag before the pages are laid out.
|
||||
@@ -929,6 +939,7 @@ impl DocView {
|
||||
frame_queue: Vec::new(),
|
||||
frame_geometry_changed: false,
|
||||
frame_visible: HashSet::new(),
|
||||
frame_demanded: HashSet::new(),
|
||||
stale_thumbs: HashSet::new(),
|
||||
stale_tiles: HashSet::new(),
|
||||
current_scale_tag: 0,
|
||||
@@ -996,6 +1007,8 @@ impl DocView {
|
||||
last_scale: 0.0,
|
||||
sharp_hold_until: 0.0,
|
||||
last_scroll: Vec2::ZERO,
|
||||
scroll_dir: 0,
|
||||
scroll_content: Vec2::ZERO,
|
||||
sync_scroll: None,
|
||||
page_widget: None,
|
||||
pan_hold_until: 0.0,
|
||||
@@ -2917,6 +2930,56 @@ fn read_wheel(ui: &mut egui::Ui, view: &mut DocView, avail: Rect, single_paging:
|
||||
Wheel { precise_scroll, line_base, boost_extra, has_page, paging_owns_vertical }
|
||||
}
|
||||
|
||||
/// Rows of pages that continuous view prefetches past the window in the direction it last
|
||||
/// moved.
|
||||
const PREFETCH_ROWS: usize = 2;
|
||||
|
||||
/// The pages continuous view prefetches past the window: the next [`PREFETCH_ROWS`] rows the way
|
||||
/// it last moved, nearest first, and none before it moves. The web build renders on the UI
|
||||
/// thread, so it prefetches none.
|
||||
fn scroll_prefetch(view: &DocView, visible_now: &[usize]) -> Vec<usize> {
|
||||
if !view.continuous || cfg!(target_arch = "wasm32") {
|
||||
return Vec::new();
|
||||
}
|
||||
let n = PREFETCH_ROWS * if view.layout == PageLayout::Single { 1 } else { 2 };
|
||||
match (view.scroll_dir, visible_now.iter().min(), visible_now.iter().max()) {
|
||||
(1, _, Some(&last)) => (last.saturating_add(1)..=last.saturating_add(n)).collect(),
|
||||
(-1, Some(&first), _) => (first.saturating_sub(n)..first).rev().collect(),
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// The whole-page raster a prefetch asks for: the page at this scale, or the backdrop of a page
|
||||
/// that is tiled at this scale. A cached raster sharper than the backdrop stays demanded as it
|
||||
/// is, as `paint_page_surface` demands it under tiles.
|
||||
fn prefetch_demand(view: &DocView, p: &pdfcraft_render::PageInfo, i: usize, scale: f32, tag: u64, max_side: f32) -> (usize, f32, u64, Option<Tile>) {
|
||||
let side = p.width.max(p.height).max(1.0);
|
||||
if side * scale <= TILE_THRESHOLD.min(max_side) {
|
||||
return (i, scale, tag, None);
|
||||
}
|
||||
let bs = BASE_SIDE.min(max_side) / side;
|
||||
match view.pages.get(&i).filter(|c| c.tag != STALE_TAG && tag_scale(c.tag) >= bs) {
|
||||
Some(c) => (i, tag_scale(c.tag), c.tag, None),
|
||||
None => (i, bs, scale_tag(bs), None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `page` lies behind the way continuous view last moved: above the current page while
|
||||
/// scrolling down, below it while scrolling up. `false` outside continuous view.
|
||||
fn behind_scroll(continuous: bool, scroll_dir: i8, current: usize, page: usize) -> bool {
|
||||
continuous && ((scroll_dir > 0 && page < current) || (scroll_dir < 0 && page > current))
|
||||
}
|
||||
|
||||
/// Orders a frame's demand: pages on screen first, then off-screen pages ahead of the way the
|
||||
/// view last moved, then those behind it, each nearest the current page first.
|
||||
fn order_demand(view: &DocView, wanted: &mut [(usize, f32, u64, Option<Tile>)], visible_now: &[usize]) {
|
||||
let cur = view.current;
|
||||
wanted.sort_by_key(|w| {
|
||||
let shown = visible_now.contains(&w.0);
|
||||
(!shown, !shown && behind_scroll(view.continuous, view.scroll_dir, cur, w.0), w.0.abs_diff(cur), w.3.is_some())
|
||||
});
|
||||
}
|
||||
|
||||
/// What decides which renders a frame asks for.
|
||||
#[derive(Clone, Copy)]
|
||||
struct RenderPlan<'a> {
|
||||
@@ -2929,7 +2992,8 @@ struct RenderPlan<'a> {
|
||||
}
|
||||
|
||||
/// Queues the renders for the pages and tiles `renders` wants (nearest the view first), prefetches the
|
||||
/// rows either side, frees tiles of far pages and old zooms, and asks for the text layers.
|
||||
/// rows either side (in continuous view, the rows ahead in the scroll direction), frees tiles of far
|
||||
/// pages and old zooms, and asks for the text layers.
|
||||
fn plan_renders(ui: &egui::Ui, view: &mut DocView, info: &DocInfo, renders: PageRenders, plan: &RenderPlan<'_>) {
|
||||
let (mut wanted, visible_now) = renders;
|
||||
let RenderPlan { visible_pages, tag, scale, sharp_held, pan_held, hand } = *plan;
|
||||
@@ -2944,17 +3008,20 @@ fn plan_renders(ui: &egui::Ui, view: &mut DocView, info: &DocInfo, renders: Page
|
||||
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)
|
||||
});
|
||||
wanted.push(prefetch_demand(view, p, i, scale, tag, max_side));
|
||||
}
|
||||
}
|
||||
// Continuous view also asks for the rows ahead of the way it last moved.
|
||||
let max_side = ui.ctx().input(|inp| inp.max_texture_side) as f32;
|
||||
for i in scroll_prefetch(view, &visible_now) {
|
||||
if view.errors.contains_key(&i) || wanted.iter().any(|w| w.0 == i) {
|
||||
continue;
|
||||
}
|
||||
let Some(p) = info.pages.get(i) else { continue };
|
||||
wanted.push(prefetch_demand(view, p, i, scale, tag, max_side));
|
||||
}
|
||||
let cur = view.current;
|
||||
wanted.sort_by_key(|w| (!visible_now.contains(&w.0), w.0.abs_diff(cur), w.3.is_some()));
|
||||
order_demand(view, &mut wanted, &visible_now);
|
||||
// Tiles of far-away pages are useless: free them. Tiles of an earlier zoom stay for a page
|
||||
// that is on screen, whether or not the page is tiled at this zoom (#2196): a zoom out below
|
||||
// the tiling threshold used to drop the tiles of the zoom just left, so coming back to it
|
||||
@@ -3187,9 +3254,10 @@ fn page_scroll_area(ui: &egui::Ui, view: &mut DocView, geometry: &ScrollGeometry
|
||||
scroll
|
||||
}
|
||||
|
||||
/// Notes how far the page view scrolled this frame: reader-owned scrolling ends search following,
|
||||
/// a pan holds back fresh tile renders until it settles, and a glide keeps going or stops. Whether
|
||||
/// a pan is still settling.
|
||||
/// Notes how far and which way the page view scrolled this frame: reader-owned scrolling ends
|
||||
/// search following, a pan holds back fresh tile renders until it settles, a glide keeps going or
|
||||
/// stops, and the vertical direction is kept for continuous view's prefetch. Whether a pan is
|
||||
/// still settling.
|
||||
fn track_scroll<T>(ui: &egui::Ui, view: &mut DocView, out: &egui::containers::scroll_area::ScrollAreaOutput<T>, glide: Vec2) -> bool {
|
||||
let scroll_offset = out.state.clamped_offset();
|
||||
// The document ScrollArea's own scroll surfaces move the offset without the wheel deltas
|
||||
@@ -3213,10 +3281,27 @@ fn track_scroll<T>(ui: &egui::Ui, view: &mut DocView, out: &egui::containers::sc
|
||||
if glide != Vec2::ZERO {
|
||||
view.fling.moved((scroll_offset - view.last_scroll).length() > 0.01);
|
||||
}
|
||||
view.scroll_dir = scroll_direction(view.scroll_dir, scroll_offset.y - view.last_scroll.y, out.content_size != view.scroll_content);
|
||||
view.scroll_content = out.content_size;
|
||||
view.last_scroll = scroll_offset;
|
||||
now_settle < view.pan_hold_until
|
||||
}
|
||||
|
||||
/// The way the view moves after a frame whose offset changed by `dy` points: down (1) or up (-1)
|
||||
/// for a move of more than half a point, `dir` otherwise, and 0 when the content's size changed
|
||||
/// (a zoom or a layout change).
|
||||
fn scroll_direction(dir: i8, dy: f32, resized: bool) -> i8 {
|
||||
if resized {
|
||||
0
|
||||
} else if dy > 0.5 {
|
||||
1
|
||||
} else if dy < -0.5 {
|
||||
-1
|
||||
} else {
|
||||
dir
|
||||
}
|
||||
}
|
||||
|
||||
/// The hover tip, find bar, size hint and the in-place editors drawn over the pages. Whether the
|
||||
/// text editor was finished.
|
||||
fn frame_overlays(ui: &mut egui::Ui, view: &mut DocView, env: &PageEnv<'_>, primary_down: bool, hover_text: Option<(Pos2, String)>) -> bool {
|
||||
|
||||
@@ -158,6 +158,13 @@ impl DocView {
|
||||
k
|
||||
}
|
||||
|
||||
/// The pages with a cached whole-page raster, in page order (diagnostics and tests).
|
||||
pub fn cached_pages_debug(&self) -> Vec<usize> {
|
||||
let mut pages: Vec<usize> = self.pages.keys().copied().collect();
|
||||
pages.sort_unstable();
|
||||
pages
|
||||
}
|
||||
|
||||
/// Re-render every page and text layer (the document's appearance changed, e.g. a layer
|
||||
/// was toggled). Out-of-date rasters stay on screen until their replacements arrive, so the
|
||||
/// view never flashes blank.
|
||||
@@ -418,6 +425,7 @@ impl DocView {
|
||||
// current page first, so turning back a page shows it at once instead of rendering.
|
||||
let mut undemanded: Vec<usize> = self.pages.keys().copied().filter(|p| !pages.contains(p)).collect();
|
||||
undemanded.sort_by_key(|p| p.abs_diff(self.current));
|
||||
self.frame_demanded = pages.clone();
|
||||
let mut kept = pages.len();
|
||||
for page in undemanded {
|
||||
let bytes = self.pages.get(&page).map_or(0, |p| texture_bytes(&p.tex));
|
||||
@@ -671,7 +679,13 @@ impl DocView {
|
||||
// demand estimates. Evict those first, then let the current demand refill them.
|
||||
let on_screen = |p: &usize| !thumb && self.frame_visible.contains(p);
|
||||
while retained.saturating_add(bytes) > limit {
|
||||
let Some(page) = cache.keys().copied().filter(|p| !on_screen(p)).max_by_key(|p| p.abs_diff(self.current)) else { break };
|
||||
// Pages this frame does not ask for go first, then those behind the scroll, the
|
||||
// farthest from the current page first.
|
||||
let evict_order = |p: &usize| {
|
||||
let demanded = !thumb && self.frame_demanded.contains(p);
|
||||
(!demanded, !thumb && super::behind_scroll(self.continuous, self.scroll_dir, self.current, *p), p.abs_diff(self.current))
|
||||
};
|
||||
let Some(page) = cache.keys().copied().filter(|p| !on_screen(p)).max_by_key(evict_order) else { break };
|
||||
if let Some(old) = cache.remove(&page) {
|
||||
retained = retained.saturating_sub(texture_bytes(&old.tex));
|
||||
}
|
||||
|
||||
@@ -1273,6 +1273,141 @@ fn page_and_tile_admission_accounts_for_bytes_before_count_limits() {
|
||||
assert!(!v.admit_texture(RenderRequest::default(), PAGE_BYTES + 1));
|
||||
}
|
||||
|
||||
/// The frame demand of a continuous view showing `shown`, with its prefetch, in queue order.
|
||||
fn scroll_demand(v: &DocView, shown: &[usize]) -> Vec<RenderRequest> {
|
||||
let mut wanted: Vec<(usize, f32, u64, Option<Tile>)> = shown.iter().map(|&p| (p, 1.0, 1000, None)).collect();
|
||||
wanted.extend(scroll_prefetch(v, shown).into_iter().map(|p| (p, 1.0, 1000, None)));
|
||||
order_demand(v, &mut wanted, shown);
|
||||
wanted.into_iter().map(|(page, scale, tag, tile)| RenderRequest { page, scale, tag, tile, ..Default::default() }).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continuous_view_prefetches_the_rows_ahead_of_the_way_it_moved() {
|
||||
let mut v = view(20, PageLayout::Single, true);
|
||||
v.current = 10;
|
||||
assert!(scroll_prefetch(&v, &[9, 10]).is_empty(), "nothing before the view moves");
|
||||
v.scroll_dir = 1;
|
||||
assert_eq!(scroll_prefetch(&v, &[9, 10]), [11, 12]);
|
||||
v.scroll_dir = -1;
|
||||
assert_eq!(scroll_prefetch(&v, &[9, 10]), [8, 7]);
|
||||
assert_eq!(scroll_prefetch(&v, &[0, 1]), Vec::<usize>::new(), "nothing above the first page");
|
||||
let mut two = view(20, PageLayout::TwoUp, true);
|
||||
two.scroll_dir = 1;
|
||||
assert_eq!(scroll_prefetch(&two, &[8, 9]), [10, 11, 12, 13], "two rows of two pages");
|
||||
let mut single = view(20, PageLayout::Single, false);
|
||||
single.scroll_dir = 1;
|
||||
assert!(scroll_prefetch(&single, &[9]).is_empty(), "single-page view keeps its own prefetch");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrolling_the_other_way_drops_the_prefetch_from_the_queue() {
|
||||
let info = DocInfo {
|
||||
pages: (0..20)
|
||||
.map(|_| pdfcraft_render::PageInfo { width: 300.0, height: 400.0, label: String::new(), crop: [0.0, 0.0, 300.0, 400.0], rotation: 0 })
|
||||
.collect(),
|
||||
..Default::default()
|
||||
};
|
||||
let mut v = DocView::new(DocId(1), &info, ViewDefaults { continuous: true, ..Default::default() });
|
||||
v.current = 10;
|
||||
v.scroll_dir = 1;
|
||||
v.frame_visible = [9, 10].into();
|
||||
v.frame_queue = scroll_demand(&v, &[9, 10]);
|
||||
let down: Vec<usize> = v.prepare_render_queue(&info, 1.0).iter().map(|r| r.page).collect();
|
||||
assert_eq!(down, [10, 9, 11, 12], "on-screen pages first, then the pages below");
|
||||
v.scroll_dir = -1;
|
||||
v.frame_queue = scroll_demand(&v, &[9, 10]);
|
||||
let up: Vec<usize> = v.prepare_render_queue(&info, 1.0).iter().map(|r| r.page).collect();
|
||||
assert_eq!(up, [10, 9, 8, 7], "the pages below leave the queue, which cancels their renders");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_way_the_view_moved_does_not_reorder_the_pages_on_screen() {
|
||||
let mut v = view(20, PageLayout::Single, true);
|
||||
v.current = 10;
|
||||
let shown = [8, 9, 10, 11, 12];
|
||||
let still: Vec<usize> = scroll_demand(&v, &shown).iter().map(|r| r.page).collect();
|
||||
v.scroll_dir = 1;
|
||||
let down: Vec<usize> = scroll_demand(&v, &shown).iter().map(|r| r.page).collect();
|
||||
assert_eq!(down[..5], still[..5], "on-screen pages keep their order");
|
||||
assert_eq!(down[5..], [13, 14]);
|
||||
// Off-screen pages ahead of the move come before nearer ones behind it.
|
||||
let mut wanted = vec![(9, 1.0, 1000, None), (13, 1.0, 1000, None), (10, 1.0, 1000, None)];
|
||||
order_demand(&v, &mut wanted, &[10]);
|
||||
assert_eq!(wanted.iter().map(|w| w.0).collect::<Vec<_>>(), [10, 13, 9]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_prefetch_ahead_stays_within_the_page_allowance() {
|
||||
let mut info = thumbnail_info(20);
|
||||
for p in &mut info.pages {
|
||||
p.width = 2000.0;
|
||||
p.height = 2000.0;
|
||||
}
|
||||
let mut v = DocView::new(DocId(1), &info, ViewDefaults { continuous: true, ..Default::default() });
|
||||
v.current = 3;
|
||||
v.scroll_dir = 1;
|
||||
// Eight 16 MB pages on screen leave no room in the 128 MiB allowance for another.
|
||||
let shown: Vec<usize> = (0..8).collect();
|
||||
v.frame_visible = shown.iter().copied().collect();
|
||||
v.frame_queue = scroll_demand(&v, &shown);
|
||||
assert!(v.frame_queue.iter().any(|r| r.page == 8), "the prefetch asks for the next page");
|
||||
let pages: Vec<usize> = v.prepare_render_queue(&info, 1.0).iter().map(|r| r.page).collect();
|
||||
assert_eq!(pages.iter().copied().max(), Some(7), "no prefetch past the allowance: {pages:?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn an_arriving_raster_evicts_pages_nobody_asks_for_and_then_pages_behind_the_scroll() {
|
||||
let ctx = egui::Context::default();
|
||||
ctx.input_mut(|i| i.max_texture_side = 4096);
|
||||
let mut v = view(20, PageLayout::Single, true);
|
||||
let texture = |name| ctx.load_texture(name, egui::ColorImage::filled([4000, 4000], Color32::WHITE), TextureOptions::LINEAR);
|
||||
v.current = 10;
|
||||
v.scroll_dir = 1;
|
||||
v.frame_visible = [10].into();
|
||||
// A prefetch off screen: a raster arriving on screen would widen the allowance to hold it.
|
||||
let page = RenderRequest { page: 11, scale: 1.0, tag: 1000, ..Default::default() };
|
||||
let bytes = rgba_bytes(4000, 4000);
|
||||
// Page 7 is behind the scroll and no longer asked for; page 14 is the prefetch ahead.
|
||||
v.frame_demanded = [10, 11, 14].into();
|
||||
v.pages.insert(7, PageTex { tag: 1000, tex: texture("behind") });
|
||||
v.pages.insert(14, PageTex { tag: 1000, tex: texture("ahead") });
|
||||
assert!(v.admit_texture(page, bytes));
|
||||
assert!(v.pages.contains_key(&14) && !v.pages.contains_key(&7), "the page nobody asks for goes first");
|
||||
// Both asked for: the page behind the scroll goes, though it is nearer.
|
||||
v.frame_demanded = [7, 10, 11, 14].into();
|
||||
v.pages.insert(7, PageTex { tag: 1000, tex: texture("behind") });
|
||||
assert!(v.admit_texture(page, bytes));
|
||||
assert!(v.pages.contains_key(&14) && !v.pages.contains_key(&7), "the page behind the scroll goes first");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_tiled_page_ahead_keeps_its_sharper_raster() {
|
||||
let ctx = egui::Context::default();
|
||||
let mut v = view(20, PageLayout::Single, true);
|
||||
let p = pdfcraft_render::PageInfo { width: 300.0, height: 400.0, label: String::new(), crop: [0.0, 0.0, 300.0, 400.0], rotation: 0 };
|
||||
let (scale, max_side) = (12.0, 16384.0);
|
||||
let backdrop = BASE_SIDE / 400.0;
|
||||
// At 12× the page is 4800 pixels tall: tiled, with a 2048-pixel backdrop.
|
||||
assert_eq!(prefetch_demand(&v, &p, 3, scale, scale_tag(scale), max_side), (3, backdrop, scale_tag(backdrop), None));
|
||||
assert_eq!(prefetch_demand(&v, &p, 3, 2.0, scale_tag(2.0), max_side), (3, 2.0, scale_tag(2.0), None), "not tiled: the page as drawn");
|
||||
let texture = |name| ctx.load_texture(name, egui::ColorImage::filled([1, 1], Color32::WHITE), TextureOptions::LINEAR);
|
||||
v.pages.insert(3, PageTex { tag: scale_tag(8.0), tex: texture("sharper") });
|
||||
assert_eq!(prefetch_demand(&v, &p, 3, scale, scale_tag(scale), max_side), (3, 8.0, scale_tag(8.0), None), "the sharper raster is kept");
|
||||
v.pages.insert(3, PageTex { tag: scale_tag(2.0), tex: texture("softer") });
|
||||
assert_eq!(prefetch_demand(&v, &p, 3, scale, scale_tag(scale), max_side), (3, backdrop, scale_tag(backdrop), None));
|
||||
v.pages.insert(3, PageTex { tag: STALE_TAG, tex: texture("stale") });
|
||||
assert_eq!(prefetch_demand(&v, &p, 3, scale, scale_tag(scale), max_side), (3, backdrop, scale_tag(backdrop), None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_zoom_or_layout_change_clears_the_scroll_direction() {
|
||||
assert_eq!(scroll_direction(0, 40.0, false), 1);
|
||||
assert_eq!(scroll_direction(1, -40.0, false), -1);
|
||||
assert_eq!(scroll_direction(-1, 0.2, false), -1, "a still view keeps its direction");
|
||||
assert_eq!(scroll_direction(1, -300.0, true), 0, "a zoom out moves the offset up without a scroll");
|
||||
assert_eq!(scroll_direction(-1, 300.0, true), 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn on_screen_pages_and_tiles_are_admitted_beyond_the_prefetch_allowance() {
|
||||
let ctx = egui::Context::default();
|
||||
|
||||
@@ -72,6 +72,7 @@ mod recovery;
|
||||
mod redact;
|
||||
mod reload;
|
||||
mod script_noop;
|
||||
mod scroll_prefetch;
|
||||
mod side_panels;
|
||||
mod signing;
|
||||
mod stamps;
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
//! Continuous view renders the pages ahead of the scroll before they come into view.
|
||||
|
||||
use egui_kittest::Harness;
|
||||
use pdfcraft_ui_egui::PdfCraftApp;
|
||||
|
||||
const PAGES: usize = 20;
|
||||
|
||||
/// Twenty empty 300×400 pt pages.
|
||||
fn pages_pdf() -> Vec<u8> {
|
||||
let kids: Vec<String> = (0..PAGES).map(|i| format!("{} 0 R", 3 + i)).collect();
|
||||
let mut pdf = format!(
|
||||
"%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [{}] /Count {PAGES} /MediaBox [0 0 300 400] >> endobj\n",
|
||||
kids.join(" ")
|
||||
);
|
||||
for i in 0..PAGES {
|
||||
pdf.push_str(&format!("{} 0 obj << /Type /Page /Parent 2 0 R >> endobj\n", 3 + i));
|
||||
}
|
||||
pdf.push_str("trailer << /Root 1 0 R >>\n%%EOF");
|
||||
pdf.into_bytes()
|
||||
}
|
||||
|
||||
fn harness() -> Harness<'static, PdfCraftApp> {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("pages.pdf", None, pages_pdf()).expect("opens");
|
||||
for (k, v) in [("left", "closed"), ("panel", "none"), ("layout", "continuous"), ("zoom", "100")] {
|
||||
app.set_option(k, v).unwrap();
|
||||
}
|
||||
app
|
||||
});
|
||||
settle(&mut h);
|
||||
h
|
||||
}
|
||||
|
||||
/// Runs frames until no render is queued.
|
||||
fn settle(h: &mut Harness<'static, PdfCraftApp>) {
|
||||
h.run_steps(4);
|
||||
let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
|
||||
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));
|
||||
}
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
/// Goes to a page (1-based, as the page box takes it) and waits for its renders.
|
||||
fn go_to(h: &mut Harness<'static, PdfCraftApp>, page: usize) {
|
||||
h.state_mut().set_option("page", &page.to_string()).unwrap();
|
||||
settle(h);
|
||||
}
|
||||
|
||||
fn shown(h: &Harness<'static, PdfCraftApp>) -> Vec<usize> {
|
||||
(0..PAGES).filter(|&p| h.state().views[0].page_screen_rect(p).is_some()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn continuous_view_prefetches_the_pages_ahead_of_the_scroll() {
|
||||
let mut h = harness();
|
||||
go_to(&mut h, 14);
|
||||
let shown_down = shown(&h);
|
||||
let last = *shown_down.last().expect("pages on screen");
|
||||
let cached = h.state().views[0].cached_pages_debug();
|
||||
// The page just past the window is within the 400 pt band every frame asks for; the one
|
||||
// after it is past the band, and only the prefetch asks for it.
|
||||
for page in last + 1..=last + 2 {
|
||||
assert!(cached.contains(&page), "page {page} renders ahead of a scroll down: shown {shown_down:?}, cached {cached:?}");
|
||||
}
|
||||
|
||||
// Scrolling back up prefetches above the window instead.
|
||||
go_to(&mut h, 9);
|
||||
let shown_up = shown(&h);
|
||||
let first = *shown_up.first().expect("pages on screen");
|
||||
let cached = h.state().views[0].cached_pages_debug();
|
||||
for page in first.saturating_sub(2)..first {
|
||||
assert!(cached.contains(&page), "page {page} renders ahead of a scroll up: shown {shown_up:?}, cached {cached:?}");
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user