From 1dd86b0d0afb0de5aac03f7dca35211fcedcc524 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 20 Oct 2026 09:23:17 +0800 Subject: [PATCH 1/3] A floating picture in a table cell grows the row to hold it (#3108) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A row/cell's height was computed from the floating *tables* that grow it (float_tables -> floats_bottom in crates/layout/src/lib.rs), but a floating (anchored, non-inline wrap) picture or shape in a cell's paragraph wasn't tracked the same way. The row sized to the cell's text alone, so a picture taller than the text hung past the row's own bottom edge and overlapped whatever came after the table — reproduced with the reporter's own document and confirmed against their real Microsoft Office 365 screenshot, which lays the same file out with the table growing to fully contain the picture. - crates/layout/src/lib.rs: layout_box now tracks the deepest point any floating (Wrap != Inline) picture or shape reaches in a box, the same way it already tracks float_tables, and folds it into floats_bottom. - crates/layout/src/tests.rs: a_floating_picture_in_a_cell_grows_the_row_to_hold_it, mirroring the existing a_floating_table_in_a_cell_takes_no_room_and_the_cell_holds_it. cargo xtask ci: fmt, clippy, assets, layers and wasm all pass. Full workspace tests pass except the same 3 pre-existing, environment-only Thai-font failures already noted on #3119 and #3184 (no CRAFT_FONTS_DIR/TH Sarabun New in this build environment). Closes #3108. Co-Authored-By: Claude Sonnet 5 --- crates/layout/src/lib.rs | 12 +++++++++++- crates/layout/src/tests.rs | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index a60c5f94d4..4d367e0ad1 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -1178,6 +1178,9 @@ fn layout_box( let mut excl: Vec = Vec::new(); // Areas of the box's floating tables, which other floating tables may have to avoid. let mut float_tables: Vec = Vec::new(); + // Deepest a floating picture or shape reaches, so the box (a table cell especially) grows + // to hold it instead of letting it hang past the box's own bottom edge (#3108). + let mut float_objects_bottom = 0.0f32; let mut table_areas = Vec::new(); let mut page_table_wraps = Vec::new(); let mut y = 0.0f32; @@ -1282,6 +1285,13 @@ fn layout_box( if !p.objects.is_empty() { let at = ObjFrame { page: frame, col: (0.0, width), para_y: y, table_chr: table.map(|t| &t.chr), pinned: Some(&pinned) }; let (back, front) = place_objects(ctx, story, &path, p, &pl, (0, pl.lines.len()), (0.0, y + lead), &at, &floats, depth); + for placed in back.iter().chain(front.iter()) { + if let Placed::Object { rect, wrap, .. } = placed + && *wrap != Wrap::Inline + { + float_objects_bottom = float_objects_bottom.max(rect.bottom()); + } + } let at = behind.min(items.len()); behind += back.len(); items.splice(at..at, back); @@ -1342,7 +1352,7 @@ fn layout_box( } } } - let floats_bottom = float_tables.iter().map(|r| r.bottom()).fold(0.0, f32::max); + let floats_bottom = float_tables.iter().map(|r| r.bottom()).fold(float_objects_bottom, f32::max); ctx.hide_deleted = outer_hide; BoxLayout { items, height: y.max(0.0), floats_bottom, last_top: last_top.max(0.0), table_areas, page_table_wraps } } diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index 174dde1743..4e32f5fb20 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -4995,6 +4995,27 @@ fn a_floating_table_in_a_cell_takes_no_room_and_the_cell_holds_it() { assert!(below_y > last_y + 10.0, "text below the outer table at {below_y}, the inner table's last row at {last_y}"); } +/// A floating (anchored, non-inline) picture in a cell grows the row to hold it too, the same +/// way a floating table does: it must not hang past the row's own bottom edge and overlap +/// whatever follows the table (#3108). +#[test] +fn a_floating_picture_in_a_cell_grows_the_row_to_hold_it() { + use wordcraft_doc::para::{Anchor, Float, Wrap}; + let mut t = Table::new(1, 1, 468.0); + let mut cp = wordcraft_doc::Paragraph::with_text("Caption", Default::default()); + // Short text alone would need one line (well under 100pt); the floated picture is much + // taller than that, so only its own height should set the row's height. + let picture = Float { wrap: Wrap::Square, h_rel: Anchor::Margin, v_rel: Anchor::Margin, x: 0.0, y: 0.0, ..Default::default() }; + cp.insert_object(0, rect_shape(200.0, 300.0, picture), &Default::default()).unwrap(); + t.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(cp)]; + let mut d = Document::from_text("Below the table"); + d.insert_block(StoryRef::Body, &Path::top(0), wordcraft_doc::Block::Table(t)).unwrap(); + let l = lay(&d); + let [(_, row_y, _)] = para_at(&l, &[0, 0, 0, 0])[..] else { panic!("cell text line missing") }; + let [(_, below_y, _)] = para_at(&l, &[1])[..] else { panic!("line after the table missing") }; + assert!(below_y - row_y > 290.0, "text after the table at {below_y}, the row's text at {row_y}: the row didn't grow for the 300pt picture"); +} + #[test] fn autofit_sizes_columns_by_their_content() { let text = |s: &str| wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(s, Default::default())); From 25fb9b93f82e9316f3c008e566d5fcf1fac31c40 Mon Sep 17 00:00:00 2001 From: Michael Date: Tue, 20 Oct 2026 09:40:48 +0800 Subject: [PATCH 2/3] Use the rotated bounds for a floating picture's row-growing extent rect.bottom() measures the unrotated frame; a rotated picture's actual drawn bottom can extend further down (or less far), so use Spin::bounds(rect).bottom() like wrap_area does for text wrapping. Co-Authored-By: Claude Sonnet 5 --- crates/layout/src/lib.rs | 4 ++-- crates/layout/src/tests.rs | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 4d367e0ad1..351167f121 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -1286,10 +1286,10 @@ fn layout_box( let at = ObjFrame { page: frame, col: (0.0, width), para_y: y, table_chr: table.map(|t| &t.chr), pinned: Some(&pinned) }; let (back, front) = place_objects(ctx, story, &path, p, &pl, (0, pl.lines.len()), (0.0, y + lead), &at, &floats, depth); for placed in back.iter().chain(front.iter()) { - if let Placed::Object { rect, wrap, .. } = placed + if let Placed::Object { rect, spin, wrap, .. } = placed && *wrap != Wrap::Inline { - float_objects_bottom = float_objects_bottom.max(rect.bottom()); + float_objects_bottom = float_objects_bottom.max(spin.bounds(*rect).bottom()); } } let at = behind.min(items.len()); diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index 4e32f5fb20..e88485c802 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -5016,6 +5016,28 @@ fn a_floating_picture_in_a_cell_grows_the_row_to_hold_it() { assert!(below_y - row_y > 290.0, "text after the table at {below_y}, the row's text at {row_y}: the row didn't grow for the 300pt picture"); } +#[test] +fn a_rotated_floating_picture_grows_the_row_by_its_rotated_extent() { + use wordcraft_doc::para::{Anchor, Float, Wrap}; + let mut t = Table::new(1, 1, 468.0); + let mut cp = wordcraft_doc::Paragraph::with_text("Caption", Default::default()); + // Unrotated this shape's own `rect.bottom()` is only 40pt down. Rotated 90° about its centre + // (`Spin::bounds`: width and height swap), its drawn bottom moves to (w+h)/2 = 220pt down — + // using the unrotated rect here would undergrow the row to a fraction of that. + let picture = Float { wrap: Wrap::Square, h_rel: Anchor::Margin, v_rel: Anchor::Margin, x: 0.0, y: 0.0, rot: 90.0, ..Default::default() }; + cp.insert_object(0, rect_shape(400.0, 40.0, picture), &Default::default()).unwrap(); + t.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(cp)]; + let mut d = Document::from_text("Below the table"); + d.insert_block(StoryRef::Body, &Path::top(0), wordcraft_doc::Block::Table(t)).unwrap(); + let l = lay(&d); + let [(_, row_y, _)] = para_at(&l, &[0, 0, 0, 0])[..] else { panic!("cell text line missing") }; + let [(_, below_y, _)] = para_at(&l, &[1])[..] else { panic!("line after the table missing") }; + assert!( + below_y - row_y > 150.0, + "text after the table at {below_y}, the row's text at {row_y}: the row didn't grow for the picture's rotated extent (unrotated bottom is only 40pt, rotated should reach ~220pt)" + ); +} + #[test] fn autofit_sizes_columns_by_their_content() { let text = |s: &str| wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(s, Default::default())); From 29540d03c37bdcaf4bb9853e07808477ebded5af Mon Sep 17 00:00:00 2001 From: Michael Date: Thu, 22 Oct 2026 08:36:05 +0800 Subject: [PATCH 3/3] #3108 review: behind-text and in-front-of-text floats don't grow the row FredApps' review: the row-growth loop counted every non-inline object, so a page-anchored logo or watermark behind a cell's text would stretch its row to the object's bottom edge - contradicting table.rs (behind-text frames count for vertical alignment but don't increase the row's flow height) and Word. Only floats that take part in the text flow (Square, Tight, Through, TopAndBottom) grow the row now, the same classification layout already uses elsewhere for "takes part in wrapping". New test: a 300pt behind-text and a 300pt in-front-of-text picture each leave the row at its text height; it fails with the old condition. The existing Square-wrap tests and cell_frame_centres_empty still pass. Co-Authored-By: Claude Opus 5.5 --- crates/layout/src/lib.rs | 8 +++++--- crates/layout/src/tests.rs | 25 +++++++++++++++++++++++++ 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/crates/layout/src/lib.rs b/crates/layout/src/lib.rs index 351167f121..a93507c81b 100644 --- a/crates/layout/src/lib.rs +++ b/crates/layout/src/lib.rs @@ -1178,8 +1178,10 @@ fn layout_box( let mut excl: Vec = Vec::new(); // Areas of the box's floating tables, which other floating tables may have to avoid. let mut float_tables: Vec = Vec::new(); - // Deepest a floating picture or shape reaches, so the box (a table cell especially) grows - // to hold it instead of letting it hang past the box's own bottom edge (#3108). + // Deepest a floating picture or shape that takes part in the text flow reaches, so the box + // (a table cell especially) grows to hold it instead of letting it hang past the box's own + // bottom edge (#3108). Behind-text and in-front-of-text objects don't: a page-anchored logo or + // watermark behind a cell's text must not stretch its row (see `align_h` in table.rs). let mut float_objects_bottom = 0.0f32; let mut table_areas = Vec::new(); let mut page_table_wraps = Vec::new(); @@ -1287,7 +1289,7 @@ fn layout_box( let (back, front) = place_objects(ctx, story, &path, p, &pl, (0, pl.lines.len()), (0.0, y + lead), &at, &floats, depth); for placed in back.iter().chain(front.iter()) { if let Placed::Object { rect, spin, wrap, .. } = placed - && *wrap != Wrap::Inline + && !matches!(wrap, Wrap::Inline | Wrap::BehindText | Wrap::InFrontOfText) { float_objects_bottom = float_objects_bottom.max(spin.bounds(*rect).bottom()); } diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index e88485c802..d97b6e72b4 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -5038,6 +5038,31 @@ fn a_rotated_floating_picture_grows_the_row_by_its_rotated_extent() { ); } +/// #3108 review: only floats that take part in the text flow grow the row. A behind-text (or +/// in-front-of-text) picture taller than the cell's text leaves the row at the text's height, as +/// in Word — a page-anchored logo or watermark behind a cell must not stretch its row. +#[test] +fn a_behind_or_in_front_of_text_picture_in_a_cell_leaves_the_row_alone() { + use wordcraft_doc::para::{Anchor, Float, Wrap}; + let row_gap = |wrap: Wrap| { + let mut t = Table::new(1, 1, 468.0); + let mut cp = wordcraft_doc::Paragraph::with_text("Caption", Default::default()); + let picture = Float { wrap, h_rel: Anchor::Margin, v_rel: Anchor::Margin, x: 0.0, y: 0.0, ..Default::default() }; + cp.insert_object(0, rect_shape(200.0, 300.0, picture), &Default::default()).unwrap(); + t.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(cp)]; + let mut d = Document::from_text("Below the table"); + d.insert_block(StoryRef::Body, &Path::top(0), wordcraft_doc::Block::Table(t)).unwrap(); + let l = lay(&d); + let [(_, row_y, _)] = para_at(&l, &[0, 0, 0, 0])[..] else { panic!("cell text line missing") }; + let [(_, below_y, _)] = para_at(&l, &[1])[..] else { panic!("line after the table missing") }; + below_y - row_y + }; + for wrap in [Wrap::BehindText, Wrap::InFrontOfText] { + let gap = row_gap(wrap); + assert!(gap < 60.0, "{wrap:?}: text after the table is {gap}pt below the cell's text; the 300pt picture grew the row"); + } +} + #[test] fn autofit_sizes_columns_by_their_content() { let text = |s: &str| wordcraft_doc::para_block(wordcraft_doc::Paragraph::with_text(s, Default::default()));