From 2b34554ac81ba969cc8aa803fff02f89c60bf550 Mon Sep 17 00:00:00 2001 From: Giang Nguyen <33104857+giang17@users.noreply.github.com> Date: Mon, 12 Oct 2026 18:22:59 +0200 Subject: [PATCH] fix(text): fitHeadline sees wrapping in a frame one line tall The layout drops lines that overflow an area frame vertically, so counting the first paragraph's laid-out lines cannot see a wrapped second line when the frame holds only one. The bisection then found every tracking "one line" and ran to its 2000 ceiling, which spread the line far past the frame (most glyphs clipped away) while the command reported success. one_line now asks whether the first line reaches the end of the first paragraph, which holds regardless of dropped overflow lines. Regression test: area 240x50 at size 36 previously returned tracking 1999.9, now fills the width (avail - w < 5) with the paragraph on one line. --- crates/engine/src/cmd/typecmd.rs | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/crates/engine/src/cmd/typecmd.rs b/crates/engine/src/cmd/typecmd.rs index 0350cee1..9308bc15 100644 --- a/crates/engine/src/cmd/typecmd.rs +++ b/crates/engine/src/cmd/typecmd.rs @@ -1022,6 +1022,7 @@ mod area_tests { } /// Width of the first line and of the space it can fill, with `tracking` on the first paragraph. +/// The third value is where the first line ends in the plain text. fn headline_fit(t: &TextObject, tracking: f64) -> Option<(f64, f64, usize)> { let mut probe = t.clone(); let para_end = probe.plain_text().find('\n').unwrap_or(usize::MAX); @@ -1031,7 +1032,7 @@ fn headline_fit(t: &TextObject, tracking: f64) -> Option<(f64, f64, usize)> { let line = lay.lines.first()?; let para = t.para_at(0); let avail = cell.width() - 2.0 * t.area.inset - para.left_indent - para.right_indent; - Some((line.x1 - line.x0, avail, lay.lines.iter().filter(|l| l.start < para_end).count())) + Some((line.x1 - line.x0, avail, line.end)) } fn fit_headline(s: &mut Session, p: &Value) -> Result { @@ -1044,8 +1045,11 @@ fn fit_headline(s: &mut Session, p: &Value) -> Result { if !matches!(t.kind, vectorcraft_doc::TextKind::Area { .. }) { continue; } - // Largest tracking that keeps the first paragraph on one line (bisection). - let one_line = |tr: f64| headline_fit(t, tr).is_some_and(|(_, _, lines)| lines == 1); + // Largest tracking that keeps the first paragraph on one line (bisection). The layout + // drops lines that overflow the frame, so a frame one line tall hides the wrapped rest: + // "one line" means the first line reaches the end of the first paragraph. + let para_end = t.plain_text().find('\n').unwrap_or(t.plain_text().len()); + let one_line = |tr: f64| headline_fit(t, tr).is_some_and(|(_, _, end)| end >= para_end); let (mut lo, mut hi) = (-200.0, 2000.0); if !one_line(lo) { continue; @@ -1087,9 +1091,27 @@ mod headline_tests { let tr = s.execute("text.fitHeadline", &json!({})).unwrap()["tracking"].as_f64().unwrap(); assert!(tr > 100.0, "a short word spreads out: {tr}"); let NodeKind::Text(t) = &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind else { panic!() }; - let (w, avail, lines) = headline_fit(t, tr).unwrap(); - assert_eq!(lines, 1); + let (w, avail, end) = headline_fit(t, tr).unwrap(); + assert_eq!(end, 8); assert!(avail - w < 30.0, "{w} of {avail}"); assert_eq!(t.runs.last().unwrap().style.tracking, 0.0, "the body keeps its tracking"); } + + #[test] + fn fit_headline_fills_even_when_the_frame_holds_one_line() { + let mut s = Session::new(); + s.execute("file.new", &json!({"width": 400, "height": 400})).unwrap(); + let id = s + .execute("text.create", &json!({"x": 10, "y": 10, "text": "NEON DRIVE", "size": 36, "area": {"width": 240, "height": 50}})) + .unwrap()["id"] + .as_u64() + .unwrap(); + s.execute("select.set", &json!({"ids": [id]})).unwrap(); + let tr = s.execute("text.fitHeadline", &json!({})).unwrap()["tracking"].as_f64().unwrap(); + assert!(tr < 1500.0, "a frame one line tall must not push the tracking to the ceiling: {tr}"); + let NodeKind::Text(t) = &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind else { panic!() }; + let (w, avail, end) = headline_fit(t, tr).unwrap(); + assert_eq!(end, 10); + assert!(avail - w < 5.0, "{w} of {avail}"); + } }