From 06d29fe3a63f4447879dc4928f85bcf87a5aec36 Mon Sep 17 00:00:00 2001 From: texjer Date: Fri, 30 Oct 2026 12:24:07 -0400 Subject: [PATCH] Parents: single-sided pages in a spread show the parent page in their place --- ROADMAP.md | 1 + crates/doc/src/build.rs | 13 ++++++++++--- crates/doc/src/page.rs | 10 ++++++++++ crates/idml/src/import/spread.rs | 6 +++--- crates/idml/src/tests/facing_ltr.rs | 23 ++++++++++++++++++++++- 5 files changed, 46 insertions(+), 7 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index dfc46b121..a2f19c725 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -388,6 +388,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-30** · Without facing pages, a page of a multi-page spread shows the parent page in its place, as in InDesign: the second page of a two-page spread shows the parent's second page. Every single-sided page showed the parent's first page, so an InDesign book set up without facing pages but laid out in two-page spreads repeated the left parent page's items (a black heading bar) on its right pages and lost the right parent page's. No readiness percentage change. - **2026-10-30** · File > Package opens a dialog, as the reference app's: a Summary of fonts, links and images and inks, pages for each, a Printing Instructions step, and a PDF made with the options of a preset (#1514, #1515). - **2026-10-30** · IDML import reads TOC separators and entry styles written with the `$ID/` prefix or as `Properties` children, and the TOC separator notation (`^t`, `^y`, `^m`, `^>`) is shared by the dialog and IDML, with a special-character menu on the separator field (#1249, #1255). - **2026-10-29** · Inserting text with many returns no longer takes time that grows with their square (part of #1588): each return copied its paragraph's format with its own insert, moving every format after it. Splitting 20,000 returns into paragraphs took 15.8 s; placing a text file of them now takes 0.2 s, and one of a million returns 5.5 s. No readiness percentage change. diff --git a/crates/doc/src/build.rs b/crates/doc/src/build.rs index c7259f317..1789092ff 100644 --- a/crates/doc/src/build.rs +++ b/crates/doc/src/build.rs @@ -661,11 +661,18 @@ impl Document { } /// Parent page (spread index, page index) applied to `page`: the page on its side of its - /// parent spread. + /// parent spread, or in its place for single-sided pages ([`Spread::parent_page_at`]). pub fn parent_page_of(&self, page: &Page) -> Option<(usize, usize)> { let pi = self.parent_index(page.parent?)?; // A parent spread can come without pages from a file. - Some((pi, self.parents.get(pi)?.parent_page_for_side(page.side)?)) + let parent = self.parents.get(pi)?; + // Only a single-sided page's position matters: look for it only then. + let index = if page.side == PageSide::Single && parent.pages.len() >= 2 { + self.spreads.iter().find_map(|s| s.pages.iter().position(|p| p.id == page.id)).unwrap_or(0) + } else { + 0 + }; + Some((pi, parent.parent_page_at(page.side, index)?)) } /// The parent page applied to `page`. @@ -777,7 +784,7 @@ impl Document { c } }; - chain.into_iter().filter_map(|i| Some((i, self.parents.get(i)?.parent_page_for_side(pg.side)?))).collect() + chain.into_iter().filter_map(|i| Some((i, self.parents.get(i)?.parent_page_at(pg.side, page)?))).collect() } /// Whether basing parent `pi` on `base` would make a `based_on` cycle (or names no parent). diff --git a/crates/doc/src/page.rs b/crates/doc/src/page.rs index 7041f7ed8..caf1c102e 100644 --- a/crates/doc/src/page.rs +++ b/crates/doc/src/page.rs @@ -452,6 +452,16 @@ impl Spread { PageSide::Single => Some(0), } } + /// The parent page shown behind page `index` of a document spread, on `side`: by side for + /// facing pages ([`Self::parent_page_for_side`]); by position for single-sided pages in a + /// spread of several, as InDesign does, so the second page of a two-page spread shows the + /// parent's second page (the last one, for spreads longer than the parent). + pub fn parent_page_at(&self, side: PageSide, index: usize) -> Option { + match side { + PageSide::Single if self.pages.len() >= 2 => Some(index.min(self.pages.len() - 1)), + _ => self.parent_page_for_side(side), + } + } /// Index of the page under spread x (nearest page for the pasteboard). pub fn page_at_x(&self, x: f64) -> Option { if self.pages.is_empty() { diff --git a/crates/idml/src/import/spread.rs b/crates/idml/src/import/spread.rs index e2caa6b18..ca9a3d746 100644 --- a/crates/idml/src/import/spread.rs +++ b/crates/idml/src/import/spread.rs @@ -70,7 +70,7 @@ impl<'r> Importer<'r> { } /// A page's margins and columns, and whether they are local (differ from its parent page's). - fn page_margins(&self, p: &El, r: &Rect, master: bool, parent: Option, side: PageSide) -> (Margins, Columns, bool) { + fn page_margins(&self, p: &El, r: &Rect, master: bool, parent: Option, (side, index): (PageSide, usize)) -> (Margins, Columns, bool) { // A document page's margins are local when they differ from its parent page's; a // page without its own takes the parent page's. let from_parent = if master { @@ -78,7 +78,7 @@ impl<'r> Importer<'r> { } else { parent .and_then(|id| self.parents.iter().find(|sp| sp.id == id)) - .and_then(|sp| sp.pages.get(sp.parent_page_for_side(side)?)) + .and_then(|sp| sp.pages.get(sp.parent_page_at(side, index)?)) .map(|q| (q.margins, q.columns.clone())) }; let (margins, columns) = match (p.find("MarginPreference"), &from_parent) { @@ -126,7 +126,7 @@ impl<'r> Importer<'r> { PageSide::Right }; let parent = p.get("AppliedMaster").and_then(|m| self.parent_ids.get(m)).copied(); - let (margins, columns, local_margins) = self.page_margins(p, r, master, parent, side); + let (margins, columns, local_margins) = self.page_margins(p, r, master, parent, (side, i)); out_pages.push(Page { id: pid, width: r.width(), diff --git a/crates/idml/src/tests/facing_ltr.rs b/crates/idml/src/tests/facing_ltr.rs index b1c322ecc..45d1dc065 100644 --- a/crates/idml/src/tests/facing_ltr.rs +++ b/crates/idml/src/tests/facing_ltr.rs @@ -27,6 +27,11 @@ fn story(id: &str, text: &str) -> String { /// right page a guide at 300 and a "Right head". Three pages: 1 alone, then 2–3. Rulers measure /// from each page. fn left_to_right_fixture() -> Vec { + fixture(true) +} + +/// [`left_to_right_fixture`], with facing pages on or off. +fn fixture(facing: bool) -> Vec { let margins = r#""#; let page = |id: &str, x: f64, applied: bool, guide: Option| { let applied = if applied { r#" AppliedMaster="m1""# } else { "" }; @@ -48,7 +53,7 @@ fn left_to_right_fixture() -> Vec { r#""# ); let prefs = format!( - r#""# + r#""# ); zip_files(&[ ("designmap.xml", &map), @@ -109,3 +114,19 @@ fn left_to_right_right_pages_take_mirrored_margins_and_the_right_parent_page() { let again = import_idml(&export_idml(&d)).unwrap(); assert_right_pages_mirror_left_pages(&again); } + +/// Without facing pages, a page of a two-page spread shows the parent page in its place, as in +/// InDesign: page 3, the second page of its spread, shows the parent's second page, not the +/// first page's items again. +#[test] +fn single_sided_pages_show_the_parent_page_in_their_place() { + let d = import_idml(&fixture(false)).unwrap(); + assert!(d.spreads.iter().flat_map(|s| &s.pages).all(|p| p.side == PageSide::Single)); + assert_eq!(shown_heads(&d, 0), ["Left head"], "a page alone shows the parent's first page"); + assert_eq!(shown_heads(&d, 1), ["Left head"]); + assert_eq!(shown_heads(&d, 2), ["Right head"]); + // What the renderer draws behind each page of the second spread. + let (ppi, _) = d.parent_page_for(1).unwrap(); + assert_eq!(d.shown_parent_pages(designcraft_doc::SpreadRef::Doc(1), 0), [(ppi, 0)]); + assert_eq!(d.shown_parent_pages(designcraft_doc::SpreadRef::Doc(1), 1), [(ppi, 1)]); +}