From 1f78e2f37d09505c2dc4675ed68e22836c59978e Mon Sep 17 00:00:00 2001
From: robertobulltech <341520377+robertobulltech@users.noreply.github.com>
Date: Wed, 21 Oct 2026 13:24:58 +0200
Subject: [PATCH] M3: resize Pages inspector thumbnails (#3808)
---
ATTRIBUTION.toml | 44 ++++++++--------
README.md | 2 +
ROADMAP.md | 4 ++
crates/ui-egui/src/canvas.rs | 23 +++++++++
crates/ui-egui/src/control.rs | 1 +
crates/ui-egui/src/i18n/ar.tsv | 3 ++
crates/ui-egui/src/i18n/bg.tsv | 3 ++
crates/ui-egui/src/i18n/cs.tsv | 3 ++
crates/ui-egui/src/i18n/de.tsv | 3 ++
crates/ui-egui/src/i18n/el.tsv | 3 ++
crates/ui-egui/src/i18n/es.tsv | 3 ++
crates/ui-egui/src/i18n/fr.tsv | 3 ++
crates/ui-egui/src/i18n/hu.tsv | 3 ++
crates/ui-egui/src/i18n/it.tsv | 3 ++
crates/ui-egui/src/i18n/ja.tsv | 3 ++
crates/ui-egui/src/i18n/kk.tsv | 3 ++
crates/ui-egui/src/i18n/lt.tsv | 3 ++
crates/ui-egui/src/i18n/pl.tsv | 3 ++
crates/ui-egui/src/i18n/pt-br.tsv | 3 ++
crates/ui-egui/src/i18n/ru.tsv | 3 ++
crates/ui-egui/src/i18n/sl.tsv | 3 ++
crates/ui-egui/src/i18n/te.tsv | 3 ++
crates/ui-egui/src/i18n/tr.tsv | 3 ++
crates/ui-egui/src/i18n/uk.tsv | 3 ++
crates/ui-egui/src/i18n/vi.tsv | 3 ++
crates/ui-egui/src/i18n/zh-hans.tsv | 3 ++
crates/ui-egui/src/i18n/zh-hant.tsv | 3 ++
crates/ui-egui/src/lib.rs | 9 ++++
crates/ui-egui/src/panels.rs | 59 ++++++++++++++++++++-
crates/ui-egui/tests/control.rs | 25 +++++++++
crates/ui-egui/tests/editing.rs | 22 ++++++++
crates/ui-egui/tests/viewer.rs | 80 +++++++++++++++++++++++++++++
docs/roadmap.md | 5 +-
docs/ui-parity.md | 5 +-
parity/acrobat-features.toml | 6 +--
35 files changed, 323 insertions(+), 28 deletions(-)
diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml
index db3dd6124f..3179290f41 100644
--- a/ATTRIBUTION.toml
+++ b/ATTRIBUTION.toml
@@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "6eee0b34db6529f26235bfc07fa1fdb9030b8e7b0b10ede271ca9242cb491919"
+sha256 = "41cc0d2e9e733aff1c9bd1ede1512878ccfc976dc24477d7df0aed97c0772415"
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
@@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "9870c0fb9f7198155be16adee7f45d36cb9a254af90b545a3ce5b04cbbbf24d4"
+sha256 = "b4202ec1dd4b19fa06c2e3a3640268f54d8c5685b53cca4c1bcafdb067e0610c"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "fc31e40877ce115cfb7033c57dd6acabcae3725b249364387e3fe23e04d94e5e"
+sha256 = "f076815e568af8da1f6da0fdb0c286534fe94a88a349fc4f06093707cd4dd231"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
@@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "5de19834f293efaea68cb6b7f51a71d320391f53345f56cfebcd73dab828b665"
+sha256 = "6240c075c8943e68d289577fbfa66f738b0f7c701aace2662571a4cc3d57fbaa"
[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "cef0d22341d0722f029c6543fc846fce164331b7ad8f693a6754b7567b9d9e95"
+sha256 = "b7b1a3af968151c9ffa307f99b5be8d58e38e09992bad60cec0d5eef5bdf0356"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "c0ca7761cbabd8096bc9af54cdf6b88b3ae0cd8210003db8b3356e2706e44fc8"
+sha256 = "5b471208d76a63574000bc561b2e60d986d644b0bf2e9efbbb0fe9448895e18f"
[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "a8afd51c3296f44b0446d6b06492b1348444b3e3c448a326ad213d18ce7af714"
+sha256 = "944fe0e203e5dd3293e0199f5919eb3bed045d864848469609b4e624e1724ac6"
[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "7676a2dfaa7d66e2e9ef608d2d9de7f863557dcecbef6d94bafa981097405e6d"
+sha256 = "680a199121a636b60eab8330177515267a9158eed9e849f65836ebf243956ce0"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
@@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "9a22996c0308ea5b20ec5e328504cd83cbb560147763faaf0e6987f974844c29"
+sha256 = "7328d4aec9da790c8cc0b3bf77180ee106733b5b194c6140477b27b41ca137cf"
[[asset]]
path = "crates/ui-egui/src/i18n/it.tsv"
@@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "eacc972d65a2d951e94e1a8cf944638e8328f73f6c05759de6059a425f159cd7"
+sha256 = "511bd1965d8634118de9487394826c36006aec1559e8f1128c7b599337531a9c"
[[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv"
@@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "dfa4aa540cc7d61fa22a27a949e6eec0f78dcc5b883d0e4e004d695e460b95aa"
+sha256 = "4cd23e606a011a1b8a66de193c78f8ef1ceaf918ca6927e79e86a4ccdcf53f1e"
[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
@@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "27e02e7bb3d378bceca76988a9dceaa68d974f57cbee962954deca6184cf62aa"
+sha256 = "fd59968cdefe7170c1bd342724448906a386cf49257785c56cf727b6b22ba171"
[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
@@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "9675edd36c5ae54d9893fd10c4325aab664941bf714c08fbfeef9134fa42056c"
+sha256 = "c6c3c3f368c17d4d3c585b56f49fa267c928b5754ddbd375c3f51fe248f4f056"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "ce18fc96f8516aa917e4a8bcfd290edec13a9800b3d1ee102e4b2a71b8467d2b"
+sha256 = "bcfe80112ce09e8634c22378b482e05d486f9a3f4199d579ea43d3ee1ac55c87"
[[asset]]
path = "crates/ui-egui/src/i18n/kk.tsv"
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "286a1e2e051748eb4bcee3906918dc78d0704f8a37bde22dc50d3376d5e75017"
+sha256 = "55e01bb14b87a4ed7e9ab1b685a1a359265dcd00751851470ae29fcd9aa6a04a"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "77593b7497c67f4a79cc31fa43fdd6eae11601dd87c079a8e666b25cdd9117ba"
+sha256 = "37c63cc4cc940ac2d6d12f4276dc8d2effdd02d35eac60fff071314c734ffc82"
[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
@@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "d7d93a03f22ab380696f69b20b12104bf30e78c4440b683df63594af716894e2"
+sha256 = "3f0b1c6663c159b67546ea2154eb2f5cb5745951c3735079c443cf3d780bc553"
[[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv"
@@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "889a0d0a9ea53ca0a818c2ba672c053c232d2cca140f8d63127353e6f36505fe"
+sha256 = "b962b8e0c635aaefce83fb41f7f1f4e733524fb06171c5fa3966db5304fa6e6c"
[[asset]]
path = "crates/ui-egui/src/i18n/tr.tsv"
@@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "bae8b21651ae2a69c71c91b69ba79727254dbfb078c1be312a13383332934927"
+sha256 = "3d48f6d4e43fa960b0d6ddb9fcc122903c7652c1eeb50d57f05f6811741ca528"
[[asset]]
path = "crates/ui-egui/src/i18n/lt.tsv"
@@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "7fa70f68a9fa24d0b1cac05fe3aaabaeaca9750c57416124bd317a4eda806d5a"
+sha256 = "6db7db40528a00c32bf248e6b8db660b7ccf87e07c2d15bec0b851245ac025c5"
[[asset]]
@@ -3352,7 +3352,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "95b5fc6d10f7bc8371688772741870448de716fa2dd9af62fdfe4e009d269b45"
+sha256 = "38231359aa161168110054837e2a16427424b3c921261e37894144c570ddd30d"
[[asset]]
path = "crates/ui-egui/src/i18n/sl.tsv"
@@ -3363,7 +3363,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "3ac2b113045b7e399dd6d9ddb7773e5b0874c8b43c2ba4cca5854598a2a7ba92"
+sha256 = "fcc2d44719138ab61202d755d8fc60ecdf9c6d06ff6649c570694b561fb12b9f"
[[asset]]
diff --git a/README.md b/README.md
index af49129eca..7d3069258f 100644
--- a/README.md
+++ b/README.md
@@ -140,6 +140,8 @@ Right-click a PDF tab to close it, reopen the last closed file-backed tab, or re
Bookmarks, page thumbnails and the document's own page labels (i, ii, 1, 2…) keep you oriented in long documents. Search bookmark titles in the Bookmarks panel to find nested entries even when their parents are collapsed. Matches keep their ancestors for context; Clear restores the unfiltered tree without changing its expansion state.
+In **Inspector > Pages**, use **Thumbnail size** and the smaller/larger buttons to show more pages or inspect larger previews (50–150%). This changes only the thumbnails, keeping multi-page selection and the current page in view. The size belongs to the open document view and survives reloading its file; it is not remembered after restarting. Agents can set it with `ui.set {"key":"thumbnail-zoom","value":"75"}` and read `active.thumbnail_zoom` from `ui.state`.
+
 |
diff --git a/ROADMAP.md b/ROADMAP.md
index b1283e7751..3d3994971d 100644
--- a/ROADMAP.md
+++ b/ROADMAP.md
@@ -1,5 +1,7 @@
# PdfCraft roadmap
+> **Last reviewed:** 2026-10-21 · **Last updated:** 2026-10-21 · **Change:** minor (M3, #3808: the Pages inspector has an independent 50–150% thumbnail size slider and buttons; selection and current-page following are retained, with headless ui.set coverage. Estimates unchanged.)
+
**Stage: alpha** · next: beta, ~25% (ready for real work 50% → 75%) and ~400–700 h away
> **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)
@@ -172,6 +174,8 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md
## Progress log
+- **2026-10-21 (M3, #3808):** Page thumbnails in the Inspector can be resized independently from the document and Organize, with a slider, buttons and `thumbnail-zoom` control option. Selection, page following and reload are covered by regressions. Estimates unchanged.
+
- **2026-10-19 (M14, #3373):** The main menu and its long submenus scroll in short windows, keeping their commands reachable. Estimates unchanged.
- **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.
diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs
index eb9244a284..4f9970e8fd 100644
--- a/crates/ui-egui/src/canvas.rs
+++ b/crates/ui-egui/src/canvas.rs
@@ -563,6 +563,8 @@ pub struct DocView {
/// The current page the Pages panel last followed: it scrolls to the current page's
/// thumbnail only when the page changes.
pub(crate) panel_followed: Option,
+ /// Scale of the thumbnails in the Pages inspector, independent of document and grid zoom.
+ thumbnail_zoom: f32,
/// How large the page grid draws its pages (1.0 = the usual thumbnails).
grid_zoom: f32,
/// A zoom asked for this frame (toolbar, pinch, keys); the grid applies it once it has laid
@@ -601,6 +603,8 @@ pub struct DocView {
/// The page grid's zoom range and the step of its buttons and keys.
pub const GRID_ZOOM_RANGE: std::ops::RangeInclusive = 0.5..=3.0;
+/// Thumbnail size in the Pages inspector, relative to its usual size.
+pub const THUMBNAIL_ZOOM_RANGE: std::ops::RangeInclusive = 0.5..=1.5;
const GRID_ZOOM_STEP: f32 = 1.25;
/// How far a sharp grid render may be from the size drawn before it is redone: a pinch changes
/// the size on every frame, and a slightly soft or slightly large image is fine meanwhile.
@@ -783,6 +787,7 @@ impl DocView {
pages_panel: None,
panel_gap: None,
panel_followed: None,
+ thumbnail_zoom: 1.0,
grid_zoom: 1.0,
grid_zoom_request: None,
grid_anchor: None,
@@ -859,6 +864,7 @@ impl DocView {
fresh.current = self.current;
fresh.organize = self.organize;
fresh.grid_zoom = self.grid_zoom;
+ fresh.thumbnail_zoom = self.thumbnail_zoom;
// Same document id, so the scroll offset egui keeps for it is still this document's.
fresh.shown = self.shown;
fresh.highlight_fields = self.highlight_fields;
@@ -1495,6 +1501,23 @@ impl DocView {
self.grid_zoom
}
+ /// Scale of the thumbnails in the Pages inspector (1.0 is the usual size).
+ pub fn thumbnail_zoom(&self) -> f32 {
+ self.thumbnail_zoom
+ }
+
+ /// Resize the inspector thumbnails, keeping the current page in view after layout.
+ /// Out-of-range values are clamped; non-finite values are ignored.
+ pub fn set_thumbnail_zoom(&mut self, zoom: f32) {
+ if zoom.is_finite() {
+ let zoom = zoom.clamp(*THUMBNAIL_ZOOM_RANGE.start(), *THUMBNAIL_ZOOM_RANGE.end());
+ if zoom != self.thumbnail_zoom {
+ self.thumbnail_zoom = zoom;
+ self.panel_followed = None;
+ }
+ }
+ }
+
/// Zoom the page grid; out-of-range values are clamped and nonsense is ignored.
pub fn set_grid_zoom(&mut self, zoom: f32) {
if zoom.is_finite() {
diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs
index a4ab35d969..bca9a7784a 100644
--- a/crates/ui-egui/src/control.rs
+++ b/crates/ui-egui/src/control.rs
@@ -680,6 +680,7 @@ impl Host for crate::PdfCraftApp {
"layout": format!("{:?}", v.layout),
"organize": v.organize,
"grid_zoom": v.grid_zoom(),
+ "thumbnail_zoom": v.thumbnail_zoom(),
// Pages picked in the organize grid or the Pages panel (empty: the current page).
"selected_pages": v.selected.iter().map(|p| p + 1).collect::>(),
"auto_scrolling": v.auto_scrolling(),
diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv
index 8697887e10..60408f4ff5 100644
--- a/crates/ui-egui/src/i18n/ar.tsv
+++ b/crates/ui-egui/src/i18n/ar.tsv
@@ -2711,3 +2711,6 @@ certificate Version الإصدار
Make properties default جعل الخصائص افتراضية
Pages selected الصفحات المحددة
Page properties خصائص الصفحة
+ Thumbnail size حجم الصور المصغرة
+ Smaller thumbnails تصغير الصور المصغرة
+ Larger thumbnails تكبير الصور المصغرة
diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv
index 813a2c7ae8..f19946fc8f 100644
--- a/crates/ui-egui/src/i18n/bg.tsv
+++ b/crates/ui-egui/src/i18n/bg.tsv
@@ -2397,3 +2397,6 @@ animation effects Off Изключени
Make properties default Задаване като свойства по подразбиране
Pages selected Избрани страници
Page properties Свойства на страницата
+ Thumbnail size Размер на миниатюрите
+ Smaller thumbnails По-малки миниатюри
+ Larger thumbnails По-големи миниатюри
diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv
index 9936325523..c55a29b055 100644
--- a/crates/ui-egui/src/i18n/cs.tsv
+++ b/crates/ui-egui/src/i18n/cs.tsv
@@ -2508,3 +2508,6 @@ animation effects Off Vypnuto
Make properties default Nastavit vlastnosti jako výchozí
Pages selected Vybrané stránky
Page properties Vlastnosti stránky
+ Thumbnail size Velikost miniatur
+ Smaller thumbnails Menší miniatury
+ Larger thumbnails Větší miniatury
diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv
index 4d9207eb9d..ae4d44ec82 100644
--- a/crates/ui-egui/src/i18n/de.tsv
+++ b/crates/ui-egui/src/i18n/de.tsv
@@ -2435,3 +2435,6 @@ animation effects Off Aus
Make properties default Eigenschaften als Standard festlegen
Pages selected Ausgewählte Seiten
Page properties Seiteneigenschaften
+ Thumbnail size Miniaturgröße
+ Smaller thumbnails Kleinere Miniaturen
+ Larger thumbnails Größere Miniaturen
diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv
index 06fe2b540b..996cb4ce4b 100644
--- a/crates/ui-egui/src/i18n/el.tsv
+++ b/crates/ui-egui/src/i18n/el.tsv
@@ -2440,3 +2440,6 @@ animation effects Off Ανενεργά
Make properties default Ορισμός ιδιοτήτων ως προεπιλογή
Pages selected Επιλεγμένες σελίδες
Page properties Ιδιότητες σελίδας
+ Thumbnail size Μέγεθος μικρογραφιών
+ Smaller thumbnails Μικρότερες μικρογραφίες
+ Larger thumbnails Μεγαλύτερες μικρογραφίες
diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv
index 79c668992c..017a099b47 100644
--- a/crates/ui-egui/src/i18n/es.tsv
+++ b/crates/ui-egui/src/i18n/es.tsv
@@ -2574,3 +2574,6 @@ animation effects Off Desactivados
# Historical snapshot notice bar (View signed version / View revision).
You are viewing the signed version of “{from}” (revision {rev}). It is a snapshot: changes here won't affect the original. Estás viendo la versión firmada de «{from}» (revisión {rev}). Es una instantánea: los cambios aquí no afectarán al original.
You are viewing revision {rev} of “{from}”. It is a snapshot: changes here won't affect the original. Estás viendo la revisión {rev} de «{from}». Es una instantánea: los cambios aquí no afectarán al original.
+ Thumbnail size Tamaño de las miniaturas
+ Smaller thumbnails Miniaturas más pequeñas
+ Larger thumbnails Miniaturas más grandes
diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv
index 9bdcf0bc8a..42967e6df3 100644
--- a/crates/ui-egui/src/i18n/fr.tsv
+++ b/crates/ui-egui/src/i18n/fr.tsv
@@ -2434,3 +2434,6 @@ animation effects Off Désactivés
Make properties default Définir comme propriétés par défaut
Pages selected Pages sélectionnées
Page properties Propriétés de la page
+ Thumbnail size Taille des vignettes
+ Smaller thumbnails Réduire les vignettes
+ Larger thumbnails Agrandir les vignettes
diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv
index d537677c7d..026cb6700c 100644
--- a/crates/ui-egui/src/i18n/hu.tsv
+++ b/crates/ui-egui/src/i18n/hu.tsv
@@ -2433,3 +2433,6 @@ animation effects Off Ki
Make properties default Tulajdonságok beállítása alapértelmezettként
Pages selected Kijelölt oldalak
Page properties Oldaltulajdonságok
+ Thumbnail size Bélyegképek mérete
+ Smaller thumbnails Kisebb bélyegképek
+ Larger thumbnails Nagyobb bélyegképek
diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv
index 35cba7a410..a15a8b675b 100644
--- a/crates/ui-egui/src/i18n/it.tsv
+++ b/crates/ui-egui/src/i18n/it.tsv
@@ -2646,3 +2646,6 @@ certificate Key usage Utilizzo chiave
certificate Self-signed Autofirmato
certificate SHA-1 digest Impronta SHA-1
certificate SHA-256 digest Impronta SHA-256
+ Thumbnail size Dimensione miniature
+ Smaller thumbnails Miniature più piccole
+ Larger thumbnails Miniature più grandi
diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv
index e845a736da..66fa5fb3e9 100644
--- a/crates/ui-egui/src/i18n/ja.tsv
+++ b/crates/ui-egui/src/i18n/ja.tsv
@@ -2434,3 +2434,6 @@ animation effects Off オフ
Make properties default プロパティをデフォルトに設定
Pages selected 選択したページ
Page properties ページのプロパティ
+ Thumbnail size サムネイルのサイズ
+ Smaller thumbnails サムネイルを小さく
+ Larger thumbnails サムネイルを大きく
diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv
index 9acb4ec1be..6405754728 100644
--- a/crates/ui-egui/src/i18n/kk.tsv
+++ b/crates/ui-egui/src/i18n/kk.tsv
@@ -2432,3 +2432,6 @@ animation effects Off Өшірулі
Make properties default Қасиеттерді әдепкі ету
Pages selected Таңдалған беттер
Page properties Бет қасиеттері
+ Thumbnail size Нобай өлшемі
+ Smaller thumbnails Кішірек нобайлар
+ Larger thumbnails Үлкенірек нобайлар
diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv
index d1dfc33044..2bb77db9a7 100644
--- a/crates/ui-egui/src/i18n/lt.tsv
+++ b/crates/ui-egui/src/i18n/lt.tsv
@@ -2685,3 +2685,6 @@ key Space Tarpas
The password must have at least 6 characters. Slaptažodį turi sudaryti bent 6 simboliai.
This file changed on disk. Reload it to see the new version; your unsaved changes will be lost. Šis failas diske pasikeitė. Įkelkite jį iš naujo, kad pamatytumėte naują versiją; neįrašyti pakeitimai bus prarasti.
theme Appearance Išvaizda
+ Thumbnail size Miniatiūrų dydis
+ Smaller thumbnails Mažesnės miniatiūros
+ Larger thumbnails Didesnės miniatiūros
diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv
index c432a6a92e..99bdd94860 100644
--- a/crates/ui-egui/src/i18n/pl.tsv
+++ b/crates/ui-egui/src/i18n/pl.tsv
@@ -2410,3 +2410,6 @@ animation effects Off Wyłączone
Make properties default Ustaw właściwości jako domyślne
Pages selected Zaznaczone strony
Page properties Właściwości strony
+ Thumbnail size Rozmiar miniatur
+ Smaller thumbnails Mniejsze miniatury
+ Larger thumbnails Większe miniatury
diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv
index 89ff80f9dd..b1f46c5a00 100644
--- a/crates/ui-egui/src/i18n/pt-br.tsv
+++ b/crates/ui-egui/src/i18n/pt-br.tsv
@@ -2448,3 +2448,6 @@ animation effects Off Desativados
Make properties default Tornar propriedades padrão
Pages selected Páginas selecionadas
Page properties Propriedades da página
+ Thumbnail size Tamanho das miniaturas
+ Smaller thumbnails Miniaturas menores
+ Larger thumbnails Miniaturas maiores
diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv
index 73f21aa0d8..363fd5bf01 100644
--- a/crates/ui-egui/src/i18n/ru.tsv
+++ b/crates/ui-egui/src/i18n/ru.tsv
@@ -2347,3 +2347,6 @@ animation effects Off Выкл
Make properties default Сделать свойства стандартными
Pages selected Выделенные страницы
Page properties Свойства страницы
+ Thumbnail size Размер миниатюр
+ Smaller thumbnails Уменьшить миниатюры
+ Larger thumbnails Увеличить миниатюры
diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv
index dd169bc961..d696b491c6 100644
--- a/crates/ui-egui/src/i18n/sl.tsv
+++ b/crates/ui-egui/src/i18n/sl.tsv
@@ -2439,3 +2439,6 @@ animation effects Off Izklopljeni
Make properties default Nastavi lastnosti kot privzete
Pages selected Izbrane strani
Page properties Lastnosti strani
+ Thumbnail size Velikost sličic
+ Smaller thumbnails Manjše sličice
+ Larger thumbnails Večje sličice
diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv
index 47aee36f8b..8de3a3f3a3 100644
--- a/crates/ui-egui/src/i18n/te.tsv
+++ b/crates/ui-egui/src/i18n/te.tsv
@@ -2343,3 +2343,6 @@ animation effects Off ఆఫ్
Make properties default లక్షణాలను డిఫాల్ట్గా చేయి
Pages selected ఎంచుకున్న పేజీలు
Page properties పేజీ లక్షణాలు
+ Thumbnail size థంబ్నెయిల్ పరిమాణం
+ Smaller thumbnails చిన్న థంబ్నెయిల్లు
+ Larger thumbnails పెద్ద థంబ్నెయిల్లు
diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv
index 95ca0b608c..51defd4ef8 100644
--- a/crates/ui-egui/src/i18n/tr.tsv
+++ b/crates/ui-egui/src/i18n/tr.tsv
@@ -2430,3 +2430,6 @@ animation effects Off Kapalı
Make properties default Özellikleri varsayılan yap
Pages selected Seçilen sayfalar
Page properties Sayfa özellikleri
+ Thumbnail size Küçük resim boyutu
+ Smaller thumbnails Küçük resimleri küçült
+ Larger thumbnails Küçük resimleri büyüt
diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv
index 7993a06185..25e5c12a3d 100644
--- a/crates/ui-egui/src/i18n/uk.tsv
+++ b/crates/ui-egui/src/i18n/uk.tsv
@@ -2456,3 +2456,6 @@ animation effects Off Вимкнено
Make properties default Зробити властивості типовими
Pages selected Вибрані сторінки
Page properties Властивості сторінки
+ Thumbnail size Розмір мініатюр
+ Smaller thumbnails Зменшити мініатюри
+ Larger thumbnails Збільшити мініатюри
diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv
index bae599bf6a..c74dd0d6f2 100644
--- a/crates/ui-egui/src/i18n/vi.tsv
+++ b/crates/ui-egui/src/i18n/vi.tsv
@@ -2687,3 +2687,6 @@ key Space Space
# Historical snapshot notice bar (View signed version / View revision).
You are viewing the signed version of “{from}” (revision {rev}). It is a snapshot: changes here won't affect the original. Bạn đang xem phiên bản đã ký của “{from}” (bản sửa đổi {rev}). Đây là ảnh nhanh: các thay đổi ở đây không được áp dụng đến bản gốc.
You are viewing revision {rev} of “{from}”. It is a snapshot: changes here won't affect the original. Bạn đang xem bản sửa đổi {rev} của “{from}”. Đây là ảnh nhanh: các thay đổi ở đây không được áp dụng đến bản gốc.
+ Thumbnail size Kích thước hình thu nhỏ
+ Smaller thumbnails Giảm kích thước hình thu nhỏ
+ Larger thumbnails Tăng kích thước hình thu nhỏ
diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv
index 1ddecc655f..5e771d8832 100644
--- a/crates/ui-egui/src/i18n/zh-hans.tsv
+++ b/crates/ui-egui/src/i18n/zh-hans.tsv
@@ -2398,3 +2398,6 @@ animation effects Off 关
Make properties default 将属性设为默认
Pages selected 选定的页面
Page properties 页面属性
+ Thumbnail size 缩略图大小
+ Smaller thumbnails 缩小缩略图
+ Larger thumbnails 放大缩略图
diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv
index bb5ec3649b..86a81fa203 100644
--- a/crates/ui-egui/src/i18n/zh-hant.tsv
+++ b/crates/ui-egui/src/i18n/zh-hant.tsv
@@ -2384,3 +2384,6 @@ animation effects Off 關
Make properties default 將內容設為預設
Pages selected 選取的頁面
Page properties 頁面屬性
+ Thumbnail size 縮圖大小
+ Smaller thumbnails 縮小縮圖
+ Larger thumbnails 放大縮圖
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
index 9db9247591..71475b94ee 100644
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -2634,6 +2634,14 @@ impl PdfCraftApp {
v.open_organize();
}
}
+ // Inspector thumbnail size, in percent, independent of document and grid zoom.
+ ("thumbnail-zoom", Some(v)) => {
+ let percent = value.trim_end_matches('%').parse::().map_err(|e| e.to_string())?;
+ if !percent.is_finite() || !canvas::THUMBNAIL_ZOOM_RANGE.contains(&(percent / 100.0)) {
+ return Err("thumbnail-zoom must be between 50 and 150".into());
+ }
+ v.set_thumbnail_zoom(percent / 100.0);
+ }
// `--grid-zoom 150`: the size of the pages in the organize grid, in percent.
("grid-zoom", Some(v)) => {
let percent = value.trim_end_matches('%').parse::().map_err(|e| e.to_string())?;
@@ -2773,6 +2781,7 @@ impl PdfCraftApp {
"cover",
"organize",
"grid-zoom",
+ "thumbnail-zoom",
"fields",
"find",
"rotate",
diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs
index 0f2c601497..e50ec3fa73 100644
--- a/crates/ui-egui/src/panels.rs
+++ b/crates/ui-egui/src/panels.rs
@@ -640,6 +640,10 @@ pub fn right_panel(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
ancestor.pop();
}
}
+ if panel == RightPanel::Pages {
+ let enabled = !modal && view.panel_drag.is_none();
+ thumbnail_size(ui, &t, view, enabled);
+ }
egui::ScrollArea::vertical().auto_shrink([false, false]).show(ui, |ui| match panel {
RightPanel::Comments => {
if let Some(e) = crate::comments_panel::show(ui, &t, info, view, prefs, comment_allowed, &mut nav) {
@@ -1041,6 +1045,57 @@ fn outline_item(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, item: &OutlineIte
}
}
+/// Size controls stay above the scroll area, so they remain reachable on long documents.
+fn thumbnail_size(ui: &mut egui::Ui, t: &Tokens, view: &mut crate::DocView, enabled: bool) {
+ let before = view.thumbnail_zoom();
+ let mut zoom = before;
+ let label = ui
+ .horizontal(|ui| {
+ let label = ui.label(egui::RichText::new(tl!("Thumbnail size")).small().color(t.text_muted));
+ ui.with_layout(Layout::right_to_left(Align::Center), |ui| {
+ ui.label(egui::RichText::new(format!("{:.0}%", before * 100.0)).small().color(t.text_muted));
+ });
+ label
+ })
+ .inner;
+ ui.add_enabled_ui(enabled, |ui| {
+ ui.spacing_mut().slider_width = (ui.available_width() - 52.0 - 2.0 * ui.spacing().item_spacing.x).max(1.0);
+ ui.horizontal(|ui| {
+ if ui
+ .add_enabled_ui(zoom > *crate::canvas::THUMBNAIL_ZOOM_RANGE.start(), |ui| {
+ icons::button(ui, "zoom-out", 26.0, false, tl!("Smaller thumbnails"))
+ })
+ .inner
+ .clicked()
+ {
+ zoom -= 0.25;
+ }
+ let mut percent = zoom * 100.0;
+ let range = (*crate::canvas::THUMBNAIL_ZOOM_RANGE.start() * 100.0)..=(*crate::canvas::THUMBNAIL_ZOOM_RANGE.end() * 100.0);
+ ui.scope(|ui| {
+ // The regular field fill blends into this panel; keep the slider's full rail visible.
+ ui.visuals_mut().widgets.inactive.bg_fill = t.border;
+ let slider = ui.add(egui::Slider::new(&mut percent, range).step_by(1.0).show_value(false).trailing_fill(true)).labelled_by(label.id);
+ slider.widget_info(|| egui::WidgetInfo::slider(ui.is_enabled(), f64::from(percent), tl!("Thumbnail size")));
+ });
+ zoom = percent / 100.0;
+ if ui
+ .add_enabled_ui(zoom < *crate::canvas::THUMBNAIL_ZOOM_RANGE.end(), |ui| {
+ icons::button(ui, "zoom-in", 26.0, false, tl!("Larger thumbnails"))
+ })
+ .inner
+ .clicked()
+ {
+ zoom += 0.25;
+ }
+ });
+ });
+ if zoom != before {
+ view.set_thumbnail_zoom(zoom);
+ }
+ ui.add_space(6.0);
+}
+
/// The page thumbnails. A click goes to the page; ⌘/Ctrl-click and ⇧-click pick several pages
/// (the selection page commands and Print act on) without moving the document. Right-click
/// offers Extract, Cut, Copy and Paste on the selection, or on the page clicked; dragging
@@ -1069,7 +1124,9 @@ fn pages(ui: &mut egui::Ui, t: &Tokens, info: &DocInfo, view: &mut crate::DocVie
// scroll bar dragged by the user already wins over a scroll to a place in egui.
let follow = view.panel_followed != Some(view.current) && view.panel_drag.is_none();
view.panel_followed = Some(view.current);
- let w = (ui.available_width() - 40.0).min(150.0);
+ let available = (ui.available_width() - 16.0).max(1.0);
+ let usual = (ui.available_width() - 40.0).clamp(1.0, 150.0);
+ let w = (usual * view.thumbnail_zoom()).min(available);
let count = info.pages.len();
let mut rows: Vec = Vec::with_capacity(info.pages.len());
let mut dropped = false;
diff --git a/crates/ui-egui/tests/control.rs b/crates/ui-egui/tests/control.rs
index e6af166d77..ce23640d6c 100644
--- a/crates/ui-egui/tests/control.rs
+++ b/crates/ui-egui/tests/control.rs
@@ -86,6 +86,31 @@ fn ok(h: &mut Harness<'static, PdfCraftApp>, c: &ControlClient, method: &str, pa
call(h, c, method, params).unwrap_or_else(|e| panic!("{method}: {e}"))
}
+#[test]
+fn thumbnail_size_is_settable_and_inspectable_through_the_control_channel() {
+ let (mut h, c) = harness();
+ ok(&mut h, &c, "ui.set", json!({"key": "panel", "value": "pages"}));
+ ok(&mut h, &c, "ui.set", json!({"key": "thumbnail-zoom", "value": "50"}));
+ let state = ok(&mut h, &c, "ui.state", json!({}));
+ assert_eq!(state["active"]["thumbnail_zoom"], 0.5);
+ assert_eq!(state["active"]["grid_zoom"], 1.0);
+ assert_eq!(state["documents"][0]["dirty"], false);
+ let controls = ok(&mut h, &c, "ui.inspect", json!({"query": "thumbnails"}));
+ let widgets = controls["widgets"].as_array().unwrap();
+ assert!(widgets.iter().any(|w| w["label"] == "Larger thumbnails"));
+ assert!(widgets.iter().any(|w| w["label"] == "Smaller thumbnails" && w["enabled"] == false));
+ assert!(call(&mut h, &c, "ui.set", json!({"key": "thumbnail-zoom", "value": "NaN"})).is_err());
+ ok(&mut h, &c, "ui.click", json!({"label": "Larger thumbnails"}));
+ h.run_steps(3);
+ assert_eq!(ok(&mut h, &c, "ui.state", json!({}))["active"]["thumbnail_zoom"], 0.75);
+ let sliders = ok(&mut h, &c, "ui.inspect", json!({"query": "Thumbnail size", "role": "Slider"}));
+ let slider = sliders["widgets"].as_array().unwrap().first().expect("the thumbnail size slider is labelled");
+ ok(&mut h, &c, "ui.focus", json!({"id": slider["id"]}));
+ ok(&mut h, &c, "ui.key", json!({"key": "ArrowRight"}));
+ h.run_steps(3);
+ assert!(ok(&mut h, &c, "ui.state", json!({}))["active"]["thumbnail_zoom"].as_f64().unwrap() > 0.75);
+}
+
#[test]
fn control_resize_reports_observed_dimensions_and_rejects_invalid_sizes() {
let (mut h, c) = harness();
diff --git a/crates/ui-egui/tests/editing.rs b/crates/ui-egui/tests/editing.rs
index d41041e52e..0f4c75d454 100644
--- a/crates/ui-egui/tests/editing.rs
+++ b/crates/ui-egui/tests/editing.rs
@@ -3759,6 +3759,28 @@ fn dragging_pages_panel_thumbnails_reorders_pages() {
assert!(view.panel_drag.is_none());
}
+#[test]
+fn resized_pages_panel_thumbnails_reorder_multiple_pages_and_undo() {
+ let mut h = harness(4, |app| {
+ app.right = Some(pdfcraft_ui_egui::RightPanel::Pages);
+ app.set_option("thumbnail-zoom", "50").unwrap();
+ });
+ let before = page_texts(h.state());
+ h.state_mut().views[0].select_pages(&[1, 2]);
+ h.run_steps(2);
+ let from = panel_thumb_rect(&h, "Page 2").center();
+ let to = panel_thumb_rect(&h, "Page 1").center_top() + egui::vec2(0.0, 4.0);
+ drag(&mut h, from, to);
+ h.run_steps(4);
+ assert_eq!(page_texts(h.state()), vec![before[1].clone(), before[2].clone(), before[0].clone(), before[3].clone()]);
+ assert_eq!(h.state().views[0].target_pages(), vec![0, 1]);
+ assert_eq!(h.state().views[0].thumbnail_zoom(), 0.5);
+ assert!(h.state_mut().execute("edit.undo"));
+ h.run_steps(4);
+ assert_eq!(page_texts(h.state()), before);
+ assert_eq!(h.state().views[0].thumbnail_zoom(), 0.5);
+}
+
#[test]
fn a_thumbnail_drag_that_ends_off_the_panel_or_outlives_the_pages_is_dropped() {
let mut h = harness(4, |app| app.right = Some(pdfcraft_ui_egui::RightPanel::Pages));
diff --git a/crates/ui-egui/tests/viewer.rs b/crates/ui-egui/tests/viewer.rs
index b154694854..9fa287bd0d 100644
--- a/crates/ui-egui/tests/viewer.rs
+++ b/crates/ui-egui/tests/viewer.rs
@@ -1408,6 +1408,86 @@ fn in_panel_view(h: &Harness<'static, PdfCraftApp>, r: egui::Rect) -> bool {
h.state().views[0].pages_panel.expect("the Pages panel is shown").contains_rect(r)
}
+#[test]
+fn pages_panel_thumbnail_size_controls_show_more_pages_and_keep_the_selection() {
+ let mut h = pages_panel_harness(&[(200, 300); 30]);
+ h.state_mut().views[0].select_pages(&[0, 1]);
+ h.run_steps(2);
+ let usual = thumbnail_rect(&h, 1).width();
+ let zoom = h.state().views[0].zoom;
+ let visible = |h: &Harness<'static, PdfCraftApp>| (1..=30).filter(|n| in_panel_view(h, thumbnail_rect(h, *n))).count();
+ let before = visible(&h);
+ h.get_by_label("Smaller thumbnails").click();
+ h.run_steps(4);
+ assert!(thumbnail_rect(&h, 1).width() < usual);
+ assert!(visible(&h) > before, "smaller thumbnails show more complete pages");
+ assert_eq!(h.state().views[0].target_pages(), vec![0, 1]);
+ assert_eq!(h.state().views[0].current, 0);
+ assert_eq!(h.state().views[0].zoom, zoom, "only the thumbnails are resized");
+ assert!(!h.state().session.get(h.state().views[0].id).unwrap().dirty);
+ if let Ok(dir) = std::env::var("PDFCRAFT_THUMBNAIL_SHOTS") {
+ h.render().unwrap().save(format!("{dir}/small.png")).unwrap();
+ }
+ h.get_by_label("Larger thumbnails").click();
+ h.run_steps(4);
+ assert!((thumbnail_rect(&h, 1).width() - usual).abs() < 0.1);
+ h.get_by_label("Larger thumbnails").click();
+ h.run_steps(4);
+ assert!(thumbnail_rect(&h, 1).width() > usual);
+ assert_eq!(h.state().views[0].target_pages(), vec![0, 1]);
+ if let Ok(dir) = std::env::var("PDFCRAFT_THUMBNAIL_SHOTS") {
+ h.render().unwrap().save(format!("{dir}/large.png")).unwrap();
+ h.state_mut().set_option("theme", "dark").unwrap();
+ h.run_steps(4);
+ h.render().unwrap().save(format!("{dir}/large-dark.png")).unwrap();
+ }
+}
+
+#[test]
+fn pages_panel_thumbnail_zoom_is_bounded_independent_and_survives_reload() {
+ let mut h = pages_panel_harness(&[(200, 300); 6]);
+ let usual = thumbnail_rect(&h, 1).width();
+ let zoom = h.state().views[0].zoom;
+ h.state_mut().set_option("grid-zoom", "200").unwrap();
+ h.state_mut().set_option("thumbnail-zoom", "50%").unwrap();
+ h.run_steps(4);
+ let small = thumbnail_rect(&h, 1).width();
+ assert!(small < usual);
+ for invalid in ["0", "49", "151", "NaN", "inf", "nope"] {
+ assert!(h.state_mut().set_option("thumbnail-zoom", invalid).is_err(), "reject {invalid}");
+ }
+ h.run_steps(3);
+ assert_eq!(thumbnail_rect(&h, 1).width(), small);
+ assert_eq!(h.state().views[0].grid_zoom(), 2.0);
+ assert_eq!(h.state().views[0].zoom, zoom);
+ let info = h.state().session.get(h.state().views[0].id).unwrap().info.clone();
+ h.state_mut().views[0].document_replaced(&info);
+ h.run_steps(4);
+ assert_eq!(thumbnail_rect(&h, 1).width(), small, "file reload keeps the chosen thumbnail size");
+ h.state_mut().views[0].set_thumbnail_zoom(f32::NAN);
+ assert_eq!(h.state().views[0].thumbnail_zoom(), 0.5);
+ h.state_mut().views[0].set_thumbnail_zoom(f32::MAX);
+ assert_eq!(h.state().views[0].thumbnail_zoom(), 1.5);
+ assert!(!h.state().session.get(h.state().views[0].id).unwrap().dirty);
+ let mut empty = PdfCraftApp::new();
+ assert!(empty.set_option("thumbnail-zoom", "100").is_err());
+}
+
+#[test]
+fn pages_panel_thumbnail_zoom_keeps_a_far_current_page_visible() {
+ let mut h = pages_panel_harness(&[(200, 300); 30]);
+ h.state_mut().views[0].go_to_page(24);
+ h.run_steps(4);
+ for size in ["50", "150", "80", "100"] {
+ h.state_mut().set_option("thumbnail-zoom", size).unwrap();
+ h.run_steps(4);
+ assert!(in_panel_view(&h, thumbnail_rect(&h, 25)), "page 25 stays visible at {size}%");
+ assert_eq!(h.state().views[0].current, 24);
+ h.get_by_label("Smaller thumbnails");
+ h.get_by_label("Larger thumbnails");
+ }
+}
+
/// #2781: as in Acrobat, going to a page whose thumbnail is out of sight scrolls the Pages
/// panel to show it.
#[test]
diff --git a/docs/roadmap.md b/docs/roadmap.md
index 5d73511e9c..ab213d7bb8 100644
--- a/docs/roadmap.md
+++ b/docs/roadmap.md
@@ -1,5 +1,7 @@
# PdfCraft roadmap: milestones and what's next
+> **Last reviewed:** 2026-10-21 · **Last updated:** 2026-10-21 · **Change:** minor (M3, #3808: the Pages inspector has an independent 50–150% thumbnail size slider and buttons; selection and current-page following are retained, with headless ui.set coverage. Estimates unchanged.)
+
> **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-15 · **Last updated:** 2026-10-15 · **Change:** minor (M14/#2383 GPU-limit candidate; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M7: the Edit text caret is as tall as the row it is on; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
@@ -114,7 +116,7 @@ total matches the parity estimate.
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. Page marks, added content and print output retain small page-unit transform coefficients when serialized. |
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings. Packed TIFF-predictor RGB rows retain later rows in decoding and rendering. The renderer keeps its one-point crop-size floor in physical points after UserUnit scaling, with regression coverage for crop origins, rotations and f32 reciprocal rounding. Existing unit policy and nearly-zero fallback are retained. |
| M2 | Model, render, text | 200–350 | 15% | 130–230 | hayro bootstrap renderer (vendored patches); inspector predictor scratch is bounded by available input, and JPX sample rescaling handles 1–32 bits safely. Text extraction reaches word-F1 0.98 against pdftotext. Short same-line redraws split before placement rather than interleaving. Missing: model crate, fonts, DisplayList, renderer independent of hayro. Inspector page-label traversal and output are resource-bounded, with physical-number fallback warnings |
-| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. #3149: page-list changes discard render demand based on old geometry; actual raster dimensions are checked against the GPU limit before upload. Synthetic crash and continued-rendering regressions cover the replacement path. 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). #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
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
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. Wrapped revision buttons stay above Binding in Document Properties (#2739); a synthetic UI test checks layout and saved revision content. |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Later native-open replies stay queued while a modal or picker owns the foreground; regressions cover recipient ownership, completion/cancellation and one-shot wakes. |
| M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. #1980: exact quit modifiers preserve Ctrl+Alt+Q email input; Ctrl+Q/Cmd+Q asks before discarding unsaved documents. |
@@ -200,6 +202,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st
| Date | Change | Summary |
|---|---|---|
+| 2026-10-21 | minor | M3, #3808: the Pages inspector has an independent 50–150% thumbnail size slider and buttons; selection and current-page following are retained, with headless ui.set coverage. Estimates unchanged. |
| 2026-10-18 | minor | M5, PR #1971: nested source `/Page` and `/Pages` dictionaries export as null without traversing page content or the page tree. Synthetic saved/reopened three-page regression retains unrelated metadata and aliases. FDF import semantics, copied-graph limits and estimates unchanged. |
| 2026-10-15 | minor | M5: budget retained FDF entries before cloning; discovery keeps lazy array owners and annotation positions, with alias/cache-eviction stress fixtures. Limits cover the copied graph; source parsing/page discovery retain existing COS/parser limits, with no total source-memory bound. FDF import semantics and estimates unchanged. |
| 2026-10-15 | minor | M7: Edit text caret height follows its row, with a kittest regression; estimates unchanged |
diff --git a/docs/ui-parity.md b/docs/ui-parity.md
index a78409fade..e8edbae406 100644
--- a/docs/ui-parity.md
+++ b/docs/ui-parity.md
@@ -1,5 +1,7 @@
# UI and interaction parity
+> **Last reviewed:** 2026-10-21 · **Last updated:** 2026-10-21 · **Change:** minor (M3, #3808: the Pages inspector has an independent 50–150% thumbnail size slider and buttons; selection and current-page following are retained, with headless ui.set coverage. Estimates unchanged.)
+
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (column select #740, multi-object move #844, Shift-⌘-G #810 fixed) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
How PdfCraft's tools, panels, shortcuts and feel compare with Acrobat Pro's current interface (the
@@ -33,7 +35,7 @@ No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24)
| Split window, spreadsheet split | yes | no | planned |
| Read mode, full screen | yes | yes | shipped; presentation options planned |
| Light, dark, system theme | yes | yes | shipped |
-| Side panels: thumbnails, bookmarks, attachments, layers, comments, fields, signatures | yes | yes; panels keep their width (#704) | shipped |
+| Side panels: thumbnails, bookmarks, attachments, layers, comments, fields, signatures | yes | yes; panels keep their width (#704); the Pages inspector has a 50–150% thumbnail size slider and buttons (#3808) | shipped |
| Destinations, articles, model tree, Tags, Order, Content panels | yes | no | planned |
| Message bars (restricted, signed, PDF/A, forms) | yes | restricted and signed; no PDF/A view mode | partial |
@@ -81,5 +83,6 @@ No dump of Acrobat's menu tree has been diffed against the registry yet (gap 24)
| Date | Change | Summary |
|---|---|---|
+| 2026-10-21 | minor | Pages inspector thumbnail sizing (#3808), separate from page and Organize zoom; view setting retained on reload and exposed through UI control. Not persisted between sessions. |
| 2026-10-11 | minor | Column select shipped (#740, PR #765); several objects move together on one page (#844, PR #907); Shift-⌘-G goes back (#810, PR #835) |
| 2026-10-10 | major | Created from the command registry, automation tools, the View area of the checklist and open UI issues |
diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml
index 7aca7d5c8b..70e53370ad 100644
--- a/parity/acrobat-features.toml
+++ b/parity/acrobat-features.toml
@@ -1262,8 +1262,8 @@ milestone = "M3"
status = "shipped"
commands = []
tools = []
-evidence = ["crates/ui-egui/tests/ui.rs::theme_and_view_options_apply", "crates/ui-egui/tests/viewer.rs::going_to_a_far_page_scrolls_its_thumbnail_into_view", "crates/ui-egui/tests/viewer.rs::going_to_a_page_in_view_leaves_the_pages_panel_still", "crates/ui-egui/tests/viewer.rs::a_page_change_while_thumbnails_are_dragged_does_not_scroll_the_pages_panel"]
-notes = "The panel follows the current page (#2781): a page whose thumbnail is out of sight scrolls it into view, as little as it takes; a thumbnail already in view, or clicked, leaves the panel still, and a page change while the user scrolls the panel or drags thumbnails does not move it."
+evidence = ["crates/ui-egui/tests/editing.rs::resized_pages_panel_thumbnails_reorder_multiple_pages_and_undo", "crates/ui-egui/tests/viewer.rs::pages_panel_thumbnail_zoom_is_bounded_independent_and_survives_reload", "crates/ui-egui/tests/viewer.rs::pages_panel_thumbnail_size_controls_show_more_pages_and_keep_the_selection", "crates/ui-egui/tests/viewer.rs::pages_panel_thumbnail_zoom_keeps_a_far_current_page_visible", "crates/ui-egui/tests/control.rs::thumbnail_size_is_settable_and_inspectable_through_the_control_channel", "crates/ui-egui/tests/ui.rs::theme_and_view_options_apply", "crates/ui-egui/tests/viewer.rs::going_to_a_far_page_scrolls_its_thumbnail_into_view", "crates/ui-egui/tests/viewer.rs::going_to_a_page_in_view_leaves_the_pages_panel_still", "crates/ui-egui/tests/viewer.rs::a_page_change_while_thumbnails_are_dragged_does_not_scroll_the_pages_panel"]
+notes = "Thumbnail size: 50–150% with a slider and buttons above the scroll area (#3808), independent of document and Organize zoom. Accessible through ui.set thumbnail-zoom and ui.state active.thumbnail_zoom; retained on file reload, not persisted between sessions. The panel follows the current page (#2781): a page whose thumbnail is out of sight scrolls it into view, as little as it takes; a thumbnail already in view, or clicked, leaves the panel still, and a page change while the user scrolls the panel or drags thumbnails does not move it."
[[feature]]
id = "view.thumbnail-context-ops"
@@ -2885,7 +2885,7 @@ status = "shipped"
commands = ["page.organize"]
tools = []
evidence = ["crates/ui-egui/tests/editing.rs::page_grid_zooms_with_its_buttons_keys_and_pinch", "crates/ui-egui/tests/editing.rs::a_zoomed_page_grid_still_inserts_and_moves_at_the_right_gap", "crates/ui-egui/tests/editing.rs::zoomed_pages_in_view_are_rendered_at_the_size_drawn"]
-notes = "Organize page grid: 50–300% with toolbar buttons (and a percentage that resets to 100%), trackpad pinch or Ctrl/⌘ + wheel about the page under the pointer, and ⌘+ / ⌘− / ⌘0; pages drawn larger than their thumbnails are rendered at the size drawn while they are in view (the thumbnail stands in until then, and after an edit), so text stays sharp however long the document is. Missing: a slider as in Acrobat; the size is per document view and not remembered between sessions; the Pages side panel has no size control."
+notes = "Organize page grid: 50–300% with toolbar buttons (and a percentage that resets to 100%), trackpad pinch or Ctrl/⌘ + wheel about the page under the pointer, and ⌘+ / ⌘− / ⌘0; pages drawn larger than their thumbnails are rendered at the size drawn while they are in view (the thumbnail stands in until then, and after an edit), so text stays sharp however long the document is. Missing in Organize: a slider as in Acrobat; the size is per document view and not remembered between sessions. The Pages inspector has its own 50–150% slider and buttons (#3808)."
[[feature]]
id = "organize.split-every-n"