Text wrap: the minimum gap applies only where a wrap object reaches the column

free_slot held a column to the minimum gap width (1.5 em) whenever a wrap
object overlapped the line's band vertically, even if the object sat beside
another column of the frame: in a frame of narrow columns, an object beside
column 2 dropped column 1's lines. The minimum now applies only when the
object reaches the column itself (aiongg's review on #589).

Test: wrap_object_beside_one_narrow_column_leaves_the_other_alone (two
18 pt columns in a 14 pt paragraph, an object beside column 2) fails before
(overset) and passes after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CgGsJkVe5GYG9qK6VFdECa
This commit is contained in:
Sean Hammond
2026-10-12 15:14:08 +00:00
co-authored by Claude Opus 5.5
parent 2307b28c72
commit 1278957455
2 changed files with 24 additions and 3 deletions
+5 -3
View File
@@ -2080,8 +2080,8 @@ fn line_metrics(
/// Widest free horizontal interval of `col` in the band, or None if blocked. A gap a wrap object
/// cuts narrower than 1.5 em of `size` is skipped (a sliver beside the object); when no object
/// reaches the band the whole column is free, however narrow (a one-character frame such as an
/// article's closing tombstone).
/// reaches the column in the band the whole column is free, however narrow (a one-character frame
/// such as an article's closing tombstone, or a narrow column beside another that wraps).
fn free_slot(f: &FrameSpec, col: Rect, y0: f64, y1: f64, size: f64) -> Option<(f64, f64)> {
let mut free = vec![(col.x0, col.x1)];
let mut cut = false;
@@ -2092,7 +2092,9 @@ fn free_slot(f: &FrameSpec, col: Rect, y0: f64, y1: f64, size: f64) -> Option<(f
WrapMode::JumpToNextColumn if y1 > r.y0 && r.x0 < col.x1 && r.x1 > col.x0 => return None,
WrapMode::JumpObject if overlaps_band && r.x0 < col.x1 && r.x1 > col.x0 => return None,
WrapMode::BoundingBox | WrapMode::Contour if overlaps_band => {
cut = true;
// Held to the minimum width only when the object reaches this column: one beside
// another column of the frame leaves this one whole, however narrow.
cut |= r.x0 < col.x1 && r.x1 > col.x0;
let mut next = Vec::new();
for (a, b) in free {
if r.x1 <= a || r.x0 >= b {
+19
View File
@@ -184,6 +184,25 @@ fn narrow_frame_sets_lines_no_wrap_object_reaches() {
assert!(baseline - top < 20.0, "on the first line: {baseline} (text area from {top})");
}
/// In a frame of narrow columns, a wrap object beside column 2 leaves column 1 alone: only a gap
/// the object cuts in its own column is held to the minimum width (it was dropped, as the object
/// overlapped the line's band, and the text went past column 1).
#[test]
fn wrap_object_beside_one_narrow_column_leaves_the_other_alone() {
// Two 18 pt columns (12 pt gutter): narrower than 1.5 em of the 14 pt paragraph.
let (mut d, sid) = tombstone(Rect::new(300.0, 300.0, 348.0, 340.0), Rect::new(336.0, 300.0, 380.0, 340.0));
let fid = d.spreads[0].items.iter().find(|i| i.text_frame().is_some_and(|t| t.story == sid)).unwrap().id;
d.item_mut(fid).unwrap().text_frame_mut().unwrap().options.columns = 2;
let cs = compose_story(&d, sid, &ComposeOptions::default());
let f = &cs.frames[0];
assert_eq!(f.columns.len(), 2);
assert!(f.columns[0].width() < 21.0, "column 1 is narrower than 1.5 em: {}", f.columns[0].width());
assert!(!cs.is_overset());
let l = &f.lines[0];
assert_eq!(l.column, 0, "set in column 1");
assert!(l.baseline - f.columns[0].y0 < 20.0, "on its first line: {}", l.baseline);
}
#[test]
fn paragraph_composer_is_no_worse_than_greedy() {
// Sum of squared slack over lines (excluding last) should not exceed the greedy result.