diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml index 345a33d09..401f2527c 100644 --- a/ATTRIBUTION.toml +++ b/ATTRIBUTION.toml @@ -2,7 +2,7 @@ path = "crates/ui-egui/src/i18n/el.tsv" title = "Greek interface translation catalog" author = "Camthalion77 and contributors" -source = "Contributor-original: clean-room translations of PdfCraft English UI labels" +source = "Contributor-original: clean-room translations of PdfCraft English UI labels; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -25,7 +25,7 @@ sha256 = "53b57cc89461ecc4d48350a4b2ef969c9ba138048a8e28d6a8ee45d549847e1f" path = "crates/ui-egui/src/i18n/zh-hans.tsv" title = "Simplified Chinese interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -36,7 +36,7 @@ sha256 = "1d4b5c40d08e0c3ee328ffee487ff4133627381cf865d21bec682c247e6628dc" path = "crates/ui-egui/src/i18n/zh-hant.tsv" title = "Traditional Chinese (Taiwan) interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -47,7 +47,7 @@ sha256 = "11045efab47ed4a962861f0e3e9568836a8e9ece9ea2e0a4ec58ccf78d2e677f" path = "crates/ui-egui/src/i18n/cs.tsv" title = "Czech interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -58,7 +58,7 @@ sha256 = "cb4c53d7b938a5ec8eb3a001a6c385c60b84f9f1ac36240c490d164046757c2a" path = "crates/ui-egui/src/i18n/pt-br.tsv" title = "Brazilian Portuguese interface translation catalog" author = "André Cardoso (andrecards), PdfCraft contributors" -source = "Clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources: the ~1,826-entry gist posted in issue #288 (https://gist.github.com/andrecards/ac388c25fba17312ce83207f8b7b467e), completed and reviewed by PdfCraft contributors" +source = "Clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources: the ~1,826-entry gist posted in issue #288 (https://gist.github.com/andrecards/ac388c25fba17312ce83207f8b7b467e), completed and reviewed by PdfCraft contributors; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -69,7 +69,7 @@ sha256 = "af4b798c568cb1bb44ef23700e2d69c1e91c7cbb81e6fb565079a797cf02c878" path = "crates/ui-egui/src/i18n/es.tsv" title = "Spanish interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -80,7 +80,7 @@ sha256 = "8fe572236493148b23270ef5766ad1c7d65a58b293757f34772d01a44a2df026" path = "crates/ui-egui/src/i18n/fr.tsv" title = "French interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -91,7 +91,7 @@ sha256 = "6af18efaa6fc8d7d9e813b4411380f1f47879c3e342d800540abec5cd6cb8f81" path = "crates/ui-egui/src/i18n/de.tsv" title = "German interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -102,7 +102,7 @@ sha256 = "2b7787232635373d44b16b93051763b01cf8fa9e6877c87c013ad42171fdc915" path = "crates/ui-egui/src/i18n/te.tsv" title = "Telugu interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -113,7 +113,7 @@ sha256 = "9f67da0f5b455cf99be298e8318721ad6c8d306223e74ad42faf6edd2f8a839b" path = "crates/ui-egui/src/i18n/it.tsv" title = "Italian interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -124,7 +124,7 @@ sha256 = "f7765338632fee507a907cbabc52f72d57d903839f8a4d48b34e2cdcbb28a3bc" path = "crates/ui-egui/src/i18n/pl.tsv" title = "Polish interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -135,7 +135,7 @@ sha256 = "62517afff28af384a42aa377b86582ecddfe8dc8453f06c481246e96184add59" path = "crates/ui-egui/src/i18n/ru.tsv" title = "Russian interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -146,7 +146,7 @@ sha256 = "d46ff4a0505c6374008d55fc4112d4a41508c333411551dad8f6102243bbcccd" path = "crates/ui-egui/src/i18n/hu.tsv" title = "Hungarian interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -157,7 +157,7 @@ sha256 = "61f9efbc72c1e597cdb9a2892bb87e4d0b64c1a7736bc8f7d719e9f18c85da12" path = "crates/ui-egui/src/i18n/bg.tsv" title = "Bulgarian interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -168,7 +168,7 @@ sha256 = "90fac37b3f4c69d16841fec2bc30b66aa3cd0964a493ebdac4f71e40dc424164" path = "crates/ui-egui/src/i18n/kk.tsv" title = "Kazakh interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of the English UI labels into Kazakh, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of the English UI labels into Kazakh, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -179,7 +179,7 @@ sha256 = "18c15dd98f6efe9929d6b00360dc6d15a44bbe6bfde044688bc911011868efeb" path = "crates/ui-egui/src/i18n/ja.tsv" title = "Japanese interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -190,7 +190,7 @@ sha256 = "07b222d5b3fdd155ba57cec323dc9ca290c6e01331bb32f7181f21728362d53b" path = "crates/ui-egui/src/i18n/uk.tsv" title = "Ukrainian interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -201,7 +201,7 @@ sha256 = "b15b5fdca526de6d39d4a22b0c4d54f13eb5df0691ef6c0ea5e4987122ea45be" path = "crates/ui-egui/src/i18n/ar.tsv" title = "Arabic interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -212,7 +212,7 @@ sha256 = "882c07204a95bef324f67f891e76dae0a1a499a420e07929490e6b7f62cf4487" path = "crates/ui-egui/src/i18n/tr.tsv" title = "Turkish interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -223,7 +223,7 @@ sha256 = "7afdd36b96ac5b9c4c1023f506b98af9c559fe85ea00eb7e9518eda9fe77850b" path = "crates/ui-egui/src/i18n/lt.tsv" title = "Lithuanian interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; original catalog reviewed by a native Lithuanian-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels and user-facing messages; original catalog reviewed by a native Lithuanian-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -3378,7 +3378,7 @@ sha256 = "d1a443233345724859de8e4fb48968ba91e885e12ddbd710701548bd91b39428" path = "crates/ui-egui/src/i18n/vi.tsv" title = "Vietnamese interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" @@ -3389,7 +3389,7 @@ sha256 = "2c352743eaeec21eb9e522c71c43f3f6df34b81f024a5c72c85332e2ecc2b338" path = "crates/ui-egui/src/i18n/sl.tsv" title = "Slovene interface translation catalog" author = "PdfCraft contributors" -source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources" +source = "Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources; 2026-10-18 page-gap labels are contributor-original AI-assisted translations from English, not separately native-reviewed" licence = "MIT OR Apache-2.0" licence_file = "LICENSE-MIT" kind = "translation" diff --git a/ROADMAP.md b/ROADMAP.md index 96f6ede4b..584206750 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -180,6 +180,10 @@ Ranked; detail and the full milestone table in [docs/roadmap.md](docs/roadmap.md - **2026-10-19 (M3, #3387):** Save-prompt shortcuts yield to later consent and utility dialogs; a synthetic UI regression checks that an edited document stays open. Estimates unchanged. +- **2026-10-18 (M3, PR #3101 review):** Gap changes in Organize Pages preserve document scroll position; menu and control changes apply to both comparison panes without changing unrelated tabs. Three synthetic UI regressions were observed failing before the fixes. Readiness estimates unchanged. + +- **2026-10-18 (M3, PR #3101):** Optional gapless continuous page display with persisted defaults and UI control support; both new labels translated in all 22 catalogs. Rebased onto current main, preserving navigation and rendering fixes. Readiness estimates unchanged. + - **2026-10-18 (M10, #3120):** Export to Word, PowerPoint, HTML and RTF reads text from form XObjects nested as deep as the renderer draws them (50 levels), and up to 50 000 forms a page (the renderer's budget for nested ones); it stopped at 12 levels and 4 096 forms and left the rest out silently. A page past those limits is listed in `doc_export_office`'s `truncated_pages`, with a warning. Estimates unchanged. - **2026-10-18 (M5, PR #1971 review):** Nested FDF annotation values that identify a source `/Page` or `/Pages` dictionary become null at every copy depth. A synthetic three-page PDF saved and reopened before export verifies that destination actions cannot pull source page content into the FDF, while unrelated nested metadata and shared references remain intact. FDF import semantics, copied-graph limits and estimates are unchanged. diff --git a/apps/pdfcraft/src/main.rs b/apps/pdfcraft/src/main.rs index 721b7da1b..da9d9fe27 100644 --- a/apps/pdfcraft/src/main.rs +++ b/apps/pdfcraft/src/main.rs @@ -8,6 +8,7 @@ //! --theme light|dark|system --language auto| --mode all|read|edit|convert|sign --tool --left open|closed //! --organize on --fields on --dialog properties|shortcuts|about --palette --home on //! --cover on|off --default-layout single|continuous|two-up|two-page --default-continuous on|off --default-zoom fit-width|fit-page| +//! --page-gaps on|off --default-page-gaps on|off //! --combine-view grid|list --combine-zoom 60..200` //! //! `--new-window` opens a window of its own. Without it, on Windows, a launch that only names files diff --git a/crates/engine/src/commands.rs b/crates/engine/src/commands.rs index 9958763d1..bb0dd2806 100644 --- a/crates/engine/src/commands.rs +++ b/crates/engine/src/commands.rs @@ -202,6 +202,7 @@ pub const COMMANDS: &[CommandSpec] = &[ c("view.layout.single", "Single page", None, None, Document, "file-text"), c("view.layout.two_up", "Two-page view", None, None, Document, "columns-2"), c("view.continuous", "Continuous scrolling", None, None, Document, "arrow-up-down"), + c("view.page_gaps", "Show gaps between pages", None, None, Document, "arrow-up-down"), c("view.layout.cover", "Show cover page in two-page view", None, None, TwoPageView, "bookmark"), // Acrobat's view modes, a page display and a zoom at once (the rail's Page display menu). c("view.fit_width_scrolling", "Fit to width scrolling", None, None, Document, "arrow-left-right"), diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 3d78f952d..166a19287 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -169,6 +169,8 @@ pub struct ViewDefaults { pub layout: PageLayout, /// Whether the base layout scrolls continuously. pub continuous: bool, + /// Separate pages with a gutter and shadow; off joins adjacent page edges. + pub show_page_gaps: bool, /// [`Fit::Width`] or [`Fit::Page`], or [`Fit::None`] for a fixed `zoom`. pub fit: Fit, /// The zoom with [`Fit::None`] (1.0 = 100%). @@ -181,7 +183,7 @@ pub struct ViewDefaults { impl Default for ViewDefaults { fn default() -> Self { // Single page, scrolling continuously at fit width: the earlier "continuous" default. - Self { layout: PageLayout::Single, continuous: true, fit: Fit::Width, zoom: 1.0, highlight_fields: false } + Self { layout: PageLayout::Single, continuous: true, show_page_gaps: true, fit: Fit::Width, zoom: 1.0, highlight_fields: false } } } @@ -374,6 +376,7 @@ pub struct DocView { pub layout: PageLayout, /// Whether the base layout scrolls continuously (independent of `layout`, as in Acrobat). pub continuous: bool, + pub show_page_gaps: bool, /// View rotation in degrees clockwise (0, 90, 180, 270); display only, never saved. pub rotation: u16, pub current: usize, @@ -705,6 +708,7 @@ impl DocView { fit: defaults.fit, layout: defaults.layout, continuous: defaults.continuous, + show_page_gaps: defaults.show_page_gaps, rotation: 0, current: 0, organize: false, @@ -858,6 +862,7 @@ impl DocView { fresh.fit = self.fit; fresh.layout = self.layout; fresh.continuous = self.continuous; + fresh.show_page_gaps = self.show_page_gaps; fresh.rotation = self.rotation; fresh.current = self.current; fresh.organize = self.organize; @@ -1392,6 +1397,31 @@ impl DocView { self.continuous } + /// Change page separation without editing the PDF or losing the reader's place. + pub fn set_page_gaps(&mut self, show: bool) { + if self.show_page_gaps == show { + return; + } + if !self.organize && self.viewport_screen.is_positive() { + let at = self.viewport_screen.center(); + if let Some((page, r)) = self + .screen_rects + .iter() + .filter(|(_, r)| r.is_positive()) + .min_by(|(_, a), (_, b)| a.distance_sq_to_pos(at).total_cmp(&b.distance_sq_to_pos(at))) + { + let fraction = (at - r.min) / r.size(); + self.zoom_anchor = Some((*page, fraction.x, fraction.y, at - self.viewport_screen.min)); + } + } + self.show_page_gaps = show; + } + + /// Separation between adjacent page rectangles, in logical screen points. + fn page_gap(&self) -> f32 { + if self.show_page_gaps { GAP } else { 0.0 } + } + /// Turn continuous scrolling on or off, keeping the current page in view. A no-op when it is /// already in that state. pub fn set_continuous(&mut self, continuous: bool) { @@ -1958,9 +1988,9 @@ impl DocView { let avail_w = (self.viewport_w - 2.0 * SIDE).max(100.0); let per_row = if self.layout == PageLayout::TwoUp { 2.0 } else { 1.0 }; match self.fit { - Fit::Width => self.zoom = (avail_w - GAP * (per_row - 1.0)) / (w * PT * per_row), + Fit::Width => self.zoom = (avail_w - self.page_gap() * (per_row - 1.0)) / (w * PT * per_row), Fit::Page => { - let zw = (avail_w - GAP * (per_row - 1.0)) / (w * PT * per_row); + let zw = (avail_w - self.page_gap() * (per_row - 1.0)) / (w * PT * per_row); let zh = (self.viewport_h - 2.0 * MARGIN) / (h * PT); self.zoom = zw.min(zh); } @@ -1981,17 +2011,27 @@ impl DocView { } /// Page rects in content coordinates (origin at the scroll content's top-left). - fn layout(&self, info: &DocInfo, content_w: f32) -> Vec { + fn layout(&self, info: &DocInfo, content_w: f32, ppp: f32) -> Vec { let s = self.zoom * PT; + let gap = self.page_gap(); + let page_size = |p: &pdfcraft_render::PageInfo| { + let (w, h) = self.display_size(p); + if self.show_page_gaps { + vec2(w * s, h * s) + } else { + // Match the raster's whole physical pixels. Snapping each page's origin + // independently with fractional sizes would leave hairline seams. + vec2(device_pixels(w, s * ppp) as f32 / ppp, device_pixels(h, s * ppp) as f32 / ppp) + } + }; let mut rects = Vec::with_capacity(info.pages.len()); let mut y = MARGIN; match self.layout { PageLayout::Single => { for p in &info.pages { - let (w, h) = self.display_size(p); - let size = vec2(w * s, h * s); + let size = page_size(p); rects.push(Rect::from_min_size(pos2(((content_w - size.x) / 2.0).max(SIDE), y), size)); - y += size.y + GAP; + y += size.y + gap; } } PageLayout::TwoUp => { @@ -2002,15 +2042,15 @@ impl DocView { info.pages.chunks(2).collect() }; for (ri, pair) in rows.into_iter().enumerate() { - let sizes: Vec = pair.iter().map(|p| self.display_size(p)).map(|(w, h)| vec2(w * s, h * s)).collect(); - let row_w: f32 = sizes.iter().map(|v| v.x).sum::() + GAP * (sizes.len() as f32 - 1.0); + let sizes: Vec = pair.iter().map(page_size).collect(); + let row_w: f32 = sizes.iter().map(|v| v.x).sum::() + gap * (sizes.len() as f32 - 1.0); let row_h = sizes.iter().map(|v| v.y).fold(0.0, f32::max); - let mut x = if self.cover && ri == 0 { (content_w / 2.0 + GAP / 2.0).max(SIDE) } else { ((content_w - row_w) / 2.0).max(SIDE) }; + let mut x = if self.cover && ri == 0 { (content_w / 2.0 + gap / 2.0).max(SIDE) } else { ((content_w - row_w) / 2.0).max(SIDE) }; for size in sizes { rects.push(Rect::from_min_size(pos2(x, y + (row_h - size.y) / 2.0), size)); - x += size.x + GAP; + x += size.x + gap; } - y += row_h + GAP; + y += row_h + gap; } } } @@ -2506,7 +2546,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { } let content_w = view.content_width(info, avail.width()); - let rects = view.layout(info, content_w); + let rects = view.layout(info, content_w, ui.ctx().pixels_per_point()); // When not scrolling continuously, only one row is shown at a time: the current page, or the // current two-page spread (with the cover rule). These indices drive paging, which pages are // drawn, and the content height (#752). Empty for a document with no pages. @@ -2708,11 +2748,11 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { } else if let Some((page, frac)) = view.goto.take() { let page = page.min(rects.len() - 1); let r = rects[page]; - // Clamped here: egui clamps only after drawing, so a page that fits was drawn MARGIN - GAP + // Clamped here: egui clamps only after drawing, so a page that fits was drawn MARGIN - page_gap() // too high for a frame and then hopped into place. (`avail` is the viewport, as the // scroll bars float.) let max_y = (content_h - avail.height()).max(0.0); - scroll = scroll.vertical_scroll_offset((r.top() - y_shift - GAP + frac * r.height()).max(0.0).min(max_y)); + scroll = scroll.vertical_scroll_offset((r.top() - y_shift - view.page_gap() + frac * r.height()).max(0.0).min(max_y)); // Horizontally too, as Acrobat does (#2665): beside a much wider page the others sit off // to one side. A page already shown whole, or one filling the window's width, keeps the // scroll, so paging through a zoomed-in document stays on the same column. Otherwise a @@ -2926,8 +2966,10 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { current = i; } let xf = PageXform { rect: r, rot: view.rotation, pw: info.pages[i].width.max(1.0), ph: info.pages[i].height.max(1.0) }; - // Soft shadow + paper. - painter.add(egui::epaint::Shadow { offset: [0, 3], blur: 14, spread: 0, color: t.page_shadow }.as_shape(r, CornerRadius::ZERO)); + // Shadows would darken the preceding page when pages touch. + if view.show_page_gaps { + painter.add(egui::epaint::Shadow { offset: [0, 3], blur: 14, spread: 0, color: t.page_shadow }.as_shape(r, CornerRadius::ZERO)); + } painter.rect_filled(r, CornerRadius::ZERO, Color32::WHITE); if let Some(err) = view.errors.get(&i) { painter.rect_filled(r, CornerRadius::ZERO, Color32::from_rgb(0xFB, 0xF4, 0xF4)); @@ -3048,7 +3090,9 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { let page_tex = view.pages.get(&i).map(|p| p.tex.id()); view.move_background.paint(ui, painter, &xf, i, info, &view.content, page_tex); } - painter.rect_stroke(r, CornerRadius::ZERO, Stroke::new(0.5, t.border.gamma_multiply(0.8)), egui::StrokeKind::Outside); + if view.show_page_gaps { + painter.rect_stroke(r, CornerRadius::ZERO, Stroke::new(0.5, t.border.gamma_multiply(0.8)), egui::StrokeKind::Outside); + } // Comments: tools, selection, moving and resizing come before text selection. let pcx = comments::PageCx { page: i, xf: &xf, info, tool, prefs, allowed, hidden: comments_hidden, redact_mark }; @@ -3756,7 +3800,7 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) { // below the window's top). Within a point, as for wheel paging, so layout rounding // can't hide an edge. let max_y = (out.content_size.y - out.inner_rect.height()).max(0.0); - (page, out.state.offset.y <= MARGIN - GAP + 1.0, out.state.offset.y >= max_y - 1.0) + (page, out.state.offset.y <= MARGIN - view.page_gap() + 1.0, out.state.offset.y >= max_y - 1.0) }); view.viewport_screen = out.inner_rect; @@ -6399,7 +6443,7 @@ trailer << /Root 1 0 R >> v.fit_zoom(&info); let narrow = v.zoom; // The shown page fills the width between the side gutters, not centred for the wide page. - let shown = v.layout(&info, v.content_width(&info, 1000.0))[0]; + let shown = v.layout(&info, v.content_width(&info, 1000.0), 1.0)[0]; assert!((shown.left() - SIDE).abs() < 1e-2 && (shown.width() - 860.0).abs() < 1e-2, "{shown:?}"); v.current = 1; v.fit_zoom(&info); diff --git a/crates/ui-egui/src/chrome.rs b/crates/ui-egui/src/chrome.rs index 48d1d3c44..fc66b6df3 100644 --- a/crates/ui-egui/src/chrome.rs +++ b/crates/ui-egui/src/chrome.rs @@ -812,6 +812,10 @@ fn page_display_menu(ui: &mut egui::Ui, view: &DocView) -> Option<&'static str> if ui.add(widgets::checkbox(&mut continuous, tl!("Continuous scrolling"))).changed() { picked = Some("view.continuous"); } + let mut gaps = view.show_page_gaps; + if ui.add(widgets::checkbox(&mut gaps, tl!("Show gaps between pages"))).changed() { + picked = Some("view.page_gaps"); + } let mut cover = view.cover; if ui.add_enabled(view.cover_applies(), widgets::checkbox(&mut cover, tl!("Show cover page in two-page view"))).changed() { picked = Some("view.layout.cover"); diff --git a/crates/ui-egui/src/commands.rs b/crates/ui-egui/src/commands.rs index 49a0e20b7..f75390e9b 100644 --- a/crates/ui-egui/src/commands.rs +++ b/crates/ui-egui/src/commands.rs @@ -251,6 +251,11 @@ impl PdfCraftApp { v.set_continuous(!v.continuous()); } } + "view.page_gaps" => { + if let Some(view) = active.and_then(|i| self.views.get(i)) { + self.set_active_page_gaps(!view.show_page_gaps); + } + } "view.layout.cover" => { if let Some(i) = active { let v = &mut self.views[i]; diff --git a/crates/ui-egui/src/compare_ui.rs b/crates/ui-egui/src/compare_ui.rs index f9c1cbcee..51ca0eff5 100644 --- a/crates/ui-egui/src/compare_ui.rs +++ b/crates/ui-egui/src/compare_ui.rs @@ -163,6 +163,17 @@ pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, state: &Option, } impl PdfCraftApp { + /// Set gaps for the active document and both visible comparison panes, leaving other tabs alone. + pub(crate) fn set_active_page_gaps(&mut self, show: bool) { + let Some((index, id)) = self.active_ids() else { return }; + let partner = side_by_side(self, index).and_then(|(old, _)| self.views.get(old).map(|v| v.id)); + for view in &mut self.views { + if view.id == id || partner == Some(view.id) { + view.set_page_gaps(show); + } + } + } + /// Compare the chosen older document with the active one and show the differences. pub fn run_compare(&mut self) { let Some((i, new)) = self.active_ids() else { return }; @@ -318,17 +329,24 @@ pub(crate) fn compare_area(app: &mut PdfCraftApp, old: usize, new: usize, ui: &m let leader = app.compare.as_ref().map_or(1, |c| c.leader); let (lead, follow) = if leader == 0 { (old, new) } else { (new, old) }; // The follower takes the leader's zoom and layout before either is drawn. - let (zoom, fit, layout, continuous, rotation) = { + let (zoom, fit, layout, continuous, page_gaps, rotation) = { let v = &app.views[lead]; - (v.zoom, v.fit, v.layout, v.continuous, v.rotation) + (v.zoom, v.fit, v.layout, v.continuous, v.show_page_gaps, v.rotation) }; { let v = &mut app.views[follow]; - if v.zoom != zoom || v.fit != fit || v.layout != layout || v.continuous != continuous || v.rotation != rotation { + if v.zoom != zoom + || v.fit != fit + || v.layout != layout + || v.continuous != continuous + || v.show_page_gaps != page_gaps + || v.rotation != rotation + { v.zoom = zoom; v.fit = fit; v.layout = layout; v.continuous = continuous; + v.show_page_gaps = page_gaps; v.rotation = rotation; } } diff --git a/crates/ui-egui/src/control.rs b/crates/ui-egui/src/control.rs index a4ab35d96..aa8cd66d5 100644 --- a/crates/ui-egui/src/control.rs +++ b/crates/ui-egui/src/control.rs @@ -678,6 +678,8 @@ impl Host for crate::PdfCraftApp { "zoom_percent": (v.zoom * 100.0).round(), "fit": format!("{:?}", v.fit), "layout": format!("{:?}", v.layout), + "continuous": v.continuous(), + "page_gaps": v.show_page_gaps, "organize": v.organize, "grid_zoom": v.grid_zoom(), // Pages picked in the organize grid or the Pages panel (empty: the current page). diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv index f6b3d093d..bcedd2158 100644 --- a/crates/ui-egui/src/i18n/ar.tsv +++ b/crates/ui-egui/src/i18n/ar.tsv @@ -138,6 +138,8 @@ Next view العرض التالي Page display عرض الصفحات Continuous scrolling تمرير متواصل + Show gaps between pages إظهار الفراغات بين الصفحات + Hide gaps to join page edges. Margins inside the PDF remain unchanged. أخفِ الفراغات لوصل حواف الصفحات. تبقى الهوامش داخل ملف PDF دون تغيير. Two-page view عرض صفحتين Single page صفحة واحدة Show cover page in two-page view إظهار صفحة الغلاف في عرض الصفحتين diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 90e152c27..ee189fb49 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -966,6 +966,8 @@ Next view Следващ изглед Page display Показване на страниците Continuous scrolling Непрекъснато превъртане + Show gaps between pages Показване на разстоянията между страниците + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Скрийте разстоянията, за да съедините ръбовете на страниците. Полетата вътре в PDF остават непроменени. Two-page view Изглед с две страници Single page Единична страница Show cover page in two-page view Показвай корицата в изглед с две страници diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 1366cb254..e5ea0e063 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -125,6 +125,8 @@ About PdfCraft O aplikaci PdfCraft Auto Automaticky Continuous scrolling Plynulé posouvání + Show gaps between pages Zobrazovat mezery mezi stránkami + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Skrytím mezer spojíte okraje stránek. Okraje uvnitř PDF zůstanou beze změny. Single page Jednostránkové zobrazení Two-page view Dvoustránkové zobrazení Show cover page in two-page view Zobrazit titulní stránku ve dvoustránkovém zobrazení diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index ac136cd91..1368bfe51 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -111,6 +111,8 @@ Next view Nächste Ansicht Page display Seitenanzeige Continuous scrolling Fortlaufendes Scrollen + Show gaps between pages Abstände zwischen Seiten anzeigen + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Blenden Sie die Abstände aus, um die Seitenkanten aneinanderzusetzen. Die Ränder innerhalb der PDF bleiben unverändert. Two-page view Doppelseitenansicht Single page Einzelseite Show cover page in two-page view Deckblatt in Doppelseitenansicht anzeigen diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 133e8ca8e..be05cb11d 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -117,6 +117,8 @@ Next view Επόμενη προβολή Page display Εμφάνιση σελίδων Continuous scrolling Συνεχής κύλιση + Show gaps between pages Εμφάνιση κενών ανάμεσα στις σελίδες + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Αποκρύψτε τα κενά για να ενώσετε τις άκρες των σελίδων. Τα περιθώρια μέσα στο PDF παραμένουν αμετάβλητα. Two-page view Προβολή δύο σελίδων Single page Μονή σελίδα Show cover page in two-page view Εμφάνιση εξωφύλλου στην προβολή δύο σελίδων diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 7c7b7c8d7..e988f0dc4 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -134,6 +134,8 @@ Next view Vista siguiente Page display Presentación de página Continuous scrolling Desplazamiento continuo + Show gaps between pages Mostrar espacios entre páginas + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Oculte los espacios para unir los bordes de las páginas. Los márgenes dentro del PDF no cambian. Two-page view Vista de dos páginas Single page Una sola página Show cover page in two-page view Mostrar la portada en la vista de dos páginas diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index aa83f8197..6f04bee61 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -111,6 +111,8 @@ Next view Affichage suivant Page display Affichage des pages Continuous scrolling Défilement continu + Show gaps between pages Afficher les espaces entre les pages + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Masquez les espaces pour joindre les bords des pages. Les marges à l’intérieur du PDF restent inchangées. Two-page view Affichage sur deux pages Single page Une seule page Show cover page in two-page view Afficher la couverture en affichage sur deux pages diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index 123be343f..1ae6d489e 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -109,6 +109,8 @@ Next view Következő nézet Page display Oldalmegjelenítés Continuous scrolling Folyamatos görgetés + Show gaps between pages Oldalak közötti térköz megjelenítése + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Rejtse el a térközöket az oldalszélek összeillesztéséhez. A PDF-en belüli margók változatlanok maradnak. Two-page view Kétoldalas nézet Single page Egy oldal Show cover page in two-page view Borító megjelenítése kétoldalas nézetben diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 71715dc5d..13df4cb47 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -135,6 +135,8 @@ Next view Vista successiva Page display Visualizzazione pagina Continuous scrolling Scorrimento continuo + Show gaps between pages Mostra spazi tra le pagine + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Nascondi gli spazi per unire i bordi delle pagine. I margini all’interno del PDF rimangono invariati. Two-page view Visualizzazione doppia Single page Pagina singola Show cover page in two-page view Mostra la copertina in visualizzazione doppia diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index f73e753de..1c3338345 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -138,6 +138,8 @@ Next view 次の表示 Page display ページ表示 Continuous scrolling 連続スクロール + Show gaps between pages ページ間の余白を表示 + Hide gaps to join page edges. Margins inside the PDF remain unchanged. ページ間の余白を非表示にしてページの端をつなげます。PDF 内の余白は変更されません。 Two-page view 見開き表示 Single page 単一ページ Show cover page in two-page view 見開き表示で表紙を単独表示 diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv index 7f113d4bf..71390ced8 100644 --- a/crates/ui-egui/src/i18n/kk.tsv +++ b/crates/ui-egui/src/i18n/kk.tsv @@ -102,6 +102,8 @@ Next view Келесі көрініс Page display Беттерді көрсету Continuous scrolling Үздіксіз айналдыру + Show gaps between pages Беттер арасындағы бос орындарды көрсету + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Беттердің шеттерін біріктіру үшін олардың арасындағы бос орындарды жасырыңыз. PDF ішіндегі жиектер өзгеріссіз қалады. Two-page view Екі беттік көрініс Single page Бір бет Show cover page in two-page view Екі беттік көріністе мұқаба бетін көрсету diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv index 6448c6949..223b6b3e8 100644 --- a/crates/ui-egui/src/i18n/lt.tsv +++ b/crates/ui-egui/src/i18n/lt.tsv @@ -74,6 +74,8 @@ Single page Vienas puslapis Two-page view Dviejų puslapių rodinys Continuous scrolling Nepertraukiamas slinkimas + Show gaps between pages Rodyti tarpus tarp puslapių + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Paslėpkite tarpus, kad puslapių kraštai susiliestų. Paraštės PDF viduje nesikeičia. Page display Puslapių rodymas Default page display Numatytasis puslapių rodymas Default zoom Numatytasis mastelis diff --git a/crates/ui-egui/src/i18n/mod.rs b/crates/ui-egui/src/i18n/mod.rs index 13ddd50e7..9da0d44db 100644 --- a/crates/ui-egui/src/i18n/mod.rs +++ b/crates/ui-egui/src/i18n/mod.rs @@ -1011,6 +1011,20 @@ mod tests { } } + /// Both page-gap labels must be translated, even in catalogs without full UI coverage. + #[test] + fn page_gap_labels_are_translated_in_every_catalog() { + for language in LANGUAGES.iter().filter(|language| !language.source.is_empty()) { + let lang = Lang::from_code(language.code).expect("registered language"); + for label in ["Show gaps between pages", "Hide gaps to join page edges. Margins inside the PDF remain unchanged."] { + assert!(has(lang, label), "{} lacks {label:?}", language.code); + let translated = tr(lang, label); + assert!(!translated.trim().is_empty(), "{} has an empty translation for {label:?}", language.code); + assert_ne!(translated, label, "{} falls back to English for {label:?}", language.code); + } + } + } + /// Every catalog says the labels of a visible signature, which are looked up by the sign /// crate's own English words (so the `tl!` scan can't see them), and the note shown when they /// fall back to English. Each label that runs into its value keeps a separator at its end. diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index 578b512bb..8a687a375 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -134,6 +134,8 @@ Next view Następny widok Page display Wyświetlanie stron Continuous scrolling Przewijanie ciągłe + Show gaps between pages Pokaż odstępy między stronami + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Ukryj odstępy, aby połączyć krawędzie stron. Marginesy wewnątrz pliku PDF pozostają bez zmian. Two-page view Widok dwóch stron Single page Pojedyncza strona Show cover page in two-page view Pokaż okładkę w widoku dwóch stron diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index ee12fe7ab..5be2dee67 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -912,6 +912,8 @@ Next view Próxima exibição Page display Exibição de página Continuous scrolling Rolagem contínua + Show gaps between pages Mostrar espaços entre páginas + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Oculte os espaços para unir as bordas das páginas. As margens dentro do PDF permanecem inalteradas. Two-page view Exibição de duas páginas Single page Página única Show cover page in two-page view Mostrar página de capa na exibição de duas páginas diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 3baec4560..b1f9bc528 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -131,6 +131,8 @@ Next view Следующий вид Page display Отображение страниц Continuous scrolling Непрерывная прокрутка + Show gaps between pages Показывать промежутки между страницами + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Скройте промежутки, чтобы соединить края страниц. Поля внутри PDF останутся без изменений. Two-page view Двухстраничный вид Single page Одна страница Show cover page in two-page view Показывать титульную страницу в двухстраничном виде diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv index 5ba9dc90c..3f027cf53 100644 --- a/crates/ui-egui/src/i18n/sl.tsv +++ b/crates/ui-egui/src/i18n/sl.tsv @@ -442,6 +442,8 @@ Content copying for accessibility Kopiranje vsebine za dostopnost Continue Nadaljuj Continuous scrolling Neprekinjeno pomikanje + Show gaps between pages Pokaži razmike med stranmi + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Skrijte razmike, da združite robove strani. Robovi znotraj PDF-ja ostanejo nespremenjeni. Contributors Sodelavci Convert Pretvori Convert colors Pretvori barve diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv index e0d6a2b34..df96618da 100644 --- a/crates/ui-egui/src/i18n/te.tsv +++ b/crates/ui-egui/src/i18n/te.tsv @@ -115,6 +115,8 @@ comment menu Edit సవరించు Next view తదుపరి వీక్షణ Page display పేజీ ప్రదర్శన Continuous scrolling నిరంతర స్క్రోలింగ్ + Show gaps between pages పేజీల మధ్య ఖాళీలను చూపు + Hide gaps to join page edges. Margins inside the PDF remain unchanged. పేజీల అంచులను కలపడానికి ఖాళీలను దాచండి. PDF లోపలి మార్జిన్లు మారవు. Two-page view రెండు పేజీల వీక్షణ Single page ఒకే పేజీ Show cover page in two-page view రెండు పేజీల వీక్షణలో కవర్ పేజీని చూపించు diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 48d34a30d..afaa28491 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -96,6 +96,8 @@ Next view Sonraki görünüm Page display Sayfa görünümü Continuous scrolling Sürekli kaydırma + Show gaps between pages Sayfalar arasındaki boşlukları göster + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Sayfa kenarlarını birleştirmek için boşlukları gizleyin. PDF içindeki kenar boşlukları değişmez. Two-page view İki sayfa görünümü Single page Tek sayfa Show cover page in two-page view İki sayfa görünümünde kapak sayfasını göster diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 246a4a2c5..8dc752b20 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -136,6 +136,8 @@ Next view Наступний вигляд Page display Відображення сторінок Continuous scrolling Безперервне прокручування + Show gaps between pages Показувати проміжки між сторінками + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Приховайте проміжки, щоб з’єднати краї сторінок. Поля всередині PDF залишаться без змін. Two-page view Дві сторінки Single page Одна сторінка Show cover page in two-page view Показувати обкладинку в режимі двох сторінок diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index 56caf3885..4dcb69cad 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -78,6 +78,8 @@ Single page Một trang Two-page view Chế độ hai trang Continuous scrolling Cuộn liên tục + Show gaps between pages Hiện khoảng cách giữa các trang + Hide gaps to join page edges. Margins inside the PDF remain unchanged. Ẩn khoảng cách để nối các mép trang. Lề bên trong PDF vẫn giữ nguyên. Page display Hiển thị trang Default page display Hiển thị trang mặc định Default zoom Thu phóng mặc định diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 645c0ab30..7745cdae2 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -973,6 +973,8 @@ Next view 下一个视图 Page display 页面显示 Continuous scrolling 连续滚动 + Show gaps between pages 显示页面间距 + Hide gaps to join page edges. Margins inside the PDF remain unchanged. 隐藏间距以连接页面边缘。PDF 内的页边距保持不变。 Two-page view 双页视图 Single page 单页 Show cover page in two-page view 在双页视图中显示封面页 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 712f0666a..bd0a88e3a 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -917,6 +917,8 @@ Next view 下一個檢視 Page display 頁面顯示 Continuous scrolling 連續捲動 + Show gaps between pages 顯示頁面間距 + Hide gaps to join page edges. Margins inside the PDF remain unchanged. 隱藏間距以連接頁面邊緣。PDF 內的頁邊距保持不變。 Two-page view 雙頁檢視 Single page 單頁 Show cover page in two-page view 在雙頁檢視中顯示封面 diff --git a/crates/ui-egui/src/js_ui.rs b/crates/ui-egui/src/js_ui.rs index df461fc6e..d5e3dbc3d 100644 --- a/crates/ui-egui/src/js_ui.rs +++ b/crates/ui-egui/src/js_ui.rs @@ -351,6 +351,8 @@ pub(crate) fn preferences_body(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tok } ui.add(widgets::checkbox(&mut defaults.continuous, tl!("Continuous scrolling"))); }); + ui.add(widgets::checkbox(&mut defaults.show_page_gaps, tl!("Show gaps between pages"))); + ui.label(egui::RichText::new(tl!("Hide gaps to join page edges. Margins inside the PDF remain unchanged.")).small().color(t.text_muted)); ui.horizontal(|ui| { use crate::canvas::Fit; ui.label(tl!("Default zoom")); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index c3b882779..c22179414 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -2316,6 +2316,7 @@ impl PdfCraftApp { "right_panel_closed": self.right_panel_closed, }); // Set apart: one more key inside the macro above passes serde_json's recursion limit. + v["default_page_gaps"] = serde_json::Value::Bool(self.view_defaults.show_page_gaps); v["redact_prefs"] = self.redact_defaults.to_json(); // Set outside json! above, which is at serde_json's macro recursion limit. v["hide_empty_highlight_hover"] = serde_json::Value::Bool(self.comment_prefs.hide_empty_highlight_hover); @@ -2378,6 +2379,9 @@ impl PdfCraftApp { if let Some((layout, continuous)) = v["default_layout"].as_str().and_then(canvas::parse_display_token) { (self.view_defaults.layout, self.view_defaults.continuous) = (layout, continuous); } + if let Some(show) = v["default_page_gaps"].as_bool() { + self.view_defaults.show_page_gaps = show; + } if let Ok(units) = serde_json::from_value::(v["units"].clone()) { self.units = units; } @@ -2610,6 +2614,21 @@ impl PdfCraftApp { "off" => v.set_continuous(false), _ => return Err("continuous must be on or off".into()), }, + ("page-gaps", Some(_)) => { + let show = match value { + "on" => true, + "off" => false, + _ => return Err("page-gaps must be on or off".into()), + }; + self.set_active_page_gaps(show); + } + ("default-page-gaps", _) => { + self.view_defaults.show_page_gaps = match value { + "on" => true, + "off" => false, + _ => return Err("default-page-gaps must be on or off".into()), + }; + } ("cover", Some(v)) => { let on = match value { "on" => true, @@ -2787,6 +2806,7 @@ impl PdfCraftApp { "zoom", "layout", "continuous", + "page-gaps", "cover", "organize", "grid-zoom", diff --git a/crates/ui-egui/tests/view.rs b/crates/ui-egui/tests/view.rs index 39ea83df7..ed4784dad 100644 --- a/crates/ui-egui/tests/view.rs +++ b/crates/ui-egui/tests/view.rs @@ -146,6 +146,169 @@ fn continuous_layout_stacks_pages_vertically() { assert!((a.center().x - b.center().x).abs() < 1.0, "same column"); } +/// The toggle joins page edges while preserving zoom and leaving the PDF unmodified. +#[test] +fn page_gaps_toggle_joins_edges_without_changing_the_document() { + let mut h = harness(&[("layout", "continuous"), ("zoom", "50")]); + let zoom = h.state().views[0].zoom; + let original = rect(&h, 1).unwrap().top() - rect(&h, 0).unwrap().bottom(); + assert!(original > 1.0); + h.state_mut().set_option("page-gaps", "off").unwrap(); + h.run_steps(4); + for page in 0..2 { + let (a, b) = (rect(&h, page).unwrap(), rect(&h, page + 1).unwrap()); + assert!((b.top() - a.bottom()).abs() < 0.01, "pages must touch: {a:?} {b:?}"); + } + assert_eq!(h.state().views[0].zoom, zoom); + let id = h.state().views[0].id; + assert!(!h.state().session.get(id).unwrap().dirty, "a view option must not edit the PDF"); + assert!(h.state_mut().execute("view.page_gaps")); + h.run_steps(4); + assert!(((rect(&h, 1).unwrap().top() - rect(&h, 0).unwrap().bottom()) - original).abs() < 0.01); +} + +/// Changing gaps in the organizer must not anchor document pages to the grid viewport. +#[test] +fn page_gaps_in_organizer_do_not_move_the_document_scroll_position() { + let mut h = harness(&[("layout", "continuous"), ("zoom", "100"), ("page", "2")]); + let before = rect(&h, 1).unwrap(); + let gap = before.top() - rect(&h, 0).unwrap().bottom(); + assert!(gap > 1.0); + assert!(h.state_mut().execute("page.organize")); + h.run_steps(4); + assert!(h.state().views[0].organize); + assert!(h.state_mut().execute("view.page_gaps")); + h.run_steps(2); + assert!(!h.state().views[0].show_page_gaps); + assert!(h.state_mut().execute("page.organize")); + h.run_steps(4); + let after = rect(&h, 1).unwrap(); + // Keeping the scroll offset moves page 2 up by exactly the removed gap before it. + assert!((after.top() - before.top() + gap).abs() < 1.0, "stale grid anchoring moved the page: {before:?} -> {after:?}, gap={gap}"); + assert_eq!(h.state().views[0].current, 1); +} + +/// A three-page comparison, with the old pane leading while the new document stays active. +fn page_gaps_comparison_harness() -> Harness<'static, PdfCraftApp> { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.set_option("left", "closed").unwrap(); + app.open_bytes("old.pdf", None, PAGES.to_vec()).unwrap(); + app.open_bytes("new.pdf", None, PAGES.to_vec()).unwrap(); + app.set_option("zoom", "50").unwrap(); + app.compare_old = Some(app.views[0].id); + app.run_compare(); + app + }); + h.run_steps(4); + let at = h.state().views[0].viewport_rect().center(); + h.event(egui::Event::PointerMoved(at)); + h.run_steps(2); + assert_eq!(h.state().compare.as_ref().unwrap().leader, 0); + assert_eq!(h.state().active, Some(1)); + h +} + +/// A page-display menu choice must reach both panes before the old leader synchronizes them. +#[test] +fn page_gaps_comparison_menu_preserves_the_active_panes_choice() { + use egui_kittest::kittest::Queryable; + let mut h = page_gaps_comparison_harness(); + h.get_by_label_contains("Page display").click(); + h.run_steps(2); + h.get_by_label("Show gaps between pages").click(); + h.run_steps(4); + assert!(h.state().views.iter().all(|v| !v.show_page_gaps), "the old leader must not restore gaps after the active pane's menu action"); +} + +/// The control setting has the same synchronized behavior and leaves unrelated views alone. +#[test] +fn page_gaps_comparison_control_preserves_both_states() { + let mut h = page_gaps_comparison_harness(); + for (value, show) in [("off", false), ("on", true)] { + h.state_mut().set_option("page-gaps", value).unwrap(); + h.run_steps(4); + assert!(h.state().views.iter().all(|v| v.show_page_gaps == show), "comparison rejected page-gaps={value}"); + } + h.state_mut().open_bytes("unrelated.pdf", None, PAGES.to_vec()).unwrap(); + h.state_mut().set_option("page-gaps", "off").unwrap(); + assert!(h.state().views[0].show_page_gaps && h.state().views[1].show_page_gaps); + assert!(!h.state().views[2].show_page_gaps); +} + +/// The saved default survives restart and applies to newly opened documents. +#[test] +fn page_gaps_default_survives_restart_and_applies_to_new_documents() { + let mut app = PdfCraftApp::new(); + app.set_option("default-page-gaps", "off").unwrap(); + let mut restored = PdfCraftApp::new(); + restored.restore(&app.persist()); + restored.open_bytes("pages.pdf", None, PAGES.to_vec()).unwrap(); + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| restored); + h.run_steps(8); + h.state_mut().set_option("zoom", "50").unwrap(); + h.run_steps(4); + assert!((rect(&h, 1).unwrap().top() - rect(&h, 0).unwrap().bottom()).abs() < 0.01); + assert!(h.state_mut().set_option("page-gaps", "invalid").is_err()); +} + +/// The real menu action removes gutters, shadows and outlines from the seam pixels. +#[test] +fn page_gaps_menu_joins_pages_without_a_shadow_at_the_seam() { + use egui_kittest::kittest::Queryable; + let _gpu = gpu(); + let mut h = harness(&[("layout", "continuous"), ("zoom", "50"), ("theme", "light")]); + h.get_by_label_contains("Page display").click(); + h.run_steps(3); + h.get_by_label("Show gaps between pages").click(); + h.run_steps(4); + let (a, b) = (rect(&h, 0).unwrap(), rect(&h, 1).unwrap()); + assert!((b.top() - a.bottom()).abs() < 0.01); + let ppp = h.ctx.pixels_per_point(); + let image = h.render().expect("renders the gapless view"); + for dy in [-3.0, 0.0, 3.0] { + let pixel = image.get_pixel((a.center().x * ppp) as u32, ((a.bottom() + dy) * ppp) as u32); + assert_eq!(*pixel, image::Rgba([255, 255, 255, 255]), "no gap or shadow at the seam"); + } +} + +/// Gapless geometry supports mixed sizes and two-page rows and survives document reload. +#[test] +fn page_gaps_work_for_mixed_sizes_and_two_page_rows() { + let h = mixed_harness(&[("layout", "continuous"), ("zoom", "50"), ("page-gaps", "off")]); + let (a, b) = (rect(&h, 0).unwrap(), rect(&h, 1).unwrap()); + assert!((b.top() - a.bottom()).abs() < 0.01); + assert!((a.center().x - b.center().x).abs() < 0.01); + let mut h = harness(&[("layout", "two-up"), ("zoom", "50"), ("page-gaps", "off")]); + let (a, b, c) = (rect(&h, 0).unwrap(), rect(&h, 1).unwrap(), rect(&h, 2).unwrap()); + assert!((b.left() - a.right()).abs() < 0.01); + assert!((c.top() - a.bottom()).abs() < 0.01); + let info = h.state().session.get(h.state().views[0].id).unwrap().info.clone(); + h.state_mut().views[0].document_replaced(&info); + assert!(!h.state().views[0].show_page_gaps, "a file reload preserves the view setting"); +} + +/// Page edges stay adjacent at integer and fractional physical-pixel scales. +#[test] +fn page_gaps_stay_closed_at_fractional_device_scales() { + for ppp in [1.0, 1.25, 1.5, 2.0] { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).with_pixels_per_point(ppp).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.open_bytes("pages.pdf", None, PAGES.to_vec()).unwrap(); + for (key, value) in [("left", "closed"), ("panel", "none"), ("layout", "continuous"), ("zoom", "43"), ("page-gaps", "off")] { + app.set_option(key, value).unwrap(); + } + app + }); + h.run_steps(8); + for page in 0..2 { + let (a, b) = (rect(&h, page).unwrap(), rect(&h, page + 1).unwrap()); + assert!((b.top() - a.bottom()).abs() < 0.01, "{ppp} pixels/point: {a:?} {b:?}"); + } + } +} + #[test] fn two_up_layout_places_pages_side_by_side() { let mut h = harness(&[("layout", "two-up"), ("zoom", "50")]); diff --git a/docs/page-display.md b/docs/page-display.md new file mode 100644 index 000000000..fa2c7f1aa --- /dev/null +++ b/docs/page-display.md @@ -0,0 +1,32 @@ +# Continuous page display + +> **Last updated:** 2026-10-18 · Viewer: optional page gaps. + +To read pages as a continuous strip, open **View → Page display**, choose **Single page**, +turn **Continuous scrolling** on, and turn **Show gaps between pages** off. The Page display +button on the viewer rail opens the same choices. The command palette also offers +**Show gaps between pages** as a toggle. + +Hiding gaps joins the displayed page edges and removes page shadows. It does not crop or +change the PDF: white margins already in a page remain. Pages of different widths stay +centered. In two-page view, it removes both the gutter within a row and the gap between rows. + +For future documents, open **Preferences → Documents and view** and uncheck +**Show gaps between pages**. This default survives a restart. Existing documents retain their +current view; the per-document toggle does not change the preference for future documents. + +Desktop launch options: + +```sh +pdfcraft --layout continuous --page-gaps off document.pdf +pdfcraft --default-page-gaps off +``` + +The opt-in UI control channel accepts `set key=page-gaps value=off` for the active document +and `set key=default-page-gaps value=off` for future documents. Its `command` method accepts +`id=view.page_gaps`, and `state` reports `active.page_gaps` and `active.continuous`. + +Revision history: 2026-10-17 — added local gapless-display controls and documentation. + +In a side-by-side comparison, the gap setting applies to both panes. Changing it in +Organize Pages keeps your document scroll position when you return to the viewer. diff --git a/docs/roadmap.md b/docs/roadmap.md index 201cc1d6c..033d468f6 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,7 @@ # PdfCraft roadmap: milestones and what's next > **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M5, PR #1971: bounded nested FDF export excludes source page/page-tree dictionaries; refreshed against upstream 4db6ac4a; estimates unchanged) +> **Last reviewed:** 2026-10-18 · **Last updated:** 2026-10-18 · **Change:** minor (M3, PR #3101: optional gapless page display with translations in all 22 catalogs; organizer and comparison toggles corrected; estimates unchanged) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M14/#2383 GPU-limit candidate; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M7: the Edit text caret is as tall as the row it is on; estimates unchanged) · **Target:** Adobe Acrobat Pro (Acrobat DC, continuous track 26.002.21931, macOS) > **Last reviewed:** 2026-10-15 · **Last updated:** 2026-10-15 · **Change:** minor (M6: CSV tab-containing headers; estimates unchanged) @@ -116,7 +117,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). 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. Optional page gaps join page edges without cropping PDF margins; current-document toggle, persisted default and UI control options are covered by viewer tests. Gap changes in Organize Pages preserve document scroll position, and both comparison panes follow menu and control choices. | | M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). Selected-text Copy in Edit and the canvas context menu is covered by focused regressions and native clipboard checks (#1225); document copy restrictions apply to the floating action too. #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Page-label navigation displays logical labels, retains the current page on unchanged submission and refreshes focused same-document replacements; clean file-watcher reload preserves the chosen scrolling mode, preventing a Single Page reader from jumping to page 1 during replacement. Synthetic control regressions cover these behaviors. Wrapped revision buttons stay above Binding in Document Properties (#2739); a synthetic UI test checks layout and saved revision content. | | M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. Later native-open replies stay queued while a modal or picker owns the foreground; regressions cover recipient ownership, completion/cancellation and one-shot wakes. | | M3 | Viewer app (native + web) | 80–150 | 90% | 10–20 | Acrobat-style shell, find, select, panels, tiles, web build, UI control channel for agents (opt-in). Missing: 60 fps test on a 500-page document, snapshot tests of every panel
Done since: fit visible, document title in the window; Linux middle-button scrolling; shared three-state theme preferences in the toolbar, View menu and control channel; numeric Linux portal events and idle wakeups completing #881, with native appearance inheritance retained (#446); late browser startup downloads preserve the current document and Save target (#171). #1364 candidate: absolute local GoToR links ask before target access; headless callers explicitly opt in. Native-opening candidate: chosen tools survive preparation and password retries; startup intake waits for pending preparation. #1980: exact quit modifiers preserve Ctrl+Alt+Q email input; Ctrl+Q/Cmd+Q asks before discarding unsaved documents. | diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml index ad00b0115..b9283ccff 100644 --- a/parity/acrobat-features.toml +++ b/parity/acrobat-features.toml @@ -852,10 +852,10 @@ area = "B" tier = "P0" milestone = "M3" status = "shipped" -commands = ["view.continuous"] +commands = ["view.continuous", "view.page_gaps"] tools = [] -evidence = ["crates/ui-egui/tests/view.rs::continuous_layout_stacks_pages_vertically", "crates/ui-egui/tests/view.rs::page_display_commands_switch_layouts", "crates/ui-egui/tests/view.rs::default_page_display_is_used_for_new_documents", "crates/ui-egui/src/canvas.rs::fit_page_and_height_hold_still_while_mixed_page_sizes_scroll_past", "crates/ui-egui/tests/view.rs::touchpad_swipe_glides_on_after_the_fingers_lift_and_stops", "crates/ui-egui/tests/view.rs::touching_the_touchpad_again_or_turning_animations_off_stops_the_glide", "crates/ui-egui/tests/view.rs::rapid_wheel_notches_accelerate_and_follow_touchpad_input", "crates/ui-egui/tests/view.rs::changing_continuous_layout_resets_wheel_acceleration"] -notes = "the default for PDFs that don't ask for a layout (Preferences ▸ Default page display); pages stack with no snapping. Fit page and Fit height fit the largest page, so the zoom holds still across mixed page sizes. Rapid line-wheel notches gain a bounded, constant-space arrival-rate boost (#1072); isolated notches still use the chosen Scroll speed, and page units stay unboosted. A touchpad swipe glides on after the fingers lift on Wayland (#531; macOS glides by itself; Windows and X11 report no lift through winit), unless Animation effects is off; a click, key, zoom or new scroll catches it. Not yet: fingers resting on the touchpad don't catch it (needs Wayland hold gestures, which the vendored winit doesn't bind)" +evidence = ["crates/ui-egui/tests/view.rs::page_gaps_in_organizer_do_not_move_the_document_scroll_position", "crates/ui-egui/tests/view.rs::page_gaps_comparison_menu_preserves_the_active_panes_choice", "crates/ui-egui/tests/view.rs::page_gaps_comparison_control_preserves_both_states", "crates/ui-egui/tests/view.rs::page_gaps_stay_closed_at_fractional_device_scales", "crates/ui-egui/tests/view.rs::page_gaps_toggle_joins_edges_without_changing_the_document", "crates/ui-egui/tests/view.rs::page_gaps_default_survives_restart_and_applies_to_new_documents", "crates/ui-egui/tests/view.rs::page_gaps_menu_joins_pages_without_a_shadow_at_the_seam", "crates/ui-egui/tests/view.rs::page_gaps_work_for_mixed_sizes_and_two_page_rows", "crates/ui-egui/tests/view.rs::continuous_layout_stacks_pages_vertically", "crates/ui-egui/tests/view.rs::page_display_commands_switch_layouts", "crates/ui-egui/tests/view.rs::default_page_display_is_used_for_new_documents", "crates/ui-egui/src/canvas.rs::fit_page_and_height_hold_still_while_mixed_page_sizes_scroll_past", "crates/ui-egui/tests/view.rs::touchpad_swipe_glides_on_after_the_fingers_lift_and_stops", "crates/ui-egui/tests/view.rs::touching_the_touchpad_again_or_turning_animations_off_stops_the_glide", "crates/ui-egui/tests/view.rs::rapid_wheel_notches_accelerate_and_follow_touchpad_input", "crates/ui-egui/tests/view.rs::changing_continuous_layout_resets_wheel_acceleration"] +notes = "Page gaps, shadows and outlines can be hidden through the page-display menu, with a persisted default in Preferences and matching UI control options; page margins in the PDF are unchanged. Continuous scrolling remains the default for PDFs that don't ask for a layout (Preferences ▸ Default page display); pages stack with no snapping. Fit page and Fit height fit the largest page, so the zoom holds still across mixed page sizes. Rapid line-wheel notches gain a bounded, constant-space arrival-rate boost (#1072); isolated notches still use the chosen Scroll speed, and page units stay unboosted. A touchpad swipe glides on after the fingers lift on Wayland (#531; macOS glides by itself; Windows and X11 report no lift through winit), unless Animation effects is off; a click, key, zoom or new scroll catches it. Not yet: fingers resting on the touchpad don't catch it (needs Wayland hold gestures, which the vendored winit doesn't bind)" [[feature]] id = "view.layout-single-page"