Parents: single-sided pages in a spread show the parent page in their place

This commit is contained in:
texjer
2026-10-31 00:04:41 -04:00
parent 7e70203206
commit 06d29fe3a6
5 changed files with 46 additions and 7 deletions
+1
View File
@@ -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.
+10 -3
View File
@@ -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).
+10
View File
@@ -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<usize> {
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<usize> {
if self.pages.is_empty() {
+3 -3
View File
@@ -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<SpreadId>, side: PageSide) -> (Margins, Columns, bool) {
fn page_margins(&self, p: &El, r: &Rect, master: bool, parent: Option<SpreadId>, (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(),
+22 -1
View File
@@ -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<u8> {
fixture(true)
}
/// [`left_to_right_fixture`], with facing pages on or off.
fn fixture(facing: bool) -> Vec<u8> {
let margins = r#"<MarginPreference ColumnCount="1" ColumnGutter="12" Top="36" Bottom="36" Left="36" Right="120"/>"#;
let page = |id: &str, x: f64, applied: bool, guide: Option<f64>| {
let applied = if applied { r#" AppliedMaster="m1""# } else { "" };
@@ -48,7 +53,7 @@ fn left_to_right_fixture() -> Vec<u8> {
r#"<Document {NS} Self="d" StoryList="sl sr"><idPkg:Preferences src="Resources/Preferences.xml"/><Layer Self="L1" Name="Layer 1"/><idPkg:MasterSpread src="MasterSpreads/m1.xml"/><idPkg:Spread src="Spreads/sp0.xml"/><idPkg:Spread src="Spreads/sp1.xml"/><idPkg:Story src="Stories/sl.xml"/><idPkg:Story src="Stories/sr.xml"/></Document>"#
);
let prefs = format!(
r#"<idPkg:Preferences {NS}><DocumentPreference PageWidth="400" PageHeight="600" FacingPages="true" PageBinding="LeftToRight"/><ViewPreference RulerOrigin="PageOrigin"/></idPkg:Preferences>"#
r#"<idPkg:Preferences {NS}><DocumentPreference PageWidth="400" PageHeight="600" FacingPages="{facing}" PageBinding="LeftToRight"/><ViewPreference RulerOrigin="PageOrigin"/></idPkg:Preferences>"#
);
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)]);
}