From 54ea1559d79b8414724a9df8a746e7da65cf2a3d Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 17 Oct 2026 14:33:21 +0000 Subject: [PATCH 1/2] Parent pages: show running-header placeholders, keep page overlays off them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a parent page a running-header variable has no page to read, so it composed blank; it now shows , as the header's placeholder. While parents were edited, document frames weren't laid out and fell back to the identity canvas transform, so their hidden characters (a flood of ¶), spelling squiggles and other overlays were drawn over the right parent page. Items on spreads that aren't shown now have no canvas transform. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa --- ROADMAP.md | 1 + crates/compose/src/vars.rs | 11 ++++++----- crates/engine/src/cmd/variables.rs | 10 ++++++++++ crates/ui-egui/src/canvas.rs | 29 ++++++++++++++++++++++++++++- 4 files changed, 45 insertions(+), 6 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9bd0274ef..0588bca30 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -348,6 +348,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-17** · Parent pages: a running-header variable shows its name in angle brackets on the parent (it was blank), and while parents are edited the document pages' hidden characters, spelling squiggles and other overlays no longer pile onto the right parent page. No readiness percentage change. - **2026-10-16** · Threading: the loaded text cursor threads into a text frame inside a group (it hit the group and showed it could not thread); a plain click still selects the group (#1075). Gap 27 closed. No readiness percentage change. - **2026-10-16** · Greek text hyphenates. The patterns were never compiled in, so Greek set unhyphenated however narrow the column; `"Greek"`, `"Greek: Greece"`, `el` and `el-GR` all resolve now and ανθρωπος breaks αν-θρω-πος. They are the monotonic patterns (hyph-el-monoton, Dimitrios Filippou, under MIT), which is what polytonic text hyphenates by too. Five of InDesign's ~40 hyphenation languages. No readiness percentage change. - **2026-10-16** · A swatch applied to a placed greyscale or bitmap image colours it, as in InDesign: IDML's `` is read and written, greyscale pixels become a tint of the swatch on opaque white and a bitmap's white lets the frame show through, on screen and in PDF (spot swatches print on their plate). Colour images keep their colours. Applying the swatch from the Swatches panel comes next. No readiness percentage change. diff --git a/crates/compose/src/vars.rs b/crates/compose/src/vars.rs index 945d8bd04..dfb8928c7 100644 --- a/crates/compose/src/vars.rs +++ b/crates/compose/src/vars.rs @@ -139,11 +139,12 @@ pub fn values(doc: &Document, page: Option, running: Option<&RunningIndex .enumerate() .map(|(i, v)| match &v.kind { VarKind::RunningHeader { style, use_, character } => { - let body = match (running, page) { - (Some(r), Some(p)) => r.value(style, *character, *use_, p), - _ => String::new(), - }; - format!("{}{}{}", v.before, body, v.after) + match (running, page) { + (Some(r), Some(p)) => format!("{}{}{}", v.before, r.value(style, *character, *use_, p), v.after), + (None, Some(_)) => format!("{}{}", v.before, v.after), + // A parent page has no entries of its own: show which variable sits there. + (_, None) => format!("<{}>", v.name), + } } _ => doc.variable_value(i, page).unwrap_or_default(), }) diff --git a/crates/engine/src/cmd/variables.rs b/crates/engine/src/cmd/variables.rs index d2e513430..1ea789721 100644 --- a/crates/engine/src/cmd/variables.rs +++ b/crates/engine/src/cmd/variables.rs @@ -176,6 +176,11 @@ fn define(s: &mut Session, p: &Value) -> Result { mod tests { use super::*; + fn shown_on_parent(s: &Session, d: &designcraft_doc::Document, sid: designcraft_doc::StoryId) -> usize { + let cs = s.cache.get(d, sid, None); + cs.frames.iter().flat_map(|f| f.lines.iter()).map(|l| l.glyphs.iter().filter(|g| g.visible && g.adv > 0.0).count()).sum() + } + /// A parent-page page-number marker follows Numbering & Section Options: changing a section's /// start number renames its pages without changing their index or the number of sections, and /// the marker composed for a page shows the new name. @@ -352,6 +357,11 @@ mod tests { assert_eq!(idx.value("Heading", false, Default::default(), 2), "Epsilon"); let list = s.execute("variables.list", &json!({})).unwrap(); assert!(list.as_array().unwrap().iter().any(|v| v["name"] == "Last Page Number" && v["value"] == "3")); + // On the parent page itself the header shows which variable it holds, not a blank. + let head = d.variable("Chapter Head").unwrap(); + assert_eq!(crate::compose::vars::values(&d, None, Some(&idx))[head], ""); + assert_eq!(crate::compose::vars::values(&d, Some(2), Some(&idx))[head], "Epsilon"); + assert!(shown_on_parent(&s, &d, sid) > p1, "the parent shows , longer than Alpha"); // Deleting a variable flattens its instances to text. s.execute("variables.delete", &json!({"name": "Last Page Number"})).unwrap(); assert!(s.doc().unwrap().doc.story(sid).unwrap().text.ends_with(" · 3")); diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 34e1aa2fd..1a8cd7728 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1014,7 +1014,10 @@ fn draw_guides(app: &DesignApp, painter: &egui::Painter, xf: &Xf, doc: &Document /// Spread-space → canvas transform for an item at `loc`. fn item_canvas_xf(doc: &Document, layout: &CanvasLayout, id: designcraft_doc::ItemId) -> Option<(Affine, &'static str)> { let loc = doc.find(id)?; - Some((layout.xf(loc.spread) * doc.parent_xf(&loc), "")) + // Items on spreads that aren't laid out (document pages while parents are being edited, or + // the reverse) have no place on the canvas. + let slot = layout.slot(loc.spread)?; + Some((slot.xf * doc.parent_xf(&loc), "")) } fn path_screen(it: &Item, xf: &Xf, a: Affine) -> Vec> { @@ -3678,6 +3681,30 @@ mod tests { /// The always-on overset mark sits at the frame's out port, a step along its own right edge /// (so a rotated frame gets it inside its edge, not straight up from the corner), and a /// selected overset frame gets only the mark its ports draw. + #[test] + fn document_pages_have_no_place_on_the_parent_canvas() { + // Regression: while parents were edited, document frames fell back to the identity + // transform and their ¶ marks (and other overlays) piled onto the right parent page. + let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); + app.run("file.new", json!({"pages": 2})).unwrap(); + let s = &mut app.session; + let body = + s.execute("frame.create", &json!({"rect": [36, 36, 300, 300], "content": "text", "text": "one\ntwo"})).unwrap()["id"].as_u64().unwrap(); + let head = s + .execute("frame.create", &json!({"spread": {"kind": "parent", "index": 0}, "rect": [36, 10, 300, 30], "content": "text", "text": "head"})) + .unwrap()["id"] + .as_u64() + .unwrap(); + let doc = app.session.doc().unwrap().doc.clone(); + let (body, head) = (designcraft_doc::ItemId(body), designcraft_doc::ItemId(head)); + let pages = CanvasLayout::new(&doc, false); + assert!(item_canvas_xf(&doc, &pages, body).is_some()); + assert!(item_canvas_xf(&doc, &pages, head).is_none()); + let parents = CanvasLayout::new(&doc, true); + assert!(item_canvas_xf(&doc, &parents, body).is_none(), "a page frame is not drawn over the parent"); + assert!(item_canvas_xf(&doc, &parents, head).is_some()); + } + #[test] fn overset_mark_sits_on_the_out_port_once() { let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default()); From e1d072f3bc1132199d9852a5d74600207fe16179 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 30 Oct 2026 16:56:41 +0000 Subject: [PATCH 2/2] Parent pages: this branch keeps only the canvas fix The running-header placeholder on parent pages moves to its own branch (running-header-placeholder), as asked in review, so each lands on its own. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa --- ROADMAP.md | 2 +- crates/compose/src/vars.rs | 11 +++++------ crates/engine/src/cmd/variables/tests.rs | 10 ---------- 3 files changed, 6 insertions(+), 17 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 1118a9555..b0fce1ba0 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -462,7 +462,7 @@ Newest first. - **2026-10-21** · The Bulleted and Numbered List buttons (Properties, and Type › Bulleted & Numbered Lists › Apply Bullets / Apply Numbers) work as InDesign's: a paragraph with no indents gets the default hanging indent (0.25 in, −0.25 in), so the text lines up after the bullet instead of at the next default tab stop, and pressing the button again removes the list and that indent (`list.bullets`, `list.numbers`). Found in the Learn Skills Daily course, chapter 21. No readiness percentage change. - **2026-10-17** · Inline and anchored objects paint under their frame's text, on screen and in PDF, as InDesign draws them: a picture whose frame reaches into the next line (a negative Y offset) no longer hides the top of that line. No readiness percentage change. - **2026-10-17** · Running headers: *first/last on page* read a page in text order (story order, then column by column), not by height — a multi-column page's right header showed the lowest entry of column 1 instead of the page's last entry. No readiness percentage change. -- **2026-10-17** · Parent pages: a running-header variable shows its name in angle brackets on the parent (it was blank), and while parents are edited the document pages' hidden characters, spelling squiggles and other overlays no longer pile onto the right parent page. No readiness percentage change. +- **2026-10-17** · Parent pages: while parents are edited, the document pages' hidden characters, spelling squiggles and other overlays no longer pile onto the right parent page. No readiness percentage change. - **2026-10-16** · Type › Reload Fonts (`font.reload`): fonts installed, edited or removed while DesignCraft runs are read again without a restart, and the open documents recompose with them (#328, #547). An edited font file is noticed by its length and modification time; bundled, added and document fonts stay as they are. InDesign does this by itself, so noticing changes automatically is still open (gap 6). No readiness percentage change. - **2026-10-16** · Parents can be deleted from their Pages-panel context menu or `layout.parents.delete {id}`. Applied-page and based-on references are cleared, local override copies survive, and Undo restores the parent. Addresses the deletion portion of #956; no readiness percentage change. - **2026-10-17** · File › Package writes the document's and the IDML's links relative to the package (`Links/logo.png`, `file:Links/logo.png`), so a package holds no path from the machine that made it; opening a document resolves them to the files beside it. Links › Copy Link(s) To still links by full path. (#340). No readiness percentage change. diff --git a/crates/compose/src/vars.rs b/crates/compose/src/vars.rs index f8f5eef2a..508ac4a3e 100644 --- a/crates/compose/src/vars.rs +++ b/crates/compose/src/vars.rs @@ -165,12 +165,11 @@ pub fn values(doc: &Document, page: Option, running: Option<&RunningIndex .enumerate() .map(|(i, v)| match &v.kind { VarKind::RunningHeader { style, use_, character } => { - match (running, page) { - (Some(r), Some(p)) => format!("{}{}{}", v.before, r.value(style, *character, *use_, p), v.after), - (None, Some(_)) => format!("{}{}", v.before, v.after), - // A parent page has no entries of its own: show which variable sits there. - (_, None) => format!("<{}>", v.name), - } + let body = match (running, page) { + (Some(r), Some(p)) => r.value(style, *character, *use_, p), + _ => String::new(), + }; + format!("{}{}{}", v.before, body, v.after) } _ => doc.variable_value(i, page).unwrap_or_default(), }) diff --git a/crates/engine/src/cmd/variables/tests.rs b/crates/engine/src/cmd/variables/tests.rs index 35efa12ad..75c4bcaed 100644 --- a/crates/engine/src/cmd/variables/tests.rs +++ b/crates/engine/src/cmd/variables/tests.rs @@ -1,10 +1,5 @@ use super::*; -fn shown_on_parent(s: &Session, d: &designcraft_doc::Document, sid: designcraft_doc::StoryId) -> usize { - let cs = s.cache.get(d, sid, None); - cs.frames.iter().flat_map(|f| f.lines.iter()).map(|l| l.glyphs.iter().filter(|g| g.visible && g.adv > 0.0).count()).sum() -} - /// A parent-page page-number marker follows Numbering & Section Options: changing a section's /// start number renames its pages without changing their index or the number of sections, and /// the marker composed for a page shows the new name. @@ -355,11 +350,6 @@ fn running_header_on_parent_page_follows_headings() { assert_eq!(idx.value("Heading", false, Default::default(), 2), "Epsilon"); let list = s.execute("variables.list", &json!({})).unwrap(); assert!(list.as_array().unwrap().iter().any(|v| v["name"] == "Last Page Number" && v["value"] == "3")); - // On the parent page itself the header shows which variable it holds, not a blank. - let head = d.variable("Chapter Head").unwrap(); - assert_eq!(crate::compose::vars::values(&d, None, Some(&idx))[head], ""); - assert_eq!(crate::compose::vars::values(&d, Some(2), Some(&idx))[head], "Epsilon"); - assert!(shown_on_parent(&s, &d, sid) > p1, "the parent shows , longer than Alpha"); // Deleting a variable flattens its instances to text. s.execute("variables.delete", &json!({"name": "Last Page Number"})).unwrap(); assert!(s.doc().unwrap().doc.story(sid).unwrap().text.ends_with(" · 3"));