Clamp the zoom-anchor scroll to the content so zooming out no longer jolts the page (#3266) (#3350)

This commit is contained in:
MarekSpodBiedry authored and GitHub committed 2026-10-19 02:48:06 -04:00
1 parent 6e61b50503
commit 9d23505dd7
1 file changed
+22 -1
+22 -1
View File
@@ -2058,6 +2058,14 @@ fn annotation_hover_text(a: &pdfcraft_render::Annotation) -> Option<String> {
Some(if body.is_empty() { who } else { format!("{who}\n{body}") })
}
/// The scroll offset that puts `point` (content coordinates) at `rel` in the viewport, kept within
/// the content as egui keeps it. egui clamps only after drawing, so an offset past the end showed
/// the page misplaced for a frame and then jumped (zooming out beside a page that fits).
fn anchored_scroll(point: Pos2, rel: Vec2, content: Vec2, viewport: Vec2) -> Vec2 {
let max = vec2((content.x - viewport.x).max(0.0), (content.y - viewport.y).max(0.0));
vec2((point.x - rel.x).clamp(0.0, max.x), (point.y - rel.y).clamp(0.0, max.y))
}
/// The request tag for a raster at `scale`: equal tags mean the same scale (to 1/65536).
fn scale_tag(scale: f32) -> u64 {
(f64::from(scale) * 65536.0).round() as u64
@@ -2676,7 +2684,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
if let Some((page, fx, fy, rel)) = view.zoom_anchor.take() {
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)));
scroll = scroll.scroll_offset(anchored_scroll(point, rel, vec2(content_w, content_h), avail.size()));
} else if let Some((page, frac)) = view.goto.take() {
let page = page.min(rects.len() - 1);
let r = rects[page];
@@ -5285,6 +5293,19 @@ fn grayscale(mut img: egui::ColorImage) -> egui::ColorImage {
#[cfg(test)]
mod tests {
#[test]
fn zoom_anchor_scroll_stays_within_the_content() {
let (content, viewport) = (vec2(1000.0, 2000.0), vec2(800.0, 600.0));
// Past the end: clamped to the maximum (200, 1400), not left for egui to snap back.
assert_eq!(anchored_scroll(pos2(900.0, 1900.0), vec2(0.0, 0.0), content, viewport), vec2(200.0, 1400.0));
// Before the start.
assert_eq!(anchored_scroll(pos2(10.0, 10.0), vec2(300.0, 300.0), content, viewport), Vec2::ZERO);
// Inside: unchanged.
assert_eq!(anchored_scroll(pos2(150.0, 800.0), vec2(100.0, 100.0), content, viewport), vec2(50.0, 700.0));
// Content smaller than the viewport: no scrolling.
assert_eq!(anchored_scroll(pos2(400.0, 400.0), Vec2::ZERO, vec2(500.0, 500.0), viewport), vec2(0.0, 0.0));
}
#[test]
fn page_size_hint_zone_is_the_bottom_left_quadrant() {
let area = Rect::from_min_max(pos2(100.0, 50.0), pos2(500.0, 450.0));