diff --git a/crates/layout/src/para.rs b/crates/layout/src/para.rs index 1097cafd3..35be42a93 100644 --- a/crates/layout/src/para.rs +++ b/crates/layout/src/para.rs @@ -1845,9 +1845,11 @@ fn ptab_stop(align: PTabAlign, base: PTabBase, leader: TabLeader, width: f32, in } fn break_lines(pl: &mut ParaLayout, env: &ParaEnv, mark_style: u32, level: Option<&Level>) { + // Narrow cells and float spans must retain their usable width. + const MIN_LINE_WIDTH: f32 = 1.0; let suffix = level.map(|l| l.suffix); let rp = pl.rp.clone(); - let width = env.width.max(12.0); + let width = env.width.max(MIN_LINE_WIDTH); let base_right = (width - rp.indent_right).max(1.0); let default_tab = env.doc.settings.default_tab; let first_left = rp.indent_left + rp.indent_first; @@ -1929,7 +1931,7 @@ fn break_lines(pl: &mut ParaLayout, env: &ParaEnv, mark_style: u32, level: Optio narrowed = !rest.is_empty() || a > lo || b < base_right; row_rest = rest; row_first = lines.len(); - break (a, b.max(a + 12.0)); + break (a, b.max(a + MIN_LINE_WIDTH)); } Err(below) if below > top && guard < 50 => { top = below; @@ -1937,7 +1939,7 @@ fn break_lines(pl: &mut ParaLayout, env: &ParaEnv, mark_style: u32, level: Optio } Ok((None, _)) | Err(_) => { row_first = lines.len(); - break (lo, base_right.max(lo + 12.0)); + break (lo, base_right.max(lo + MIN_LINE_WIDTH)); } } } diff --git a/crates/layout/src/tests.rs b/crates/layout/src/tests.rs index 80bc98b21..6e13b0312 100644 --- a/crates/layout/src/tests.rs +++ b/crates/layout/src/tests.rs @@ -952,6 +952,44 @@ fn positional_tab_after_only_a_bookmark_stays_on_its_line() { assert!((x - 108.0).abs() < 0.5, "X at the indent: {x}"); } +#[test] +fn narrow_cells_keep_their_width() { + let mut p = wordcraft_doc::Paragraph::with_text("WW", wordcraft_doc::CharProps { size: Some(6.0), ..Default::default() }); + p.props.space_before = Some(0.0); + p.props.space_after = Some(0.0); + let mut d = Document::new(); + d.body = vec![wordcraft_doc::para_block(p.clone())]; + let wide = lay(&d); + let advance = wide.pages[0] + .items + .iter() + .find_map(|item| match item { + Placed::Lines { para, .. } => Some(para.clusters.iter().map(|c| c.adv).sum::()), + _ => None, + }) + .unwrap(); + let narrow = advance - 0.25; + assert!(narrow > 6.0 && advance < 12.0, "probe width {advance}"); + for (width, expected_lines) in [(narrow, 2), (1.0, 2), (12.0, 1)] { + let mut table = Table::new(1, 1, width); + table.props.style = None; + table.props.fixed = true; + table.props.cell_margins = Some([0.0; 4]); + table.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(p.clone())]; + d.body = vec![std::sync::Arc::new(Block::Table(table))]; + let l = lay(&d); + let lines = l.pages[0] + .items + .iter() + .find_map(|item| match item { + Placed::Lines { para, .. } => Some(para.lines.len()), + _ => None, + }) + .unwrap(); + assert_eq!(lines, expected_lines, "cell width {width}, text width {advance}"); + } +} + #[test] fn lists_get_labels_and_indent() { let mut d = Document::from_text("one\ntwo\nthree"); diff --git a/docs/layout-parity.md b/docs/layout-parity.md index a9b9c4f1f..aadb9983f 100644 --- a/docs/layout-parity.md +++ b/docs/layout-parity.md @@ -23,6 +23,7 @@ Additive paragraph spacing retains the full before-space after a section page br | 2026-10-16 | trivial | Split-cell content retains its resolved top margin and border band on continuation pages, matching original Word Mac16.113.2 controls. Synthetic regressions cover margins, borders, line spacing and image frames. | | Hint-reliant CJK glyph outlines (DFKai-SB / 標楷體) | 🟡 | Required TrueType interpreter runs at units-per-em size; local Windows glyph-bounds regression and synthetic DOCX page/window comparison pass. Other platforms and hint-reliant families remain unmeasured; no native Word comparison. | — | | Line breaking (UAX #14 with Word's exceptions: no break after `/`) | ✅ | `layout::tests::no_line_break_right_after_a_slash` | — | +| Narrow table cells | ✅ | `layout::tests::narrow_cells_keep_their_width`: usable widths below 12 pt wrap without expansion; oversized glyphs still make progress | 0 | | Justification, Word 2013+ space shrinking (compat mode 15) | ✅ | #109 | — | | Spaces at the end of a line hang past the right margin instead of wrapping; the caret after them stays at the margin, in the body and in headers and footers | ✅ | `layout::tests::caret_after_hanging_spaces_stays_at_the_margin` (#2146): a letter typed after them wraps to the next line | — | | Complex scripts (Myanmar, Khmer, Indic): shaped per script run with OpenType fallback fonts, one cluster per syllable, Myanmar wraps between syllables | 🟡 | `layout::tests_complex` (#604). Khmer, Thai and Lao still wrap only at spaces (need a dictionary) | 4–8 | @@ -211,6 +212,7 @@ Behind-text pictures in table cells participate in centre and bottom vertical al | 2026-10-16 | trivial | Behind-text pictures in table cells participate in centre and bottom vertical alignment without increasing row flow height. | | 2026-10-15 | trivial | Absent or zero DrawingML effect extents remain zero on rotated pictures; contracted axes no longer reserve invented effects room. Synthetic quarter-turn import regression (`zero_effects_stay_zero_rotated`). | | 2026-10-15 | trivial | Additive paragraph spacing retains the full before-space after a section page break; ordinary overlapping spacing is unchanged. | +| 2026-10-15 | trivial | Removed the 12 pt line-width floor in narrow cells and float spans; synthetic wrapping and oversized-glyph controls | | 2026-10-14 | trivial | Row table border exceptions draw without leaking into other rows | | 2026-10-14 | trivial | Consistent cell widths override a stale table grid | | 2026-10-14 | trivial | Paragraphs inside complex-field instructions take no flow height; empty result paragraphs keep their line. |