diff --git a/ROADMAP.md b/ROADMAP.md index bad75fc3be..96586dec4f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -181,6 +181,7 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-18 (M4, PR #1919 review):** repeated page indices apply each rotation to the latest page-local value, preserving indirect/inherited angles. Synthetic adjacent/interleaved duplicates retain siblings and page contents through incremental and full saves. Estimates unchanged. - **2026-10-15 (M4, #1919 refresh):** Integrated upstream `99941daa` while retaining the indirect page rotation correction and all upstream progress. Only independent documentation conflicts required resolution; focused production and test files are unchanged from the previously green CI head. Estimates unchanged. - **2026-10-18 (M3, #3149):** Page replacement cancels render demand collected for the previous geometry; all viewer raster upload paths reject empty or oversized textures before cache eviction. Synthetic regressions reproduce the reported 1880×2350 upload at a 2048 limit, retain cached images and unsaved edits, and resume bounded backdrops and tiles. Estimates unchanged. +- **2026-10-19 (M3, #3360):** Switching between Read and the docking workspace retains the current document’s scroll position at fixed zoom, including within-page position; separate tabs retain their own offsets. Estimates unchanged. - **2026-10-18 (M8, #1350):** Integrated current main while preserving content-stream comment boundaries. Hidden-text cleanup retires unused named replacement descriptions on rewritten pages and forms, including inherited/shared resource holders, without removing descriptions still used by surviving content. Synthetic saved-byte and preservation regressions cover the cleanup. Estimates unchanged. - **2026-10-17 (M4, #2997):** Dragging page thumbnails in the Organize grid with Ctrl held (Option on macOS) drops copies at the gap instead of moving the pages: a "+" badge sits by the pointer, the copies land at the gap selected, and one undo step ("Duplicate pages") takes them out again. `page_duplicate` takes `at` (1-based, where the first copy goes; left out, after the last of them) so agents can drive the same thing, and the UI control channel's `ui.drag` modifiers now reach gestures that read the input state. Estimates unchanged. - **2026-10-15 (M6, AcroForm XML text):** Plain AcroForm XML imports retain all direct text around comments and processing instructions, including CDATA and character references. Synthetic regressions verify exact values and unknown catalog data after incremental and full save/reopen. Nested-element and XFA interpretation are unchanged. Estimates unchanged. diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 3fcb1e9b6c..16ae634901 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -2354,6 +2354,17 @@ const SUPPRESS_SECS: f64 = 0.25; /// Short rolling extension while egui still has an unprocessed wheel tail. const SUPPRESS_TAIL_SECS: f64 = 0.05; +// The viewer moves between docking and the central panel as modes change; keep its scroll +// identity tied to the document instead of either parent UI. +fn page_scroll( + ui: &mut egui::Ui, + scroll: egui::ScrollArea, + id: egui::Id, + draw: impl FnOnce(&mut egui::Ui, egui::Rect) -> R, +) -> egui::scroll_area::ScrollAreaOutput { + ui.scope_builder(egui::UiBuilder::new().id(id), |ui| scroll.show_viewport(ui, draw)).inner +} + pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); // Keyboard scrolling applies to this frame's page view only, never later. @@ -2820,7 +2831,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { } } let selection_wheel = view.selection_drag && navigation_enabled && unobstructed && !single_paging && ui.rect_contains_pointer(avail); - let out = scroll.show_viewport(ui, |ui, viewport| { + let out = page_scroll(ui, scroll, egui::Id::new(("page-view-scope", view.id)), |ui, viewport| { // egui's drag responses accept every pointer button. Keep a wheel gesture from also // selecting text, drawing a mark, or panning with the Hand tool. if middle_gesture { diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index 9b6d57bf67..fd1da9cffa 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -1566,3 +1566,55 @@ fn two_up_scroll_end_reaches_an_unpaired_final_page() { fn two_up_scroll_end_with_cover_reaches_a_taller_left_page() { assert_final_spread_bottom_reachable(&[400, 400, 400, 400, 100], true); } + +#[test] +fn switching_modes_keeps_the_current_page_at_fixed_zoom() { + use egui_kittest::kittest::Queryable; + let kids = (3..33).map(|i| format!("{i} 0 R")).collect::>().join(" "); + let mut pdf = format!( + "%PDF-1.7\n1 0 obj << /Type /Catalog /Pages 2 0 R >> endobj\n2 0 obj << /Type /Pages /Kids [{kids}] /Count 30 /MediaBox [0 0 300 400] >> endobj\n" + ); + for i in 3..33 { + pdf.push_str(&format!("{i} 0 obj << /Type /Page /Parent 2 0 R >> endobj\n")); + } + pdf.push_str("trailer << /Root 1 0 R >>\n%%EOF"); + let other = pdf.clone().into_bytes(); + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.open_bytes("pages.pdf", None, pdf.into_bytes()).unwrap(); + app.set_option("zoom", "100").unwrap(); + app + }); + h.run_steps(8); + h.state_mut().views[0].go_to_page(17); + h.run_steps(8); + assert_eq!(h.state().views[0].current, 17); + h.hover_at(h.state().views[0].viewport_rect().center()); + h.event(wheel(MouseWheelUnit::Point, -35.0, TouchPhase::Move)); + h.run_steps(8); + let before = rect(&h, 17).unwrap().top() - h.state().views[0].viewport_rect().top(); + h.get_by_label("Read").click(); + h.run_steps(8); + assert_eq!(h.state().views[0].current, 17, "switching to Read should keep the current page"); + let after = rect(&h, 17).unwrap().top() - h.state().views[0].viewport_rect().top(); + assert!((before - after).abs() < 1.0, "the position within the page moved: {before} to {after}"); + h.state_mut().views[0].go_to_page(5); + h.run_steps(8); + assert_eq!(h.state().views[0].current, 5); + h.get_by_label("All tools").click(); + h.run_steps(8); + assert_eq!(h.state().views[0].current, 5, "switching to All tools should keep the current page"); + h.state_mut().open_bytes("other.pdf", None, other).unwrap(); + h.state_mut().set_option("zoom", "100").unwrap(); + h.run_steps(8); + h.state_mut().views[1].go_to_page(9); + h.run_steps(8); + assert_eq!(h.state().views[1].current, 9); + h.get_by_label("pages.pdf").click(); + h.run_steps(8); + assert_eq!(h.state().views[0].current, 5, "the first tab keeps its own position"); + h.get_by_label("other.pdf").click(); + h.run_steps(8); + assert_eq!(h.state().views[1].current, 9, "the second tab keeps its own position"); +} diff --git a/docs/roadmap.md b/docs/roadmap.md index e1fdce6505..4b506ae5bd 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -200,6 +200,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st | Date | Change | Summary | |---|---|---| | 2026-10-15 | minor | M7: Edit text caret height follows its row, with a kittest regression; estimates unchanged | +| 2026-10-19 | minor | M3 #3360: retain each document’s scroll position when switching Read and the docking workspace at fixed zoom. Estimates unchanged. | | 2026-10-18 | minor | M4, PR #1919 review: repeated page indices apply each rotation to the latest page-local value, preserving indirect/inherited angles. Synthetic adjacent/interleaved duplicates retain siblings and page contents through incremental and full saves. Estimates unchanged. | | 2026-10-18 | minor | M3, #3149: page replacement cancels render demand collected for the previous geometry; all viewer raster upload paths reject empty or oversized textures before cache eviction. Synthetic regressions reproduce the reported 1880×2350 upload at a 2048 limit, retain cached images and unsaved edits, and resume bounded backdrops and tiles. Estimates unchanged. | | 2026-10-17 | minor | M4 #2997: Ctrl-drag (Option on macOS) in the Organize grid copies the dragged pages into the drop gap (a "+" badge says so; the copies land selected, one undo step); `page_duplicate` takes `at`, and the UI control channel's `ui.drag` modifiers reach the input state. Estimates unchanged. | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index b1805d5223..9adae543ce 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1094,8 +1094,8 @@ milestone = "M3" status = "shipped" commands = ["view.read_mode"] tools = [] -evidence = ["crates/ui-egui/tests/control.rs::inspect_and_click_by_label_and_id", "crates/ui-egui/tests/commands.rs::every_registered_command_is_implemented"] -notes = "" +evidence = ["crates/ui-egui/tests/control.rs::inspect_and_click_by_label_and_id", "crates/ui-egui/tests/commands.rs::every_registered_command_is_implemented", "crates/ui-egui/tests/view.rs::switching_modes_keeps_the_current_page_at_fixed_zoom"] +notes = "Switching between Read and the docking workspace preserves the document’s current scroll position at fixed zoom; each tab keeps its own position" [[feature]] id = "view.full-screen"