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"