From c1146d0d8dcfae08a75e7bf5ec71968bee42c325 Mon Sep 17 00:00:00 2001 From: srt8hellcat2-ui Date: Thu, 15 Oct 2026 02:47:33 -0400 Subject: [PATCH] feat(ui): improve back and forward page navigation (#2080) (#2151) Co-authored-by: srt8hellcat2-ui <225485253+srt8hellcat2-ui@users.noreply.github.com> Co-authored-by: Brandon Thomas --- ROADMAP.md | 3 ++ crates/automation/README.md | 16 ++++++++ crates/engine/src/commands.rs | 3 ++ crates/ui-egui/src/canvas.rs | 70 ++++++++++++++++++++++++++++++++ crates/ui-egui/src/chrome.rs | 10 +++++ crates/ui-egui/src/commands.rs | 14 +++++++ crates/ui-egui/tests/commands.rs | 7 ++++ crates/ui-egui/tests/control.rs | 60 +++++++++++++++++++++++++++ docs/roadmap.md | 2 + parity/acrobat-features.toml | 6 +-- 10 files changed, 188 insertions(+), 3 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 2fd712bbf..3b1c39a14 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -2,6 +2,7 @@ **Stage: alpha** · next: beta, ~25% (ready for real work 50% → 75%) and ~400–700 h away +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M3 #2080 candidate: explicit search jumps join per-tab history; rail and control-channel Back/Forward; estimates unchanged) > **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M4 #1910: Organize Pages opens on the current page; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M3 candidate: coordinate control clicks keep press/release together on slow frames and preserve text multi-click selection; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M3 candidate: native opens retain chosen tools through password retries and defer startup intake while preparation is pending; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) @@ -156,6 +157,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md ## Progress log - **2026-10-13 (G14, Vietnamese localization):** Added Vietnamese (`vi`, Tiếng Việt) as an interface and date language with a 2,591-entry catalog (`plural_none`) covering 100% of the current localizable key union (plus `theme`/`Appearance`). Locale tags `vi` / `vi-VN` / `vi_VN.UTF-8` resolve; calendar months use numeric `Tháng 1`…`12` / `tháng 1`…`12`. Native-speaker community review recorded. The twelve-language localization mean moves from ≈59% to ≈68%; weighted readiness ≈49% → ≈50%. Status stays `partial`: engine/OS details, document contents, filenames, CLI/MCP and external help stay outside the catalog by design. +- **2026-10-14 (M3 #2080 candidate):** Existing per-tab page history gains explicit search jumps, rail buttons and control-channel commands. Continuous scrolling and automatic search refresh stay outside history; page-level restoration and estimates unchanged. + - **2026-10-13 (M2, page-unit transform precision):** Preserve small transform coefficients in page marks, added content and print output. Regression cases cover accepted page units of 4096 and 75000; existing page-unit interpretation and estimates are unchanged. - **2026-10-14 (M5, PR #1457 branch refresh):** Merged upstream main and reconciled roadmap entries while preserving comment submission shortcuts. Estimates unchanged. diff --git a/crates/automation/README.md b/crates/automation/README.md index 3d3a46c0f..32dec0bf2 100644 --- a/crates/automation/README.md +++ b/crates/automation/README.md @@ -188,3 +188,19 @@ unapplied with a warning. `doc_batch` runs conversion, OCR and compression over files and optionally recursive folders. The CLI also exposes it as `pdfcraft-cli batch`. See the [batch guide](../../docs/cli-batch.md) for examples, output protection, errors and limits. + +## Viewer navigation history + +A running PdfCraft instance launched with `--control FILE` exposes its per-tab history through +`pdfcraft-cli ui --control FILE command id=view.previous_view` and +`pdfcraft-cli ui --control FILE command id=view.next_view`. `commands` reports whether each action +is enabled for the active document. These are the same actions as the rail buttons, View → Page +navigation, and Ctrl+[ / Ctrl+] (⌘[ / ⌘] on macOS). + +Explicit page jumps, including search-result selection and Next/Previous match, enter history +when the page changes. Automatic search refresh, continuous scrolling, and within-page scroll +movement do not. Existing page-step commands and single-page wheel turns retain their history +behavior. Back/Forward +returns to page tops, does not restore zoom, does not switch documents, and is not saved across +restarts. Standalone engine/MCP sessions have no viewport, so they do not have a separate +navigation-history tool; use the opt-in UI control channel to navigate the displayed document. diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index 228bdd0ff..414cf7553 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -222,6 +222,9 @@ pub const COMMANDS: &[CommandSpec] = &[ // also run from Combine files, which has no document open. c("view.next_tab", "Next tab", VIEW, Some(Shortcut { command: false, shift: false, mac_ctrl: true, key: "Tab" }), Nothing, "chevron-right"), c("view.previous_tab", "Previous tab", VIEW, Some(Shortcut { command: false, shift: true, mac_ctrl: true, key: "Tab" }), Nothing, "chevron-left"), + // View-local history also appears in the Page navigation section; don't duplicate its menu. + c("view.previous_view", "Previous view", None, Some(Shortcut::cmd("[")), Document, "chevron-left"), + c("view.next_view", "Next view", None, Some(Shortcut::cmd("]")), Document, "chevron-right"), c("view.focus_page_input", "Go to page…", VIEW, Some(Shortcut::cmd_shift("N")), Document, "text-cursor-input"), c("ui.dock.reset", "Reset panel layout", VIEW, None, Nothing, "settings"), c("view.theme", "Switch light / dark theme", None, None, Nothing, "moon"), diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index e05815bfd..c3100e3bb 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1005,6 +1005,8 @@ impl DocView { let Some(f) = self.find.as_mut() else { return }; let Some(&(p, _)) = f.matches.get(i) else { return }; f.current = Some(i); + self.go_to_page(p); + self.goto_point = None; self.flash_match(p); } @@ -1019,6 +1021,9 @@ impl DocView { let next = if forward { (c + 1) % n } else { (c + n - 1) % n }; f.current = Some(next); let p = f.matches[next].0; + // Only explicit search navigation joins history; background refresh uses flash_match alone. + self.go_to_page(p); + self.goto_point = None; self.flash_match(p); } @@ -4887,6 +4892,71 @@ trailer << /Root 1 0 R >> DocView::new(DocId(1), &info, ViewDefaults { layout, continuous, ..Default::default() }) } + fn history_search_view() -> DocView { + let mut v = view(5, PageLayout::Single, true); + v.texts.insert( + 3, + pdfcraft_render::PageText { + glyphs: vec![pdfcraft_render::TextGlyph { + text: "match".into(), + rect: [20.0, 260.0, 60.0, 280.0], + quad: [[20.0, 260.0], [60.0, 260.0], [60.0, 280.0], [20.0, 280.0]], + direction: [1.0, 0.0], + }], + line_of: vec![0], + space_before: vec![false], + } + .into(), + ); + v.find = Some(Find { matches: vec![(3, 0..1), (3, 0..1), (1, 0..1)], current: Some(2), ..Default::default() }); + v + } + + #[test] + fn explicit_search_jumps_record_history_and_keep_match_destination() { + for step in [None, Some(true), Some(false)] { + let mut v = history_search_view(); + v.goto_point = Some((3, Some(0.8), Some(0.9), Vec2::ZERO)); + if let Some(forward) = step { + v.find_step(forward); + } else { + v.go_to_match(0); + } + assert_eq!(v.back, vec![0], "explicit search jump must remember its origin"); + assert_eq!(v.goto, Some((3, 0.5)), "keep the match offset instead of the page top"); + assert_eq!(v.page_input, "4"); + assert!(v.goto_point.is_none(), "an earlier link destination cannot override the search offset"); + v.go_to_match(1); + assert_eq!(v.back, vec![0], "same-page matches do not duplicate history"); + assert!(v.view_history(false)); + assert_eq!(v.current, 0); + assert!(v.view_history(true)); + assert_eq!(v.current, 3); + assert!(v.view_history(false)); + v.go_to_match(2); + assert!(v.forward.is_empty(), "a new explicit jump discards Forward"); + assert_eq!(v.back, vec![0]); + } + } + + #[test] + fn automatic_search_updates_do_not_record_history_or_discard_forward() { + let mut v = history_search_view(); + v.forward = vec![4]; + v.find = Some(Find { query: "match".into(), ..Default::default() }); + v.rerun_find(); + assert_eq!(v.current, 3); + assert_eq!(v.goto, Some((3, 0.5))); + assert!(v.back.is_empty()); + assert_eq!(v.forward, vec![4]); + v.current = 0; + v.find.as_mut().unwrap().current = None; + v.refresh_find_page(3); + assert_eq!(v.current, 3); + assert!(v.back.is_empty()); + assert_eq!(v.forward, vec![4]); + } + #[test] fn text_layers_evict_far_pages_but_keep_visible_and_matches() { let info = thumbnail_info(100); diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 9e376e373..25faedc16 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -700,6 +700,16 @@ pub fn right_rail(app: &mut PdfCraftApp, ui: &mut egui::Ui) { if icons::button(ui, "chevron-up", 30.0, false, tl!("Previous page")).clicked() { view.step_page(false); } + // History is distinct from stepping to the adjacent page, and belongs to this tab. + for (forward, icon, label, shortcut, enabled) in [ + (true, "chevron-right", tl!("Next view"), crate::commands::NEXT_VIEW, !view.forward.is_empty()), + (false, "chevron-left", tl!("Previous view"), crate::commands::PREV_VIEW, !view.back.is_empty()), + ] { + let tip = format!("{label} ({})", crate::commands::shortcut_label(ui.ctx(), shortcut)); + if ui.add_enabled_ui(enabled, |ui| icons::button(ui, icon, 30.0, false, &tip)).inner.clicked() { + view.view_history(forward); + } + } ui.label(egui::RichText::new(page_count.to_string()).font(theme::regular(11.0)).color(t.text_muted)); let page_input_id = view.page_input_id(); // Consume replacement selection once, including when the editor has lost focus. diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index e887d1c91..a989f6035 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -86,6 +86,11 @@ impl PdfCraftApp { } commands::is_enabled(spec, &self.session, self.active_ids().map(|(_, id)| id)) && (spec.needs != commands::Needs::TwoPageView || self.active.and_then(|i| self.views.get(i)).is_some_and(crate::DocView::cover_applies)) + && match spec.id { + "view.previous_view" => self.active.and_then(|i| self.views.get(i)).is_some_and(|v| !v.back.is_empty()), + "view.next_view" => self.active.and_then(|i| self.views.get(i)).is_some_and(|v| !v.forward.is_empty()), + _ => true, + } } /// Run a registered command by id. Returns `false` when the id is unknown or the command @@ -267,6 +272,11 @@ impl PdfCraftApp { } } "view.read_mode" => self.switch_mode(if self.mode == Mode::Read { Mode::AllTools } else { Mode::Read }, false), + "view.previous_view" | "view.next_view" => { + if let Some(v) = active.and_then(|i| self.views.get_mut(i)) { + return v.view_history(id == "view.next_view"); + } + } "view.next_tab" => self.step_tab(true), "view.previous_tab" => self.step_tab(false), "view.theme" => { @@ -681,6 +691,10 @@ impl PdfCraftApp { specs.sort_by_key(|c| std::cmp::Reverse(c.shortcut.map(|s| s.modifier_count()).unwrap_or(0))); for spec in specs { let Some(s) = spec.shortcut else { continue }; + // Keep the canvas's silent no-op when there is no previous/next view to visit. + if matches!(spec.id, "view.previous_view" | "view.next_view") && !self.command_enabled(spec) { + continue; + } if typing && !spec.in_text { continue; } diff --git a/crates/ui-egui/tests/commands.rs b/crates/ui-egui/tests/commands.rs index d741fea52..2f22fc27d 100644 --- a/crates/ui-egui/tests/commands.rs +++ b/crates/ui-egui/tests/commands.rs @@ -148,6 +148,13 @@ fn every_registered_command_is_implemented() { if spec.id == "view.layout.cover" { app.set_option("layout", "two-up").unwrap(); } + // History commands need a real jump (and Back first to make Forward available). + if matches!(spec.id, "view.previous_view" | "view.next_view") { + app.views[0].go_to_page(1); + if spec.id == "view.next_view" { + assert!(app.views[0].view_history(false)); + } + } let dir = std::env::temp_dir().join(format!("pdfcraft-cmd-{}", std::process::id())); std::fs::create_dir_all(&dir).unwrap(); app.save_override = Some(dir.join("out.pdf").to_string_lossy().into_owned()); diff --git a/crates/ui-egui/tests/control.rs b/crates/ui-egui/tests/control.rs index 10df41a50..afa4d72bb 100644 --- a/crates/ui-egui/tests/control.rs +++ b/crates/ui-egui/tests/control.rs @@ -1599,3 +1599,63 @@ fn page_size_shows_while_the_pointer_is_in_the_bottom_left_quadrant() { h.run_steps(3); assert!(!shown(&h), "pointer left: hidden again"); } + +#[test] +fn navigation_history_commands_and_rail_are_per_tab_and_non_editing() { + let (mut h, c) = harness(); + let enabled = + |list: Value, id: &str| list["commands"].as_array().unwrap().iter().find(|cmd| cmd["id"] == id).unwrap()["enabled"].as_bool().unwrap(); + assert!(!enabled(ok(&mut h, &c, "ui.commands", json!({})), "view.previous_view")); + assert!(call(&mut h, &c, "ui.command", json!({"id": "view.previous_view"})).is_err()); + // Empty-history shortcuts were silent before registry integration and must stay silent. + for key in ["[", "]"] { + ok(&mut h, &c, "ui.key", json!({"key": key, "modifiers": ["command"]})); + assert!(h.state().toast.is_none(), "an unavailable history action must not ask to open the already-open document"); + } + let before = h.state().session.get(h.state().views[0].id).unwrap().bytes.clone(); + ok(&mut h, &c, "ui.set", json!({"key": "page", "value": "4"})); + assert!(enabled(ok(&mut h, &c, "ui.commands", json!({})), "view.previous_view")); + // The real rail button is accessible and invokes the same history, not the adjacent page. + ok(&mut h, &c, "ui.click", json!({"label": &format!("Previous view ({})", if cfg!(target_os = "macos") { "⌘[" } else { "Ctrl+[" })})); + assert_eq!(h.state().views[0].current, 0); + assert!(enabled(ok(&mut h, &c, "ui.commands", json!({})), "view.next_view")); + ok(&mut h, &c, "ui.command", json!({"id": "view.next_view"})); + assert_eq!(h.state().views[0].current, 3); + ok(&mut h, &c, "ui.set", json!({"key": "page", "value": "5"})); + ok(&mut h, &c, "ui.key", json!({"key": "[", "modifiers": ["command"]})); + assert_eq!(h.state().views[0].current, 3, "shortcut traverses exactly one history entry"); + ok(&mut h, &c, "ui.key", json!({"key": "]", "modifiers": ["command"]})); + assert_eq!(h.state().views[0].current, 4); + ok(&mut h, &c, "ui.command", json!({"id": "view.previous_view"})); + h.state_mut().open_bytes("other.pdf", None, fixture(5)).unwrap(); + assert!(!enabled(ok(&mut h, &c, "ui.commands", json!({})), "view.previous_view")); + ok(&mut h, &c, "ui.set", json!({"key": "page", "value": "2"})); + ok(&mut h, &c, "ui.command", json!({"id": "view.previous_view"})); + assert_eq!(h.state().views[1].current, 0); + assert_eq!(h.state().views[0].current, 3, "other tab's history does not move this document"); + h.state_mut().active = Some(0); + ok(&mut h, &c, "ui.command", json!({"id": "view.previous_view"})); + assert_eq!(h.state().views[0].current, 0); + assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().bytes, before); + assert!(!h.state().session.get(h.state().views[0].id).unwrap().dirty); + h.state_mut().open_bytes("reopened.pdf", None, before.as_ref().clone()).unwrap(); + assert!(h.state().views.last().unwrap().back.is_empty()); + assert!(h.state().views.last().unwrap().forward.is_empty()); +} + +#[test] +fn routine_continuous_scrolling_does_not_add_navigation_history() { + let (mut h, c) = harness_pages(20); + ok(&mut h, &c, "ui.set", json!({"key": "animation_effects", "value": "off"})); + h.hover_at(egui::pos2(700.0, 450.0)); + h.event(egui::Event::MouseWheel { + unit: egui::MouseWheelUnit::Point, + delta: egui::vec2(0.0, -1200.0), + modifiers: egui::Modifiers::NONE, + phase: egui::TouchPhase::Move, + }); + h.run_steps(8); + assert!(h.state().views[0].current > 0, "the wheel really moved to a later page"); + assert!(h.state().views[0].back.is_empty()); + assert!(h.state().views[0].forward.is_empty()); +} diff --git a/docs/roadmap.md b/docs/roadmap.md index 4a8deb92a..1ec9c10e1 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -2,6 +2,7 @@ > **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-13 · **Change:** minor (M2/M6: spot-colour images convert once per pixel value; widget placement and the Fields panel scale with the field count; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M3 candidate: coordinate control clicks on slow frames and coalesced text selection; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) +> **Last reviewed:** 2026-10-14 · **Last updated:** 2026-10-14 · **Change:** minor (M3 #2080 candidate: explicit search jumps join per-tab history; rail and control-channel Back/Forward; estimates unchanged) > **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M3 candidate: native opens retain chosen tools through password retries and defer startup intake while preparation is pending; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M4 #1310 #1311 #1312: undo, revert and the next save keep the last saved revision; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-13 · **Last updated:** 2026-10-13 · **Change:** minor (M3 #1980: exact quit modifiers preserve Ctrl+Alt+Q email input; M3 candidate: native opens retain chosen tools through password retries and defer startup intake while preparation is pending; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) @@ -101,6 +102,7 @@ total matches the parity estimate. | M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Later native-open replies stay queued while a modal or picker owns the foreground; regressions cover recipient ownership, completion/cancellation and one-shot wakes. | | M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. #1980: exact quit modifiers preserve Ctrl+Alt+Q email input; Ctrl+Q/Cmd+Q asks before discarding unsaved documents. | | M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation.
Candidate: coordinate control clicks pair press/release across slow frames; coalesced primary presses retain text multi-click selection. Regression cases cover slow-frame button activation, disabled controls and text multi-click selection; estimates unchanged. | +| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. #2080 candidate: search-result jumps enter per-tab history; rail and control-channel Back/Forward; no exact-view or restart persistence. | | M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. A custom text match that ends on a reconstructed separator stops before the next glyph (#1315). Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings | | M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); Lab Bradford adaptation uses LMS white-point ratios, covered by independent neutral/chromatic and direct/Indexed render oracles; inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings | | M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 9fbced700..8d57c31c0 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -1164,10 +1164,10 @@ area = "B" tier = "P1" milestone = "M3" status = "shipped" -commands = [] +commands = ["view.previous_view", "view.next_view"] tools = [] -evidence = ["crates/ui-egui/tests/viewer.rs::view_history_select_all_and_find_options"] -notes = "⌘[ / ⌘] and View ▸ Page navigation" +evidence = ["crates/ui-egui/tests/viewer.rs::view_history_select_all_and_find_options", "crates/ui-egui/tests/control.rs::navigation_history_commands_and_rail_are_per_tab_and_non_editing", "crates/ui-egui/tests/control.rs::routine_continuous_scrolling_does_not_add_navigation_history"] +notes = "Per-tab page history: explicit search-result jumps, page navigation, bookmarks and links; no continuous-scroll history or restart persistence; existing page-step history is retained. Rail buttons, View ▸ Page navigation and Ctrl/⌘[ / Ctrl/⌘]. UI control ui.command exposes view.previous_view / view.next_view; standalone engine sessions have no viewport history. Restores page tops, not exact scroll/zoom positions." [[feature]] id = "view.follow-links"