From d52ef6b6b9ce9539134c109ba1d9df07dbab3cc5 Mon Sep 17 00:00:00 2001 From: moppeeer Date: Thu, 15 Oct 2026 00:18:55 +0200 Subject: [PATCH] A line is set when its baseline fits in the frame, as in InDesign MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit InDesign sets a text line when its baseline fits in the frame; the descenders may hang below the frame's bottom. DesignCraft required the baseline plus the descent to fit, so a frame sized to a heading's caps oversets: Zero2Graphic's magazine tutorial draws 2.7682 cm frames for 103 pt headings (control bar, 4:12–5:44), and all three were overset. The keep-lines IDML fixture's frames are now 236 pt tall, so they hold the same 19 lines under either rule. Co-Authored-By: Claude Opus 5.5 --- ROADMAP.md | 1 + crates/compose/src/lib.rs | 3 ++- crates/engine/src/tests.rs | 20 ++++++++++++++++++++ crates/engine/src/tests_idml.rs | 3 ++- 4 files changed, 25 insertions(+), 2 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 9f8168e50..06fcc7e9c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -132,6 +132,7 @@ list) · [roadmap.md](docs/roadmap.md) (milestones, plan) · [parity-checklist.m Newest first. +- **2026-10-15** · A line of text is set when its baseline fits in the frame, as in InDesign: descenders may hang below the frame's bottom. A frame sized to the cap height of a big heading (103 pt in a 2.77 cm frame, as Zero2Graphic's magazine tutorial draws them) no longer oversets. No readiness percentage change. - **2026-10-13** · Object › Fitting has InDesign's shortcuts: Fill Frame Proportionally ⌘⌥⇧C, Fit Content Proportionally ⌘⌥⇧E, Fit Frame to Content ⌘⌥C, Fit Content to Frame ⌘⌥E, Center Content ⌘⇧E. No readiness percentage change. - **2026-10-13** · Gradients between process colours blend as inks, as InDesign shows them: magenta to cyan passes through C 50 M 50 (a light lilac) on screen instead of the darker RGB mix, and a midpoint between CMYK or grey stops no longer turns the PDF shading RGB. No readiness percentage change. - **2026-10-14** · The Swatches panel has InDesign's header: a Fill/Stroke proxy, Formatting Affects Container/Text and the Tint of the colour in front (a tint alone keeps each object's own swatch). Clicking a swatch applies it to the fill or stroke in front (of the text, when formatting affects text), and the bottom row has New Color Group, New Swatch and Delete Swatch. No readiness percentage change. diff --git a/crates/compose/src/lib.rs b/crates/compose/src/lib.rs index c6312e9f5..0f260ecf5 100644 --- a/crates/compose/src/lib.rs +++ b/crates/compose/src/lib.rs @@ -1024,7 +1024,8 @@ fn compose_with_db(doc: &Document, story: &Story, frames: &[FrameSpec], opts: &C let col_w = cols[cur.fi][cur.col.min(cols[cur.fi].len() - 1)].width(); let reserve = notes.reserve(doc, cur.fi, cur.col, &line_notes, col_w, f, opts); let reserve = if cur.last_baseline.is_none() { notes.reserve(doc, cur.fi, cur.col, &[], col_w, f, opts) } else { reserve }; - let fits = baseline + desc <= col.y1 - reserve + 0.01 && !capped; + // As in InDesign, a line is set when its baseline fits: its descenders may hang below the frame. + let fits = baseline <= col.y1 - reserve + 0.01 && !capped; if !fits { if cur.can_next_sub() { g0 = s; diff --git a/crates/engine/src/tests.rs b/crates/engine/src/tests.rs index 712dc06bc..01e15ee2d 100644 --- a/crates/engine/src/tests.rs +++ b/crates/engine/src/tests.rs @@ -877,3 +877,23 @@ fn duplicating_a_page_to_an_index_is_one_undo_step() { assert!(s.execute("layout.pages.duplicateSpread", &json!({"spread": 0, "to": 4})).is_err()); assert_eq!(widths(&s), [300.0, 400.0, 500.0]); } + +/// A line is set when its baseline fits in the frame; its descenders may hang below the frame, as +/// in InDesign. A 103 pt heading in a 78.5 pt frame (Cap Height first baseline) was overset. +#[test] +fn a_line_fits_when_its_baseline_does() { + let mut s = Session::new(); + s.execute("file.new", &json!({})).unwrap(); + let overset = |s: &mut Session, height: f64| { + let r = s.execute("frame.create", &json!({"rect": [36, 36, 400, 36.0 + height], "content": "text", "text": "Gyp"})).unwrap(); + s.execute("object.textFrameOptions", &json!({"ids": [r["id"]], "firstBaseline": "capHeight", "inset": 0})).unwrap(); + s.execute("text.select", &json!({"story": r["story"], "anchor": 0, "focus": 3})).unwrap(); + s.execute("type.char", &json!({"attrs": {"size": 103}})).unwrap(); + let st = s.execute("story.get", &json!({"story": r["story"]})).unwrap(); + !st["overset"].is_null() + }; + // Cap height of the default font at 103 pt is about 70 pt: the baseline fits in 78.5 pt, the + // descent (about 25 pt more) doesn't. + assert!(!overset(&mut s, 78.5), "the line is set; its descenders hang below the frame"); + assert!(overset(&mut s, 50.0), "a baseline below the frame still oversets the line"); +} diff --git a/crates/engine/src/tests_idml.rs b/crates/engine/src/tests_idml.rs index 5e84532cf..c6ecc539c 100644 --- a/crates/engine/src/tests_idml.rs +++ b/crates/engine/src/tests_idml.rs @@ -107,7 +107,8 @@ fn keep_lines_idml(body_attrs: &str, lines: &[usize]) -> Vec { }) .collect(); let frame = |me: &str, x: f64, prev: &str, next: &str| { - let pts: String = [(x, 36.0), (x, 276.0), (x + 180.0, 276.0), (x + 180.0, 36.0)] + // 236 pt tall: 19 lines of 12 pt leading, whether or not the last line's descent must fit. + let pts: String = [(x, 36.0), (x, 272.0), (x + 180.0, 272.0), (x + 180.0, 36.0)] .iter() .map(|(x, y)| format!(r#""#)) .collect();