From bed9a48ccd68f40732ca21c71a7a51dd489c2d03 Mon Sep 17 00:00:00 2001 From: Omar Elders <118695915+omarelders@users.noreply.github.com> Date: Sun, 11 Oct 2026 20:32:38 +0300 Subject: [PATCH] fix(ui): preserve scroll position when collapsing or resizing sidebars (#1056) (#1069) * fix(ui): preserve scroll position when collapsing or resizing sidebars (#1056) * Run cargo fmt --------- Co-authored-by: Brandon Thomas --- crates/ui-egui/src/canvas.rs | 20 ++++++++++++ crates/ui-egui/tests/view.rs | 63 ++++++++++++++++++++++++++++++++++++ 2 files changed, 83 insertions(+) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index f5e1b00e..7aeb0c20 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1803,7 +1803,27 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { let avail = ui.available_rect_before_wrap(); view.viewport_w = avail.width(); view.viewport_h = avail.height(); + let old_zoom = view.zoom; view.fit_zoom(info); + if view.shown && view.zoom_anchor.is_none() && view.goto.is_none() && view.goto_point.is_none() && (view.zoom - old_zoom).abs() > 1e-4 { + let prev_viewport = view.viewport_screen; + let top_y = prev_viewport.min.y; + let anchor = view + .screen_rects + .iter() + .find(|(_, r)| r.min.y <= top_y && r.max.y > top_y) + .map(|(p, r)| { + let fy = if r.height() > 0.0 { ((top_y - r.min.y) / r.height()).clamp(0.0, 1.0) } else { 0.0 }; + (*p, fy, 0.0f32) + }) + .or_else(|| view.screen_rects.iter().find(|(_, r)| r.min.y >= top_y).map(|(p, r)| (*p, 0.0f32, (r.min.y - top_y).max(0.0)))) + .or_else(|| view.screen_rects.last().map(|(p, r)| (*p, 1.0f32, (r.max.y - top_y).max(0.0)))); + if let Some((page, fy, rel_y)) = anchor { + view.zoom_anchor = Some((page, 0.5, fy, vec2(avail.width() / 2.0, rel_y))); + } else { + view.zoom_anchor = Some((view.current, 0.5, 0.0, vec2(avail.width() / 2.0, MARGIN))); + } + } // Pinch / ctrl+scroll zoom, anchored on the current page. let zoom_delta = ui.input(|i| i.zoom_delta()); diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index bc216c23..12c7e904 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -920,3 +920,66 @@ trailer << /Root 1 0 R >> let first = detail(&mut h); assert!(first * 10 > sharp * 7, "the first frame after zooming lost its detail: {first} against {sharp}"); } + +#[test] +fn sidebar_collapse_and_resize_preserves_scroll_position() { + use pdfcraft_ui_egui::canvas::Fit; + 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.to_vec()).expect("opens"); + app.set_option("left", "closed").unwrap(); + app.set_option("panel", "bookmarks").unwrap(); + app.set_option("layout", "continuous").unwrap(); + app + }); + let mut width = 0.0; + for _ in 0..400 { + h.run_steps(1); + let w = h.state().views[0].viewport_rect().width(); + if w == width { + break; + } + width = w; + } + h.state_mut().views[0].go_to_page(2); + h.run_steps(6); + + let v = &h.state().views[0]; + assert_eq!(v.current, 2); + assert_eq!(v.fit, Fit::Width); + let zoom_before = v.zoom; + let rect_before = v.page_screen_rect(2).expect("page 2 on screen"); + let vp_before = v.viewport_rect(); + let rel_y_before = rect_before.min.y - vp_before.min.y; + + // Collapse bookmarks panel: available width increases, so fit-width zoom increases. + h.state_mut().set_option("panel", "none").unwrap(); + h.run_steps(6); + + let v = &h.state().views[0]; + let zoom_after = v.zoom; + assert!(zoom_after > zoom_before, "zoom increased when panel collapsed: {zoom_after} vs {zoom_before}"); + assert_eq!(v.current, 2, "still on page 2 after panel collapsed"); + let rect_after = v.page_screen_rect(2).expect("page 2 still on screen"); + let vp_after = v.viewport_rect(); + let rel_y_after = rect_after.min.y - vp_after.min.y; + assert!((rel_y_after - rel_y_before).abs() < 1.0, "page 2 scroll position preserved: before {rel_y_before}, after {rel_y_after}"); + + // Reopen bookmarks panel: available width decreases, so fit-width zoom decreases. + h.state_mut().set_option("panel", "bookmarks").unwrap(); + for _ in 0..400 { + h.run_steps(1); + let w = h.state().views[0].viewport_rect().width(); + if w == width { + break; + } + width = w; + } + let v = &h.state().views[0]; + assert_eq!(v.current, 2, "still on page 2 after panel reopened"); + let rect_reopened = v.page_screen_rect(2).expect("page 2 still on screen"); + let vp_reopened = v.viewport_rect(); + let rel_y_reopened = rect_reopened.min.y - vp_reopened.min.y; + assert!((rel_y_reopened - rel_y_before).abs() < 1.0, "page 2 scroll position preserved on reopen: before {rel_y_before}, after {rel_y_reopened}"); +}