M3: rename Smooth scrolling to Animation effects; it decides every animation

The preference also governs Ctrl-wheel zooming, so "Smooth scrolling"
described only part of it. As "Animation effects" (Use system setting, On,
Off) it now decides every animation in the window, the way the Windows
setting of the same name does:

- the wheel's scrolling and zooming, and scrolling to a place (as before);
- egui's own animations, the progress card's sweep, and the chrome's hover
  and press washes (new on main, with durations of their own), which used
  to follow the system alone or not at all.

Saved and set on the control channel as animation_effects (system, on,
off); the unreleased smooth_scrolling name is gone. The three labels are
retranslated in all 20 catalogs.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Jeremy YaoandClaude Opus 5.5 committed 2026-10-11 22:24:23 -04:00
1 parent d6a9901bf3
commit 486970a4d4
32 files changed
+254 -207

No files matched your search

+20 -20
View File
@@ -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 = "657bc9a96e997acb2e896d22abd366a897e3382f27fc7c410a1ef5bdc69104d0"
sha256 = "0c12b624e677e2ec61afa20333aa7b2e3e2303ab596b305269f4fcfb1183cdaa"
# 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 = "8488be5cb07151c4c310b766b8bb76672f92971e3e54219accc1f2f8bed35c83"
sha256 = "9ce8e70f139336bae217f6b96408d93b66fb3b7a2cae2c4df3e7473ba0f40d5e"
[[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 = "0370dee8a3b4c24097747edf01ca091fb2914166f0151e1e23d105d73dfaf161"
sha256 = "d16f126aa4082aa7d22f539d0a0256c87f486615e9088f812f76c750b64b7c49"
[[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 = "41c2138cdd04c39d6d661e6db70774e0223746130c6536e3556c8f06fa76f731"
sha256 = "60cfcc7003f50d5466a7e376ae5e4710e05833d7f30a5b82d7a866e802c479e6"
[[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 = "676bd5fe27d2f8a1b1aac4ca2153207fae4a0a4f279f66beb3a648973af3d02a"
sha256 = "20f5e4d96efd67556c90d3b20cb8f224f3fcca7ccca679f79b28e8a785db4519"
[[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 = "935ad1e5a12d67f52c8007022faf54d61928e70bac1b184ec623f0641af3d2eb"
sha256 = "4073d6909780954808c27ed510f41b5f34d9037d435e8ecb9baa03b3dae34900"
[[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 = "2b88514e089f08bf302411e418bc31e3445fd7772caf6c9cbdd22d830c16886e"
sha256 = "fb7b933a43439715ac7a702bee483f007b2dd7c34a72805acb89786bc0c26d04"
[[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 = "e7b755aae3c0f45e9c936f37220af290e133bd0bb6d1e106fd349b3517cd0247"
sha256 = "82759859786d66d3c2e10cd57911d66834dd73e93c6bdbdf173956cb6c7c00a9"
[[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 = "b789db385ccede5774382beef49d2fc6a75a76082d21276855eb3a0a6c6837ed"
sha256 = "a0157cc0dc8a129f08a22f194b5bd57215f88b4c983af6725dd56d4a375b1f1b"
[[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 = "05b10895c062df8a2f6e42cab801fcaaa1f0e793d7b62d9b5ea9eed8baf8e914"
sha256 = "e3ce7cac5de5a4561e1b93072bce74a14651d7b3d49e13d2df8b125d6cb5a424"
[[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 = "478e0b058359f1f213721657157a2c6f6c9759e98a1bd30f0852f06571a83e8d"
sha256 = "15328644bb17127f0df378ab7ece03edffcda6eb42438c0613e032dd20641aa0"
[[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 = "fb815a01750c0d870a953cd9d450664b223266e3d2bdd056b2ffe03d3b0d2917"
sha256 = "f1df5b6a526dcf80e9d9917892508eb5eb229955dc5c704fd69cec3536241564"
[[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 = "0a46c705ed0a00731b2834b1e5669eba9be8944d1db5f341d1efc6a6ff7efee1"
sha256 = "b1d367feb23ccd7d2bd59691fa6a82ae7392545e7bfc00b0f00b3f430f0cf8d8"
[[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 = "ba5bffa22a11ab087db9777b7710895ec75213d2efd284f73b8e14d277d209ab"
sha256 = "48fefa761b28fafe7a0f07184d8280aae3eed6956d4a5291895f4158011894e3"
[[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 = "2b6d916eb54c26aa439864e718eeea4cf2eab29f44d1ba5aab2ed3c92d34d1d3"
sha256 = "0bc1061796a4bee05e817cd023d677231dba3ba0472d41d9584b2a06d4cdb34c"
[[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 = "6d6a974b3ed8ebfbf5db9967bd26964aa3a02ad34f2e16be7facffbb2129cd34"
sha256 = "e34088eef6d6fe517da21dc4a32c4ffd72decf642f12f4c6dc50b794cccfb742"
[[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 = "013f483cc43314ab8aa6aac5a2f922b6d62f05b79d386162b9e2179783a12914"
sha256 = "a7eb1c513ec11a04d16bed1b3e165125ae5dd10fcec2b3caa8871eee21a56daf"
[[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 = "5c7a877cc61db1f706d8c83fb0269e99a9d243dc8a8648fd7407c4fae4ffde99"
sha256 = "1939aa38b6ce305c0cf1b84d247e6a49f948826163712ff789a3c339f3f7b488"
[[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 = "26309a1de18df3873662b8d7c5d63b6b1f43cfa4a3c43a2fe0d01637debe5a6a"
sha256 = "a8f52da804453baf6546941442dfc860a0ea3bb25e274037c3dab607887a2f29"
[[asset]]
path = "assets/icons/accessibility.svg"
@@ -3339,4 +3339,4 @@ 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 = "eddbea7c050c06c628ec1bb64eb564013f378e798ebfe62c331b0604402f256e"
sha256 = "1e1d5662e8674698f2c526b12cea3eb280e6168512f77db32676e79e80a4c43b"
+3 -3
View File
@@ -3,7 +3,7 @@
**Stage: alpha** · next: beta, ~26% (ready for real work 49% → 75%) and ~400–700 h away
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M2 short same-line redraws split before placement; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (M3: Windows' Animation effects setting is followed, and Preferences has Smooth scrolling: system, on or off); earlier: M10, #1028: OCR reads pictures on pages that have text, on request; earlier: M14 #307 Plan 3 uses one marker traversal on open and full refresh, and the combined scanner borrows metadata instead of cloning scan-only containers; includes a behavior-preserving `organize` binding for the workspace Clippy gate · earlier: minor (localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (M3: Windows' Animation effects setting is followed, and Preferences has Animation effects: system, on or off); earlier: M10, #1028: OCR reads pictures on pages that have text, on request; earlier: M14 #307 Plan 3 uses one marker traversal on open and full refresh, and the combined scanner borrows metadata instead of cloning scan-only containers; includes a behavior-preserving `organize` binding for the workspace Clippy gate · earlier: minor (localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (#594 paragraph drags validated in the Linux test environment; localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; earlier: second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (architectural paper controls from #599; localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; earlier: second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (closed issues synced: #774, #756 and the #799–#821 batch; earlier: localized shortcut labels, #496; readiness table with hours per audience; full number as weighted sum 49%; earlier: second look: ready 40% → 47%, mainstream 55% added; core-workflow gate applied → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
@@ -156,7 +156,7 @@ Newest first: the date, what moved, and the new overall estimate.
- **2026-10-11 (M5.7, scanned signatures, #1110):** Fill & Sign ▸ Add signature has a Scan tab: a phone photo of a signature on paper is framed automatically (all words; table, sheet edge and dust ignored; a sideways signature turned upright), the frame can be moved, resized or redrawn, the photo turned, and an ink-sensitivity slider picks up faint ink. A morphological paper estimate removes shadows and uneven light, and the trimmed ink is saved and reused like an image signature. Photos and images can also be dropped on the Image and Scan tabs. Photos keep the image-signature limits (#1124: a 4 MiB file, up to 32 megapixels decoded, scaled down to 4 megapixels), so a scan never processes more than 4 megapixels. `fill_sign_add` takes `scan` and `sensitivity`. Synthetic engine, UI and automation tests; checked by hand on real phone photos. Milestone and effort estimates unchanged.
- **2026-10-12 (M10.5, hosted CI):** Integrated current main, including its date-table formatting and static-storage fixes. The Arabic synthetic drawing regression now checks the original serialized-output golden with and without the optional font face: the face-dependent FontName and FontFamily are checked separately and normalized only in the fixture. The hosted FreeBSD fingerprint is reproduced by those two metadata fields alone. Drawing bytes and all other fields retain the original golden; no printing behavior or readiness estimate changes.
- **2026-10-11 (M3, smooth scrolling and Windows' Animation effects setting):** PdfCraft follows Windows' Settings ▸ Accessibility ▸ Visual effects ▸ Animation effects, and Preferences has Smooth scrolling: Use system setting (the default), On or Off. With Animation effects off, a wheel notch scrolls its whole distance, and Ctrl with the wheel zooms its whole step, in the frame it arrives. Both were eased over about a quarter of a second whatever the setting: egui spreads a coarse wheel step over later frames and has no switch for it, so `crates/ui-egui/src/motion.rs` reads each frame's wheel input the way egui does, minus the easing, and hands that to the scroll areas and the views (a test sums what egui eases and compares it with what is read at once, and fails if egui stops easing). egui's own animations (menus, sections, scrolling to a row) take no time, and the progress card's bar steps without its sweep. The desktop app asks WinRT's `UISettings.AnimationsEnabled` at launch and every two seconds of use, so a change applies without a restart. How far a notch scrolls or zooms is unchanged. Smooth scrolling set to On or Off decides the wheel's scrolling and zooming, and scrolling to a place, whatever the system says (menus, sections and the progress card always follow the system); it is saved, set as `smooth_scrolling` (`system`, `on`, `off`) on the control channel, and translated in all 20 catalogs. Not done: macOS (Reduce motion), Linux (GNOME's animations switch) and the browser (`prefers-reduced-motion`) aren't asked yet, so Use system setting eases there as before; Off works everywhere. Estimates unchanged.
- **2026-10-11 (M3, animation effects):** PdfCraft follows Windows' Settings ▸ Accessibility ▸ Visual effects ▸ Animation effects, and Preferences has its own Animation effects: Use system setting (the default), On or Off. With animation effects off, a wheel notch scrolls its whole distance, and Ctrl with the wheel zooms its whole step, in the frame it arrives. Both were eased over about a quarter of a second whatever the setting: egui spreads a coarse wheel step over later frames and has no switch for it, so `crates/ui-egui/src/motion.rs` reads each frame's wheel input the way egui does, minus the easing, and hands that to the scroll areas and the views (a test sums what egui eases and compares it with what is read at once, and fails if egui stops easing). egui's own animations (menus, sections, scrolling to a row) take no time, the chrome's hover and press washes are simply there, and the progress card's bar steps without its sweep. The desktop app asks WinRT's `UISettings.AnimationsEnabled` at launch and every two seconds of use, so a change applies without a restart. How far a notch scrolls or zooms is unchanged. The preference set to On or Off decides all of this whatever the system says; it is saved, set as `animation_effects` (`system`, `on`, `off`) on the control channel, and translated in all 20 catalogs. Not done: macOS (Reduce motion), Linux (GNOME's animations switch) and the browser (`prefers-reduced-motion`) aren't asked yet, so Use system setting animates there as before; Off works everywhere. Estimates unchanged.
- **2026-10-11 (M10.5):** Extracted printer discovery, options and PDF submission into craft-libs/craft-print, shared with DesignCraft. Windows preserves composed sheet size and orientation without fitting into default margins; duplex binding edges are corrected. PdfCraft retains permission checks and imposition. Shared transport and local composition, engine, automation and UI coverage validate the boundary; native Windows CI passes, including paused-queue submission and cleanup; physical printer fidelity remains a hardware check. Rust 1.95 is required by the shared crate. No readiness percentage change.
- **2026-10-11 (M10, #1028):** Recognize Text can read pages that already have text: "Also recognize pages that already have text" in the dialog (remembered with the other choices, and used by Recognize text in multiple files) and `skip_text_pages: false` on `ocr_recognize` and, newly, `ocr_recognize_files`. A picture on such a page becomes searchable, and a recognised word at least half covered by the page's own text (an earlier OCR layer included) is left out, so nothing is found twice; `ocr_recognize` reports how many were left out as `already_on_page`. Before, the option existed only for agents and added the page's text a second time. The default still skips pages with text. Synthetic tests (a picture of a sentence plus real text) cover the engine, the tool and the dialog, and fail with the filter turned off. Still missing: reading only the picture areas (the whole page is read, as for a scan). Separately, `recognize_text_makes_a_scanned_page_searchable` fails on main wherever the models are installed (the model reads "quick" as "auick"). Estimates unchanged.
- **2026-10-11 (M2, page-label prefixes):** The page-label readers bound the total size of decoded prefixes. Each range decodes its own prefix, and ranges may share one prefix object, so a small file could ask for a copy per range: within the 200,000-entry tree limit, about 200 MiB of copies of one 1,024-byte prefix. Inspection and Organize now refuse a tree whose decoded prefixes pass 4 MiB in total (`MAX_PREFIX_TOTAL_BYTES`), before any label is made, as the other tree limits do; relabelling such a tree changes nothing. Synthetic tests share one 1,000-byte prefix across 5,000 ranges and keep exactly 4 MiB of prefixes readable. Overall estimate unchanged.
@@ -379,7 +379,7 @@ Newest first: the date, what moved, and the new overall estimate.
| Date | Change | Summary |
|---|---|---|
| 2026-10-11 | minor | M10: borderless inferred tables and positioned invoice watermarks (#773); estimates unchanged. |
| 2026-10-11 | minor | M3: Windows' Animation effects setting is followed (wheel scrolling and zooming without easing, egui's animations off), and Preferences has Smooth scrolling (Use system setting, On, Off). Estimates unchanged. |
| 2026-10-11 | minor | M3: Windows' Animation effects setting is followed (wheel scrolling and zooming without easing, no other animations), and Preferences has Animation effects (Use system setting, On, Off). Estimates unchanged. |
| 2026-10-11 | minor | M7: #594 paragraph-drag candidate and source regressions prepared; subsequent Linux validation passed all eight CI gates. Estimates unchanged. |
| 2026-10-10 | minor | Recorded the remaining System appearance repair after #881; estimates unchanged |
| 2026-10-11 | minor | Contributor fork: document-font selection and subset completion, CFF, style ranges, contextual/bidi save/reopen and all-page Chinese keyboard acceptance; curve/hole hit testing (shared clips stay fixed, as on main). Scope remains bounded; percentages unchanged |
+2 -2
View File
@@ -314,8 +314,8 @@ fn app_creator<'a>(
}
app.integrated_titlebar = integrated;
app.system_text_scale = text_scale::system();
// Windows' Animation effects setting: with it off nothing animates, and the wheel doesn't
// ease unless Preferences ▸ Smooth scrolling says so.
// Windows' Animation effects setting, which Preferences ▸ Animation effects follows unless
// it is set to On or Off.
#[cfg(target_os = "windows")]
{
app.motion.source = Some(motion::source());
+2 -2
View File
@@ -1,5 +1,5 @@
//! The system's "no animations" setting, which `pdfcraft_ui_egui::motion` follows (and Preferences ▸
//! Smooth scrolling, unless it is set to On or Off).
//! The system's "no animations" setting, which Preferences ▸ Animation effects follows unless it is
//! set to On or Off (`pdfcraft_ui_egui::motion`).
//!
//! Windows: Settings ▸ Accessibility ▸ Visual effects ▸ Animation effects (in the Control Panel,
//! "Show animations in Windows"), read through WinRT's `UISettings.AnimationsEnabled`. Other
+3 -3
View File
@@ -2277,9 +2277,9 @@ key Space مسافة
scroll speed Slow بطيئة
scroll speed Normal عادية
scroll speed Fast سريعة
Smooth scrolling التمرير السلس
smooth scrolling On مفعّل
smooth scrolling Off معطّل
Animation effects تأثيرات الحركة
animation effects On مفعّلة
animation effects Off معطّلة
Enhance camera image تحسين صورة الكاميرا
Drag the corners onto the document's edges, then choose Enhance page. اسحب الزوايا إلى حواف المستند، ثم اختر تحسين الصفحة.
Enhance page تحسين الصفحة
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -3
View File
@@ -2219,9 +2219,9 @@ key Space Интервал
scroll speed Slow Ниска
scroll speed Normal Нормална
scroll speed Fast Висока
Smooth scrolling Плавно превъртане
smooth scrolling On Включено
smooth scrolling Off Изключено
Animation effects Анимационни ефекти
animation effects On Включени
animation effects Off Изключени
Enhance camera image Подобри снимка от камера
Drag the corners onto the document's edges, then choose Enhance page. Плъзнете ъглите до краищата на документа, след това изберете „Подобри страницата“.
Enhance page Подобри страницата
Can't render this file because it contains an unexpected character in line 8 and column 16.
+3 -3
View File
@@ -2307,9 +2307,9 @@ certificate Subject Subjekt
scroll speed Slow Pomalá
scroll speed Normal Normální
scroll speed Fast Rychlá
Smooth scrolling Hladké posouvání
smooth scrolling On Zapnuto
smooth scrolling Off Vypnuto
Animation effects Animační efekty
animation effects On Zapnuto
animation effects Off Vypnuto
# Counts that take plural forms: optimized images, discarded items, bytes, pixels and deleted fields.
@plural {n} image optimized|{n} images optimized {n} obrázek optimalizován|{n} obrázky optimalizovány|{n} obrázků optimalizováno
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -3
View File
@@ -2257,9 +2257,9 @@ key Space Leertaste
scroll speed Slow Langsam
scroll speed Normal Normal
scroll speed Fast Schnell
Smooth scrolling Sanftes Scrollen
smooth scrolling On Ein
smooth scrolling Off Aus
Animation effects Animationseffekte
animation effects On Ein
animation effects Off Aus
Enhance camera image Kamerafoto verbessern
Drag the corners onto the document's edges, then choose Enhance page. Ziehen Sie die Ecken auf die Kanten des Dokuments und wählen Sie dann Seite verbessern.
Enhance page Seite verbessern
Can't render this file because it contains an unexpected character in line 6 and column 51.
+3 -3
View File
@@ -2269,9 +2269,9 @@ comment menu Edit Επεξεργασία
scroll speed Slow Αργή
scroll speed Normal Κανονική
scroll speed Fast Γρήγορη
Smooth scrolling Ομαλή κύλιση
smooth scrolling On Ενεργή
smooth scrolling Off Ανενεργή
Animation effects Εφέ κίνησης
animation effects On Ενεργά
animation effects Off Ανενεργά
Enhance camera image Βελτίωση εικόνας κάμερας
Drag the corners onto the document's edges, then choose Enhance page. Σύρετε τις γωνίες στις άκρες του εγγράφου και επιλέξτε Βελτίωση σελίδας.
Enhance page Βελτίωση σελίδας
Can't render this file because it contains an unexpected character in line 479 and column 12.
+3 -3
View File
@@ -2386,9 +2386,9 @@ key Space Espacio
scroll speed Slow Lenta
scroll speed Normal Normal
scroll speed Fast Rápida
Smooth scrolling Desplazamiento suave
smooth scrolling On Activado
smooth scrolling Off Desactivado
Animation effects Efectos de animación
animation effects On Activados
animation effects Off Desactivados
Enhance camera image Mejorar foto de cámara
Drag the corners onto the document's edges, then choose Enhance page. Arrastra las esquinas hasta los bordes del documento y elige Mejorar página.
Enhance page Mejorar página
Can't render this file because it contains an unexpected character in line 6 and column 60.
+3 -3
View File
@@ -2256,9 +2256,9 @@ key Space Espace
scroll speed Slow Lente
scroll speed Normal Normale
scroll speed Fast Rapide
Smooth scrolling Défilement fluide
smooth scrolling On Activé
smooth scrolling Off Désactivé
Animation effects Effets d'animation
animation effects On Activés
animation effects Off Désactivés
Enhance camera image Améliorer la photo d'un document
Drag the corners onto the document's edges, then choose Enhance page. Faites glisser les coins sur les bords du document, puis choisissez Améliorer la page.
Enhance page Améliorer la page
Can't render this file because it contains an unexpected character in line 6 and column 39.
+3 -3
View File
@@ -2255,9 +2255,9 @@ key Space Szóköz
scroll speed Slow Lassú
scroll speed Normal Normál
scroll speed Fast Gyors
Smooth scrolling Finom görgetés
smooth scrolling On Be
smooth scrolling Off Ki
Animation effects Animációs effektusok
animation effects On Be
animation effects Off Ki
Enhance camera image Kamerakép javítása
Drag the corners onto the document's edges, then choose Enhance page. Húzza a sarkokat a dokumentum széleire, majd válassza az Oldal javítása lehetőséget.
Enhance page Oldal javítása
Can't render this file because it contains an unexpected character in line 7 and column 44.
+3 -3
View File
@@ -2265,9 +2265,9 @@ key Space Spazio
scroll speed Slow Lenta
scroll speed Normal Normale
scroll speed Fast Veloce
Smooth scrolling Scorrimento fluido
smooth scrolling On Attivato
smooth scrolling Off Disattivato
Animation effects Effetti di animazione
animation effects On Attivati
animation effects Off Disattivati
Enhance camera image Migliora foto della fotocamera
Drag the corners onto the document's edges, then choose Enhance page. Trascina gli angoli sui bordi del documento, poi scegli Migliora la pagina.
Enhance page Migliora la pagina
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -3
View File
@@ -2256,9 +2256,9 @@ key Space スペース
scroll speed Slow 遅い
scroll speed Normal 標準
scroll speed Fast 速い
Smooth scrolling スムーズスクロール
smooth scrolling On オン
smooth scrolling Off オフ
Animation effects アニメーション効果
animation effects On オン
animation effects Off オフ
Enhance camera image カメラ画像を補正
Drag the corners onto the document's edges, then choose Enhance page. 四隅を文書の端までドラッグしてから、「ページを補正」を選択します。
Enhance page ページを補正
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -3
View File
@@ -2251,9 +2251,9 @@ key Space Бос орын
scroll speed Slow Баяу
scroll speed Normal Қалыпты
scroll speed Fast Жылдам
Smooth scrolling Тегіс айналдыру
smooth scrolling On Қосулы
smooth scrolling Off Өшірулі
Animation effects Анимация әсерлері
animation effects On Қосулы
animation effects Off Өшірулі
Enhance camera image Камера суретін жақсарту
Drag the corners onto the document's edges, then choose Enhance page. Бұрыштарды құжаттың жиектеріне сүйреп, содан кейін «Бетті жақсарту» таңдаңыз.
Enhance page Бетті жақсарту
Can't render this file because it contains an unexpected character in line 819 and column 12.
+3 -3
View File
@@ -2238,9 +2238,9 @@ key Space Spacja
scroll speed Slow Wolna
scroll speed Normal Normalna
scroll speed Fast Szybka
Smooth scrolling Płynne przewijanie
smooth scrolling On Włączone
smooth scrolling Off Wyłączone
Animation effects Efekty animacji
animation effects On Włączone
animation effects Off Wyłączone
Enhance camera image Popraw zdjęcie z aparatu
Drag the corners onto the document's edges, then choose Enhance page. Przeciągnij narożniki na krawędzie dokumentu, a następnie wybierz Popraw stronę.
Enhance page Popraw stronę
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -3
View File
@@ -2270,9 +2270,9 @@ calendar date December dezembro
scroll speed Slow Lenta
scroll speed Normal Normal
scroll speed Fast Rápida
Smooth scrolling Rolagem suave
smooth scrolling On Ativada
smooth scrolling Off Desativada
Animation effects Efeitos de animação
animation effects On Ativados
animation effects Off Desativados
Enhance camera image Aprimorar foto da câmera
Drag the corners onto the document's edges, then choose Enhance page. Arraste os cantos até as bordas do documento e escolha Aprimorar página.
Enhance page Aprimorar página
Can't render this file because it contains an unexpected character in line 9 and column 16.
+3 -3
View File
@@ -2169,9 +2169,9 @@ key Space Пробел
scroll speed Slow Низкая
scroll speed Normal Обычная
scroll speed Fast Высокая
Smooth scrolling Плавная прокрутка
smooth scrolling On Вкл
smooth scrolling Off Выкл
Animation effects Эффекты анимации
animation effects On Вкл
animation effects Off Выкл
Enhance camera image Улучшить снимок с камеры
Drag the corners onto the document's edges, then choose Enhance page. Перетащите углы к краям документа, затем выберите «Улучшить страницу».
Enhance page Улучшить страницу
Can't render this file because it contains an unexpected character in line 9 and column 16.
+3 -3
View File
@@ -2258,9 +2258,9 @@ key Space Preslednica
scroll speed Slow Počasi
scroll speed Normal Običajno
scroll speed Fast Hitro
Smooth scrolling Gladko drsenje
smooth scrolling On Vklopljeno
smooth scrolling Off Izklopljeno
Animation effects Učinki animacije
animation effects On Vklopljeni
animation effects Off Izklopljeni
Enhance camera image Izboljšaj fotografijo s kamere
Drag the corners onto the document's edges, then choose Enhance page. Povlecite vogale na robove dokumenta, nato izberite Izboljšaj stran.
Enhance page Izboljšaj stran
Can't render this file because it contains an unexpected character in line 9 and column 16.
+3 -3
View File
@@ -2172,9 +2172,9 @@ key Space స్పేస్
scroll speed Slow నెమ్మదిగా
scroll speed Normal సాధారణం
scroll speed Fast వేగంగా
Smooth scrolling సున్నితమైన స్క్రోలింగ్
smooth scrolling On ఆన్
smooth scrolling Off ఆఫ్
Animation effects యానిమేషన్ ప్రభావాలు
animation effects On ఆన్
animation effects Off ఆఫ్
Enhance camera image కెమెరా చిత్రాన్ని మెరుగుపరచు
Drag the corners onto the document's edges, then choose Enhance page. మూలలను పత్రం అంచుల వరకు లాగి, ఆపై పేజీని మెరుగుపరచు ఎంచుకోండి.
Enhance page పేజీని మెరుగుపరచు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+3 -3
View File
@@ -2249,9 +2249,9 @@ key Space Boşluk
scroll speed Slow Yavaş
scroll speed Normal Orta
scroll speed Fast Hızlı
Smooth scrolling Yumuşak kaydırma
smooth scrolling On Açık
smooth scrolling Off Kapalı
Animation effects Animasyon efektleri
animation effects On Açık
animation effects Off Kapalı
Enhance camera image Kamera görüntüsünü iyileştir
Drag the corners onto the document's edges, then choose Enhance page. Köşeleri belgenin kenarlarına sürükleyin, ardından Sayfayı iyileştir'i seçin.
Enhance page Sayfayı iyileştir
Can't render this file because it contains an unexpected character in line 818 and column 12.
+3 -3
View File
@@ -2278,9 +2278,9 @@ key Space Пробіл
scroll speed Slow Повільна
scroll speed Normal Звичайна
scroll speed Fast Швидка
Smooth scrolling Плавне прокручування
smooth scrolling On Увімкнено
smooth scrolling Off Вимкнено
Animation effects Ефекти анімації
animation effects On Увімкнено
animation effects Off Вимкнено
Enhance camera image Поліпшити знімок з камери
Drag the corners onto the document's edges, then choose Enhance page. Перетягніть кути до країв документа, потім виберіть «Поліпшити сторінку».
Enhance page Поліпшити сторінку
Can't render this file because it contains an unexpected character in line 6 and column 47.
+3 -3
View File
@@ -2220,9 +2220,9 @@ key Space 空格
scroll speed Slow 慢
scroll speed Normal 正常
scroll speed Fast 快
Smooth scrolling 平滑滚动
smooth scrolling On 开
smooth scrolling Off 关
Animation effects 动画效果
animation effects On 开
animation effects Off 关
Enhance camera image 增强相机图像
Drag the corners onto the document's edges, then choose Enhance page. 将四个角拖到文档边缘,然后选择“增强页面”。
Enhance page 增强页面
Can't render this file because it contains an unexpected character in line 9 and column 16.
+3 -3
View File
@@ -2131,9 +2131,9 @@ key Space 空格
scroll speed Slow 慢
scroll speed Normal 正常
scroll speed Fast 快
Smooth scrolling 平滑捲動
smooth scrolling On 開
smooth scrolling Off 關
Animation effects 動畫效果
animation effects On 開
animation effects Off 關
Table rows belong directly in a table or its head, body or foot. Fix the table's tags. 表格列應直接位於表格或其表頭、主體、表尾內。請修正表格標記。
Enhance camera image 增強相機影像
Drag the corners onto the document's edges, then choose Enhance page. 將四個角拖曳到文件邊緣,然後選擇「增強頁面」。
Can't render this file because it contains an unexpected character in line 9 and column 16.
+5 -4
View File
@@ -357,11 +357,12 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok
widgets::radio_value(ui, &mut app.scroll_speed, speed, tl_ctx!("scroll speed", speed.label()));
}
});
// Whether the wheel eases; left alone it follows the system's animation setting.
// Whether anything animates, the wheel's scrolling and zooming included; left alone it
// follows the system's animation setting.
ui.horizontal(|ui| {
ui.label(tl!("Smooth scrolling"));
for choice in crate::motion::SmoothScrolling::ALL {
widgets::radio_value(ui, &mut app.smooth_scrolling, choice, tl_ctx!("smooth scrolling", choice.label()));
ui.label(tl!("Animation effects"));
for choice in crate::motion::AnimationEffects::ALL {
widgets::radio_value(ui, &mut app.animation_effects, choice, tl_ctx!("animation effects", choice.label()));
}
});
ui.add_space(8.0);
+10 -10
View File
@@ -468,8 +468,8 @@ pub struct PdfCraftApp {
desktop_theme_enabled: bool,
/// How far a mouse-wheel notch scrolls.
pub scroll_speed: scroll_speed::ScrollSpeed,
/// Preferences ▸ Smooth scrolling: eased, at once, or as the system's animation setting says.
pub smooth_scrolling: motion::SmoothScrolling,
/// Preferences ▸ Animation effects: on, off, or as the system's animation setting says.
pub animation_effects: motion::AnimationEffects,
/// Whether the system asked for no animations, and how the desktop app asks it.
pub motion: motion::Motion,
/// Interface language preference: `auto` (follow the system) or a code from [`i18n::LANGUAGES`].
@@ -797,7 +797,7 @@ impl PdfCraftApp {
#[cfg(target_os = "linux")]
desktop_theme_enabled: false,
scroll_speed: scroll_speed::ScrollSpeed::default(),
smooth_scrolling: motion::SmoothScrolling::default(),
animation_effects: motion::AnimationEffects::default(),
motion: motion::Motion::default(),
language: i18n::AUTO.to_string(),
ui_scale: None,
@@ -1636,7 +1636,7 @@ impl PdfCraftApp {
"pinned_folders": self.pinned.folders,
"theme": self.theme_preference,
"scroll_speed": self.scroll_speed,
"smooth_scrolling": self.smooth_scrolling,
"animation_effects": self.animation_effects,
"default_mode": self.default_mode,
"default_layout": canvas::display_token(self.view_defaults.layout, self.view_defaults.continuous),
"default_zoom": self.view_defaults.zoom_name(),
@@ -1722,8 +1722,8 @@ impl PdfCraftApp {
if let Ok(speed) = serde_json::from_value::<scroll_speed::ScrollSpeed>(v["scroll_speed"].clone()) {
self.scroll_speed = speed;
}
if let Ok(smooth) = serde_json::from_value::<motion::SmoothScrolling>(v["smooth_scrolling"].clone()) {
self.smooth_scrolling = smooth;
if let Ok(effects) = serde_json::from_value::<motion::AnimationEffects>(v["animation_effects"].clone()) {
self.animation_effects = effects;
}
if let Ok(mode) = serde_json::from_value::<Mode>(v["default_mode"].clone()) {
self.default_mode = mode;
@@ -1851,8 +1851,8 @@ impl PdfCraftApp {
("scroll_speed", _) => {
self.scroll_speed = scroll_speed::ScrollSpeed::parse(value).ok_or("scroll_speed must be slow, normal, or fast")?;
}
("smooth_scrolling", _) => {
self.smooth_scrolling = motion::SmoothScrolling::parse(value).ok_or("smooth_scrolling must be system, on, or off")?;
("animation_effects", _) => {
self.animation_effects = motion::AnimationEffects::parse(value).ok_or("animation_effects must be system, on, or off")?;
}
("panel", _) => {
self.right = match value {
@@ -2285,8 +2285,8 @@ impl eframe::App for PdfCraftApp {
self.startup_superseded |= !self.views.is_empty() || self.combine_showing() || self.dialog.is_some() || self.password_prompt.is_some();
// Also done in `raw_input_hook`, before input; here for hosts that don't call the hook.
self.scroll_speed.set_line_speed(ctx);
// Before anything reads this frame's wheel input, and with the line speed just set.
self.motion.begin_pass(ctx, self.smooth_scrolling);
// Before anything reads this frame's wheel input or draws, and with the line speed just set.
self.motion.begin_pass(ctx, self.animation_effects);
// Before taking this frame's drop: the grid must be drawn once with the pointer where
// the files were let go before the gap is read.
self.finish_grid_drop(ctx);
+99 -61
View File
@@ -1,8 +1,10 @@
//! Motion the system asked to do without.
//! Animation effects, and the system's request to do without them.
//!
//! Windows' Settings ▸ Accessibility ▸ Visual effects ▸ Animation effects, turned off, asks
//! apps not to animate (other systems have a Reduce motion switch for it). The desktop app
//! reads the setting ([`MotionSource`]); while motion is reduced, nothing in the window eases:
//! reads the setting ([`MotionSource`]), and Preferences ▸ Animation effects
//! ([`AnimationEffects`]) follows it unless the user chose On or Off there. With animation
//! effects off, nothing in the window eases:
//!
//! - A wheel notch scrolls its whole distance in the frame it arrives. egui spreads a notch
//! over about a quarter of a second and has no switch for that, so each frame's wheel input
@@ -11,29 +13,27 @@
//! - ⌘/Ctrl-wheel zooms by a notch's whole step at once: egui makes its zoom factor from the
//! same eased wheel motion. Views read [`zoom_delta`] instead of egui's.
//! - Scrolling to a place (a row revealed by the keys, the Hand tool's drag) takes no time.
//! - egui's other animations (menus fading in, sections opening) take no time.
//!
//! Preferences ▸ Smooth scrolling ([`SmoothScrolling`]) decides the first three by itself when
//! it is On or Off, whatever the system says; the last always follows the system.
//! - egui's other animations (menus fading in, sections opening) take no time: its style's
//! animation time is zero, which the app's own fades and sweeps ask about too ([`animated`]).
//!
//! How far anything scrolls or zooms doesn't change, only how long it takes.
use egui::style::ScrollAnimation;
use egui::{Event, Modifiers, MouseWheelUnit, TouchPhase, Vec2, vec2};
/// Preferences ▸ Smooth scrolling: whether the wheel's scrolling and zooming, and scrolling to
/// a place, are eased.
/// Preferences ▸ Animation effects: whether anything in the window is animated, the wheel's
/// scrolling and zooming included.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SmoothScrolling {
/// Eased unless the system asks for no animations ([`Motion::reduced`]).
pub enum AnimationEffects {
/// Animated unless the system asks for no animations ([`Motion::reduced`]).
#[default]
System,
On,
Off,
}
impl SmoothScrolling {
impl AnimationEffects {
pub const ALL: [Self; 3] = [Self::System, Self::On, Self::Off];
pub fn label(self) -> &'static str {
@@ -57,8 +57,8 @@ impl SmoothScrolling {
Self::ALL.into_iter().find(|choice| choice.name().eq_ignore_ascii_case(value))
}
/// Whether scrolling is eased, given whether the system asks for no animations.
pub fn eases(self, system_reduced: bool) -> bool {
/// Whether the window animates, given whether the system asks for no animations.
pub fn animates(self, system_reduced: bool) -> bool {
match self {
Self::System => !system_reduced,
Self::On => true,
@@ -76,7 +76,7 @@ pub type MotionSource = Box<dyn Fn() -> Option<bool>>;
/// this away from the window, so the first frame back already follows it.
const ASK_EVERY: f64 = 2.0;
/// Whether the window animates, and what it takes to stop egui from doing so.
/// What the system says about animations, and what it takes to stop egui's.
pub struct Motion {
/// The system asks for no animations. The desktop app's [`Self::source`] sets it; false
/// elsewhere, so tests and screenshots don't depend on the machine.
@@ -97,8 +97,8 @@ impl Default for Motion {
}
impl Motion {
/// Call once at the start of every pass, before any ui reads the wheel.
pub(crate) fn begin_pass(&mut self, ctx: &egui::Context, smooth: SmoothScrolling) {
/// Call once at the start of every pass, before any ui reads the wheel or draws.
pub(crate) fn begin_pass(&mut self, ctx: &egui::Context, effects: AnimationEffects) {
let now = ctx.input(|i| i.time);
// `>=` is false for a NaN time, which then never asks.
if now - self.asked_at >= ASK_EVERY
@@ -109,9 +109,9 @@ impl Motion {
self.reduced = reduced;
}
}
let eases = smooth.eases(self.reduced);
self.sync_style(ctx, eases);
if eases {
let animate = effects.animates(self.reduced);
self.sync_style(ctx, animate);
if animate {
// A gesture begun while easing is egui's to finish.
self.wheel = WheelReader::default();
return;
@@ -123,32 +123,37 @@ impl Motion {
ctx.data_mut(|d| d.insert_temp(zoom_id(), (pass, motion.zoom)));
}
/// egui's animations take no time while the system asks for none, and scrolling to a place
/// takes none unless scrolling `eases`; otherwise both keep the times egui had.
fn sync_style(&mut self, ctx: &egui::Context, eases: bool) {
let unchanged = !self.reduced && eases;
if unchanged && self.egui_times.is_none() {
/// egui's animations, and its scrolling to a place, take no time unless the window is to
/// `animate`; then both get back the times egui had.
fn sync_style(&mut self, ctx: &egui::Context, animate: bool) {
if animate && self.egui_times.is_none() {
return;
}
// Checked every pass, so a style replaced since can't bring the animations back.
let style = ctx.global_style();
let (time, scroll) = *self.egui_times.get_or_insert((style.animation_time, style.scroll_animation));
let wanted = (if self.reduced { 0.0 } else { time }, if eases { scroll } else { ScrollAnimation::none() });
let egui_times = *self.egui_times.get_or_insert((style.animation_time, style.scroll_animation));
let wanted = if animate { egui_times } else { (0.0, ScrollAnimation::none()) };
if (style.animation_time, style.scroll_animation) != wanted {
ctx.all_styles_mut(|s| (s.animation_time, s.scroll_animation) = wanted);
}
if unchanged {
if animate {
self.egui_times = None;
}
}
}
/// Whether the window animates this pass. The app's own fades and sweeps, which have their own
/// durations, ask this; egui's animations stop by themselves ([`Motion::begin_pass`]).
pub(crate) fn animated(ctx: &egui::Context) -> bool {
ctx.global_style().animation_time > 0.0
}
fn zoom_id() -> egui::Id {
egui::Id::new("pdfcraft-motion-wheel-zoom")
}
/// This pass's zoom factor from a pinch or ⌘/Ctrl with the wheel (1.0 for none): egui's
/// `zoom_delta`, without the easing unless scrolling is eased.
/// `zoom_delta`, without the easing while animation effects are off.
pub(crate) fn zoom_delta(ctx: &egui::Context) -> f32 {
let read: Option<(u64, f32)> = ctx.data(|d| d.get_temp(zoom_id()));
let pass = ctx.cumulative_pass_nr();
@@ -374,7 +379,7 @@ mod tests {
let mut events = vec![notch(-1.0, Modifiers::NONE)];
for _ in 0..30 {
pass(&ctx, &mut time, std::mem::take(&mut events), |ctx| {
motion.begin_pass(ctx, SmoothScrolling::System);
motion.begin_pass(ctx, AnimationEffects::System);
seen.push(ctx.input(|i| i.smooth_scroll_delta));
});
}
@@ -392,7 +397,7 @@ mod tests {
let mut events = vec![notch(1.0, Modifiers::COMMAND)];
for _ in 0..30 {
pass(&ctx, &mut time, std::mem::take(&mut events), |ctx| {
motion.begin_pass(ctx, SmoothScrolling::System);
motion.begin_pass(ctx, AnimationEffects::System);
zooms.push(zoom_delta(ctx));
});
}
@@ -405,7 +410,7 @@ mod tests {
let mut events = vec![notch(1.0, Modifiers::COMMAND)];
for _ in 0..3 {
pass(&ctx, &mut time, std::mem::take(&mut events), |ctx| {
motion.begin_pass(ctx, SmoothScrolling::System);
motion.begin_pass(ctx, AnimationEffects::System);
eased.push((zoom_delta(ctx), ctx.input(|i| i.zoom_delta())));
});
}
@@ -416,66 +421,99 @@ mod tests {
}
#[test]
fn the_preference_decides_scrolling_and_the_system_the_other_animations() {
use SmoothScrolling::{Off, On, System};
assert_eq!(SmoothScrolling::default(), System, "the system's setting is followed unless the user chooses");
for choice in SmoothScrolling::ALL {
assert_eq!(SmoothScrolling::parse(choice.name()), Some(choice));
fn the_preference_overrides_the_system_for_every_animation() {
use AnimationEffects::{Off, On, System};
assert_eq!(AnimationEffects::default(), System, "the system's setting is followed unless the user chooses");
for choice in AnimationEffects::ALL {
assert_eq!(AnimationEffects::parse(choice.name()), Some(choice));
}
assert_eq!((SmoothScrolling::parse("OFF"), SmoothScrolling::parse("smooth")), (Some(Off), None));
assert_eq!((AnimationEffects::parse("OFF"), AnimationEffects::parse("smooth")), (Some(Off), None));
let (line, egui_default) = (egui::InputOptions::default().line_scroll_speed, egui::Style::default());
// The system's answer, the choice, and whether a notch then lands whole in its pass.
for (reduced, smooth, at_once) in
[(false, System, false), (true, System, true), (false, On, false), (true, On, false), (false, Off, true), (true, Off, true)]
let egui_times = (egui_default.animation_time, egui_default.scroll_animation);
// The system's answer, the choice, and whether the window then animates.
for (reduced, effects, animates) in
[(false, System, true), (true, System, false), (false, On, true), (true, On, true), (false, Off, false), (true, Off, false)]
{
assert_eq!(smooth.eases(reduced), !at_once, "{smooth:?}, system reduced: {reduced}");
assert_eq!(effects.animates(reduced), animates, "{effects:?}, system reduced: {reduced}");
let ctx = egui::Context::default();
let mut motion = Motion { reduced, ..Default::default() };
let (mut time, mut first) = (0.0, None);
pass(&ctx, &mut time, vec![notch(-1.0, Modifiers::NONE)], |ctx| {
motion.begin_pass(ctx, smooth);
first = Some(ctx.input(|i| i.smooth_scroll_delta));
motion.begin_pass(ctx, effects);
first = Some((ctx.input(|i| i.smooth_scroll_delta), animated(ctx)));
});
assert_eq!(first == Some(vec2(0.0, -line)), at_once, "{smooth:?}, system reduced: {reduced}: {first:?}");
// The wheel, egui's animations and the app's own fades all go the same way.
let whole_notch = first.map(|(scroll, _)| scroll) == Some(vec2(0.0, -line));
assert_eq!(
(whole_notch, first.map(|(_, animated)| animated)),
(!animates, Some(animates)),
"{effects:?}, system reduced: {reduced}: {first:?}"
);
let style = ctx.global_style();
// Scrolling to a place goes with the wheel; menus and sections follow the system alone.
let scroll_to = if at_once { ScrollAnimation::none() } else { egui_default.scroll_animation };
let other = if reduced { 0.0 } else { egui_default.animation_time };
assert_eq!((style.scroll_animation, style.animation_time), (scroll_to, other), "{smooth:?}, system reduced: {reduced}");
let times = if animates { egui_times } else { (0.0, ScrollAnimation::none()) };
assert_eq!((style.animation_time, style.scroll_animation), times, "{effects:?}, system reduced: {reduced}");
}
// A choice made while running applies from the next pass, and choosing back undoes it.
let ctx = egui::Context::default();
let mut motion = Motion::default();
let mut time = 0.0;
for (smooth, scroll_to) in [(Off, ScrollAnimation::none()), (System, egui_default.scroll_animation), (Off, ScrollAnimation::none())] {
pass(&ctx, &mut time, Vec::new(), |ctx| motion.begin_pass(ctx, smooth));
for (effects, times) in [(Off, (0.0, ScrollAnimation::none())), (System, egui_times), (Off, (0.0, ScrollAnimation::none())), (On, egui_times)]
{
pass(&ctx, &mut time, Vec::new(), |ctx| motion.begin_pass(ctx, effects));
let style = ctx.global_style();
assert_eq!((style.scroll_animation, style.animation_time), (scroll_to, egui_default.animation_time), "{smooth:?}");
assert_eq!((style.animation_time, style.scroll_animation), times, "{effects:?}");
}
// The row's labels are translated: the heading, and On and Off as they suit it.
let de = crate::i18n::Lang::from_code("de").expect("German ships");
assert_eq!(crate::i18n::tr(de, "Smooth scrolling"), "Sanftes Scrollen");
let labels: Vec<&str> = SmoothScrolling::ALL.into_iter().map(|choice| crate::i18n::tr_ctx(de, "smooth scrolling", choice.label())).collect();
assert_eq!(crate::i18n::tr(de, "Animation effects"), "Animationseffekte");
let labels: Vec<&str> =
AnimationEffects::ALL.into_iter().map(|choice| crate::i18n::tr_ctx(de, "animation effects", choice.label())).collect();
assert_eq!(labels, ["Systemeinstellung verwenden", "Ein", "Aus"]);
for lang in crate::i18n::Lang::all() {
for english in ["Smooth scrolling", "On", "Off"] {
let shown = if english == "Smooth scrolling" {
for english in ["Animation effects", "On", "Off"] {
let shown = if english == "Animation effects" {
crate::i18n::tr(lang, english)
} else {
crate::i18n::tr_ctx(lang, "smooth scrolling", english)
crate::i18n::tr_ctx(lang, "animation effects", english)
};
assert!(lang.code() == "en" || shown != english, "{}: {english} is not translated", lang.code());
}
}
}
/// The chrome's hover wash has a duration of its own (`theme::Press`): it eases in with
/// animation effects on and is simply there with them off.
#[test]
fn the_hover_wash_eases_only_with_animation_effects_on() {
for (effects, eased) in [(AnimationEffects::On, true), (AnimationEffects::Off, false)] {
let ctx = egui::Context::default();
let mut motion = Motion::default();
let (mut time, mut hover) = (0.0, Vec::new());
for _ in 0..12 {
time += 1.0 / 60.0;
let raw = egui::RawInput { time: Some(time), events: vec![Event::PointerMoved(egui::pos2(20.0, 20.0))], ..Default::default() };
ctx.run_ui(raw, |ui| {
motion.begin_pass(ui.ctx(), effects);
let (_, response) = ui.allocate_exact_size(vec2(100.0, 40.0), egui::Sense::click());
hover.push(crate::theme::Press::track(ui, &response).hover);
})
.textures_delta
.clear();
}
// The first frame the pointer is found over the control.
let first = hover.iter().copied().find(|amount| *amount > 0.0).expect("the pointer is over the control");
assert_eq!(first < 1.0, eased, "{effects:?}: {hover:?}");
assert_eq!(hover.last(), Some(&1.0), "{effects:?}: the wash is whole in the end either way: {hover:?}");
}
}
#[test]
fn a_zoom_read_in_an_earlier_pass_is_never_used_again() {
let ctx = egui::Context::default();
let mut motion = Motion { reduced: true, ..Default::default() };
let mut time = 0.0;
pass(&ctx, &mut time, vec![notch(1.0, Modifiers::COMMAND)], |ctx| {
motion.begin_pass(ctx, SmoothScrolling::System);
motion.begin_pass(ctx, AnimationEffects::System);
assert!(zoom_delta(ctx) > 1.0);
});
// A pass that didn't begin here (a host that draws without `logic`): egui's own answer.
@@ -499,21 +537,21 @@ mod tests {
..Default::default()
};
let mut time = 0.0;
pass(&ctx, &mut time, Vec::new(), |ctx| motion.begin_pass(ctx, SmoothScrolling::System));
pass(&ctx, &mut time, Vec::new(), |ctx| motion.begin_pass(ctx, AnimationEffects::System));
assert!(motion.reduced, "asked in the first pass");
answer.set(Some(false));
for _ in 0..30 {
pass(&ctx, &mut time, Vec::new(), |ctx| motion.begin_pass(ctx, SmoothScrolling::System));
pass(&ctx, &mut time, Vec::new(), |ctx| motion.begin_pass(ctx, AnimationEffects::System));
}
assert!(motion.reduced && asked.get() == 1, "not asked again within half a second ({} times)", asked.get());
// Back at the window later: the first pass asks.
time += ASK_EVERY;
pass(&ctx, &mut time, Vec::new(), |ctx| motion.begin_pass(ctx, SmoothScrolling::System));
pass(&ctx, &mut time, Vec::new(), |ctx| motion.begin_pass(ctx, AnimationEffects::System));
assert!(!motion.reduced);
answer.set(None);
motion.reduced = true;
time += ASK_EVERY;
pass(&ctx, &mut time, Vec::new(), |ctx| motion.begin_pass(ctx, SmoothScrolling::System));
pass(&ctx, &mut time, Vec::new(), |ctx| motion.begin_pass(ctx, AnimationEffects::System));
assert!(motion.reduced && asked.get() == 3, "no answer leaves the last one standing");
}
}
+4 -2
View File
@@ -141,9 +141,11 @@ impl Press {
pub fn track(ui: &egui::Ui, resp: &Response) -> Self {
let hot = resp.hovered() || resp.is_pointer_button_down_on();
let ease = egui::emath::easing::quadratic_out;
// With animation effects off the wash and the press are simply there.
let (hover, down) = if crate::motion::animated(ui.ctx()) { (0.12, 0.06) } else { (0.0, 0.0) };
Self {
hover: ui.ctx().animate_bool_with_time_and_easing(resp.id.with("chrome-hover"), hot, 0.12, ease),
down: ui.ctx().animate_bool_with_time_and_easing(resp.id.with("chrome-down"), resp.is_pointer_button_down_on(), 0.06, ease),
hover: ui.ctx().animate_bool_with_time_and_easing(resp.id.with("chrome-hover"), hot, hover, ease),
down: ui.ctx().animate_bool_with_time_and_easing(resp.id.with("chrome-down"), resp.is_pointer_button_down_on(), down, ease),
}
}
+2 -2
View File
@@ -430,7 +430,7 @@ const PROGRESS_HEIGHT: &str = "progress-notice-height";
/// Draw [`PdfCraftApp::progress_notice`]: the label, a bar that eases towards the fraction with
/// a light sweeping across it, the percentage and Cancel. Returns true when Cancel was clicked.
/// With motion reduced ([`crate::motion`]) the bar steps to the fraction and has no sweep.
/// With animation effects off ([`crate::motion`]) the bar steps to the fraction and has no sweep.
pub fn progress_notice(app: &PdfCraftApp, ctx: &egui::Context) -> bool {
let Some(p) = &app.progress_notice else { return false };
let t = Tokens::get(ctx);
@@ -442,7 +442,7 @@ pub fn progress_notice(app: &PdfCraftApp, ctx: &egui::Context) -> bool {
(Color32::from_rgb(0x2A, 0x2A, 0x2F), Color32::WHITE)
};
let fraction = if p.fraction.is_finite() { p.fraction.clamp(0.0, 1.0) } else { 0.0 };
let animated = !app.motion.reduced;
let animated = crate::motion::animated(ctx);
let shown = ctx.animate_value_with_time(egui::Id::new("progress-notice-bar"), fraction, if animated { 0.3 } else { 0.0 });
let now = ctx.input(|i| i.time);
let mut cancel = false;
+20 -17
View File
@@ -856,37 +856,40 @@ fn preferences_selects_default_workspace_for_next_open() {
assert_eq!(h.state().mode, Mode::Read);
}
/// Preferences ▸ Smooth scrolling: the system's animation setting until the user chooses On or
/// Preferences ▸ Animation effects: the system's animation setting until the user chooses On or
/// Off. The choice is saved, and settings holding anything else leave it alone.
#[test]
fn preferences_choose_smooth_scrolling_and_the_choice_is_saved() {
fn preferences_choose_animation_effects_and_the_choice_is_saved() {
use egui::accesskit::Role;
use pdfcraft_ui_egui::motion::SmoothScrolling;
use pdfcraft_ui_egui::motion::AnimationEffects;
let mut h = harness(|app| app.set_option("dialog", "preferences").unwrap());
assert_eq!(h.state().smooth_scrolling, SmoothScrolling::System);
h.get_by_label("Smooth scrolling");
for (label, choice) in
[("Off", SmoothScrolling::Off), ("On", SmoothScrolling::On), ("Use system setting", SmoothScrolling::System), ("Off", SmoothScrolling::Off)]
{
assert_eq!(h.state().animation_effects, AnimationEffects::System);
h.get_by_label("Animation effects");
for (label, choice) in [
("Off", AnimationEffects::Off),
("On", AnimationEffects::On),
("Use system setting", AnimationEffects::System),
("Off", AnimationEffects::Off),
] {
h.get_by_role_and_label(Role::RadioButton, label).scroll_to_me();
h.run_steps(4);
h.get_by_role_and_label(Role::RadioButton, label).click();
h.run_steps(2);
assert_eq!(h.state().smooth_scrolling, choice, "{label}");
assert_eq!(h.state().animation_effects, choice, "{label}");
}
let saved = h.state().persist();
assert!(saved.contains(r#""smooth_scrolling":"off""#), "{saved}");
assert!(saved.contains(r#""animation_effects":"off""#), "{saved}");
let mut restored = PdfCraftApp::new();
restored.restore(&saved);
assert_eq!(restored.smooth_scrolling, SmoothScrolling::Off);
assert_eq!(restored.animation_effects, AnimationEffects::Off);
let mut fresh = PdfCraftApp::new();
fresh.restore(&saved.replace(r#""smooth_scrolling":"off""#, r#""smooth_scrolling":"sideways""#));
assert_eq!(fresh.smooth_scrolling, SmoothScrolling::System);
fresh.restore(&saved.replace(r#""animation_effects":"off""#, r#""animation_effects":"sideways""#));
assert_eq!(fresh.animation_effects, AnimationEffects::System);
// The control channel and the command line take the same three names.
fresh.set_option("smooth_scrolling", "ON").unwrap();
assert_eq!(fresh.smooth_scrolling, SmoothScrolling::On);
assert!(fresh.set_option("smooth_scrolling", "sideways").unwrap_err().contains("system, on, or off"));
assert_eq!(fresh.smooth_scrolling, SmoothScrolling::On);
fresh.set_option("animation_effects", "ON").unwrap();
assert_eq!(fresh.animation_effects, AnimationEffects::On);
assert!(fresh.set_option("animation_effects", "sideways").unwrap_err().contains("system, on, or off"));
assert_eq!(fresh.animation_effects, AnimationEffects::On);
}
#[test]
+24 -21
View File
@@ -257,12 +257,12 @@ fn precise_touchpad_wheel_scrolls_one_to_one() {
}
/// The 40 frames from one wheel notch on, with the system asking for no animations (Windows'
/// Animation effects turned off) or not, and Preferences ▸ Smooth scrolling as given: how far
/// Animation effects turned off) or not, and Preferences ▸ Animation effects as given: how far
/// page 1 has moved up in each frame, or with ⌘/Ctrl held, the view's zoom.
fn frames_after_a_notch(system_reduced: bool, smooth_scrolling: &str, zooming: bool) -> Vec<f32> {
fn frames_after_a_notch(system_reduced: bool, animation_effects: &str, zooming: bool) -> Vec<f32> {
let mut h = harness_stepping(1.0 / 60.0, &[("layout", "continuous"), ("zoom", "100")]);
h.state_mut().motion.reduced = system_reduced;
h.state_mut().set_option("smooth_scrolling", smooth_scrolling).unwrap();
h.state_mut().set_option("animation_effects", animation_effects).unwrap();
let at = h.state().views[0].viewport_rect().center();
h.hover_at(at);
h.run_steps(2);
@@ -277,52 +277,55 @@ fn frames_after_a_notch(system_reduced: bool, smooth_scrolling: &str, zooming: b
.collect()
}
/// The system's answer and the Smooth scrolling choice, for each way to scroll at once and each
/// way to ease.
/// The system's answer and the Animation effects choice, for each way to have them off and
/// each way to have them on.
const AT_ONCE: [(bool, &str); 3] = [(true, "system"), (false, "off"), (true, "off")];
const EASED: [(bool, &str); 3] = [(false, "system"), (false, "on"), (true, "on")];
#[test]
fn a_wheel_notch_scrolls_at_once_unless_scrolling_is_smooth() {
fn a_wheel_notch_scrolls_at_once_with_animation_effects_off() {
// Eased, egui spreads a notch over about a quarter of a second. At once, the page moves the
// notch's whole distance in one frame.
let notch = pdfcraft_ui_egui::scroll_speed::ScrollSpeed::default().line_points();
for (system_reduced, smooth) in AT_ONCE {
let moved = frames_after_a_notch(system_reduced, smooth, false);
for (system_reduced, effects) in AT_ONCE {
let moved = frames_after_a_notch(system_reduced, effects, false);
// The frame the notch arrives in scrolls; the next one draws the page where it stays.
assert!(
moved[2..].iter().all(|y| (y - notch).abs() < 0.5),
"{smooth}, system reduced {system_reduced}: one notch is {notch} points, all at once: {moved:?}"
"{effects}, system reduced {system_reduced}: one notch is {notch} points, all at once: {moved:?}"
);
}
for (system_reduced, smooth) in EASED {
let moved = frames_after_a_notch(system_reduced, smooth, false);
assert!(moved[2] < notch * 0.75, "{smooth}, system reduced {system_reduced}: egui eases the notch: {moved:?}");
for (system_reduced, effects) in EASED {
let moved = frames_after_a_notch(system_reduced, effects, false);
assert!(moved[2] < notch * 0.75, "{effects}, system reduced {system_reduced}: egui eases the notch: {moved:?}");
assert!(
(moved[39] - notch).abs() < 1.0,
"{smooth}, system reduced {system_reduced}: easing changes how long a notch takes, not how far it goes: {moved:?}"
"{effects}, system reduced {system_reduced}: easing changes how long a notch takes, not how far it goes: {moved:?}"
);
}
}
#[test]
fn the_wheel_zooms_a_whole_step_at_once_unless_scrolling_is_smooth() {
fn the_wheel_zooms_a_whole_step_at_once_with_animation_effects_off() {
// ⌘/Ctrl with the wheel: egui eases the zoom as it eases scrolling.
let step = frames_after_a_notch(true, "system", true)[0];
assert!(step > 1.1, "the notch zoomed in: {step}");
for (system_reduced, smooth) in AT_ONCE {
let zooms = frames_after_a_notch(system_reduced, smooth, true);
for (system_reduced, effects) in AT_ONCE {
let zooms = frames_after_a_notch(system_reduced, effects, true);
assert!(
zooms.iter().all(|zoom| *zoom == step),
"{smooth}, system reduced {system_reduced}: the whole step in the frame the notch arrives, and no more after it: {zooms:?}"
"{effects}, system reduced {system_reduced}: the whole step in the frame the notch arrives, and no more after it: {zooms:?}"
);
}
for (system_reduced, smooth) in EASED {
let zooms = frames_after_a_notch(system_reduced, smooth, true);
assert!(zooms[0] > 1.0 && zooms[0] < 1.0 + (step - 1.0) * 0.75, "{smooth}, system reduced {system_reduced}: egui eases the notch: {zooms:?}");
for (system_reduced, effects) in EASED {
let zooms = frames_after_a_notch(system_reduced, effects, true);
assert!(
zooms[0] > 1.0 && zooms[0] < 1.0 + (step - 1.0) * 0.75,
"{effects}, system reduced {system_reduced}: egui eases the notch: {zooms:?}"
);
assert!(
(zooms[39] / step - 1.0).abs() < 0.01,
"{smooth}, system reduced {system_reduced}: easing changes how long a zoom step takes, not how far it goes: {zooms:?}"
"{effects}, system reduced {system_reduced}: easing changes how long a zoom step takes, not how far it goes: {zooms:?}"
);
}
}
+3 -3
View File
@@ -1,7 +1,7 @@
# PdfCraft roadmap: milestones and what's next
> **Last reviewed:** 2026-10-12 · **Last updated:** 2026-10-12 · **Change:** minor (M2: short same-line redraws split before placement; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (M3: Windows' Animation effects setting is followed, and Preferences has Smooth scrolling; earlier: M10, #1028: OCR reads pictures on pages that have text, on request; earlier: direct Organize overview and page actions; localized shortcut labels, #496; alpha gate added; convert-and-share re-judged with the cross-app rule → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (M3: Windows' Animation effects setting is followed, and Preferences has Animation effects; earlier: M10, #1028: OCR reads pictures on pages that have text, on request; earlier: direct Organize overview and page actions; localized shortcut labels, #496; alpha gate added; convert-and-share re-judged with the cross-app rule → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-11 · **Last updated:** 2026-10-11 · **Change:** minor (M13/#971: bound per-page search match allocation; text cache byte budget and extraction warnings remain open) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (#594 paragraph drags validated in the Linux test environment; localized shortcut labels, #496; alpha gate added; convert-and-share re-judged with the cross-app rule → alpha) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-11 · **Change:** minor (architectural paper controls from #599; M6: respect existing form-font encodings; retain source fonts on appearance fallback; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS)
@@ -79,7 +79,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). 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). |
| M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:<br>- command registry (menus, shortcuts and palette all use it);<br>- undo/redo; incremental, atomic and encrypted saves;<br>- autosave and crash recovery;<br>- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;<br>- CLI `edit/combine/extract/split`.<br>Done since: Organize opens directly into the page overview, with checkbox selection and per-page rotate/delete controls; Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize, Replace pages, bookmarks from tagged headings. Missing: page transitions, recovery on the web |
| 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). Renderer-creation errors return to the existing OpenGL startup fallback; a real-GPU regression covers errors and device reuse. The underlying x86 DX12 shader compiler failure remains unresolved; see `windows-renderer-startup.md`. With Windows' Animation effects turned off, the wheel scrolls and zooms without easing and egui's animations take no time (`motion.rs`; macOS, Linux and the browser aren't asked yet); Preferences ▸ Smooth scrolling (Use system setting, On, Off) decides the wheel's easing on every platform. |
| 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). Renderer-creation errors return to the existing OpenGL startup fallback; a real-GPU regression covers errors and device reuse. The underlying x86 DX12 shader compiler failure remains unresolved; see `windows-renderer-startup.md`. With Windows' Animation effects turned off, the wheel scrolls and zooms without easing and egui's animations take no time (`motion.rs`; macOS, Linux and the browser aren't asked yet); Preferences ▸ Animation effects (Use system setting, On, Off) decides it on every platform. |
| M4 | Engine, history, save, organize | 100–180 | 95% | 8–15 | Done:<br>- command registry (menus, shortcuts and palette all use it);<br>- undo/redo; incremental, atomic and encrypted saves;<br>- autosave and crash recovery;<br>- organize, combine, extract, split and insert-from-file, with identical fonts and images stored once;<br>- CLI `edit/combine/extract/split`.<br>Done since: Combine files in its own tab (sortable, resizable columns; folders; early warnings; unlocking protected files), bookmark editing, page labels (Number pages), CLI `run`, Set Page Boxes, Crop tool, Duplicate pages, Ctrl+A / Cmd+A to select all pages in Organize, Replace pages, bookmarks from tagged headings. Missing: page transitions, recovery on the web |
| M5 | Comments (all annotation types, XFDF) | 120–200 | 88% | 12–25 | Done: notes, highlight/underline/strikeout/squiggly, text boxes, ink, lines, arrows, rectangles, ovals, stamps (dynamic, Sign Here, Standard Business), with appearance streams; replies, status, checkmarks, locking, move/resize/restyle/delete; properties dialog; panel filter and sort; hide all; summaries (comments only); XFDF/FDF import and export (partial imports preserve existing reply and review-status links); flatten; Fill & Sign; agent tools.<br>Done since: polygons, connected lines, clouds, callouts, inserted text, colour and checkmark filters; image signature pointer previews, selection handles and live image movement/resizing with proportional corners and independent edge handles. Missing: custom stamps, replace-text proposals, summary layouts with the page |
| M6 | Forms + JavaScript | 160–320 | 50% | 40–75 | Done: filling all field types with regenerated appearances; Prepare a form (every field type, multi-select/shared properties, move/resize/delete, Field Properties General/Appearance/Position/Options/Format/Validate/Calculate); Acrobat's AF format/keystroke/validate/calculate functions and simplified field notation run natively in Acrobat's event order; tab order; push-button actions; flatten; agent tools. Form data exchange (FDF, XFDF, XML, CSV, text) done.<br>Done since: JavaScript engine (boa, sandboxed) running custom keystroke/validate/calculate/format and button scripts, document JavaScripts, console. Missing: Actions tab, wider object model (annotations, layers, dialogs), XFA |
@@ -142,7 +142,7 @@ M2 (renderer), M7 (content editing), M10 (OCR and Office), M11 (Preflight and st
|---|---|---|
| 2026-10-11 | minor | M10: Word invoice export regressions (#773); estimates unchanged. |
| 2026-10-12 | minor | M2: short same-line redraws split before placement; estimates unchanged. |
| 2026-10-11 | minor | M3: Windows' Animation effects setting is followed: wheel scrolling and zooming without easing, egui's animations off. Preferences ▸ Smooth scrolling (Use system setting, On, Off) overrides it for the wheel. macOS, Linux and the browser aren't asked yet. Estimates unchanged. |
| 2026-10-11 | minor | M3: Windows' Animation effects setting is followed: wheel scrolling and zooming without easing, no other animations. Preferences ▸ Animation effects (Use system setting, On, Off) overrides it. macOS, Linux and the browser aren't asked yet. Estimates unchanged. |
| 2026-10-11 | minor | M13/#971: cap materialized text-search matches to the requested limit plus one continuation match. |
| 2026-10-11 | minor | M7: #594 ordinary paragraph drags validated in the Linux test environment with source-loss baseline failures, UI/engine regressions, native save/reopen and all eight CI gates. Estimates unchanged. |
| 2026-10-11 | minor | M4: direct Organize overview, checkbox selection and individual page actions. Estimates unchanged. |