From 7137c41f47029e4388b8cb39114057226c8c86f0 Mon Sep 17 00:00:00 2001 From: DjPeepjay <120863013+DjPeepjay@users.noreply.github.com> Date: Sat, 17 Oct 2026 16:54:04 -0500 Subject: [PATCH 1/2] feat(viewer): add optional gapless continuous page display M3: preserve reading position while joining page edges, with persisted defaults and opt-in UI control. Translate both labels in all 22 catalogs, update attribution, and cover catalog completeness with a regression test. Rebase PR #3101 onto current main while retaining navigation and rendering fixes. --- ATTRIBUTION.md | 44 +++++++-------- ATTRIBUTION.toml | 88 ++++++++++++++--------------- ROADMAP.md | 2 + apps/pdfcraft/src/main.rs | 1 + crates/engine/src/commands.rs | 1 + crates/ui-egui/src/canvas.rs | 83 ++++++++++++++++++++------- crates/ui-egui/src/chrome.rs | 4 ++ crates/ui-egui/src/commands.rs | 6 ++ crates/ui-egui/src/compare_ui.rs | 13 ++++- crates/ui-egui/src/control.rs | 2 + crates/ui-egui/src/i18n/ar.tsv | 2 + crates/ui-egui/src/i18n/bg.tsv | 2 + crates/ui-egui/src/i18n/cs.tsv | 2 + crates/ui-egui/src/i18n/de.tsv | 2 + crates/ui-egui/src/i18n/el.tsv | 2 + crates/ui-egui/src/i18n/es.tsv | 2 + crates/ui-egui/src/i18n/fr.tsv | 2 + crates/ui-egui/src/i18n/hu.tsv | 2 + crates/ui-egui/src/i18n/it.tsv | 2 + crates/ui-egui/src/i18n/ja.tsv | 2 + crates/ui-egui/src/i18n/kk.tsv | 2 + crates/ui-egui/src/i18n/lt.tsv | 2 + crates/ui-egui/src/i18n/mod.rs | 14 +++++ crates/ui-egui/src/i18n/pl.tsv | 2 + crates/ui-egui/src/i18n/pt-br.tsv | 2 + crates/ui-egui/src/i18n/ru.tsv | 2 + crates/ui-egui/src/i18n/sl.tsv | 2 + crates/ui-egui/src/i18n/te.tsv | 2 + crates/ui-egui/src/i18n/tr.tsv | 2 + crates/ui-egui/src/i18n/uk.tsv | 2 + crates/ui-egui/src/i18n/vi.tsv | 2 + crates/ui-egui/src/i18n/zh-hans.tsv | 2 + crates/ui-egui/src/i18n/zh-hant.tsv | 2 + crates/ui-egui/src/js_ui.rs | 2 + crates/ui-egui/src/lib.rs | 20 +++++++ crates/ui-egui/tests/view.rs | 88 +++++++++++++++++++++++++++++ docs/page-display.md | 29 ++++++++++ docs/roadmap.md | 4 +- parity/acrobat-features.toml | 4 +- 39 files changed, 357 insertions(+), 92 deletions(-) create mode 100644 docs/page-display.md diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 5cfee8d55..eec9e7851 100644 --- a/ATTRIBUTION.md +++ b/ATTRIBUTION.md @@ -8,26 +8,26 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | Asset | Title | Author | Licence | Source | Used for | |---|---|---|---|---|---| -| `crates/ui-egui/src/i18n/el.tsv` | Greek interface translation catalog | Camthalion77 and contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft English UI labels | Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/zh-hans.tsv` | Simplified Chinese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/zh-hant.tsv` | Traditional Chinese (Taiwan) interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/cs.tsv` | Czech interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/pt-br.tsv` | Brazilian Portuguese interface translation catalog | André Cardoso (andrecards), PdfCraft contributors | MIT OR Apache-2.0 | Clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources: the ~1,826-entry gist posted in issue #288 (https://gist.github.com/andrecards/ac388c25fba17312ce83207f8b7b467e), completed and reviewed by PdfCraft contributors | Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/es.tsv` | Spanish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/fr.tsv` | French interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/de.tsv` | German interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/te.tsv` | Telugu interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/it.tsv` | Italian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/pl.tsv` | Polish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/ru.tsv` | Russian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/hu.tsv` | Hungarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/bg.tsv` | Bulgarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/kk.tsv` | Kazakh interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of the English UI labels into Kazakh, no proprietary localisation resources | Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/ja.tsv` | Japanese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/uk.tsv` | Ukrainian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/ar.tsv` | Arabic interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/tr.tsv` | Turkish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/lt.tsv` | Lithuanian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; original catalog reviewed by a native Lithuanian-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed | Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/el.tsv` | Greek interface translation catalog | Camthalion77 and contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft English UI labels; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/zh-hans.tsv` | Simplified Chinese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/zh-hant.tsv` | Traditional Chinese (Taiwan) interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/cs.tsv` | Czech interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/pt-br.tsv` | Brazilian Portuguese interface translation catalog | André Cardoso (andrecards), PdfCraft contributors | MIT OR Apache-2.0 | Clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources: the ~1,826-entry gist posted in issue #288 (https://gist.github.com/andrecards/ac388c25fba17312ce83207f8b7b467e), completed and reviewed by PdfCraft contributors; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/es.tsv` | Spanish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/fr.tsv` | French interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/de.tsv` | German interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/te.tsv` | Telugu interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/it.tsv` | Italian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/pl.tsv` | Polish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/ru.tsv` | Russian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/hu.tsv` | Hungarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/bg.tsv` | Bulgarian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/kk.tsv` | Kazakh interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of the English UI labels into Kazakh, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/ja.tsv` | Japanese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/uk.tsv` | Ukrainian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/ar.tsv` | Arabic interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/tr.tsv` | Turkish interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/lt.tsv` | Lithuanian interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; original catalog reviewed by a native Lithuanian-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `assets/icons/accessibility.svg` | Lucide icon "accessibility" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/accessibility.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/align-left.svg` | Lucide icon "align-left" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-left.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | | `assets/icons/align-center.svg` | Lucide icon "align-center" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/align-center.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | @@ -277,8 +277,8 @@ Every asset PdfCraft includes, bundles or uses to build its published material, | `packaging/macos/dmg/background.tiff` | PdfCraft macOS DMG window background (rendered) | @XusBadia | MIT OR Apache-2.0 | Rendered from packaging/macos/dmg/background.svg by packaging/macos/dmg/generate.py | 1x + 2x HiDPI 16-colour palette TIFF copied into the macOS DMG by packaging/macos/package.sh; includes the app icon and the free LittleCMS built-in sRGB profile | | `crates/sign/tests/data/x509-rsa-sha1.pdf` | One-page PDF with a legacy adbe.x509.rsa_sha1 signature by the RSA test key | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: hand-written PDF objects; signature made with `openssl dgst -sha1 -sign` over its byte ranges (see crates/sign/tests/data/README.md) | Signature validation tests (crates/sign/tests/pdf.rs): the legacy adbe.x509.rsa_sha1 format | | `assets/icons/monitor.svg` | Lucide icon "monitor" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/monitor.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs | -| `crates/ui-egui/src/i18n/vi.tsv` | Vietnamese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed | Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | -| `crates/ui-egui/src/i18n/sl.tsv` | Slovene interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/vi.tsv` | Vietnamese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | +| `crates/ui-egui/src/i18n/sl.tsv` | Slovene interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed | Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs | | `vendor/vello_common/assets/probe.png` | vello_common probe reference image (rendered from probe.rgba) | Linebender vello contributors | MIT OR Apache-2.0 | vello_common 0.0.8 (crates.io), vendored in vendor/vello_common; regenerated by its vello_sparse_tests regenerate_probe_reference binary | Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file | ## Compiled in through dependencies (6) diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index db3dd6124..7dce6d850 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -2,12 +2,12 @@ path = "crates/ui-egui/src/i18n/el.tsv" title = "Greek interface translation catalog" author = "Camthalion77 and contributors" -source = "Contributor-original: clean-room translations of PdfCraft English UI labels" +source = "Contributor-original: clean-room translations of PdfCraft English UI labels; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "6eee0b34db6529f26235bfc07fa1fdb9030b8e7b0b10ede271ca9242cb491919" +sha256 = "a1a4c748bbae5386f43f7891e9725cde5797b884ee0dcdf9eff7a520be56dc2a" # ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences. # ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`). @@ -25,210 +25,210 @@ sha256 = "6eee0b34db6529f26235bfc07fa1fdb9030b8e7b0b10ede271ca9242cb491919" path = "crates/ui-egui/src/i18n/zh-hans.tsv" title = "Simplified Chinese interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9870c0fb9f7198155be16adee7f45d36cb9a254af90b545a3ce5b04cbbbf24d4" +sha256 = "0c13be6332805f89ad482d4302db12ae7a590d0791091236a881e0adaa98e151" [[asset]] path = "crates/ui-egui/src/i18n/zh-hant.tsv" title = "Traditional Chinese (Taiwan) interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "fc31e40877ce115cfb7033c57dd6acabcae3725b249364387e3fe23e04d94e5e" +sha256 = "1b9f67541f35054e468d7a8f7a3df36bedb61bc416c04671cdef98b90c769ac0" [[asset]] path = "crates/ui-egui/src/i18n/cs.tsv" title = "Czech interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "5de19834f293efaea68cb6b7f51a71d320391f53345f56cfebcd73dab828b665" +sha256 = "7aca006c9ca4ebce5ce0571f490018e7f912bf959794f6116d256fed0e326a94" [[asset]] path = "crates/ui-egui/src/i18n/pt-br.tsv" title = "Brazilian Portuguese interface translation catalog" author = "André Cardoso (andrecards), PdfCraft contributors" -source = "Clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources: the ~1,826-entry gist posted in issue #288 (https://gist.github.com/andrecards/ac388c25fba17312ce83207f8b7b467e), completed and reviewed by PdfCraft contributors" +source = "Clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources: the ~1,826-entry gist posted in issue #288 (https://gist.github.com/andrecards/ac388c25fba17312ce83207f8b7b467e), completed and reviewed by PdfCraft contributors; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "cef0d22341d0722f029c6543fc846fce164331b7ad8f693a6754b7567b9d9e95" +sha256 = "be120012f48413f8aea6fa54e5d149b50f49da4b629476483f744462d7a5aa37" [[asset]] path = "crates/ui-egui/src/i18n/es.tsv" title = "Spanish interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "c0ca7761cbabd8096bc9af54cdf6b88b3ae0cd8210003db8b3356e2706e44fc8" +sha256 = "c645398757ca260c8a63b405301a37012b0d617e184b350fae1a50da92048967" [[asset]] path = "crates/ui-egui/src/i18n/fr.tsv" title = "French interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "a8afd51c3296f44b0446d6b06492b1348444b3e3c448a326ad213d18ce7af714" +sha256 = "2f36eccaeffdd728ad54d8d4a88924e3f3ceef1c80fb98606dd3457d9094c9b4" [[asset]] path = "crates/ui-egui/src/i18n/de.tsv" title = "German interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "7676a2dfaa7d66e2e9ef608d2d9de7f863557dcecbef6d94bafa981097405e6d" +sha256 = "bd6aba499d598f53e05d6bbd27d706105efaf08cb430e1844a9815f70bad3c4f" [[asset]] path = "crates/ui-egui/src/i18n/te.tsv" title = "Telugu interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9a22996c0308ea5b20ec5e328504cd83cbb560147763faaf0e6987f974844c29" +sha256 = "e4a524a1ad1647879ea9d0693a56a312d63536bd5d684e8ea58704cab4e56575" [[asset]] path = "crates/ui-egui/src/i18n/it.tsv" title = "Italian interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "eacc972d65a2d951e94e1a8cf944638e8328f73f6c05759de6059a425f159cd7" +sha256 = "1bc99553aac7877bb01c8467ab5ea6ea6bb79849081ecc5b276e8f66abd0983a" [[asset]] path = "crates/ui-egui/src/i18n/pl.tsv" title = "Polish interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "dfa4aa540cc7d61fa22a27a949e6eec0f78dcc5b883d0e4e004d695e460b95aa" +sha256 = "305ebcb7f24804f49378d9a40421c70d06c67679093cb8371db984e328616bc4" [[asset]] path = "crates/ui-egui/src/i18n/ru.tsv" title = "Russian interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "27e02e7bb3d378bceca76988a9dceaa68d974f57cbee962954deca6184cf62aa" +sha256 = "4df30ba1947037d4d2a814dbbe37f83e39cae0a718008f848596e2bd7c8b90b5" [[asset]] path = "crates/ui-egui/src/i18n/hu.tsv" title = "Hungarian interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "9675edd36c5ae54d9893fd10c4325aab664941bf714c08fbfeef9134fa42056c" +sha256 = "130bffccd7e0a3861521c3ba46678da4504231ec947dfff469830d328cbe3a0b" [[asset]] path = "crates/ui-egui/src/i18n/bg.tsv" title = "Bulgarian interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "ce18fc96f8516aa917e4a8bcfd290edec13a9800b3d1ee102e4b2a71b8467d2b" +sha256 = "c8aed31742f3306b49b0b643861071aa79a5ddebc97998ae422a440a9752a574" [[asset]] path = "crates/ui-egui/src/i18n/kk.tsv" title = "Kazakh interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of the English UI labels into Kazakh, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of the English UI labels into Kazakh, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "286a1e2e051748eb4bcee3906918dc78d0704f8a37bde22dc50d3376d5e75017" +sha256 = "d1812452455c88581efeede088ede4d6d3387e6599a1a24b1794257806dc9ffa" [[asset]] path = "crates/ui-egui/src/i18n/ja.tsv" title = "Japanese interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "77593b7497c67f4a79cc31fa43fdd6eae11601dd87c079a8e666b25cdd9117ba" +sha256 = "094744e743922d16e47a6a534d33992e30fdba7bf236d185b37a0859ab0efefe" [[asset]] path = "crates/ui-egui/src/i18n/uk.tsv" title = "Ukrainian interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "d7d93a03f22ab380696f69b20b12104bf30e78c4440b683df63594af716894e2" +sha256 = "7b89f1126405415b9ff808e3f27e1dd49bfc8b1c81c96517b4e326f9cba4ebd9" [[asset]] path = "crates/ui-egui/src/i18n/ar.tsv" title = "Arabic interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "889a0d0a9ea53ca0a818c2ba672c053c232d2cca140f8d63127353e6f36505fe" +sha256 = "60d2248ee1c5823e1aa7f1e3e4a764a207f2e5baf1d0cc97c7c6c4947b8bed2b" [[asset]] path = "crates/ui-egui/src/i18n/tr.tsv" title = "Turkish interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "bae8b21651ae2a69c71c91b69ba79727254dbfb078c1be312a13383332934927" +sha256 = "6191c185c234f84da75edb285251ac26e77e4ef3c1c817df1429e99b988d6669" [[asset]] path = "crates/ui-egui/src/i18n/lt.tsv" title = "Lithuanian interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; original catalog reviewed by a native Lithuanian-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; original catalog reviewed by a native Lithuanian-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "7fa70f68a9fa24d0b1cac05fe3aaabaeaca9750c57416124bd317a4eda806d5a" +sha256 = "a6446eacd66e277ebea3ba692fbc463bffac1ed5196f38a5ec58ff889853aabd" [[asset]] @@ -3347,23 +3347,23 @@ sha256 = "d1a443233345724859de8e4fb48968ba91e885e12ddbd710701548bd91b39428" path = "crates/ui-egui/src/i18n/vi.tsv" title = "Vietnamese interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "95b5fc6d10f7bc8371688772741870448de716fa2dd9af62fdfe4e009d269b45" +sha256 = "aabf10ce077b34607ad754b94347cc136e9c9a7c09929b3e8726f2ca99c940b8" [[asset]] path = "crates/ui-egui/src/i18n/sl.tsv" title = "Slovene interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" -sha256 = "3ac2b113045b7e399dd6d9ddb7773e5b0874c8b43c2ba4cca5854598a2a7ba92" +sha256 = "6d27db776816cb7041505f7bc9b91e9166f7d21d573604adfcfe30542ed36d02" [[asset]] diff --git a/ROADMAP.md b/ROADMAP.md index bad75fc3b..0f9d5757f 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -171,6 +171,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md ## Progress log +- **2026-10-18 (M3, PR #3101):** Optional gapless continuous page display with persisted defaults and UI control support; both new labels translated in all 22 catalogs. Rebased onto current main, preserving navigation and rendering fixes. Readiness estimates unchanged. + - **2026-10-18 (M6, #2892):** Push-button normal labels are editable per widget through Options and form_set_props, independently of field names; rendering, undo/redo and incremental/full save/reopen are covered. Estimates unchanged. - **2026-10-18 (M7, #3212):** Headers, Bates numbers, watermarks and added text stay visible after unfinished marked content, with original content preserved. diff --git a/apps/pdfcraft/src/main.rs b/apps/pdfcraft/src/main.rs index 721b7da1b..da9d9fe27 100644 --- a/apps/pdfcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -8,6 +8,7 @@ //! --theme light|dark|system --language auto| --mode all|read|edit|convert|sign --tool --left open|closed //! --organize on --fields on --dialog properties|shortcuts|about --palette --home on //! --cover on|off --default-layout single|continuous|two-up|two-page --default-continuous on|off --default-zoom fit-width|fit-page| +//! --page-gaps on|off --default-page-gaps on|off //! --combine-view grid|list --combine-zoom 60..200` //! //! `--new-window` opens a window of its own. Without it, on Windows, a launch that only names files diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index fcd56efee..54932ea43 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -202,6 +202,7 @@ pub const COMMANDS: &[CommandSpec] = &[ c("view.layout.single", "Single page", None, None, Document, "file-text"), c("view.layout.two_up", "Two-page view", None, None, Document, "columns-2"), c("view.continuous", "Continuous scrolling", None, None, Document, "arrow-up-down"), + c("view.page_gaps", "Show gaps between pages", None, None, Document, "arrow-up-down"), c("view.layout.cover", "Show cover page in two-page view", None, None, TwoPageView, "bookmark"), // Acrobat's view modes, a page display and a zoom at once (the rail's Page display menu). c("view.fit_width_scrolling", "Fit to width scrolling", None, None, Document, "arrow-left-right"), diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index ad7483465..3580064a5 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -169,6 +169,8 @@ pub struct ViewDefaults { pub layout: PageLayout, /// Whether the base layout scrolls continuously. pub continuous: bool, + /// Separate pages with a gutter and shadow; off joins adjacent page edges. + pub show_page_gaps: bool, /// [`Fit::Width`] or [`Fit::Page`], or [`Fit::None`] for a fixed `zoom`. pub fit: Fit, /// The zoom with [`Fit::None`] (1.0 = 100%). @@ -181,7 +183,7 @@ pub struct ViewDefaults { impl Default for ViewDefaults { fn default() -> Self { // Single page, scrolling continuously at fit width: the earlier "continuous" default. - Self { layout: PageLayout::Single, continuous: true, fit: Fit::Width, zoom: 1.0, highlight_fields: false } + Self { layout: PageLayout::Single, continuous: true, show_page_gaps: true, fit: Fit::Width, zoom: 1.0, highlight_fields: false } } } @@ -374,6 +376,7 @@ pub struct DocView { pub layout: PageLayout, /// Whether the base layout scrolls continuously (independent of `layout`, as in Acrobat). pub continuous: bool, + pub show_page_gaps: bool, /// View rotation in degrees clockwise (0, 90, 180, 270); display only, never saved. pub rotation: u16, pub current: usize, @@ -703,6 +706,7 @@ impl DocView { fit: defaults.fit, layout: defaults.layout, continuous: defaults.continuous, + show_page_gaps: defaults.show_page_gaps, rotation: 0, current: 0, organize: false, @@ -855,6 +859,7 @@ impl DocView { fresh.fit = self.fit; fresh.layout = self.layout; fresh.continuous = self.continuous; + fresh.show_page_gaps = self.show_page_gaps; fresh.rotation = self.rotation; fresh.current = self.current; fresh.organize = self.organize; @@ -1389,6 +1394,30 @@ impl DocView { self.continuous } + /// Change page separation without editing the PDF or losing the reader's place. + pub fn set_page_gaps(&mut self, show: bool) { + if self.show_page_gaps == show { + return; + } + if self.viewport_screen.is_positive() { + let at = self.viewport_screen.center(); + if let Some((page, r)) = self + .screen_rects + .iter() + .filter(|(_, r)| r.is_positive()) + .min_by(|(_, a), (_, b)| a.distance_sq_to_pos(at).total_cmp(&b.distance_sq_to_pos(at))) + { + let fraction = (at - r.min) / r.size(); + self.zoom_anchor = Some((*page, fraction.x, fraction.y, at - self.viewport_screen.min)); + } + } + self.show_page_gaps = show; + } + + fn page_gap(&self) -> f32 { + if self.show_page_gaps { GAP } else { 0.0 } + } + /// Turn continuous scrolling on or off, keeping the current page in view. A no-op when it is /// already in that state. pub fn set_continuous(&mut self, continuous: bool) { @@ -1955,9 +1984,9 @@ impl DocView { let avail_w = (self.viewport_w - 2.0 * SIDE).max(100.0); let per_row = if self.layout == PageLayout::TwoUp { 2.0 } else { 1.0 }; match self.fit { - Fit::Width => self.zoom = (avail_w - GAP * (per_row - 1.0)) / (w * PT * per_row), + Fit::Width => self.zoom = (avail_w - self.page_gap() * (per_row - 1.0)) / (w * PT * per_row), Fit::Page => { - let zw = (avail_w - GAP * (per_row - 1.0)) / (w * PT * per_row); + let zw = (avail_w - self.page_gap() * (per_row - 1.0)) / (w * PT * per_row); let zh = (self.viewport_h - 2.0 * MARGIN) / (h * PT); self.zoom = zw.min(zh); } @@ -1978,17 +2007,27 @@ impl DocView { } /// Page rects in content coordinates (origin at the scroll content's top-left). - fn layout(&self, info: &DocInfo, content_w: f32) -> Vec { + fn layout(&self, info: &DocInfo, content_w: f32, ppp: f32) -> Vec { let s = self.zoom * PT; + let gap = self.page_gap(); + let page_size = |p: &pdfcraft_render::PageInfo| { + let (w, h) = self.display_size(p); + if self.show_page_gaps { + vec2(w * s, h * s) + } else { + // Match the raster's whole physical pixels. Snapping each page's origin + // independently with fractional sizes would leave hairline seams. + vec2(device_pixels(w, s * ppp) as f32 / ppp, device_pixels(h, s * ppp) as f32 / ppp) + } + }; let mut rects = Vec::with_capacity(info.pages.len()); let mut y = MARGIN; match self.layout { PageLayout::Single => { for p in &info.pages { - let (w, h) = self.display_size(p); - let size = vec2(w * s, h * s); + let size = page_size(p); rects.push(Rect::from_min_size(pos2(((content_w - size.x) / 2.0).max(SIDE), y), size)); - y += size.y + GAP; + y += size.y + gap; } } PageLayout::TwoUp => { @@ -1999,15 +2038,15 @@ impl DocView { info.pages.chunks(2).collect() }; for (ri, pair) in rows.into_iter().enumerate() { - let sizes: Vec = pair.iter().map(|p| self.display_size(p)).map(|(w, h)| vec2(w * s, h * s)).collect(); - let row_w: f32 = sizes.iter().map(|v| v.x).sum::() + GAP * (sizes.len() as f32 - 1.0); + let sizes: Vec = pair.iter().map(page_size).collect(); + let row_w: f32 = sizes.iter().map(|v| v.x).sum::() + gap * (sizes.len() as f32 - 1.0); let row_h = sizes.iter().map(|v| v.y).fold(0.0, f32::max); - let mut x = if self.cover && ri == 0 { (content_w / 2.0 + GAP / 2.0).max(SIDE) } else { ((content_w - row_w) / 2.0).max(SIDE) }; + let mut x = if self.cover && ri == 0 { (content_w / 2.0 + gap / 2.0).max(SIDE) } else { ((content_w - row_w) / 2.0).max(SIDE) }; for size in sizes { rects.push(Rect::from_min_size(pos2(x, y + (row_h - size.y) / 2.0), size)); - x += size.x + GAP; + x += size.x + gap; } - y += row_h + GAP; + y += row_h + gap; } } } @@ -2478,7 +2517,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { } let content_w = view.content_width(info, avail.width()); - let rects = view.layout(info, content_w); + let rects = view.layout(info, content_w, ui.ctx().pixels_per_point()); // When not scrolling continuously, only one row is shown at a time: the current page, or the // current two-page spread (with the cover rule). These indices drive paging, which pages are // drawn, and the content height (#752). Empty for a document with no pages. @@ -2680,11 +2719,11 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { } else if let Some((page, frac)) = view.goto.take() { let page = page.min(rects.len() - 1); let r = rects[page]; - // Clamped here: egui clamps only after drawing, so a page that fits was drawn MARGIN - GAP + // Clamped here: egui clamps only after drawing, so a page that fits was drawn MARGIN - page_gap() // too high for a frame and then hopped into place. (`avail` is the viewport, as the // scroll bars float.) let max_y = (content_h - avail.height()).max(0.0); - scroll = scroll.vertical_scroll_offset((r.top() - y_shift - GAP + frac * r.height()).max(0.0).min(max_y)); + scroll = scroll.vertical_scroll_offset((r.top() - y_shift - view.page_gap() + frac * r.height()).max(0.0).min(max_y)); // Horizontally too, as Acrobat does (#2665): beside a much wider page the others sit off // to one side. A page already shown whole, or one filling the window's width, keeps the // scroll, so paging through a zoomed-in document stays on the same column. Otherwise a @@ -2896,8 +2935,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { current = i; } let xf = PageXform { rect: r, rot: view.rotation, pw: info.pages[i].width.max(1.0), ph: info.pages[i].height.max(1.0) }; - // Soft shadow + paper. - painter.add(egui::epaint::Shadow { offset: [0, 3], blur: 14, spread: 0, color: t.page_shadow }.as_shape(r, CornerRadius::ZERO)); + // Shadows would darken the preceding page when pages touch. + if view.show_page_gaps { + painter.add(egui::epaint::Shadow { offset: [0, 3], blur: 14, spread: 0, color: t.page_shadow }.as_shape(r, CornerRadius::ZERO)); + } painter.rect_filled(r, CornerRadius::ZERO, Color32::WHITE); if let Some(err) = view.errors.get(&i) { painter.rect_filled(r, CornerRadius::ZERO, Color32::from_rgb(0xFB, 0xF4, 0xF4)); @@ -3018,7 +3059,9 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { let page_tex = view.pages.get(&i).map(|p| p.tex.id()); view.move_background.paint(ui, painter, &xf, i, info, &view.content, page_tex); } - painter.rect_stroke(r, CornerRadius::ZERO, Stroke::new(0.5, t.border.gamma_multiply(0.8)), egui::StrokeKind::Outside); + if view.show_page_gaps { + painter.rect_stroke(r, CornerRadius::ZERO, Stroke::new(0.5, t.border.gamma_multiply(0.8)), egui::StrokeKind::Outside); + } // Comments: tools, selection, moving and resizing come before text selection. let pcx = comments::PageCx { page: i, xf: &xf, info, tool, prefs, allowed, hidden: comments_hidden, redact_mark }; @@ -3715,7 +3758,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { // below the window's top). Within a point, as for wheel paging, so layout rounding // can't hide an edge. let max_y = (out.content_size.y - out.inner_rect.height()).max(0.0); - (page, out.state.offset.y <= MARGIN - GAP + 1.0, out.state.offset.y >= max_y - 1.0) + (page, out.state.offset.y <= MARGIN - view.page_gap() + 1.0, out.state.offset.y >= max_y - 1.0) }); view.viewport_screen = out.inner_rect; @@ -6333,7 +6376,7 @@ trailer << /Root 1 0 R >> v.fit_zoom(&info); let narrow = v.zoom; // The shown page fills the width between the side gutters, not centred for the wide page. - let shown = v.layout(&info, v.content_width(&info, 1000.0))[0]; + let shown = v.layout(&info, v.content_width(&info, 1000.0), 1.0)[0]; assert!((shown.left() - SIDE).abs() < 1e-2 && (shown.width() - 860.0).abs() < 1e-2, "{shown:?}"); v.current = 1; v.fit_zoom(&info); diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index fb9e0a7c0..6131523d5 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -800,6 +800,10 @@ fn page_display_menu(ui: &mut egui::Ui, view: &DocView) -> Option<&'static str> if ui.add(widgets::checkbox(&mut continuous, tl!("Continuous scrolling"))).changed() { picked = Some("view.continuous"); } + let mut gaps = view.show_page_gaps; + if ui.add(widgets::checkbox(&mut gaps, tl!("Show gaps between pages"))).changed() { + picked = Some("view.page_gaps"); + } let mut cover = view.cover; if ui.add_enabled(view.cover_applies(), widgets::checkbox(&mut cover, tl!("Show cover page in two-page view"))).changed() { picked = Some("view.layout.cover"); diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index 0d230982e..15b9e9677 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -251,6 +251,12 @@ impl PdfCraftApp { v.set_continuous(!v.continuous()); } } + "view.page_gaps" => { + if let Some(i) = active { + let v = &mut self.views[i]; + v.set_page_gaps(!v.show_page_gaps); + } + } "view.layout.cover" => { if let Some(i) = active { let v = &mut self.views[i]; diff --git a/crates/ui-egui/src/compare_ui.rs b/crates/ui-egui/src/compare_ui.rs index f9c1cbcee..353059e87 100644 --- a/crates/ui-egui/src/compare_ui.rs +++ b/crates/ui-egui/src/compare_ui.rs @@ -318,17 +318,24 @@ pub(crate) fn compare_area(app: &mut PdfCraftApp, old: usize, new: usize, ui: &m let leader = app.compare.as_ref().map_or(1, |c| c.leader); let (lead, follow) = if leader == 0 { (old, new) } else { (new, old) }; // The follower takes the leader's zoom and layout before either is drawn. - let (zoom, fit, layout, continuous, rotation) = { + let (zoom, fit, layout, continuous, page_gaps, rotation) = { let v = &app.views[lead]; - (v.zoom, v.fit, v.layout, v.continuous, v.rotation) + (v.zoom, v.fit, v.layout, v.continuous, v.show_page_gaps, v.rotation) }; { let v = &mut app.views[follow]; - if v.zoom != zoom || v.fit != fit || v.layout != layout || v.continuous != continuous || v.rotation != rotation { + if v.zoom != zoom + || v.fit != fit + || v.layout != layout + || v.continuous != continuous + || v.show_page_gaps != page_gaps + || v.rotation != rotation + { v.zoom = zoom; v.fit = fit; v.layout = layout; v.continuous = continuous; + v.show_page_gaps = page_gaps; v.rotation = rotation; } } diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index a4ab35d96..aa8cd66d5 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -678,6 +678,8 @@ impl Host for crate::PdfCraftApp { "zoom_percent": (v.zoom * 100.0).round(), "fit": format!("{:?}", v.fit), "layout": format!("{:?}", v.layout), + "continuous": v.continuous(), + "page_gaps": v.show_page_gaps, "organize": v.organize, "grid_zoom": v.grid_zoom(), // Pages picked in the organize grid or the Pages panel (empty: the current page). diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index 8697887e1..c93bc1199 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -138,6 +138,8 @@ Next view العرض التالي Page display عرض الصفحات Continuous scrolling تمرير متواصل + Show gaps between pages إظهار الفراغات بين الصفحات + Hide gaps to join page edges. Margins inside the PDF remain unchanged. أخفِ الفراغات لوصل حواف الصفحات. تبقى الهوامش داخل ملف PDF دون تغيير. Two-page view عرض صفحتين Single page صفحة واحدة Show cover page in two-page view إظهار صفحة الغلاف في عرض الصفحتين diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 813a2c7ae..7857f8adc 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -936,6 +936,8 @@ Next view Следващ изглед Page display Показване на страниците Continuous scrolling Непрекъснато превъртане + Show gaps between pages Показване на разстоянията между страниците + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Скрийте разстоянията, за да съедините ръбовете на страниците. Полетата вътре в PDF остават непроменени. Two-page view Изглед с две страници Single page Единична страница Show cover page in two-page view Показвай корицата в изглед с две страници diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 993632552..99de78c90 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -125,6 +125,8 @@ About PdfCraft O aplikaci PdfCraft Auto Automaticky Continuous scrolling Plynulé posouvání + Show gaps between pages Zobrazovat mezery mezi stránkami + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Skrytím mezer spojíte okraje stránek. Okraje uvnitř PDF zůstanou beze změny. Single page Jednostránkové zobrazení Two-page view Dvoustránkové zobrazení Show cover page in two-page view Zobrazit titulní stránku ve dvoustránkovém zobrazení diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 4d9207eb9..71ff2e729 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -111,6 +111,8 @@ Next view Nächste Ansicht Page display Seitenanzeige Continuous scrolling Fortlaufendes Scrollen + Show gaps between pages Abstände zwischen Seiten anzeigen + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Blenden Sie die Abstände aus, um die Seitenkanten aneinanderzusetzen. Die Ränder innerhalb der PDF bleiben unverändert. Two-page view Doppelseitenansicht Single page Einzelseite Show cover page in two-page view Deckblatt in Doppelseitenansicht anzeigen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 06fe2b540..2fb6b1471 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -117,6 +117,8 @@ Next view Επόμενη προβολή Page display Εμφάνιση σελίδων Continuous scrolling Συνεχής κύλιση + Show gaps between pages Εμφάνιση κενών ανάμεσα στις σελίδες + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Αποκρύψτε τα κενά για να ενώσετε τις άκρες των σελίδων. Τα περιθώρια μέσα στο PDF παραμένουν αμετάβλητα. Two-page view Προβολή δύο σελίδων Single page Μονή σελίδα Show cover page in two-page view Εμφάνιση εξωφύλλου στην προβολή δύο σελίδων diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 79c668992..e9b1bc1a4 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -134,6 +134,8 @@ Next view Vista siguiente Page display Presentación de página Continuous scrolling Desplazamiento continuo + Show gaps between pages Mostrar espacios entre páginas + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Oculte los espacios para unir los bordes de las páginas. Los márgenes dentro del PDF no cambian. Two-page view Vista de dos páginas Single page Una sola página Show cover page in two-page view Mostrar la portada en la vista de dos páginas diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index 9bdcf0bc8..e4d0cd85b 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -111,6 +111,8 @@ Next view Affichage suivant Page display Affichage des pages Continuous scrolling Défilement continu + Show gaps between pages Afficher les espaces entre les pages + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Masquez les espaces pour joindre les bords des pages. Les marges à l’intérieur du PDF restent inchangées. Two-page view Affichage sur deux pages Single page Une seule page Show cover page in two-page view Afficher la couverture en affichage sur deux pages diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index d537677c7..bef5f7cff 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -109,6 +109,8 @@ Next view Következő nézet Page display Oldalmegjelenítés Continuous scrolling Folyamatos görgetés + Show gaps between pages Oldalak közötti térköz megjelenítése + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Rejtse el a térközöket az oldalszélek összeillesztéséhez. A PDF-en belüli margók változatlanok maradnak. Two-page view Kétoldalas nézet Single page Egy oldal Show cover page in two-page view Borító megjelenítése kétoldalas nézetben diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 35cba7a41..e4c8b22a5 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -135,6 +135,8 @@ Next view Vista successiva Page display Visualizzazione pagina Continuous scrolling Scorrimento continuo + Show gaps between pages Mostra spazi tra le pagine + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Nascondi gli spazi per unire i bordi delle pagine. I margini all’interno del PDF rimangono invariati. Two-page view Visualizzazione doppia Single page Pagina singola Show cover page in two-page view Mostra la copertina in visualizzazione doppia diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index e845a736d..48d7264bd 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -138,6 +138,8 @@ Next view 次の表示 Page display ページ表示 Continuous scrolling 連続スクロール + Show gaps between pages ページ間の余白を表示 + Hide gaps to join page edges. Margins inside the PDF remain unchanged. ページ間の余白を非表示にしてページの端をつなげます。PDF 内の余白は変更されません。 Two-page view 見開き表示 Single page 単一ページ Show cover page in two-page view 見開き表示で表紙を単独表示 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index 9acb4ec1b..2e776f4ce 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -102,6 +102,8 @@ Next view Келесі көрініс Page display Беттерді көрсету Continuous scrolling Үздіксіз айналдыру + Show gaps between pages Беттер арасындағы бос орындарды көрсету + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Беттердің шеттерін біріктіру үшін олардың арасындағы бос орындарды жасырыңыз. PDF ішіндегі жиектер өзгеріссіз қалады. Two-page view Екі беттік көрініс Single page Бір бет Show cover page in two-page view Екі беттік көріністе мұқаба бетін көрсету diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index d1dfc3304..149c8773a 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -74,6 +74,8 @@ Single page Vienas puslapis Two-page view Dviejų puslapių rodinys Continuous scrolling Nepertraukiamas slinkimas + Show gaps between pages Rodyti tarpus tarp puslapių + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Paslėpkite tarpus, kad puslapių kraštai susiliestų. Paraštės PDF viduje nesikeičia. Page display Puslapių rodymas Default page display Numatytasis puslapių rodymas Default zoom Numatytasis mastelis diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 20c0fd046..65daa6239 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -1001,6 +1001,20 @@ mod tests { } } + /// Both page-gap labels must be translated, even in catalogs without full UI coverage. + #[test] + fn page_gap_labels_are_translated_in_every_catalog() { + for language in LANGUAGES.iter().filter(|language| !language.source.is_empty()) { + let lang = Lang::from_code(language.code).expect("registered language"); + for label in ["Show gaps between pages", "Hide gaps to join page edges. Margins inside the PDF remain unchanged."] { + assert!(has(lang, label), "{} lacks {label:?}", language.code); + let translated = tr(lang, label); + assert!(!translated.trim().is_empty(), "{} has an empty translation for {label:?}", language.code); + assert_ne!(translated, label, "{} falls back to English for {label:?}", language.code); + } + } + } + /// Every catalog says the labels of a visible signature, which are looked up by the sign /// crate's own English words (so the `tl!` scan can't see them), and the note shown when they /// fall back to English. Each label that runs into its value keeps a separator at its end. diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index c432a6a92..fb4350180 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -134,6 +134,8 @@ Next view Następny widok Page display Wyświetlanie stron Continuous scrolling Przewijanie ciągłe + Show gaps between pages Pokaż odstępy między stronami + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Ukryj odstępy, aby połączyć krawędzie stron. Marginesy wewnątrz pliku PDF pozostają bez zmian. Two-page view Widok dwóch stron Single page Pojedyncza strona Show cover page in two-page view Pokaż okładkę w widoku dwóch stron diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index 89ff80f9d..5dae637db 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -882,6 +882,8 @@ Next view Próxima exibição Page display Exibição de página Continuous scrolling Rolagem contínua + Show gaps between pages Mostrar espaços entre páginas + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Oculte os espaços para unir as bordas das páginas. As margens dentro do PDF permanecem inalteradas. Two-page view Exibição de duas páginas Single page Página única Show cover page in two-page view Mostrar página de capa na exibição de duas páginas diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 73f21aa0d..6d0f95346 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -131,6 +131,8 @@ Next view Следующий вид Page display Отображение страниц Continuous scrolling Непрерывная прокрутка + Show gaps between pages Показывать промежутки между страницами + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Скройте промежутки, чтобы соединить края страниц. Поля внутри PDF останутся без изменений. Two-page view Двухстраничный вид Single page Одна страница Show cover page in two-page view Показывать титульную страницу в двухстраничном виде diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index dd169bc96..e30a6eb00 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -442,6 +442,8 @@ Content copying for accessibility Kopiranje vsebine za dostopnost Continue Nadaljuj Continuous scrolling Neprekinjeno pomikanje + Show gaps between pages Pokaži razmike med stranmi + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Skrijte razmike, da združite robove strani. Robovi znotraj PDF-ja ostanejo nespremenjeni. Contributors Sodelavci Convert Pretvori Convert colors Pretvori barve diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index 47aee36f8..4065d1993 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -115,6 +115,8 @@ comment menu Edit సవరించు Next view తదుపరి వీక్షణ Page display పేజీ ప్రదర్శన Continuous scrolling నిరంతర స్క్రోలింగ్ + Show gaps between pages పేజీల మధ్య ఖాళీలను చూపు + Hide gaps to join page edges. Margins inside the PDF remain unchanged. పేజీల అంచులను కలపడానికి ఖాళీలను దాచండి. PDF లోపలి మార్జిన్లు మారవు. Two-page view రెండు పేజీల వీక్షణ Single page ఒకే పేజీ Show cover page in two-page view రెండు పేజీల వీక్షణలో కవర్ పేజీని చూపించు diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 95ca0b608..14a77a95b 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -96,6 +96,8 @@ Next view Sonraki görünüm Page display Sayfa görünümü Continuous scrolling Sürekli kaydırma + Show gaps between pages Sayfalar arasındaki boşlukları göster + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Sayfa kenarlarını birleştirmek için boşlukları gizleyin. PDF içindeki kenar boşlukları değişmez. Two-page view İki sayfa görünümü Single page Tek sayfa Show cover page in two-page view İki sayfa görünümünde kapak sayfasını göster diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 7993a0618..9eec6c5eb 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -136,6 +136,8 @@ Next view Наступний вигляд Page display Відображення сторінок Continuous scrolling Безперервне прокручування + Show gaps between pages Показувати проміжки між сторінками + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Приховайте проміжки, щоб з’єднати краї сторінок. Поля всередині PDF залишаться без змін. Two-page view Дві сторінки Single page Одна сторінка Show cover page in two-page view Показувати обкладинку в режимі двох сторінок diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index bae599bf6..576e76bcf 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -78,6 +78,8 @@ Single page Một trang Two-page view Chế độ hai trang Continuous scrolling Cuộn liên tục + Show gaps between pages Hiện khoảng cách giữa các trang + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Ẩn khoảng cách để nối các mép trang. Lề bên trong PDF vẫn giữ nguyên. Page display Hiển thị trang Default page display Hiển thị trang mặc định Default zoom Thu phóng mặc định diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 1ddecc655..23d1ee0f4 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -943,6 +943,8 @@ Next view 下一个视图 Page display 页面显示 Continuous scrolling 连续滚动 + Show gaps between pages 显示页面间距 + Hide gaps to join page edges. Margins inside the PDF remain unchanged. 隐藏间距以连接页面边缘。PDF 内的页边距保持不变。 Two-page view 双页视图 Single page 单页 Show cover page in two-page view 在双页视图中显示封面页 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index bb5ec3649..7a13f4ab0 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -887,6 +887,8 @@ Next view 下一個檢視 Page display 頁面顯示 Continuous scrolling 連續捲動 + Show gaps between pages 顯示頁面間距 + Hide gaps to join page edges. Margins inside the PDF remain unchanged. 隱藏間距以連接頁面邊緣。PDF 內的頁邊距保持不變。 Two-page view 雙頁檢視 Single page 單頁 Show cover page in two-page view 在雙頁檢視中顯示封面 diff --git a/crates/ui-egui/src/js_ui.rs b/crates/ui-egui/src/js_ui.rs index df461fc6e..d5e3dbc3d 100644 --- a/crates/ui-egui/src/js_ui.rs +++ b/crates/ui-egui/src/js_ui.rs @@ -351,6 +351,8 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok } ui.add(widgets::checkbox(&mut defaults.continuous, tl!("Continuous scrolling"))); }); + ui.add(widgets::checkbox(&mut defaults.show_page_gaps, tl!("Show gaps between pages"))); + ui.label(egui::RichText::new(tl!("Hide gaps to join page edges. Margins inside the PDF remain unchanged.")).small().color(t.text_muted)); ui.horizontal(|ui| { use crate::canvas::Fit; ui.label(tl!("Default zoom")); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 7b421263d..7ee50e83f 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2306,6 +2306,7 @@ impl PdfCraftApp { "right_panel_closed": self.right_panel_closed, }); // Set apart: one more key inside the macro above passes serde_json's recursion limit. + v["default_page_gaps"] = serde_json::Value::Bool(self.view_defaults.show_page_gaps); v["redact_prefs"] = self.redact_defaults.to_json(); // Set outside json! above, which is at serde_json's macro recursion limit. v["hide_empty_highlight_hover"] = serde_json::Value::Bool(self.comment_prefs.hide_empty_highlight_hover); @@ -2363,6 +2364,9 @@ impl PdfCraftApp { if let Some((layout, continuous)) = v["default_layout"].as_str().and_then(canvas::parse_display_token) { (self.view_defaults.layout, self.view_defaults.continuous) = (layout, continuous); } + if let Some(show) = v["default_page_gaps"].as_bool() { + self.view_defaults.show_page_gaps = show; + } if let Ok(units) = serde_json::from_value::(v["units"].clone()) { self.units = units; } @@ -2595,6 +2599,21 @@ impl PdfCraftApp { "off" => v.set_continuous(false), _ => return Err("continuous must be on or off".into()), }, + ("page-gaps", Some(v)) => { + let show = match value { + "on" => true, + "off" => false, + _ => return Err("page-gaps must be on or off".into()), + }; + v.set_page_gaps(show); + } + ("default-page-gaps", _) => { + self.view_defaults.show_page_gaps = match value { + "on" => true, + "off" => false, + _ => return Err("default-page-gaps must be on or off".into()), + }; + } ("cover", Some(v)) => { let on = match value { "on" => true, @@ -2770,6 +2789,7 @@ impl PdfCraftApp { "zoom", "layout", "continuous", + "page-gaps", "cover", "organize", "grid-zoom", diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index 9b6d57bf6..ff6a4cbeb 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -146,6 +146,94 @@ fn continuous_layout_stacks_pages_vertically() { assert!((a.center().x - b.center().x).abs() < 1.0, "same column"); } +#[test] +fn page_gaps_toggle_joins_edges_without_changing_the_document() { + let mut h = harness(&[("layout", "continuous"), ("zoom", "50")]); + let zoom = h.state().views[0].zoom; + let original = rect(&h, 1).unwrap().top() - rect(&h, 0).unwrap().bottom(); + assert!(original > 1.0); + h.state_mut().set_option("page-gaps", "off").unwrap(); + h.run_steps(4); + for page in 0..2 { + let (a, b) = (rect(&h, page).unwrap(), rect(&h, page + 1).unwrap()); + assert!((b.top() - a.bottom()).abs() < 0.01, "pages must touch: {a:?} {b:?}"); + } + assert_eq!(h.state().views[0].zoom, zoom); + let id = h.state().views[0].id; + assert!(!h.state().session.get(id).unwrap().dirty, "a view option must not edit the PDF"); + assert!(h.state_mut().execute("view.page_gaps")); + h.run_steps(4); + assert!(((rect(&h, 1).unwrap().top() - rect(&h, 0).unwrap().bottom()) - original).abs() < 0.01); +} + +#[test] +fn page_gaps_default_survives_restart_and_applies_to_new_documents() { + let mut app = PdfCraftApp::new(); + app.set_option("default-page-gaps", "off").unwrap(); + let mut restored = PdfCraftApp::new(); + restored.restore(&app.persist()); + restored.open_bytes("pages.pdf", None, PAGES.to_vec()).unwrap(); + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| restored); + h.run_steps(8); + h.state_mut().set_option("zoom", "50").unwrap(); + h.run_steps(4); + assert!((rect(&h, 1).unwrap().top() - rect(&h, 0).unwrap().bottom()).abs() < 0.01); + assert!(h.state_mut().set_option("page-gaps", "invalid").is_err()); +} + +#[test] +fn page_gaps_menu_joins_pages_without_a_shadow_at_the_seam() { + use egui_kittest::kittest::Queryable; + let _gpu = gpu(); + let mut h = harness(&[("layout", "continuous"), ("zoom", "50"), ("theme", "light")]); + h.get_by_label_contains("Page display").click(); + h.run_steps(3); + h.get_by_label("Show gaps between pages").click(); + h.run_steps(4); + let (a, b) = (rect(&h, 0).unwrap(), rect(&h, 1).unwrap()); + assert!((b.top() - a.bottom()).abs() < 0.01); + let ppp = h.ctx.pixels_per_point(); + let image = h.render().expect("renders the gapless view"); + for dy in [-3.0, 0.0, 3.0] { + let pixel = image.get_pixel((a.center().x * ppp) as u32, ((a.bottom() + dy) * ppp) as u32); + assert_eq!(*pixel, image::Rgba([255, 255, 255, 255]), "no gap or shadow at the seam"); + } +} + +#[test] +fn page_gaps_work_for_mixed_sizes_and_two_page_rows() { + let h = mixed_harness(&[("layout", "continuous"), ("zoom", "50"), ("page-gaps", "off")]); + let (a, b) = (rect(&h, 0).unwrap(), rect(&h, 1).unwrap()); + assert!((b.top() - a.bottom()).abs() < 0.01); + assert!((a.center().x - b.center().x).abs() < 0.01); + let mut h = harness(&[("layout", "two-up"), ("zoom", "50"), ("page-gaps", "off")]); + let (a, b, c) = (rect(&h, 0).unwrap(), rect(&h, 1).unwrap(), rect(&h, 2).unwrap()); + assert!((b.left() - a.right()).abs() < 0.01); + assert!((c.top() - a.bottom()).abs() < 0.01); + let info = h.state().session.get(h.state().views[0].id).unwrap().info.clone(); + h.state_mut().views[0].document_replaced(&info); + assert!(!h.state().views[0].show_page_gaps, "a file reload preserves the view setting"); +} + +#[test] +fn page_gaps_stay_closed_at_fractional_device_scales() { + for ppp in [1.0, 1.25, 1.5, 2.0] { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_pixels_per_point(ppp).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.open_bytes("pages.pdf", None, PAGES.to_vec()).unwrap(); + for (key, value) in [("left", "closed"), ("panel", "none"), ("layout", "continuous"), ("zoom", "43"), ("page-gaps", "off")] { + app.set_option(key, value).unwrap(); + } + app + }); + h.run_steps(8); + for page in 0..2 { + let (a, b) = (rect(&h, page).unwrap(), rect(&h, page + 1).unwrap()); + assert!((b.top() - a.bottom()).abs() < 0.01, "{ppp} pixels/point: {a:?} {b:?}"); + } + } +} + #[test] fn two_up_layout_places_pages_side_by_side() { let mut h = harness(&[("layout", "two-up"), ("zoom", "50")]); diff --git a/docs/page-display.md b/docs/page-display.md new file mode 100644 index 000000000..8decd6834 --- /dev/null +++ b/docs/page-display.md @@ -0,0 +1,29 @@ +# Continuous page display + +> **Last updated:** 2026-10-17 · Viewer: optional page gaps. + +To read pages as a continuous strip, open **View → Page display**, choose **Single page**, +turn **Continuous scrolling** on, and turn **Show gaps between pages** off. The Page display +button on the viewer rail opens the same choices. The command palette also offers +**Show gaps between pages** as a toggle. + +Hiding gaps joins the displayed page edges and removes page shadows. It does not crop or +change the PDF: white margins already in a page remain. Pages of different widths stay +centered. In two-page view, it removes both the gutter within a row and the gap between rows. + +For future documents, open **Preferences → Documents and view** and uncheck +**Show gaps between pages**. This default survives a restart. Existing documents retain their +current view; the per-document toggle does not change the preference for future documents. + +Desktop launch options: + +```sh +pdfcraft --layout continuous --page-gaps off document.pdf +pdfcraft --default-page-gaps off +``` + +The opt-in UI control channel accepts `set key=page-gaps value=off` for the active document +and `set key=default-page-gaps value=off` for future documents. Its `command` method accepts +`id=view.page_gaps`, and `state` reports `active.page_gaps` and `active.continuous`. + +Revision history: 2026-10-17 — added local gapless-display controls and documentation. diff --git a/docs/roadmap.md b/docs/roadmap.md index e1fdce650..d3c21d8d9 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,5 +1,7 @@ # PdfCraft roadmap: milestones and what's next +> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M3, PR #3101: optional gapless page display with translations in all 22 catalogs; estimates unchanged) + > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M14/#2383 GPU-limit candidate; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M7: the Edit text caret is as tall as the row it is on; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M6: CSV tab-containing headers; estimates unchanged) @@ -113,7 +115,7 @@ total matches the parity estimate. | 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. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. Page marks, added content and print output retain small page-unit transform coefficients when serialized. | | 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. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. The renderer keeps its one-point crop-size floor in physical points after UserUnit scaling, with regression coverage for crop origins, rotations and f32 reciprocal rounding. Existing unit policy and nearly-zero fallback are retained. | | 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. 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). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #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. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. #3149: page-list changes discard render demand based on old geometry; actual raster dimensions are checked against the GPU limit before upload. Synthetic crash and continued-rendering regressions cover the replacement path. | +| 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). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #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. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. #3149: page-list changes discard render demand based on old geometry; actual raster dimensions are checked against the GPU limit before upload. Synthetic crash and continued-rendering regressions cover the replacement path. Optional page gaps join page edges without cropping PDF margins; current-document toggle, persisted default and UI control options are covered by viewer tests. | | 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). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #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. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. Wrapped revision buttons stay above Binding in Document Properties (#2739); a synthetic UI test checks layout and saved revision content. | | 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. | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index b1805d522..50c005914 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -854,8 +854,8 @@ milestone = "M3" status = "shipped" commands = ["view.continuous"] tools = [] -evidence = ["crates/ui-egui/tests/view.rs::continuous_layout_stacks_pages_vertically", "crates/ui-egui/tests/view.rs::page_display_commands_switch_layouts", "crates/ui-egui/tests/view.rs::default_page_display_is_used_for_new_documents", "crates/ui-egui/src/canvas.rs::fit_page_and_height_hold_still_while_mixed_page_sizes_scroll_past", "crates/ui-egui/tests/view.rs::touchpad_swipe_glides_on_after_the_fingers_lift_and_stops", "crates/ui-egui/tests/view.rs::touching_the_touchpad_again_or_turning_animations_off_stops_the_glide", "crates/ui-egui/tests/view.rs::rapid_wheel_notches_accelerate_and_follow_touchpad_input", "crates/ui-egui/tests/view.rs::changing_continuous_layout_resets_wheel_acceleration"] -notes = "the default for PDFs that don't ask for a layout (Preferences ▸ Default page display); pages stack with no snapping. Fit page and Fit height fit the largest page, so the zoom holds still across mixed page sizes. Rapid line-wheel notches gain a bounded, constant-space arrival-rate boost (#1072); isolated notches still use the chosen Scroll speed, and page units stay unboosted. A touchpad swipe glides on after the fingers lift on Wayland (#531; macOS glides by itself; Windows and X11 report no lift through winit), unless Animation effects is off; a click, key, zoom or new scroll catches it. Not yet: fingers resting on the touchpad don't catch it (needs Wayland hold gestures, which the vendored winit doesn't bind)" +evidence = ["crates/ui-egui/tests/view.rs::page_gaps_stay_closed_at_fractional_device_scales", "crates/ui-egui/tests/view.rs::page_gaps_toggle_joins_edges_without_changing_the_document", "crates/ui-egui/tests/view.rs::page_gaps_default_survives_restart_and_applies_to_new_documents", "crates/ui-egui/tests/view.rs::page_gaps_menu_joins_pages_without_a_shadow_at_the_seam", "crates/ui-egui/tests/view.rs::page_gaps_work_for_mixed_sizes_and_two_page_rows", "crates/ui-egui/tests/view.rs::continuous_layout_stacks_pages_vertically", "crates/ui-egui/tests/view.rs::page_display_commands_switch_layouts", "crates/ui-egui/tests/view.rs::default_page_display_is_used_for_new_documents", "crates/ui-egui/src/canvas.rs::fit_page_and_height_hold_still_while_mixed_page_sizes_scroll_past", "crates/ui-egui/tests/view.rs::touchpad_swipe_glides_on_after_the_fingers_lift_and_stops", "crates/ui-egui/tests/view.rs::touching_the_touchpad_again_or_turning_animations_off_stops_the_glide", "crates/ui-egui/tests/view.rs::rapid_wheel_notches_accelerate_and_follow_touchpad_input", "crates/ui-egui/tests/view.rs::changing_continuous_layout_resets_wheel_acceleration"] +notes = "Page gaps, shadows and outlines can be hidden through the page-display menu, with a persisted default in Preferences and matching UI control options; page margins in the PDF are unchanged. Continuous scrolling remains the default for PDFs that don't ask for a layout (Preferences ▸ Default page display); pages stack with no snapping. Fit page and Fit height fit the largest page, so the zoom holds still across mixed page sizes. Rapid line-wheel notches gain a bounded, constant-space arrival-rate boost (#1072); isolated notches still use the chosen Scroll speed, and page units stay unboosted. A touchpad swipe glides on after the fingers lift on Wayland (#531; macOS glides by itself; Windows and X11 report no lift through winit), unless Animation effects is off; a click, key, zoom or new scroll catches it. Not yet: fingers resting on the touchpad don't catch it (needs Wayland hold gestures, which the vendored winit doesn't bind)" [[feature]] id = "view.layout-single-page" From fefdccc8903ec4c96b63443dd02ac1d93633bea8 Mon Sep 17 00:00:00 2001 From: DjPeepjay <120863013+DjPeepjay@users.noreply.github.com> Date: Sun, 18 Oct 2026 17:27:05 -0500 Subject: [PATCH 2/2] fix(viewer): preserve gap toggles in organize and comparison views M3 / PR #3101: avoid stale organizer anchors and synchronize the active comparison gap choice across both panes through menu and control paths. Three regression tests fail before these fixes and pass after them. Document the gap helpers and test contracts. --- ROADMAP.md | 2 + crates/ui-egui/src/canvas.rs | 3 +- crates/ui-egui/src/commands.rs | 5 +-- crates/ui-egui/src/compare_ui.rs | 11 +++++ crates/ui-egui/src/lib.rs | 4 +- crates/ui-egui/tests/view.rs | 75 ++++++++++++++++++++++++++++++++ docs/page-display.md | 5 ++- docs/roadmap.md | 4 +- parity/acrobat-features.toml | 4 +- 9 files changed, 102 insertions(+), 11 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 0f9d5757f..70ca97350 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -171,6 +171,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md ## Progress log +- **2026-10-18 (M3, PR #3101 review):** Gap changes in Organize Pages preserve document scroll position; menu and control changes apply to both comparison panes without changing unrelated tabs. Three synthetic UI regressions were observed failing before the fixes. Readiness estimates unchanged. + - **2026-10-18 (M3, PR #3101):** Optional gapless continuous page display with persisted defaults and UI control support; both new labels translated in all 22 catalogs. Rebased onto current main, preserving navigation and rendering fixes. Readiness estimates unchanged. - **2026-10-18 (M6, #2892):** Push-button normal labels are editable per widget through Options and form_set_props, independently of field names; rendering, undo/redo and incremental/full save/reopen are covered. Estimates unchanged. diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 3580064a5..14bb654bc 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1399,7 +1399,7 @@ impl DocView { if self.show_page_gaps == show { return; } - if self.viewport_screen.is_positive() { + if !self.organize && self.viewport_screen.is_positive() { let at = self.viewport_screen.center(); if let Some((page, r)) = self .screen_rects @@ -1414,6 +1414,7 @@ impl DocView { self.show_page_gaps = show; } + /// Separation between adjacent page rectangles, in logical screen points. fn page_gap(&self) -> f32 { if self.show_page_gaps { GAP } else { 0.0 } } diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index 15b9e9677..2dca1bb3d 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -252,9 +252,8 @@ impl PdfCraftApp { } } "view.page_gaps" => { - if let Some(i) = active { - let v = &mut self.views[i]; - v.set_page_gaps(!v.show_page_gaps); + if let Some(view) = active.and_then(|i| self.views.get(i)) { + self.set_active_page_gaps(!view.show_page_gaps); } } "view.layout.cover" => { diff --git a/crates/ui-egui/src/compare_ui.rs b/crates/ui-egui/src/compare_ui.rs index 353059e87..51ca0eff5 100644 --- a/crates/ui-egui/src/compare_ui.rs +++ b/crates/ui-egui/src/compare_ui.rs @@ -163,6 +163,17 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &Option, } impl PdfCraftApp { + /// Set gaps for the active document and both visible comparison panes, leaving other tabs alone. + pub(crate) fn set_active_page_gaps(&mut self, show: bool) { + let Some((index, id)) = self.active_ids() else { return }; + let partner = side_by_side(self, index).and_then(|(old, _)| self.views.get(old).map(|v| v.id)); + for view in &mut self.views { + if view.id == id || partner == Some(view.id) { + view.set_page_gaps(show); + } + } + } + /// Compare the chosen older document with the active one and show the differences. pub fn run_compare(&mut self) { let Some((i, new)) = self.active_ids() else { return }; diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 7ee50e83f..d44a78690 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2599,13 +2599,13 @@ impl PdfCraftApp { "off" => v.set_continuous(false), _ => return Err("continuous must be on or off".into()), }, - ("page-gaps", Some(v)) => { + ("page-gaps", Some(_)) => { let show = match value { "on" => true, "off" => false, _ => return Err("page-gaps must be on or off".into()), }; - v.set_page_gaps(show); + self.set_active_page_gaps(show); } ("default-page-gaps", _) => { self.view_defaults.show_page_gaps = match value { diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index ff6a4cbeb..7c7d19896 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -146,6 +146,7 @@ fn continuous_layout_stacks_pages_vertically() { assert!((a.center().x - b.center().x).abs() < 1.0, "same column"); } +/// The toggle joins page edges while preserving zoom and leaving the PDF unmodified. #[test] fn page_gaps_toggle_joins_edges_without_changing_the_document() { let mut h = harness(&[("layout", "continuous"), ("zoom", "50")]); @@ -166,6 +167,77 @@ fn page_gaps_toggle_joins_edges_without_changing_the_document() { assert!(((rect(&h, 1).unwrap().top() - rect(&h, 0).unwrap().bottom()) - original).abs() < 0.01); } +/// Changing gaps in the organizer must not anchor document pages to the grid viewport. +#[test] +fn page_gaps_in_organizer_do_not_move_the_document_scroll_position() { + let mut h = harness(&[("layout", "continuous"), ("zoom", "100"), ("page", "2")]); + let before = rect(&h, 1).unwrap(); + let gap = before.top() - rect(&h, 0).unwrap().bottom(); + assert!(gap > 1.0); + assert!(h.state_mut().execute("page.organize")); + h.run_steps(4); + assert!(h.state().views[0].organize); + assert!(h.state_mut().execute("view.page_gaps")); + h.run_steps(2); + assert!(!h.state().views[0].show_page_gaps); + assert!(h.state_mut().execute("page.organize")); + h.run_steps(4); + let after = rect(&h, 1).unwrap(); + // Keeping the scroll offset moves page 2 up by exactly the removed gap before it. + assert!((after.top() - before.top() + gap).abs() < 1.0, "stale grid anchoring moved the page: {before:?} -> {after:?}, gap={gap}"); + assert_eq!(h.state().views[0].current, 1); +} + +/// A three-page comparison, with the old pane leading while the new document stays active. +fn page_gaps_comparison_harness() -> Harness<'static, PdfCraftApp> { + 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.set_option("left", "closed").unwrap(); + app.open_bytes("old.pdf", None, PAGES.to_vec()).unwrap(); + app.open_bytes("new.pdf", None, PAGES.to_vec()).unwrap(); + app.set_option("zoom", "50").unwrap(); + app.compare_old = Some(app.views[0].id); + app.run_compare(); + app + }); + h.run_steps(4); + let at = h.state().views[0].viewport_rect().center(); + h.event(egui::Event::PointerMoved(at)); + h.run_steps(2); + assert_eq!(h.state().compare.as_ref().unwrap().leader, 0); + assert_eq!(h.state().active, Some(1)); + h +} + +/// A page-display menu choice must reach both panes before the old leader synchronizes them. +#[test] +fn page_gaps_comparison_menu_preserves_the_active_panes_choice() { + use egui_kittest::kittest::Queryable; + let mut h = page_gaps_comparison_harness(); + h.get_by_label_contains("Page display").click(); + h.run_steps(2); + h.get_by_label("Show gaps between pages").click(); + h.run_steps(4); + assert!(h.state().views.iter().all(|v| !v.show_page_gaps), "the old leader must not restore gaps after the active pane's menu action"); +} + +/// The control setting has the same synchronized behavior and leaves unrelated views alone. +#[test] +fn page_gaps_comparison_control_preserves_both_states() { + let mut h = page_gaps_comparison_harness(); + for (value, show) in [("off", false), ("on", true)] { + h.state_mut().set_option("page-gaps", value).unwrap(); + h.run_steps(4); + assert!(h.state().views.iter().all(|v| v.show_page_gaps == show), "comparison rejected page-gaps={value}"); + } + h.state_mut().open_bytes("unrelated.pdf", None, PAGES.to_vec()).unwrap(); + h.state_mut().set_option("page-gaps", "off").unwrap(); + assert!(h.state().views[0].show_page_gaps && h.state().views[1].show_page_gaps); + assert!(!h.state().views[2].show_page_gaps); +} + +/// The saved default survives restart and applies to newly opened documents. #[test] fn page_gaps_default_survives_restart_and_applies_to_new_documents() { let mut app = PdfCraftApp::new(); @@ -181,6 +253,7 @@ fn page_gaps_default_survives_restart_and_applies_to_new_documents() { assert!(h.state_mut().set_option("page-gaps", "invalid").is_err()); } +/// The real menu action removes gutters, shadows and outlines from the seam pixels. #[test] fn page_gaps_menu_joins_pages_without_a_shadow_at_the_seam() { use egui_kittest::kittest::Queryable; @@ -200,6 +273,7 @@ fn page_gaps_menu_joins_pages_without_a_shadow_at_the_seam() { } } +/// Gapless geometry supports mixed sizes and two-page rows and survives document reload. #[test] fn page_gaps_work_for_mixed_sizes_and_two_page_rows() { let h = mixed_harness(&[("layout", "continuous"), ("zoom", "50"), ("page-gaps", "off")]); @@ -215,6 +289,7 @@ fn page_gaps_work_for_mixed_sizes_and_two_page_rows() { assert!(!h.state().views[0].show_page_gaps, "a file reload preserves the view setting"); } +/// Page edges stay adjacent at integer and fractional physical-pixel scales. #[test] fn page_gaps_stay_closed_at_fractional_device_scales() { for ppp in [1.0, 1.25, 1.5, 2.0] { diff --git a/docs/page-display.md b/docs/page-display.md index 8decd6834..fa2c7f1aa 100644 --- a/docs/page-display.md +++ b/docs/page-display.md @@ -1,6 +1,6 @@ # Continuous page display -> **Last updated:** 2026-10-17 · Viewer: optional page gaps. +> **Last updated:** 2026-10-18 · Viewer: optional page gaps. To read pages as a continuous strip, open **View → Page display**, choose **Single page**, turn **Continuous scrolling** on, and turn **Show gaps between pages** off. The Page display @@ -27,3 +27,6 @@ and `set key=default-page-gaps value=off` for future documents. Its `command` me `id=view.page_gaps`, and `state` reports `active.page_gaps` and `active.continuous`. Revision history: 2026-10-17 — added local gapless-display controls and documentation. + +In a side-by-side comparison, the gap setting applies to both panes. Changing it in +Organize Pages keeps your document scroll position when you return to the viewer. diff --git a/docs/roadmap.md b/docs/roadmap.md index d3c21d8d9..308aefe9d 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # PdfCraft roadmap: milestones and what's next -> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M3, PR #3101: optional gapless page display with translations in all 22 catalogs; estimates unchanged) +> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M3, PR #3101: optional gapless page display with translations in all 22 catalogs; organizer and comparison toggles corrected; estimates unchanged) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M14/#2383 GPU-limit candidate; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M7: the Edit text caret is as tall as the row it is on; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) @@ -115,7 +115,7 @@ total matches the parity estimate. | 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. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. Page marks, added content and print output retain small page-unit transform coefficients when serialized. | | 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. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. The renderer keeps its one-point crop-size floor in physical points after UserUnit scaling, with regression coverage for crop origins, rotations and f32 reciprocal rounding. Existing unit policy and nearly-zero fallback are retained. | | 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. 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). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #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. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. #3149: page-list changes discard render demand based on old geometry; actual raster dimensions are checked against the GPU limit before upload. Synthetic crash and continued-rendering regressions cover the replacement path. Optional page gaps join page edges without cropping PDF margins; current-document toggle, persisted default and UI control options are covered by viewer tests. | +| 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). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #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. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. #3149: page-list changes discard render demand based on old geometry; actual raster dimensions are checked against the GPU limit before upload. Synthetic crash and continued-rendering regressions cover the replacement path. Optional page gaps join page edges without cropping PDF margins; current-document toggle, persisted default and UI control options are covered by viewer tests. Gap changes in Organize Pages preserve document scroll position, and both comparison panes follow menu and control choices. | | 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). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #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. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. Wrapped revision buttons stay above Binding in Document Properties (#2739); a synthetic UI test checks layout and saved revision content. | | 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. | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index 50c005914..4e0d19822 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -852,9 +852,9 @@ area = "B" tier = "P0" milestone = "M3" status = "shipped" -commands = ["view.continuous"] +commands = ["view.continuous", "view.page_gaps"] tools = [] -evidence = ["crates/ui-egui/tests/view.rs::page_gaps_stay_closed_at_fractional_device_scales", "crates/ui-egui/tests/view.rs::page_gaps_toggle_joins_edges_without_changing_the_document", "crates/ui-egui/tests/view.rs::page_gaps_default_survives_restart_and_applies_to_new_documents", "crates/ui-egui/tests/view.rs::page_gaps_menu_joins_pages_without_a_shadow_at_the_seam", "crates/ui-egui/tests/view.rs::page_gaps_work_for_mixed_sizes_and_two_page_rows", "crates/ui-egui/tests/view.rs::continuous_layout_stacks_pages_vertically", "crates/ui-egui/tests/view.rs::page_display_commands_switch_layouts", "crates/ui-egui/tests/view.rs::default_page_display_is_used_for_new_documents", "crates/ui-egui/src/canvas.rs::fit_page_and_height_hold_still_while_mixed_page_sizes_scroll_past", "crates/ui-egui/tests/view.rs::touchpad_swipe_glides_on_after_the_fingers_lift_and_stops", "crates/ui-egui/tests/view.rs::touching_the_touchpad_again_or_turning_animations_off_stops_the_glide", "crates/ui-egui/tests/view.rs::rapid_wheel_notches_accelerate_and_follow_touchpad_input", "crates/ui-egui/tests/view.rs::changing_continuous_layout_resets_wheel_acceleration"] +evidence = ["crates/ui-egui/tests/view.rs::page_gaps_in_organizer_do_not_move_the_document_scroll_position", "crates/ui-egui/tests/view.rs::page_gaps_comparison_menu_preserves_the_active_panes_choice", "crates/ui-egui/tests/view.rs::page_gaps_comparison_control_preserves_both_states", "crates/ui-egui/tests/view.rs::page_gaps_stay_closed_at_fractional_device_scales", "crates/ui-egui/tests/view.rs::page_gaps_toggle_joins_edges_without_changing_the_document", "crates/ui-egui/tests/view.rs::page_gaps_default_survives_restart_and_applies_to_new_documents", "crates/ui-egui/tests/view.rs::page_gaps_menu_joins_pages_without_a_shadow_at_the_seam", "crates/ui-egui/tests/view.rs::page_gaps_work_for_mixed_sizes_and_two_page_rows", "crates/ui-egui/tests/view.rs::continuous_layout_stacks_pages_vertically", "crates/ui-egui/tests/view.rs::page_display_commands_switch_layouts", "crates/ui-egui/tests/view.rs::default_page_display_is_used_for_new_documents", "crates/ui-egui/src/canvas.rs::fit_page_and_height_hold_still_while_mixed_page_sizes_scroll_past", "crates/ui-egui/tests/view.rs::touchpad_swipe_glides_on_after_the_fingers_lift_and_stops", "crates/ui-egui/tests/view.rs::touching_the_touchpad_again_or_turning_animations_off_stops_the_glide", "crates/ui-egui/tests/view.rs::rapid_wheel_notches_accelerate_and_follow_touchpad_input", "crates/ui-egui/tests/view.rs::changing_continuous_layout_resets_wheel_acceleration"] notes = "Page gaps, shadows and outlines can be hidden through the page-display menu, with a persisted default in Preferences and matching UI control options; page margins in the PDF are unchanged. Continuous scrolling remains the default for PDFs that don't ask for a layout (Preferences ▸ Default page display); pages stack with no snapping. Fit page and Fit height fit the largest page, so the zoom holds still across mixed page sizes. Rapid line-wheel notches gain a bounded, constant-space arrival-rate boost (#1072); isolated notches still use the chosen Scroll speed, and page units stay unboosted. A touchpad swipe glides on after the fingers lift on Wayland (#531; macOS glides by itself; Windows and X11 report no lift through winit), unless Animation effects is off; a click, key, zoom or new scroll catches it. Not yet: fingers resting on the touchpad don't catch it (needs Wayland hold gestures, which the vendored winit doesn't bind)" [[feature]]