mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 04:28:20 -06:00
Merge r2 track t17: choice-pass takes
# Conflicts: # ATTRIBUTION.md # ATTRIBUTION.toml # apps/pdfcraft/src/main.rs # crates/engine/src/dates.rs # crates/fonts/src/lib.rs # crates/ui-egui/src/canvas.rs # crates/ui-egui/src/i18n/mod.rs # crates/ui-egui/src/system_fonts.rs # crates/ui-egui/src/theme.rs # crates/ui-egui/tests/content.rs # crates/ui-egui/tests/fonts.rs
This commit is contained in:
+53
-22
@@ -2,7 +2,7 @@
|
||||
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"
|
||||
@@ -25,7 +25,7 @@ sha256 = "d28d638d797ff63d9906744e748a03ad8b5bd8c272ffcf07cabf656f960129a3"
|
||||
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"
|
||||
@@ -36,7 +36,7 @@ sha256 = "25f73389561052bb84f85378de97d4afafb3def7f428710cf5aee51471e6a2c3"
|
||||
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"
|
||||
@@ -47,7 +47,7 @@ sha256 = "7a1b424ed4e7602b37f37f03da7c8c142bff9af3085256b934b61baa6ceef3c8"
|
||||
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"
|
||||
@@ -58,7 +58,7 @@ sha256 = "d465fc7623ea6cf9293e110b90d1dd5ae70e1d7b76b02e0ec9754906429b86f7"
|
||||
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"
|
||||
@@ -69,7 +69,7 @@ sha256 = "60a49a50f21d711deb636a8dd415c1963094d9b36dc1b36d48f906e3364e3c92"
|
||||
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; the 2026-10-20 coverage additions are AI-assisted and not separately native-reviewed"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; the 2026-10-20 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"
|
||||
@@ -80,7 +80,7 @@ sha256 = "e6d9e79205d1fa924926963466c9e52ac2fff3590471dde2e968be1c9e702c3c"
|
||||
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"
|
||||
@@ -91,7 +91,7 @@ sha256 = "fad25d6b5cb08c0b059b9c0884b3950093cf189d60318e6f079fd211b956b480"
|
||||
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"
|
||||
@@ -113,7 +113,7 @@ sha256 = "e01dc586811c5eeedc57b502e457971bc1916506e88a902d05014fcf4f65b180"
|
||||
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"
|
||||
@@ -124,7 +124,7 @@ sha256 = "35bbf17699c22fa0ebb2ed3dfe2624c3be732dd9d84b8e71ff4293882e0d507e"
|
||||
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"
|
||||
@@ -135,7 +135,7 @@ sha256 = "5fcb2470dff0c90649c1f94126696a50162a2a45f30867177f5085e10ac98f57"
|
||||
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"
|
||||
@@ -146,7 +146,7 @@ sha256 = "723fe3f001b881e9e151547a3cf9992fa48e790a39a049f5755e28e837d87648"
|
||||
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"
|
||||
@@ -157,7 +157,7 @@ sha256 = "1c12b97b51d024c005af29af40199f18ced69caa3c75a0898a2c58124d7c9686"
|
||||
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"
|
||||
@@ -168,7 +168,7 @@ sha256 = "8bb211bb4f856105e70d8635b309ce3fc07a810459724a48cfa941efc772dd5f"
|
||||
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"
|
||||
@@ -179,7 +179,7 @@ sha256 = "a24fc9bd45c58419baa35907c1108034cdd5d9b7a595ce25d8b577db0365849b"
|
||||
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"
|
||||
@@ -190,7 +190,7 @@ sha256 = "a527037961086d9ae647a380016c23370bf1c9c7027f0d35d8336c95266413dc"
|
||||
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"
|
||||
@@ -201,7 +201,7 @@ sha256 = "c317429089c85fbc65df0d85005ff451647fd63b9c61de6f97db747e1ab71e4f"
|
||||
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"
|
||||
@@ -212,7 +212,7 @@ sha256 = "bf29aa55f9ccc61af7b2ce4f0381826cff90d89f52947f95f7f7d0f22743f167"
|
||||
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"
|
||||
@@ -234,7 +234,7 @@ sha256 = "2479d8f164612c4e8d3944df9a6b2ae345be0b26ee87511b17a4a56bc41988d3"
|
||||
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"
|
||||
@@ -245,13 +245,44 @@ sha256 = "990451cc5201b29deac2a3e5d7aaa48e11b83db0c4c14accbddab00e875fed02"
|
||||
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 = "c3f85494bc13d6a9a82429e074b79d28ef71f97eb4e40edc3dd4b8fa4bd223c6"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/hi.tsv"
|
||||
title = "Hindi interface translation catalog"
|
||||
author = "PdfCraft contributors"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 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 = "Hindi UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "e91cfde1fa084b1bf65983e0d4af903bd3c1aa0a269cf0734876c1550667001c"
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/id.tsv"
|
||||
title = "Indonesian interface translation catalog"
|
||||
author = "PdfCraft contributors"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 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 = "Indonesian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "a0a58a0d5b930973141e4dcd37b55ad0173f5c2cc12e24776b082e601e0a2bb5"
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ko.tsv"
|
||||
title = "Korean interface translation catalog"
|
||||
author = "PdfCraft contributors"
|
||||
source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-22 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 = "Korean UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
|
||||
sha256 = "5d7f568741e091a30e0f94abc8841feacd85a738330f8ea9ea458cff469d9698"
|
||||
|
||||
[[asset]]
|
||||
path = "crates/ui-egui/src/i18n/ro.tsv"
|
||||
title = "Romanian interface translation catalog"
|
||||
@@ -3390,7 +3421,7 @@ 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"
|
||||
@@ -3401,7 +3432,7 @@ sha256 = "84921433e8b1e18c9a70b70f4f069d5eb62a1e41379d481f9f57c7da6c8dc448"
|
||||
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"
|
||||
|
||||
@@ -231,6 +231,10 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
|
||||
|
||||
- **2026-10-19 (M3, #3387):** Save-prompt shortcuts yield to later consent and utility dialogs; a synthetic UI regression checks that an edited document stays open. Estimates unchanged.
|
||||
|
||||
- **2026-10-18 (M3, PR #3101 review):** Gap changes in Organize Pages preserve document scroll position; menu and control changes apply to both comparison panes without changing unrelated tabs. Three synthetic UI regressions were observed failing before the fixes. Readiness estimates unchanged.
|
||||
|
||||
- **2026-10-18 (M3, PR #3101):** Optional gapless continuous page display with persisted defaults and UI control support; both new labels translated in all 22 catalogs. Rebased onto current main, preserving navigation and rendering fixes. Readiness estimates unchanged.
|
||||
|
||||
- **2026-10-18 (M10, #3120):** Export to Word, PowerPoint, HTML and RTF reads text from form XObjects nested as deep as the renderer draws them (50 levels), and up to 50 000 forms a page (the renderer's budget for nested ones); it stopped at 12 levels and 4 096 forms and left the rest out silently. A page past those limits is listed in `doc_export_office`'s `truncated_pages`, with a warning. Estimates unchanged.
|
||||
|
||||
- **2026-10-18 (M5, PR #1971 review):** Nested FDF annotation values that identify a source `/Page` or `/Pages` dictionary become null at every copy depth. A synthetic three-page PDF saved and reopened before export verifies that destination actions cannot pull source page content into the FDF, while unrelated nested metadata and shared references remain intact. FDF import semantics, copied-graph limits and estimates are unchanged.
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
//! --theme light|dark|system --language auto|<code> --mode all|read|edit|convert|sign --tool <catalogue id> --left open|closed
|
||||
//! --organize on --fields on --dialog properties|shortcuts|about --palette <query> --home on
|
||||
//! --cover on|off --default-layout single|continuous|two-up|two-page --default-continuous on|off --default-zoom fit-width|fit-page|<percent> --always-default-view on|off
|
||||
//! --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
|
||||
|
||||
@@ -20,6 +20,11 @@
|
||||
[people.YouseifUS]
|
||||
sync_github_name = true
|
||||
|
||||
[people.brian-moon]
|
||||
real_name = "Il Min Brian Moon"
|
||||
display_name = "Il Min Brian Moon"
|
||||
sync_github_name = false
|
||||
|
||||
[people.echelon]
|
||||
real_name = "Brandon Thomas"
|
||||
display_name = "Brandon Thomas"
|
||||
|
||||
@@ -204,6 +204,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"),
|
||||
|
||||
@@ -63,7 +63,7 @@ const CKB_DAYS: [&str; 7] = ["یەکشەممە", "دووشەممە", "سێشەم
|
||||
|
||||
/// The languages month and weekday names can be written in (Preferences ▸ Date format ▸ Language):
|
||||
/// the interface languages, in the same order and with the same names.
|
||||
pub static DATE_LANGUAGES: [DateLanguage; 30] = [
|
||||
pub static DATE_LANGUAGES: [DateLanguage; 33] = [
|
||||
DateLanguage {
|
||||
code: "en",
|
||||
name: "English",
|
||||
@@ -377,6 +377,17 @@ pub static DATE_LANGUAGES: [DateLanguage; 30] = [
|
||||
days: ["sekmadienis", "pirmadienis", "antradienis", "trečiadienis", "ketvirtadienis", "penktadienis", "šeštadienis"],
|
||||
days_short: ["sk", "pr", "an", "tr", "kt", "pn", "št"],
|
||||
},
|
||||
// Indonesian. Month and weekday names are the same with and after a day number, so
|
||||
// `months_with_day` is `None`.
|
||||
DateLanguage {
|
||||
code: "id",
|
||||
name: "Bahasa Indonesia",
|
||||
months: ["Januari", "Februari", "Maret", "April", "Mei", "Juni", "Juli", "Agustus", "September", "Oktober", "November", "Desember"],
|
||||
months_with_day: None,
|
||||
months_short: ["Jan", "Feb", "Mar", "Apr", "Mei", "Jun", "Jul", "Agu", "Sep", "Okt", "Nov", "Des"],
|
||||
days: ["Minggu", "Senin", "Selasa", "Rabu", "Kamis", "Jumat", "Sabtu"],
|
||||
days_short: ["Min", "Sen", "Sel", "Rab", "Kam", "Jum", "Sab"],
|
||||
},
|
||||
// Vietnamese: numeric month forms (`tháng 1` with a day, `Tháng 1` standing alone).
|
||||
// Fill & Sign still refuses any format whose final text fails the existing WinAnsi check
|
||||
// (for example weekday names); `dd.mm.yyyy` remains writable.
|
||||
@@ -524,6 +535,26 @@ pub static DATE_LANGUAGES: [DateLanguage; 30] = [
|
||||
days: ["ວັນອາທິດ", "ວັນຈັນ", "ວັນອັງຄານ", "ວັນພຸດ", "ວັນພະຫັດ", "ວັນສຸກ", "ວັນເສົາ"],
|
||||
days_short: ["ອາທິດ", "ຈັນ", "ອັງຄານ", "ພຸດ", "ພະຫັດ", "ສຸກ", "ເສົາ"],
|
||||
},
|
||||
// Hindi (Devanagari). Month and weekday names are the same with and after a day number.
|
||||
DateLanguage {
|
||||
code: "hi",
|
||||
name: "हिन्दी",
|
||||
months: ["जनवरी", "फ़रवरी", "मार्च", "अप्रैल", "मई", "जून", "जुलाई", "अगस्त", "सितंबर", "अक्तूबर", "नवंबर", "दिसंबर"],
|
||||
months_with_day: None,
|
||||
months_short: ["जन॰", "फ़र॰", "मार्च", "अप्रैल", "मई", "जून", "जुला॰", "अग॰", "सित॰", "अक्तू॰", "नव॰", "दिस॰"],
|
||||
days: ["रविवार", "सोमवार", "मंगलवार", "बुधवार", "गुरुवार", "शुक्रवार", "शनिवार"],
|
||||
days_short: ["रवि", "सोम", "मंगल", "बुध", "गुरु", "शुक्र", "शनि"],
|
||||
},
|
||||
// Korean: months are numbered (`3월`) and weekdays end in 요일; no names change after a day.
|
||||
DateLanguage {
|
||||
code: "ko",
|
||||
name: "한국어",
|
||||
months: ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"],
|
||||
months_with_day: None,
|
||||
months_short: ["1월", "2월", "3월", "4월", "5월", "6월", "7월", "8월", "9월", "10월", "11월", "12월"],
|
||||
days: ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"],
|
||||
days_short: ["일", "월", "화", "수", "목", "금", "토"],
|
||||
},
|
||||
];
|
||||
|
||||
/// The date language for `code` (any case), or `None` when there's none.
|
||||
@@ -848,6 +879,35 @@ mod tests {
|
||||
assert_eq!(in_lang("dddd, mmmm", "cs"), "sobota, březen");
|
||||
}
|
||||
|
||||
/// Indonesian month and weekday names are the same with and after a day number. (Vietnamese
|
||||
/// has its own test upstream: its numeric month forms are writable, weekday names are not.)
|
||||
#[test]
|
||||
fn indonesian_month_and_weekday_names() {
|
||||
assert_eq!(in_lang("d mmmm yyyy", "id"), "7 Maret 2026");
|
||||
assert_eq!(in_lang("mmmm yyyy", "id"), "Maret 2026");
|
||||
assert_eq!(in_lang("dddd, d mmmm yyyy", "id"), "Sabtu, 7 Maret 2026");
|
||||
// Indonesian month names are Latin, so they reach a PDF.
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000);
|
||||
s.set_date_language(Some("id")).unwrap();
|
||||
assert_eq!(s.today_text_for_pdf(Some("d mmmm yyyy"), None).as_deref(), Ok("14 November 2023"));
|
||||
}
|
||||
|
||||
/// Hindi and Korean names are the same with and after a day number. Neither is Western
|
||||
/// European, so the date-text path refuses them with what to use instead.
|
||||
#[test]
|
||||
fn hindi_and_korean_month_and_weekday_names() {
|
||||
assert_eq!(in_lang("d mmmm yyyy", "hi"), "7 मार्च 2026");
|
||||
assert_eq!(in_lang("dddd, mmmm", "hi"), "शनिवार, मार्च");
|
||||
assert_eq!(in_lang("ddd, d mmm", "hi"), "शनि, 7 मार्च");
|
||||
assert_eq!(in_lang("yyyy\\년 mmmm d\\일 dddd", "ko"), "2026년 3월 7일 토요일");
|
||||
assert_eq!(in_lang("ddd", "ko"), "토");
|
||||
let mut s = Session::new().with_clock(|| 1_700_000_000);
|
||||
for code in ["hi", "ko"] {
|
||||
s.set_date_language(Some(code)).unwrap();
|
||||
assert!(s.today_text_for_pdf(Some("d mmmm yyyy"), None).unwrap_err().contains("dd.mm.yyyy"), "{code}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn catalan_months_elide_the_preposition_after_a_day() {
|
||||
assert_eq!(in_lang("dddd d mmmm \\de yyyy", "ca"), "dissabte 7 de març de 2026");
|
||||
|
||||
+10
-4
@@ -1,8 +1,8 @@
|
||||
//! Embeds the fonts of the optional craft-fonts build input (`CRAFT_FONTS_DIR`) as `CRAFT_FONTS`.
|
||||
//! The recipe is craft-fonts' `docs/integration.md`; unset, `CRAFT_FONTS` is empty. It only reads
|
||||
//! the local checkout (no network). On wasm32 it embeds only the UI faces (BIZ UDPGothic Regular
|
||||
//! and any `Arab`, `Telu`, `Hebr` or `Thai` face), to keep the web build within hosting limits (Cloudflare Pages:
|
||||
//! 25 MiB per file).
|
||||
//! and any `Arab`, `Telu`, `Hebr`, `Thai` or `Deva` face), to keep the web build within hosting
|
||||
//! limits (Cloudflare Pages: 25 MiB per file).
|
||||
|
||||
use std::fmt::Write as _;
|
||||
use std::path::PathBuf;
|
||||
@@ -55,8 +55,14 @@ fn craft_fonts(dir: &std::path::Path) -> Result<String, String> {
|
||||
let telugu = scripts.split(',').any(|s| s.trim() == "Telu");
|
||||
// And the Hebrew ones (small), for Hebrew text and Edit text on the web.
|
||||
let hebrew = scripts.split(',').any(|s| s.trim() == "Hebr");
|
||||
let web_face =
|
||||
(*family == "BIZ UDPGothic" && *style == "Regular") || arabic || telugu || hebrew || scripts.split(',').any(|s| s.trim() == "Thai");
|
||||
// Devanagari too, for the Hindi interface (one small face).
|
||||
let devanagari = scripts.split(',').any(|s| s.trim() == "Deva");
|
||||
let web_face = (*family == "BIZ UDPGothic" && *style == "Regular")
|
||||
|| arabic
|
||||
|| telugu
|
||||
|| hebrew
|
||||
|| scripts.split(',').any(|s| s.trim() == "Thai")
|
||||
|| devanagari;
|
||||
if wasm && !web_face {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -75,6 +75,20 @@ pub fn document_thai_font() -> Option<&'static CraftFont> {
|
||||
ui_thai_fonts().into_iter().next()
|
||||
}
|
||||
|
||||
/// The `Deva` craft-fonts faces for Devanagari interface text (the Hindi catalog, file names,
|
||||
/// document titles), in manifest order. Empty when built without craft-fonts or when its revision
|
||||
/// has no Devanagari face; a desktop build then falls back to one font already installed on the
|
||||
/// machine (`SYSTEM_DEVANAGARI_FALLBACK`), and the web build shows replacement boxes.
|
||||
pub fn ui_devanagari_fonts() -> Vec<&'static CraftFont> {
|
||||
CRAFT_FONTS.iter().filter(|f| f.covers("Deva")).collect()
|
||||
}
|
||||
|
||||
/// The face for Devanagari text written into PDFs: the first `Deva` face. `None` when built
|
||||
/// without craft-fonts or when its revision has no Devanagari face.
|
||||
pub fn document_devanagari_font() -> Option<&'static CraftFont> {
|
||||
ui_devanagari_fonts().into_iter().next()
|
||||
}
|
||||
|
||||
/// The `Hebr` craft-fonts faces for Hebrew interface text (file names, document titles, text
|
||||
/// being edited), in manifest order. Empty when built without craft-fonts or when it has no
|
||||
/// Hebrew face.
|
||||
|
||||
@@ -18,9 +18,7 @@ pub mod source_shaping;
|
||||
mod std14;
|
||||
mod std14_metrics;
|
||||
pub use craft::{
|
||||
CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_arabic_font, document_hebrew_font, document_japanese_font, document_japanese_font_for_style,
|
||||
document_japanese_fonts_for_style, document_latin_fonts, document_thai_font, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_hebrew_fonts,
|
||||
ui_japanese_fonts, ui_telugu_fonts, ui_thai_fonts,
|
||||
CRAFT_FONTS, CraftFont, SHIPPORI_MINCHO, document_arabic_font, document_devanagari_font, document_hebrew_font, document_japanese_font, document_japanese_font_for_style, document_japanese_fonts_for_style, document_latin_fonts, document_thai_font, ui_arabic_fonts, ui_chinese_fonts, ui_cjk_fonts, ui_devanagari_fonts, ui_hebrew_fonts, ui_japanese_fonts, ui_telugu_fonts, ui_thai_fonts,
|
||||
};
|
||||
pub use script::{
|
||||
GlyphError, GlyphOutline, INTER_REGULAR, MAX_SIGNATURE_CHARS, ScriptOutline, japanese_glyph, japanese_glyph_from, script_missing, script_outline,
|
||||
|
||||
@@ -182,6 +182,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%).
|
||||
@@ -194,7 +196,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: true }
|
||||
Self { layout: PageLayout::Single, continuous: true, show_page_gaps: true, fit: Fit::Width, zoom: 1.0, highlight_fields: true }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -387,6 +389,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,
|
||||
@@ -732,6 +735,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,
|
||||
@@ -889,6 +893,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;
|
||||
@@ -1530,6 +1535,31 @@ 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.organize && 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;
|
||||
}
|
||||
|
||||
/// Separation between adjacent page rectangles, in logical screen points.
|
||||
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) {
|
||||
@@ -2135,9 +2165,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);
|
||||
}
|
||||
@@ -2158,17 +2188,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<Rect> {
|
||||
fn layout(&self, info: &DocInfo, content_w: f32, ppp: f32) -> Vec<Rect> {
|
||||
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 => {
|
||||
@@ -2179,15 +2219,15 @@ impl DocView {
|
||||
info.pages.chunks(2).collect()
|
||||
};
|
||||
for (ri, pair) in rows.into_iter().enumerate() {
|
||||
let sizes: Vec<Vec2> = 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::<f32>() + GAP * (sizes.len() as f32 - 1.0);
|
||||
let sizes: Vec<Vec2> = pair.iter().map(page_size).collect();
|
||||
let row_w: f32 = sizes.iter().map(|v| v.x).sum::<f32>() + 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2727,7 +2767,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.
|
||||
@@ -2929,11 +2969,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
|
||||
@@ -3147,8 +3187,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, paper);
|
||||
if let Some(err) = view.errors.get(&i) {
|
||||
painter.rect_filled(r, CornerRadius::ZERO, Color32::from_rgb(0xFB, 0xF4, 0xF4));
|
||||
@@ -3269,7 +3311,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 };
|
||||
@@ -3987,7 +4031,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;
|
||||
@@ -6749,7 +6793,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);
|
||||
|
||||
@@ -837,6 +837,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");
|
||||
|
||||
@@ -252,6 +252,11 @@ impl PdfCraftApp {
|
||||
v.set_continuous(!v.continuous());
|
||||
}
|
||||
}
|
||||
"view.page_gaps" => {
|
||||
if let Some(view) = active.and_then(|i| self.views.get(i)) {
|
||||
self.set_active_page_gaps(!view.show_page_gaps);
|
||||
}
|
||||
}
|
||||
"view.layout.cover" => {
|
||||
if let Some(i) = active {
|
||||
let v = &mut self.views[i];
|
||||
|
||||
@@ -176,6 +176,17 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &Option<CompareState>,
|
||||
}
|
||||
|
||||
impl PdfCraftApp {
|
||||
/// Set gaps for the active document and both visible comparison panes, leaving other tabs alone.
|
||||
pub(crate) fn set_active_page_gaps(&mut self, show: bool) {
|
||||
let Some((index, id)) = self.active_ids() else { return };
|
||||
let partner = side_by_side(self, index).and_then(|(old, _)| self.views.get(old).map(|v| v.id));
|
||||
for view in &mut self.views {
|
||||
if view.id == id || partner == Some(view.id) {
|
||||
view.set_page_gaps(show);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compare the chosen older document with the active one and show the differences.
|
||||
pub fn run_compare(&mut self) {
|
||||
let Some((i, new)) = self.active_ids() else { return };
|
||||
@@ -351,17 +362,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -109,8 +109,9 @@ impl ContentView {
|
||||
}
|
||||
}
|
||||
|
||||
/// While an added item is moved, the page without it, rendered once per gesture: the item's own
|
||||
/// pixels follow the pointer over it instead of staying put until release (#1488).
|
||||
/// While an added item is moved, or a picture resized by a corner, the page without it, rendered
|
||||
/// once per gesture: the item's own pixels follow the pointer over it instead of staying put until
|
||||
/// release (#1488).
|
||||
#[derive(Default)]
|
||||
pub(crate) struct MoveBackground {
|
||||
key: Option<(u64, usize, usize)>,
|
||||
@@ -119,6 +120,8 @@ pub(crate) struct MoveBackground {
|
||||
tag: Option<u64>,
|
||||
/// The item's display-space box when the gesture started.
|
||||
from: Option<[f64; 4]>,
|
||||
/// The item is a picture: a corner stretches its pixels, and it keeps its whole applied box.
|
||||
picture: bool,
|
||||
/// The applied move's box. The page raster on screen still has the item where it was until
|
||||
/// its replacement arrives, so the preview stays up until then.
|
||||
settling: Option<[f64; 4]>,
|
||||
@@ -130,8 +133,11 @@ impl MoveBackground {
|
||||
}
|
||||
|
||||
pub fn prepare(&mut self, ctx: &egui::Context, doc: &pdfcraft_engine::Document, cv: &ContentView, scale: f32) {
|
||||
let picture = |page, index| on_page(doc.added(), page).into_iter().any(|(i, a)| i == index && matches!(a.content, AddedContent::Image(_)));
|
||||
let key = match (cv.grab, cv.selected) {
|
||||
(Some((gp, Grab::Move, _)), Some((sp, si))) if gp == sp => Some((doc.edit_generation(), sp, si)),
|
||||
// Text resized by a corner rewraps, so only a picture's pixels can follow the corner.
|
||||
(Some((gp, Grab::Corner(..), _)), Some((sp, si))) if gp == sp && picture(sp, si) => Some((doc.edit_generation(), sp, si)),
|
||||
_ => None,
|
||||
};
|
||||
if !(key.is_none() && self.settling.is_some()) && self.key != key {
|
||||
@@ -139,6 +145,7 @@ impl MoveBackground {
|
||||
if let Some((_, page, index)) = key {
|
||||
self.renderer = doc.added_content_preview(page, index).ok().map(|p| p.renderer());
|
||||
self.from = on_page(doc.added(), page).into_iter().find(|(i, _)| *i == index).map(|(_, a)| a.content.rect());
|
||||
self.picture = picture(page, index);
|
||||
}
|
||||
}
|
||||
if self.settling.is_some() {
|
||||
@@ -208,22 +215,29 @@ impl MoveBackground {
|
||||
return;
|
||||
}
|
||||
let r = screen_rect(xf, info, page, from);
|
||||
let d = if let Some((_, _, start)) = cv.grab {
|
||||
let to = if let Some((_, grab, start)) = cv.grab {
|
||||
let Some(p) = ui.input(|i| i.pointer.hover_pos()) else { return };
|
||||
p - start
|
||||
dragged(r, grab, p - start, self.picture)
|
||||
} else if let Some(to) = self.settling {
|
||||
let to = screen_rect(xf, info, page, to);
|
||||
// A moved text box keeps its top edge (its height follows its lines).
|
||||
screen_rect(xf, info, page, to).left_top() - r.left_top()
|
||||
if self.picture { to } else { r.translate(to.left_top() - r.left_top()) }
|
||||
} else {
|
||||
return;
|
||||
};
|
||||
let painter = painter.with_clip_rect(painter.clip_rect().intersect(xf.rect));
|
||||
xf.paint_image(&painter, background.id(), 0.0, 0.0, 1.0, 1.0);
|
||||
let r = r.expand(1.0);
|
||||
// A pixel around the box, so its edges come along.
|
||||
let (r, to) = (r.expand(1.0), to.expand(1.0));
|
||||
let mut mesh = egui::Mesh::with_texture(page_tex);
|
||||
for c in [r.left_top(), r.right_top(), r.right_bottom(), r.left_bottom()] {
|
||||
let (u, v) = xf.screen_to_norm(c);
|
||||
mesh.vertices.push(egui::epaint::Vertex { pos: c + d, uv: Pos2::new(u, v), color: Color32::WHITE });
|
||||
for (a, b) in [
|
||||
(r.left_top(), to.left_top()),
|
||||
(r.right_top(), to.right_top()),
|
||||
(r.right_bottom(), to.right_bottom()),
|
||||
(r.left_bottom(), to.left_bottom()),
|
||||
] {
|
||||
let (u, v) = xf.screen_to_norm(a);
|
||||
mesh.vertices.push(egui::epaint::Vertex { pos: b, uv: Pos2::new(u, v), color: Color32::WHITE });
|
||||
}
|
||||
mesh.add_triangle(0, 1, 2);
|
||||
mesh.add_triangle(0, 2, 3);
|
||||
|
||||
@@ -679,6 +679,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(),
|
||||
"thumbnail_zoom": v.thumbnail_zoom(),
|
||||
|
||||
@@ -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 إظهار صفحة الغلاف في عرض الصفحتين
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -968,6 +968,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 Показвай корицата в изглед с две страници
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 8 and column 16.
|
@@ -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í
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 51.
|
@@ -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 Εμφάνιση εξωφύλλου στην προβολή δύο σελίδων
|
||||
|
||||
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 60.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 39.
|
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 7 and column 44.
|
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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 見開き表示で表紙を単独表示
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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 Екі беттік көріністе мұқаба бетін көрсету
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
|
@@ -159,7 +159,7 @@ fn plural_sl(n: u64) -> usize {
|
||||
}
|
||||
|
||||
/// The registry. English first: it is the fallback and the source language.
|
||||
pub static LANGUAGES: [LangInfo; 30] = [
|
||||
pub static LANGUAGES: [LangInfo; 33] = [
|
||||
LangInfo { code: "en", name: "English", source: "", plural: plural_one_other, rtl: false, catalog: OnceLock::new() },
|
||||
LangInfo { code: "ja", name: "日本語", source: include_str!("ja.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
||||
// Simplified Chinese; `zh`, `zh-CN`, `zh-SG` and `zh-Hans-*` locales resolve here (see `candidates`).
|
||||
@@ -224,6 +224,9 @@ pub static LANGUAGES: [LangInfo; 30] = [
|
||||
LangInfo { code: "tr", name: "Türkçe", source: include_str!("tr.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
||||
// Lithuanian; every `lt-*` locale (`lt-LT`, `lt_LT.UTF-8`) resolves here. Three plural forms (one, few, other).
|
||||
LangInfo { code: "lt", name: "Lietuvių", source: include_str!("lt.tsv"), plural: plural_lithuanian, rtl: false, catalog: OnceLock::new() },
|
||||
// Indonesian; every `id-*` locale (`id-ID`, `id_ID.UTF-8`) resolves here. CLDR `id` counts have
|
||||
// one form ("other"), so `@plural` entries list a single form.
|
||||
LangInfo { code: "id", name: "Bahasa Indonesia", source: include_str!("id.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
||||
// Vietnamese; every `vi-*` locale (`vi-VN`, `vi_VN.UTF-8`) resolves here. CLDR `vi` counts have
|
||||
// one form ("other"), so `@plural` entries list a single form.
|
||||
LangInfo { code: "vi", name: "Tiếng Việt", source: include_str!("vi.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
||||
@@ -240,6 +243,13 @@ pub static LANGUAGES: [LangInfo; 30] = [
|
||||
LangInfo { code: "ro", name: "Română", source: include_str!("ro.tsv"), plural: plural_ro, rtl: false, catalog: OnceLock::new() },
|
||||
// Lao; every `lo-*` locale (`lo-LA`, `lo_LA.UTF-8` ...) resolves here.
|
||||
LangInfo { code: "lo", name: "ພາສາລາວ", source: include_str!("lo.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
||||
// Hindi; every `hi-*` locale (`hi-IN`, `hi_IN.UTF-8`) resolves here. CLDR `hi` has "one"
|
||||
// (0 and 1) and "other", but the catalog's nouns are the same in both ("{n} पृष्ठ"), so
|
||||
// `@plural` entries list a single form.
|
||||
LangInfo { code: "hi", name: "हिन्दी", source: include_str!("hi.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
||||
// Korean; every `ko-*` locale (`ko-KR`, `ko_KR.UTF-8`) resolves here. CLDR `ko` counts have
|
||||
// one form ("other"), so `@plural` entries list a single form.
|
||||
LangInfo { code: "ko", name: "한국어", source: include_str!("ko.tsv"), plural: plural_none, rtl: false, catalog: OnceLock::new() },
|
||||
];
|
||||
|
||||
impl LangInfo {
|
||||
@@ -1067,6 +1077,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.
|
||||
@@ -3177,6 +3201,271 @@ mod tests {
|
||||
assert!(missing.is_empty(), "untranslated Slovene UI literals: {missing:#?}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn indonesian_and_vietnamese_are_registered_and_persist() {
|
||||
let id = Lang::from_code("id").expect("id registered");
|
||||
let vi = Lang::from_code("vi").expect("vi registered");
|
||||
assert_eq!(id.name(), "Bahasa Indonesia");
|
||||
assert_eq!(vi.name(), "Tiếng Việt");
|
||||
assert_eq!(normalize_pref("ID"), Some("id"));
|
||||
assert_eq!(normalize_pref("Vi"), Some("vi"));
|
||||
for tag in ["id", "id-ID", "id_ID.UTF-8", "ID-id"] {
|
||||
assert_eq!(lang_from_tag(tag), Some(id), "{tag}");
|
||||
}
|
||||
for tag in ["vi", "vi-VN", "vi_VN.UTF-8", "VI-vn"] {
|
||||
assert_eq!(lang_from_tag(tag), Some(vi), "{tag}");
|
||||
}
|
||||
assert_eq!(first_supported("id-ID\r\nen-US"), Some(id));
|
||||
assert_eq!(first_supported("vi-VN\r\nen-US"), Some(vi));
|
||||
assert_eq!(tr(id, "Save"), "Simpan");
|
||||
assert_eq!(tr(id, "Save as…"), "Simpan sebagai…");
|
||||
assert_eq!(tr(id, "Bookmarks"), "Bookmark");
|
||||
assert_eq!(tr(id, "Fill & Sign"), "Isi & Tanda Tangani");
|
||||
assert_eq!(tr(vi, "Save"), "Lưu");
|
||||
assert_eq!(tr(vi, "Save as…"), "Lưu thành…");
|
||||
assert_eq!(tr(vi, "Bookmarks"), "Dấu trang");
|
||||
assert_eq!(tr(vi, "Fill & Sign"), "Điền & Ký");
|
||||
assert_eq!(tr(vi, "Interface language"), "Ngôn ngữ giao diện");
|
||||
assert_eq!(tr(id, "Interface language"), "Bahasa antarmuka");
|
||||
// One CLDR plural form (CLDR `id`, `vi`): the message reads the same for every count.
|
||||
assert_eq!((0..=3).map(|n| (id.0.plural)(n)).collect::<Vec<_>>(), [0, 0, 0, 0]);
|
||||
assert_eq!((0..=3).map(|n| (vi.0.plural)(n)).collect::<Vec<_>>(), [0, 0, 0, 0]);
|
||||
for n in [0, 1, 2, 5] {
|
||||
assert_eq!(trn(id, n, "{n} page", "{n} pages"), format!("{n} halaman"));
|
||||
assert_eq!(trn(vi, n, "{n} page", "{n} pages"), format!("{n} trang"));
|
||||
assert_eq!(trn(id, n, "{n} field", "{n} fields"), format!("{n} kolom"));
|
||||
assert_eq!(trn(vi, n, "{n} field", "{n} fields"), format!("{n} trường"));
|
||||
}
|
||||
let mut app = crate::PdfCraftApp::default();
|
||||
app.set_option("language", "ID").unwrap();
|
||||
assert_eq!(app.language, "id");
|
||||
let mut restored = crate::PdfCraftApp::default();
|
||||
restored.restore(&app.persist());
|
||||
assert_eq!(restored.language, "id");
|
||||
let mut app = crate::PdfCraftApp::default();
|
||||
app.set_option("language", "vi").unwrap();
|
||||
assert_eq!(app.language, "vi");
|
||||
let mut restored = crate::PdfCraftApp::default();
|
||||
restored.restore(&app.persist());
|
||||
assert_eq!(restored.language, "vi");
|
||||
assert!(restored.set_option("language", "xx").is_err());
|
||||
assert_eq!(restored.language, "vi");
|
||||
}
|
||||
|
||||
/// Indonesian and Vietnamese translate every registered command and every All tools group,
|
||||
/// section and item.
|
||||
#[test]
|
||||
fn indonesian_and_vietnamese_cover_commands_and_catalogue() {
|
||||
for lang in [Lang::from_code("id").expect("id registered"), Lang::from_code("vi").expect("vi registered")] {
|
||||
for command in pdfcraft_engine::commands::COMMANDS {
|
||||
assert!(has(lang, command.label), "{} missing command: {}", lang.code(), command.label);
|
||||
if let Some(menu) = command.menu {
|
||||
assert!(has(lang, menu), "{} missing menu: {menu}", lang.code());
|
||||
}
|
||||
}
|
||||
for group in pdfcraft_engine::catalog::TOOL_GROUPS {
|
||||
assert!(has(lang, group.label), "{} missing group: {}", lang.code(), group.label);
|
||||
for section in group.sections {
|
||||
assert!(has(lang, section.title), "{} missing section: {}", lang.code(), section.title);
|
||||
for item in section.items {
|
||||
assert!(has(lang, item.label), "{} missing item: {}", lang.code(), item.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `tl!("…")` literal in the UI source is translated in Indonesian and Vietnamese, so a
|
||||
/// new label cannot silently fall back to English in either.
|
||||
#[test]
|
||||
fn indonesian_and_vietnamese_cover_ui_literals() {
|
||||
let mut stack = vec![std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src")];
|
||||
let mut literals = std::collections::BTreeSet::new();
|
||||
while let Some(dir) = stack.pop() {
|
||||
for entry in std::fs::read_dir(dir).expect("UI source directory") {
|
||||
let path = entry.expect("UI source entry").path();
|
||||
if path.is_dir() {
|
||||
if path.file_name().is_some_and(|name| name != "i18n") {
|
||||
stack.push(path);
|
||||
}
|
||||
} else if path.extension().is_some_and(|ext| ext == "rs") {
|
||||
let source = std::fs::read_to_string(path).expect("UI source file").replace("\r\n", "\n");
|
||||
let mut rest = source.split("#[cfg(test)]\nmod ").next().unwrap_or_default();
|
||||
while let Some((_, after)) = rest.split_once("tl!(\"") {
|
||||
let mut escaped = false;
|
||||
let end = after
|
||||
.char_indices()
|
||||
.find_map(|(i, c)| {
|
||||
if c == '"' && !escaped {
|
||||
return Some(i);
|
||||
}
|
||||
escaped = c == '\\' && !escaped;
|
||||
None
|
||||
})
|
||||
.expect("closed tl! literal");
|
||||
let (raw, tail) = after.split_at(end);
|
||||
if tail.starts_with("\")") {
|
||||
let label: String = serde_json::from_str(&format!("\"{raw}\"")).expect("UI literal escapes");
|
||||
literals.insert(label);
|
||||
}
|
||||
rest = tail.strip_prefix('"').expect("closing quote");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(literals.len() > 900, "source scan found only {} literals", literals.len());
|
||||
for lang in [Lang::from_code("id").expect("id registered"), Lang::from_code("vi").expect("vi registered")] {
|
||||
let missing: Vec<_> = literals.iter().filter(|label| !has(lang, label)).collect();
|
||||
assert!(missing.is_empty(), "untranslated {} UI literals: {missing:#?}", lang.code());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hindi_and_korean_are_registered_and_persist() {
|
||||
let hi = Lang::from_code("hi").expect("hi registered");
|
||||
let ko = Lang::from_code("ko").expect("ko registered");
|
||||
assert_eq!(hi.name(), "हिन्दी");
|
||||
assert_eq!(ko.name(), "한국어");
|
||||
assert_eq!(normalize_pref("HI"), Some("hi"));
|
||||
assert_eq!(normalize_pref("Ko"), Some("ko"));
|
||||
for tag in ["hi", "hi-IN", "hi_IN.UTF-8", "HI-in"] {
|
||||
assert_eq!(lang_from_tag(tag), Some(hi), "{tag}");
|
||||
}
|
||||
for tag in ["ko", "ko-KR", "ko_KR.UTF-8", "KO-kr"] {
|
||||
assert_eq!(lang_from_tag(tag), Some(ko), "{tag}");
|
||||
}
|
||||
assert_eq!(first_supported("hi-IN\r\nen-US"), Some(hi));
|
||||
assert_eq!(first_supported("ko-KR\r\nen-US"), Some(ko));
|
||||
assert_eq!(tr(hi, "Save"), "सहेजें");
|
||||
assert_eq!(tr(hi, "Save as…"), "इस रूप में सहेजें…");
|
||||
assert_eq!(tr(hi, "Bookmarks"), "बुकमार्क");
|
||||
assert_eq!(tr(hi, "Home"), "होम");
|
||||
assert_eq!(tr(hi, "Interface language"), "इंटरफ़ेस भाषा");
|
||||
assert_eq!(tr(ko, "Save"), "저장");
|
||||
assert_eq!(tr(ko, "Save as…"), "다른 이름으로 저장…");
|
||||
assert_eq!(tr(ko, "Bookmarks"), "책갈피");
|
||||
assert_eq!(tr(ko, "Home"), "홈");
|
||||
assert_eq!(tr(ko, "Interface language"), "인터페이스 언어");
|
||||
// One plural form per catalog: the message reads the same for every count.
|
||||
assert_eq!((0..=3).map(|n| (hi.0.plural)(n)).collect::<Vec<_>>(), [0, 0, 0, 0]);
|
||||
assert_eq!((0..=3).map(|n| (ko.0.plural)(n)).collect::<Vec<_>>(), [0, 0, 0, 0]);
|
||||
for n in [0, 1, 2, 5] {
|
||||
assert_eq!(trn(hi, n, "{n} page", "{n} pages"), format!("{n} पृष्ठ"));
|
||||
assert_eq!(trn(ko, n, "{n} page", "{n} pages"), format!("{n}페이지"));
|
||||
assert_eq!(trn(ko, n, "{n} field", "{n} fields"), format!("필드 {n}개"));
|
||||
}
|
||||
// The broken-line regression: a `\n` escape stays one catalog line.
|
||||
assert_eq!(tr(ko, "This page couldn't be displayed.\n{e}"), "이 페이지를 표시할 수 없습니다.\n{e}");
|
||||
for code in ["hi", "ko"] {
|
||||
let mut app = crate::PdfCraftApp::default();
|
||||
app.set_option("language", &code.to_uppercase()).unwrap();
|
||||
assert_eq!(app.language, code);
|
||||
let mut restored = crate::PdfCraftApp::default();
|
||||
restored.restore(&app.persist());
|
||||
assert_eq!(restored.language, code);
|
||||
assert!(restored.set_option("language", "xx").is_err());
|
||||
assert_eq!(restored.language, code);
|
||||
}
|
||||
}
|
||||
|
||||
/// Redaction first marks a region; applying it irreversibly removes marked content.
|
||||
/// Korean must not present either stage as censorship ("검열") or imply that
|
||||
/// clearing un-applied marks deletes PDF content.
|
||||
#[test]
|
||||
fn korean_redaction_labels_distinguish_marks_and_permanent_removal() {
|
||||
let ko = Lang::from_code("ko").expect("ko registered");
|
||||
for english in [
|
||||
"Redact",
|
||||
"Redact a PDF",
|
||||
"Redact text and images",
|
||||
"Redact pages…",
|
||||
"Mark for redaction",
|
||||
"Apply redactions",
|
||||
"Apply redactions…",
|
||||
"Clear redaction marks",
|
||||
"Redact all",
|
||||
] {
|
||||
assert!(!tr(ko, english).contains("검열"), "Korean label misstates redaction: {english}");
|
||||
}
|
||||
assert_eq!(tr(ko, "Mark for redaction"), "제거할 영역 표시");
|
||||
assert_eq!(tr(ko, "Apply redactions"), "표시된 정보 영구 제거");
|
||||
assert_eq!(tr(ko, "Clear redaction marks"), "제거 대상 표시 지우기");
|
||||
let warning = concat!(
|
||||
"You are about to apply 1 redaction mark. ",
|
||||
"Text, images and drawings under the marks, and comments ",
|
||||
"and form fields that overlap them, are removed permanently."
|
||||
);
|
||||
assert!(tr(ko, warning).contains("영구적으로 제거됩니다"));
|
||||
}
|
||||
|
||||
/// Hindi and Korean translate every registered command and every All tools group, section
|
||||
/// and item.
|
||||
#[test]
|
||||
fn hindi_and_korean_cover_commands_and_catalogue() {
|
||||
for lang in [Lang::from_code("hi").expect("hi registered"), Lang::from_code("ko").expect("ko registered")] {
|
||||
for command in pdfcraft_engine::commands::COMMANDS {
|
||||
assert!(has(lang, command.label), "{} missing command: {}", lang.code(), command.label);
|
||||
if let Some(menu) = command.menu {
|
||||
assert!(has(lang, menu), "{} missing menu: {menu}", lang.code());
|
||||
}
|
||||
}
|
||||
for group in pdfcraft_engine::catalog::TOOL_GROUPS {
|
||||
assert!(has(lang, group.label), "{} missing group: {}", lang.code(), group.label);
|
||||
for section in group.sections {
|
||||
assert!(has(lang, section.title), "{} missing section: {}", lang.code(), section.title);
|
||||
for item in section.items {
|
||||
assert!(has(lang, item.label), "{} missing item: {}", lang.code(), item.label);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Every `tl!("…")` literal in the UI source is translated in Hindi and Korean, so a new
|
||||
/// label cannot silently fall back to English in either.
|
||||
#[test]
|
||||
fn hindi_and_korean_cover_ui_literals() {
|
||||
let mut stack = vec![std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src")];
|
||||
let mut literals = std::collections::BTreeSet::new();
|
||||
while let Some(dir) = stack.pop() {
|
||||
for entry in std::fs::read_dir(dir).expect("UI source directory") {
|
||||
let path = entry.expect("UI source entry").path();
|
||||
if path.is_dir() {
|
||||
if path.file_name().is_some_and(|name| name != "i18n") {
|
||||
stack.push(path);
|
||||
}
|
||||
} else if path.extension().is_some_and(|ext| ext == "rs") {
|
||||
let source = std::fs::read_to_string(path).expect("UI source file").replace("\r\n", "\n");
|
||||
let mut rest = source.split("#[cfg(test)]\nmod ").next().unwrap_or_default();
|
||||
while let Some((_, after)) = rest.split_once("tl!(\"") {
|
||||
let mut escaped = false;
|
||||
let end = after
|
||||
.char_indices()
|
||||
.find_map(|(i, c)| {
|
||||
if c == '"' && !escaped {
|
||||
return Some(i);
|
||||
}
|
||||
escaped = c == '\\' && !escaped;
|
||||
None
|
||||
})
|
||||
.expect("closed tl! literal");
|
||||
let (raw, tail) = after.split_at(end);
|
||||
if tail.starts_with("\")") {
|
||||
let label: String = serde_json::from_str(&format!("\"{raw}\"")).expect("UI literal escapes");
|
||||
literals.insert(label);
|
||||
}
|
||||
rest = tail.strip_prefix('"').expect("closing quote");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert!(literals.len() > 900, "source scan found only {} literals", literals.len());
|
||||
for lang in [Lang::from_code("hi").expect("hi registered"), Lang::from_code("ko").expect("ko registered")] {
|
||||
let missing: Vec<_> = literals.iter().filter(|label| !has(lang, label)).collect();
|
||||
assert!(missing.is_empty(), "untranslated {} UI literals: {missing:#?}", lang.code());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bulgarian_is_registered() {
|
||||
let bg = Lang::from_code("bg").expect("bg registered");
|
||||
|
||||
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -914,6 +914,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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -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 Показывать титульную страницу в двухстраничном виде
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -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
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -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 రెండు పేజీల వీక్షణలో కవర్ పేజీని చూపించు
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 13 and column 16.
|
@@ -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
|
||||
|
||||
|
@@ -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 Показувати обкладинку в режимі двох сторінок
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 6 and column 47.
|
@@ -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
|
||||
|
||||
|
@@ -975,6 +975,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 在双页视图中显示封面页
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -919,6 +919,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 在雙頁檢視中顯示封面
|
||||
|
||||
|
Can't render this file because it contains an unexpected character in line 9 and column 16.
|
@@ -361,6 +361,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"));
|
||||
|
||||
@@ -2416,6 +2416,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);
|
||||
@@ -2480,6 +2481,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::<units::Unit>(v["units"].clone()) {
|
||||
self.units = units;
|
||||
}
|
||||
@@ -2722,6 +2726,21 @@ impl PdfCraftApp {
|
||||
"off" => v.set_continuous(false),
|
||||
_ => return Err("continuous must be on or off".into()),
|
||||
},
|
||||
("page-gaps", Some(_)) => {
|
||||
let show = match value {
|
||||
"on" => true,
|
||||
"off" => false,
|
||||
_ => return Err("page-gaps must be on or off".into()),
|
||||
};
|
||||
self.set_active_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,
|
||||
@@ -2915,6 +2934,7 @@ impl PdfCraftApp {
|
||||
"zoom",
|
||||
"layout",
|
||||
"continuous",
|
||||
"page-gaps",
|
||||
"cover",
|
||||
"organize",
|
||||
"grid-zoom",
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
//! only draws characters none of them has, such as an Arabic file name in a build without
|
||||
//! craft-fonts, or the Simplified Chinese interface in a build without a `Hans` face (#826). A
|
||||
//! few more installed faces, [`devanagari_fallback`], [`telugu_fallback`], [`sinhala_fallback`],
|
||||
//! [`tamil_fallback`] and [`hebrew_fallback`], draw Hindi, Telugu, Sinhala, Tamil and Hebrew when
|
||||
//! one is installed. It is read at runtime and never embedded or shipped (AGENTS.md §1.4), and
|
||||
//! `PDFCRAFT_SYSTEM_FONTS=0` turns it off (published screenshots do).
|
||||
//! [`tamil_fallback`], [`hebrew_fallback`] and [`hangul_fallback`], draw Hindi, Telugu, Sinhala,
|
||||
//! Tamil, Hebrew and Korean when one is installed. It is read at runtime and never embedded or
|
||||
//! shipped (AGENTS.md §1.4), and `PDFCRAFT_SYSTEM_FONTS=0` turns it off (published screenshots do).
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::{Arc, OnceLock};
|
||||
@@ -34,6 +34,9 @@ const SINHALA_PROBE: char = '\u{0D9A}';
|
||||
const TAMIL_PROBE: char = '\u{0B95}';
|
||||
/// Hebrew letter alef: the face must have it to cover Hebrew.
|
||||
const HEBREW_PROBE: char = '\u{05D0}';
|
||||
/// Hangul syllable hih (힣, U+D7A3), the last of the 11,172 modern syllables: a face with it has
|
||||
/// the whole block. Droid Sans Fallback, for one, has 가 but few other syllables.
|
||||
const HANGUL_PROBE: char = '\u{D7A3}';
|
||||
/// 欢 (U+6B22): Simplified only, absent from the Japanese craft-fonts faces.
|
||||
const HANS_PROBE: char = '\u{6B22}';
|
||||
/// 說 (U+8AAA): Traditional only, absent from the Japanese craft-fonts faces.
|
||||
@@ -112,6 +115,13 @@ pub fn hebrew_fallback() -> Option<Arc<FontData>> {
|
||||
CACHE.get_or_init(|| script_fallback(&hebrew_candidates(), HEBREW_PROBE)).clone()
|
||||
}
|
||||
|
||||
/// The installed fallback face for Hangul (Korean), read once. `None` when it is turned off or no
|
||||
/// candidate is installed; the interface then draws what the embedded faces can.
|
||||
pub fn hangul_fallback() -> Option<Arc<FontData>> {
|
||||
static CACHE: OnceLock<Option<Arc<FontData>>> = OnceLock::new();
|
||||
CACHE.get_or_init(|| script_fallback(&hangul_candidates(), HANGUL_PROBE)).clone()
|
||||
}
|
||||
|
||||
fn script_fallback(candidates: &[PathBuf], probe: char) -> Option<Arc<FontData>> {
|
||||
if std::env::var_os("PDFCRAFT_SYSTEM_FONTS").is_some_and(|v| v == "0") {
|
||||
return None;
|
||||
@@ -353,6 +363,30 @@ fn han_candidates(traditional: bool) -> Vec<PathBuf> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Well-known locations of Korean faces, best first. Noto CJK and Source Han are left out, as for
|
||||
/// Chinese (AGENTS.md §1.1).
|
||||
fn hangul_candidates() -> Vec<PathBuf> {
|
||||
if cfg!(windows) {
|
||||
["malgun.ttf", "gulim.ttc", "batang.ttc"].iter().map(|f| windows_fonts().join(f)).collect()
|
||||
} else if cfg!(target_os = "macos") {
|
||||
["/System/Library/Fonts/AppleSDGothicNeo.ttc", "/System/Library/Fonts/Supplemental/AppleGothic.ttf", "/Library/Fonts/Arial Unicode.ttf"]
|
||||
.iter()
|
||||
.map(PathBuf::from)
|
||||
.collect()
|
||||
} else {
|
||||
unix_fonts(&[
|
||||
"truetype/nanum/NanumGothic.ttf",
|
||||
"nanum/NanumGothic.ttf",
|
||||
"naver-nanum/NanumGothic.ttf",
|
||||
"nhn-nanum/NanumGothic.ttf",
|
||||
"truetype/unfonts-core/UnDotum.ttf",
|
||||
"un-core/UnDotum.ttf",
|
||||
"truetype/baekmuk/dotum.ttf",
|
||||
"baekmuk-ttf-dotum-fonts/dotum.ttf",
|
||||
])
|
||||
}
|
||||
}
|
||||
|
||||
fn windows_fonts() -> PathBuf {
|
||||
let dir = std::env::var_os("WINDIR").or_else(|| std::env::var_os("SystemRoot")).map_or_else(|| PathBuf::from(r"C:\Windows"), PathBuf::from);
|
||||
dir.join("Fonts")
|
||||
@@ -503,6 +537,7 @@ mod tests {
|
||||
assert!(script_fallback(&candidates, 'A').is_some());
|
||||
assert!(script_fallback(&candidates, TELUGU_PROBE).is_none());
|
||||
assert!(script_fallback(&candidates, DEVANAGARI_PROBE).is_none());
|
||||
assert!(script_fallback(&candidates, HANGUL_PROBE).is_none());
|
||||
assert!(script_fallback(&candidates, SINHALA_PROBE).is_none());
|
||||
assert!(script_fallback(&candidates, TAMIL_PROBE).is_none());
|
||||
assert!(script_fallback(&candidates, HEBREW_PROBE).is_none());
|
||||
@@ -510,13 +545,27 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn script_candidates_are_absolute_font_files() {
|
||||
for list in [devanagari_candidates(), telugu_candidates(), sinhala_candidates(), tamil_candidates(), hebrew_candidates()] {
|
||||
for list in [devanagari_candidates(), telugu_candidates(), sinhala_candidates(), tamil_candidates(), hebrew_candidates(), hangul_candidates()] {
|
||||
assert!(!list.is_empty());
|
||||
assert!(list.iter().all(|p| p.is_absolute() || cfg!(windows)), "{list:?}");
|
||||
assert!(list.iter().all(|p| p.extension().is_some_and(|e| e == "ttf" || e == "ttc")), "{list:?}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hangul_candidates_are_font_files_without_adobe_designs() {
|
||||
let list = hangul_candidates();
|
||||
assert!(!list.is_empty());
|
||||
for path in list {
|
||||
assert!(path.extension().is_some_and(|e| e == "ttf" || e == "ttc"), "{path:?}");
|
||||
let name = path.to_string_lossy().to_lowercase();
|
||||
assert!(!["notosanscjk", "notoserifcjk", "sourcehan", "source-han"].iter().any(|b| name.contains(b)), "{name}");
|
||||
}
|
||||
// Inter has no Hangul.
|
||||
let inter = include_bytes!("../../../assets/fonts/Inter-Regular.ttf");
|
||||
assert_eq!(face_with(inter, HANGUL_PROBE), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn han_candidates_skip_adobe_designs() {
|
||||
for need in [Need::Hans, Need::Hant] {
|
||||
|
||||
@@ -226,6 +226,8 @@ pub const SYSTEM_SINHALA_FALLBACK: &str = "system-sinhala-fallback";
|
||||
pub const SYSTEM_TAMIL_FALLBACK: &str = "system-tamil-fallback";
|
||||
/// The name of the installed Hebrew face [`installed_font_definitions`] may add.
|
||||
pub const SYSTEM_HEBREW_FALLBACK: &str = "system-hebrew-fallback";
|
||||
/// The name of the installed Hangul (Korean) face [`installed_font_definitions`] may add.
|
||||
pub const SYSTEM_HANGUL_FALLBACK: &str = "system-hangul-fallback";
|
||||
|
||||
/// What [`install_fonts_for`] installs: [`font_definitions_for`], then, on desktop, one face
|
||||
/// already installed on this machine. It only draws characters no embedded face has.
|
||||
@@ -238,9 +240,10 @@ pub const SYSTEM_HEBREW_FALLBACK: &str = "system-hebrew-fallback";
|
||||
/// - Otherwise it is the last fallback of every family (an Arabic file name in a build without
|
||||
/// craft-fonts).
|
||||
///
|
||||
/// Installed Devanagari, Telugu, Sinhala, Tamil and Hebrew faces, when there are, end every
|
||||
/// family just before that one, so Hindi, Telugu (#1531), Sinhala and Tamil (#1688) text, such as
|
||||
/// a file name, and Hebrew file names (#586) draw in a build without such faces.
|
||||
/// Installed Devanagari, Telugu, Sinhala, Tamil, Hebrew and Hangul faces, when there are, end
|
||||
/// every family just before that one, so Hindi, Telugu (#1531), Sinhala and Tamil (#1688) and
|
||||
/// Korean text, such as a file name or a tab title, and Hebrew file names (#586) draw in a build
|
||||
/// without such faces.
|
||||
///
|
||||
/// `PDFCRAFT_SYSTEM_FONTS=0` leaves them out.
|
||||
pub fn installed_font_definitions(cjk: CjkPreference) -> FontDefinitions {
|
||||
@@ -256,6 +259,7 @@ pub fn installed_font_definitions(cjk: CjkPreference) -> FontDefinitions {
|
||||
(SYSTEM_SINHALA_FALLBACK, crate::system_fonts::sinhala_fallback()),
|
||||
(SYSTEM_TAMIL_FALLBACK, crate::system_fonts::tamil_fallback()),
|
||||
(SYSTEM_HEBREW_FALLBACK, crate::system_fonts::hebrew_fallback()),
|
||||
(SYSTEM_HANGUL_FALLBACK, crate::system_fonts::hangul_fallback()),
|
||||
] {
|
||||
let Some(data) = data else { continue };
|
||||
fonts.font_data.insert(name.to_owned(), data);
|
||||
@@ -385,13 +389,14 @@ fn font_definitions_for_platform(prefer_hans: bool, platform: FontPolicyPlatform
|
||||
}
|
||||
}
|
||||
// Arabic-script faces (file names, document titles), Telugu faces (the Telugu interface,
|
||||
// file names), Hebrew faces (file names, text being edited) and Thai faces after the CJK ones; the
|
||||
// ranges don't overlap.
|
||||
// file names), Hebrew faces (file names, text being edited), Thai faces and Devanagari faces
|
||||
// (the Hindi interface) after the CJK ones; the ranges don't overlap.
|
||||
for face in pdfcraft_fonts::ui_arabic_fonts()
|
||||
.into_iter()
|
||||
.chain(pdfcraft_fonts::ui_telugu_fonts())
|
||||
.chain(pdfcraft_fonts::ui_hebrew_fonts())
|
||||
.chain(pdfcraft_fonts::ui_thai_fonts())
|
||||
.chain(pdfcraft_fonts::ui_devanagari_fonts())
|
||||
{
|
||||
let name = face.name();
|
||||
if !fonts.font_data.contains_key(&name) {
|
||||
|
||||
@@ -778,3 +778,78 @@ fn mode_tabs_keep_the_hand_over_added_text_scrolled_under_them() {
|
||||
click(&mut h, convert);
|
||||
assert_eq!(h.state().mode, pdfcraft_ui_egui::Mode::Convert, "the tab takes the click, not the box");
|
||||
}
|
||||
|
||||
/// Red pixels in `r`: the test picture.
|
||||
fn red(pixels: &image::RgbaImage, r: egui::Rect) -> usize {
|
||||
let (x0, y0, x1, y1) = (r.left().max(0.0) as u32, r.top().max(0.0) as u32, r.right() as u32, r.bottom() as u32);
|
||||
(y0..y1.min(pixels.height()))
|
||||
.flat_map(|y| (x0..x1.min(pixels.width())).map(move |x| (x, y)))
|
||||
.filter(|(x, y)| matches!(pixels.get_pixel(*x, *y).0, [r, g, b, _] if r > 150 && g < 90 && b < 90))
|
||||
.count()
|
||||
}
|
||||
|
||||
/// Steps until no render is pending.
|
||||
fn settle(h: &mut Harness<'static, PdfCraftApp>) {
|
||||
for _ in 0..200 {
|
||||
h.run_steps(1);
|
||||
if !h.state().render_pending() {
|
||||
return;
|
||||
}
|
||||
std::thread::sleep(std::time::Duration::from_millis(5));
|
||||
}
|
||||
panic!("the page did not finish rendering");
|
||||
}
|
||||
|
||||
/// An added picture resized by a corner is drawn at its new size while dragged and after the
|
||||
/// release until the page is rendered again. It used to stay at its old size under the outline for
|
||||
/// the whole drag, and to show there again after the release.
|
||||
#[test]
|
||||
fn a_resized_added_picture_shows_only_at_its_new_size() {
|
||||
let mut h = harness();
|
||||
assert!(h.state_mut().execute("edit.edit_text"));
|
||||
h.run_steps(2);
|
||||
let mut png = Vec::new();
|
||||
{
|
||||
let mut enc = png::Encoder::new(&mut png, 60, 60);
|
||||
enc.set_color(png::ColorType::Rgb);
|
||||
let mut w = enc.write_header().unwrap();
|
||||
w.write_image_data(&[200u8, 30, 30].repeat(3600)).unwrap();
|
||||
}
|
||||
h.state_mut().add_image("red.png".into(), png);
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().views[0].content.selected, Some((0, 0)), "the picture is selected");
|
||||
settle(&mut h);
|
||||
let rect = added(&h)[0].content.rect();
|
||||
let r = item_on_screen(&h, rect);
|
||||
// The lower right corner to the middle: half the size, the upper left corner kept.
|
||||
let (grip, to) = (r.expand(1.0).right_bottom(), r.center());
|
||||
let kept = egui::Rect::from_min_max(r.min, r.center()).shrink(3.0);
|
||||
let gone = egui::Rect::from_min_max(r.center() + egui::vec2(4.0, 4.0), r.max - egui::vec2(3.0, 3.0));
|
||||
h.hover_at(grip);
|
||||
h.run_steps(1);
|
||||
h.drag_at(grip);
|
||||
h.run_steps(1);
|
||||
for k in 1..=4 {
|
||||
h.hover_at(grip + (to - grip) * (k as f32 / 4.0));
|
||||
h.run_steps(1);
|
||||
}
|
||||
settle(&mut h);
|
||||
let pixels = h.render().unwrap();
|
||||
assert!(red(&pixels, kept) > 100, "while dragged, the picture is drawn at its new size");
|
||||
assert_eq!(red(&pixels, gone), 0, "while dragged, the picture shows at its old size");
|
||||
assert_eq!(added(&h)[0].content.rect(), rect, "nothing is applied before release");
|
||||
h.state_mut().views[0].hold_rasters = true;
|
||||
h.drop_at(to);
|
||||
for frame in 0..4 {
|
||||
h.step();
|
||||
let pixels = h.render().unwrap();
|
||||
assert!(red(&pixels, kept) > 100, "frame {frame} after the release: the picture is not at its new size");
|
||||
assert_eq!(red(&pixels, gone), 0, "frame {frame} after the release: the picture shows at its old size");
|
||||
}
|
||||
assert_ne!(added(&h)[0].content.rect(), rect, "the resize is applied");
|
||||
h.state_mut().views[0].hold_rasters = false;
|
||||
settle(&mut h);
|
||||
h.run_steps(2);
|
||||
let pixels = h.render().unwrap();
|
||||
assert!(red(&pixels, kept) > 100 && red(&pixels, gone) == 0, "the page raster has the picture at its new size");
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ const CHINESE: &str = "简体中文欢迎";
|
||||
const ARABIC: &str = "واحد اثنين";
|
||||
const TELUGU: &str = "తెలుగు ఫైల్";
|
||||
const HEBREW: &str = "ייפוי כח.pdf";
|
||||
const DEVANAGARI: &str = "हिंदी पाठ सम्पादन";
|
||||
|
||||
fn families() -> Vec<FontId> {
|
||||
vec![FontId::proportional(13.0), FontId::monospace(13.0), theme::medium(13.0), theme::semibold(17.0)]
|
||||
@@ -164,7 +165,31 @@ fn system_fallback_covers_devanagari() {
|
||||
assert_eq!(stack.iter().filter(|n| *n == theme::SYSTEM_DEVANAGARI_FALLBACK).count(), 1, "{family:?}");
|
||||
}
|
||||
let mut fonts = Fonts::new(TextOptions::default(), defs);
|
||||
const DEVANAGARI: &str = "हिंदी पाठ सम्पादन";
|
||||
for id in families() {
|
||||
assert!(fonts.has_glyphs(&id, DEVANAGARI), "{id:?} lacks {DEVANAGARI}");
|
||||
}
|
||||
assert!(layout_widths(&mut fonts, DEVANAGARI).iter().all(|w| w.is_finite() && *w > 0.0));
|
||||
}
|
||||
|
||||
/// Built with craft-fonts, Hindi text renders with real glyphs (no tofu) in every family: a
|
||||
/// `Deva` face is a fallback after the app's own fonts. Without craft-fonts it is the installed
|
||||
/// Devanagari fallback's job (see `system_fallback_covers_devanagari`), and the web build has
|
||||
/// neither.
|
||||
#[test]
|
||||
fn devanagari_ui_text_uses_craft_fonts() {
|
||||
if pdfcraft_fonts::ui_devanagari_fonts().is_empty() {
|
||||
eprintln!("skipping devanagari_ui_text_uses_craft_fonts: built without craft-fonts (set CRAFT_FONTS_DIR to run it)");
|
||||
return;
|
||||
}
|
||||
let defs = theme::font_definitions();
|
||||
for family in [FontFamily::Proportional, FontFamily::Monospace] {
|
||||
let stack = &defs.families[&family];
|
||||
let first =
|
||||
stack.iter().position(|n| pdfcraft_fonts::ui_devanagari_fonts().iter().any(|f| f.name() == *n)).expect("a Deva face is a fallback");
|
||||
let own = stack.iter().position(|n| n == "Inter" || n == "JetBrainsMono").expect("the app's own font");
|
||||
assert!(own < first, "{family:?}: {stack:?}");
|
||||
}
|
||||
let mut fonts = Fonts::new(TextOptions::default(), defs);
|
||||
for id in families() {
|
||||
assert!(fonts.has_glyphs(&id, DEVANAGARI), "{id:?} lacks {DEVANAGARI}");
|
||||
}
|
||||
@@ -314,7 +339,7 @@ fn ui_fonts_work_without_craft_fonts() {
|
||||
theme::install_fonts(&ctx);
|
||||
}
|
||||
|
||||
/// Latin- and Cyrillic-script catalogs (Czech, Brazilian Portuguese, German, Spanish, French, Russian, Bulgarian, Hungarian, Ukrainian, Italian, Slovene, Albanian, Catalan) are drawn entirely by the app's own faces
|
||||
/// Latin- and Cyrillic-script catalogs (Czech, Brazilian Portuguese, German, Spanish, French, Russian, Bulgarian, Kazakh, Hungarian, Ukrainian, Italian, Slovene, Lithuanian, Greek, Turkish, Indonesian, Vietnamese, Albanian, Catalan) are drawn entirely by the app's own faces
|
||||
/// (Inter, JetBrains Mono with Inter fallback): no letter falls through to egui's defaults or a CJK fallback. (egui's
|
||||
/// `has_glyphs` can't answer this: with only the primary face it is also the replacement face.)
|
||||
#[test]
|
||||
@@ -341,13 +366,18 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() {
|
||||
("tr", include_str!("../src/i18n/tr.tsv")),
|
||||
("sl", include_str!("../src/i18n/sl.tsv")),
|
||||
("lt", include_str!("../src/i18n/lt.tsv")),
|
||||
("id", include_str!("../src/i18n/id.tsv")),
|
||||
("vi", include_str!("../src/i18n/vi.tsv")),
|
||||
("fi", include_str!("../src/i18n/fi.tsv")),
|
||||
("sq", include_str!("../src/i18n/sq.tsv")),
|
||||
("ca", include_str!("../src/i18n/ca.tsv")),
|
||||
("ro", include_str!("../src/i18n/ro.tsv")),
|
||||
] {
|
||||
let mut text = String::from("áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…ґҐєЄіІїЇәғқңөұүһӘҒҚҢӨҰҮҺ");
|
||||
// Vietnamese stacks its diacritics: the precomposed letters and the base letters they
|
||||
// combine with, including `đ` and the tone marks `ạ ả ấ ầ ẩ ẫ ậ ắ ằ ẳ ẵ ặ ế ề ể ễ ệ`.
|
||||
let mut text = String::from(
|
||||
"áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽãõçâêôàÃÕÇÂÊÔÀñÑüÜäÄöÖőűß„“¿¡«»…ґҐєЄіІїЇәғқңөұүһӘҒҚҢӨҰҮҺđĐạảấầẩẫậắằẳẵặếềểễệọỏốồổỗộớờởỡợụủứừửữựỳỵỷỹ",
|
||||
);
|
||||
for line in catalog.lines().filter(|l| !l.starts_with('#')) {
|
||||
if let Some(translation) = line.split('\t').nth(2) {
|
||||
text.extend(translation.chars().filter(|c| !c.is_whitespace()));
|
||||
@@ -365,3 +395,27 @@ fn primary_ui_fonts_cover_latin_and_cyrillic_catalogs() {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// On a machine with an installed Korean font, the installed definitions include it and Hangul has
|
||||
/// glyphs, so a Korean file name in a tab title is not boxes. macOS always ships Apple SD Gothic
|
||||
/// Neo and Windows Malgun Gothic, so there the test never skips.
|
||||
#[test]
|
||||
fn system_fallback_covers_hangul() {
|
||||
const HANGUL: &str = "성가대 찬양곡 목록";
|
||||
assert!(!theme::font_definitions().font_data.contains_key(theme::SYSTEM_HANGUL_FALLBACK));
|
||||
let defs = theme::installed_font_definitions(theme::CjkPreference::Default);
|
||||
let off = std::env::var_os("PDFCRAFT_SYSTEM_FONTS").is_some_and(|v| v == "0");
|
||||
if !defs.font_data.contains_key(theme::SYSTEM_HANGUL_FALLBACK) && (off || cfg!(all(unix, not(target_os = "macos")))) {
|
||||
eprintln!("skipping system_fallback_covers_hangul: no installed Korean fallback font (or PDFCRAFT_SYSTEM_FONTS=0)");
|
||||
assert!(defs.families.values().all(|stack| !stack.iter().any(|n| n == theme::SYSTEM_HANGUL_FALLBACK)));
|
||||
return;
|
||||
}
|
||||
for (family, stack) in &defs.families {
|
||||
assert_eq!(stack.iter().filter(|n| *n == theme::SYSTEM_HANGUL_FALLBACK).count(), 1, "{family:?}: {stack:?}");
|
||||
}
|
||||
let mut fonts = Fonts::new(TextOptions::default(), defs);
|
||||
for id in families() {
|
||||
assert!(fonts.has_glyphs(&id, HANGUL), "{id:?} lacks {HANGUL}");
|
||||
}
|
||||
assert!(layout_widths(&mut fonts, HANGUL).iter().all(|w| w.is_finite() && *w > 0.0));
|
||||
}
|
||||
|
||||
@@ -147,6 +147,169 @@ fn continuous_layout_stacks_pages_vertically() {
|
||||
assert!((a.center().x - b.center().x).abs() < 1.0, "same column");
|
||||
}
|
||||
|
||||
/// The toggle joins page edges while preserving zoom and leaving the PDF unmodified.
|
||||
#[test]
|
||||
fn page_gaps_toggle_joins_edges_without_changing_the_document() {
|
||||
let mut h = harness(&[("layout", "continuous"), ("zoom", "50")]);
|
||||
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);
|
||||
}
|
||||
|
||||
/// Changing gaps in the organizer must not anchor document pages to the grid viewport.
|
||||
#[test]
|
||||
fn page_gaps_in_organizer_do_not_move_the_document_scroll_position() {
|
||||
let mut h = harness(&[("layout", "continuous"), ("zoom", "100"), ("page", "2")]);
|
||||
let before = rect(&h, 1).unwrap();
|
||||
let gap = before.top() - rect(&h, 0).unwrap().bottom();
|
||||
assert!(gap > 1.0);
|
||||
assert!(h.state_mut().execute("page.organize"));
|
||||
h.run_steps(4);
|
||||
assert!(h.state().views[0].organize);
|
||||
assert!(h.state_mut().execute("view.page_gaps"));
|
||||
h.run_steps(2);
|
||||
assert!(!h.state().views[0].show_page_gaps);
|
||||
assert!(h.state_mut().execute("page.organize"));
|
||||
h.run_steps(4);
|
||||
let after = rect(&h, 1).unwrap();
|
||||
// Keeping the scroll offset moves page 2 up by exactly the removed gap before it.
|
||||
assert!((after.top() - before.top() + gap).abs() < 1.0, "stale grid anchoring moved the page: {before:?} -> {after:?}, gap={gap}");
|
||||
assert_eq!(h.state().views[0].current, 1);
|
||||
}
|
||||
|
||||
/// A three-page comparison, with the old pane leading while the new document stays active.
|
||||
fn page_gaps_comparison_harness() -> Harness<'static, PdfCraftApp> {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.set_option("left", "closed").unwrap();
|
||||
app.open_bytes("old.pdf", None, PAGES.to_vec()).unwrap();
|
||||
app.open_bytes("new.pdf", None, PAGES.to_vec()).unwrap();
|
||||
app.set_option("zoom", "50").unwrap();
|
||||
app.compare_old = Some(app.views[0].id);
|
||||
app.run_compare();
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
let at = h.state().views[0].viewport_rect().center();
|
||||
h.event(egui::Event::PointerMoved(at));
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().compare.as_ref().unwrap().leader, 0);
|
||||
assert_eq!(h.state().active, Some(1));
|
||||
h
|
||||
}
|
||||
|
||||
/// A page-display menu choice must reach both panes before the old leader synchronizes them.
|
||||
#[test]
|
||||
fn page_gaps_comparison_menu_preserves_the_active_panes_choice() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
let mut h = page_gaps_comparison_harness();
|
||||
h.get_by_label_contains("Page display").click();
|
||||
h.run_steps(2);
|
||||
h.get_by_label("Show gaps between pages").click();
|
||||
h.run_steps(4);
|
||||
assert!(h.state().views.iter().all(|v| !v.show_page_gaps), "the old leader must not restore gaps after the active pane's menu action");
|
||||
}
|
||||
|
||||
/// The control setting has the same synchronized behavior and leaves unrelated views alone.
|
||||
#[test]
|
||||
fn page_gaps_comparison_control_preserves_both_states() {
|
||||
let mut h = page_gaps_comparison_harness();
|
||||
for (value, show) in [("off", false), ("on", true)] {
|
||||
h.state_mut().set_option("page-gaps", value).unwrap();
|
||||
h.run_steps(4);
|
||||
assert!(h.state().views.iter().all(|v| v.show_page_gaps == show), "comparison rejected page-gaps={value}");
|
||||
}
|
||||
h.state_mut().open_bytes("unrelated.pdf", None, PAGES.to_vec()).unwrap();
|
||||
h.state_mut().set_option("page-gaps", "off").unwrap();
|
||||
assert!(h.state().views[0].show_page_gaps && h.state().views[1].show_page_gaps);
|
||||
assert!(!h.state().views[2].show_page_gaps);
|
||||
}
|
||||
|
||||
/// The saved default survives restart and applies to newly opened documents.
|
||||
#[test]
|
||||
fn page_gaps_default_survives_restart_and_applies_to_new_documents() {
|
||||
let mut app = PdfCraftApp::new();
|
||||
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());
|
||||
}
|
||||
|
||||
/// The real menu action removes gutters, shadows and outlines from the seam pixels.
|
||||
#[test]
|
||||
fn page_gaps_menu_joins_pages_without_a_shadow_at_the_seam() {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
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");
|
||||
}
|
||||
}
|
||||
|
||||
/// Gapless geometry supports mixed sizes and two-page rows and survives document reload.
|
||||
#[test]
|
||||
fn page_gaps_work_for_mixed_sizes_and_two_page_rows() {
|
||||
let h = mixed_harness(&[("layout", "continuous"), ("zoom", "50"), ("page-gaps", "off")]);
|
||||
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");
|
||||
}
|
||||
|
||||
/// Page edges stay adjacent at integer and fractional physical-pixel scales.
|
||||
#[test]
|
||||
fn page_gaps_stay_closed_at_fractional_device_scales() {
|
||||
for ppp in [1.0, 1.25, 1.5, 2.0] {
|
||||
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")]);
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
# Continuous page display
|
||||
|
||||
> **Last updated:** 2026-10-18 · Viewer: optional page gaps.
|
||||
|
||||
To read pages as a continuous strip, open **View → Page display**, choose **Single page**,
|
||||
turn **Continuous scrolling** on, and turn **Show gaps between pages** off. The Page display
|
||||
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.
|
||||
|
||||
In a side-by-side comparison, the gap setting applies to both panes. Changing it in
|
||||
Organize Pages keeps your document scroll position when you return to the viewer.
|
||||
@@ -17,6 +17,7 @@
|
||||
> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M5, PR #1971: bounded nested FDF export excludes source page/page-tree dictionaries; refreshed against upstream 4db6ac4a; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** patch (M2 #3086: dotted TOC leaders no longer hide word spaces; estimates unchanged)
|
||||
|
||||
> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M3, PR #3101: optional gapless page display with translations in all 22 catalogs; organizer and comparison toggles corrected; estimates unchanged)
|
||||
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M14/#2383 GPU-limit candidate; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
|
||||
> **Last reviewed:** 2026-10-20 - **Last updated:** 2026-10-20 - **Change:** minor (M6, #3502: form detection uses the existing content-stream boundary policy; estimates unchanged)
|
||||
> **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)
|
||||
@@ -154,6 +155,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 |
|
||||
| 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). #3808: the Pages inspector offers a 50–150% thumbnail slider and buttons, also available through `ui.set`. Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>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. Save-prompt shortcuts yield to dialogs above them; stacked link-consent UI regression covers Escape and Return. |
|
||||
| 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<br>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. Save-prompt shortcuts yield to dialogs above them; stacked link-consent UI regression covers Escape and Return. Comment hover follows individual marked lines and original annotation paint order (#3541/#3620), independently of spatial inspection sorting; synthetic rotation, differing-upper-edge overlap and unchanged-document regressions. |
|
||||
| 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<br>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. Save-prompt shortcuts yield to dialogs above them; stacked link-consent UI regression covers Escape and Return. Optional page gaps join page edges without cropping PDF margins; current-document toggle, persisted default and UI control options are covered by viewer tests. Gap changes in Organize Pages preserve document scroll position, and both comparison panes follow menu and control choices. |
|
||||
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel<br>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<br>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<br>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. |
|
||||
|
||||
@@ -852,10 +852,10 @@ area = "B"
|
||||
tier = "P0"
|
||||
milestone = "M3"
|
||||
status = "shipped"
|
||||
commands = ["view.continuous"]
|
||||
commands = ["view.continuous", "view.page_gaps"]
|
||||
tools = []
|
||||
evidence = ["crates/ui-egui/tests/view.rs::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_in_organizer_do_not_move_the_document_scroll_position", "crates/ui-egui/tests/view.rs::page_gaps_comparison_menu_preserves_the_active_panes_choice", "crates/ui-egui/tests/view.rs::page_gaps_comparison_control_preserves_both_states", "crates/ui-egui/tests/view.rs::page_gaps_stay_closed_at_fractional_device_scales", "crates/ui-egui/tests/view.rs::page_gaps_toggle_joins_edges_without_changing_the_document", "crates/ui-egui/tests/view.rs::page_gaps_default_survives_restart_and_applies_to_new_documents", "crates/ui-egui/tests/view.rs::page_gaps_menu_joins_pages_without_a_shadow_at_the_seam", "crates/ui-egui/tests/view.rs::page_gaps_work_for_mixed_sizes_and_two_page_rows", "crates/ui-egui/tests/view.rs::continuous_layout_stacks_pages_vertically", "crates/ui-egui/tests/view.rs::page_display_commands_switch_layouts", "crates/ui-egui/tests/view.rs::default_page_display_is_used_for_new_documents", "crates/ui-egui/src/canvas.rs::fit_page_and_height_hold_still_while_mixed_page_sizes_scroll_past", "crates/ui-egui/tests/view.rs::touchpad_swipe_glides_on_after_the_fingers_lift_and_stops", "crates/ui-egui/tests/view.rs::touching_the_touchpad_again_or_turning_animations_off_stops_the_glide", "crates/ui-egui/tests/view.rs::rapid_wheel_notches_accelerate_and_follow_touchpad_input", "crates/ui-egui/tests/view.rs::changing_continuous_layout_resets_wheel_acceleration"]
|
||||
notes = "Page gaps, shadows and outlines can be hidden through the page-display menu, with a persisted default in Preferences and matching UI control options; page margins in the PDF are unchanged. Continuous scrolling remains the default for PDFs that don't ask for a layout (Preferences ▸ Default page display); pages stack with no snapping. Fit page and Fit height fit the largest page, so the zoom holds still across mixed page sizes. Rapid line-wheel notches gain a bounded, constant-space arrival-rate boost (#1072); isolated notches still use the chosen Scroll speed, and page units stay unboosted. A touchpad swipe glides on after the fingers lift on Wayland (#531; macOS glides by itself; Windows and X11 report no lift through winit), unless Animation effects is off; a click, key, zoom or new scroll catches it. Not yet: fingers resting on the touchpad don't catch it (needs Wayland hold gestures, which the vendored winit doesn't bind)"
|
||||
|
||||
[[feature]]
|
||||
id = "view.layout-single-page"
|
||||
|
||||
Reference in New Issue
Block a user