diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md index 5cfee8d55a..eec9e78513 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 db3dd6124f..7dce6d850c 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 bad75fc3be..0f9d5757fc 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 721b7da1b3..da9d9fe27e 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 fcd56efee4..54932ea434 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 ad74834656..3580064a59 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 fb9e0a7c0d..6131523d56 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 0d230982eb..15b9e96770 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 f9c1cbcee4..353059e879 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 a4ab35d969..aa8cd66d5e 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 8697887e10..c93bc11997 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 813a2c7ae8..7857f8adcb 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 9936325523..99de78c900 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 4d9207eb9d..71ff2e7296 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 06fe2b540b..2fb6b1471d 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 79c668992c..e9b1bc1a4c 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 9bdcf0bc8a..e4d0cd85b4 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 d537677c7d..bef5f7cff7 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 35cba7a410..e4c8b22a5c 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 e845a736da..48d7264bdc 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 9acb4ec1be..2e776f4cef 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 d1dfc33044..149c8773aa 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 20c0fd0467..65daa62397 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 c432a6a92e..fb43501800 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 89ff80f9dd..5dae637dbf 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 73f21aa0d8..6d0f953469 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 dd169bc961..e30a6eb00a 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 47aee36f8b..4065d19932 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 95ca0b608c..14a77a95b7 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 7993a06185..9eec6c5ebf 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 bae599bf6a..576e76bcfd 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 1ddecc655f..23d1ee0f48 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 bb5ec3649b..7a13f4ab0b 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 df461fc6e7..d5e3dbc3de 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 7b421263d3..7ee50e83ff 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 9b6d57bf67..ff6a4cbeb2 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 0000000000..8decd6834c --- /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 e1fdce6505..d3c21d8d9e 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 b1805d5223..50c0059145 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"