From 927a3059433d0f2b56284066baea375fa8d08bfd Mon Sep 17 00:00:00 2001 From: naxvog Date: Wed, 14 Oct 2026 07:54:07 +0200 Subject: [PATCH] Layout: a picture after a content control's start stays on its line (regression from #392) (#1102) Since content controls are kept (#392), a picture content control puts a zero-width start marker before its picture. Line breaking keeps a line's first item even when it doesn't fit, but counted the marker as that item, so a picture a little wider than its line wrapped onto a second line, leaving a marker-only line above it; in a fixed-height text box the picture's line was then cut off. A logo in a header text box (which 0.5.0 drew) disappeared. Markers before an item no longer count, as a display equation's check already does. Co-authored-by: Naxvog <4963183+naxvog@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 --- crates/layout/src/para.rs | 4 +++- crates/layout/src/tests.rs | 18 ++++++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/layout/src/para.rs b/crates/layout/src/para.rs index 8ac9dd9e0d..14c064a2c3 100644 --- a/crates/layout/src/para.rs +++ b/crates/layout/src/para.rs @@ -1728,7 +1728,9 @@ fn break_lines(pl: &mut ParaLayout, env: &ParaEnv, mark_style: u16, level: Optio }); let shrink = if shrink_spaces && !has_tab { space_w * MAX_SPACE_SHRINK } else { 0.0 }; let fits = absorbs || x + c.adv <= right_edge + shrink + 0.01 || c.kind == ClKind::Space || c.kind == ClKind::Marker; - if !fits && j > c0 { + // A line's first item stays on it even when it doesn't fit; markers before it + // (a content control's start, a bookmark) take no room and don't count (Word). + if !fits && (c0..j).any(|k| pl.clusters.get(k).is_some_and(|c| c.kind != ClKind::Marker)) { // Wrap: back up to the last break opportunity on this line. end = LineEnd::Wrap; // Hyphenate only when the plain break leaves more than the hyphenation zone empty. diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index 8b09d0e263..a46b5ceda2 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -3102,6 +3102,24 @@ fn inline_picture_wider_than_the_text_keeps_its_size() { assert!((r.w - 1000.0).abs() < 0.01 && (r.h - 100.0).abs() < 0.01, "{r:?}"); } +#[test] +fn a_picture_too_wide_after_a_content_control_start_stays_on_the_first_line() { + // The picture opening a picture content control (a logo in a text box a little too + // narrow for it): the control's start marker before it takes no room, so the picture is + // still the line's first item and stays there instead of leaving an empty line above it. + let mut d = Document::from_text(""); + if let Some(wordcraft_doc::Block::Para(p)) = d.body.get_mut(0).map(std::sync::Arc::make_mut) { + p.insert_object(0, picture(500.0, 100.0, Default::default()), &Default::default()).unwrap(); + let start = InlineObject::ControlStart { control: Box::default() }; + p.insert_object(0, start, &Default::default()).unwrap(); + } + let l = lay(&d); + let Some(Placed::Lines { para, .. }) = l.pages[0].items.iter().find(|i| matches!(i, Placed::Lines { .. })) else { panic!() }; + assert_eq!(para.lines.len(), 1, "{:?}", para.lines); + let line = ¶.lines[0]; + assert!((line.baseline - line.top - 100.0).abs() < 0.01, "the picture is on the first line: {line:?}"); +} + /// The paragraph at `path` on page 0: its first line's x, its y and its line count. fn para_at(l: &DocLayout, path: &[u32]) -> Vec<(f32, f32, usize)> { l.pages[0]