cu20we: anchor box floats above paragraph spacing

Use the paragraph origin before effective space-before when placing floats in
headers, footers, table cells and text boxes. Keep text and wrap exclusions
relative to the text top, matching the existing body layout convention.

Cover all four box contexts with synthetic layout regressions and update the
gap, parity and progress records. Native Word comparison remains pending.

Co-Authored-By: GPT-6 <codex@openai.com>
This commit is contained in:
nexotiq
2026-10-23 17:10:59 -05:00
co-authored by GPT-6
parent 806a945d85
commit f5ce292fae
5 changed files with 75 additions and 6 deletions
+4 -1
View File
File diff suppressed because one or more lines are too long
+6 -3
View File
@@ -1274,6 +1274,9 @@ fn layout_box(
} else if i > 0 && before > 0.0 && prev_after > 0.0 && !ctx.doc.settings.sum_paragraph_spacing {
before = (before - prev_after).max(0.0);
}
// Paragraph-relative floats start above the effective space before, as in the
// body path. Keep text and wrap exclusions relative to the text's actual top.
let para_top = y;
y += if i == 0 { before } else { before.max(0.0) };
// Floating objects anchored here: place them, then wrap the text around them.
// One left out of the layout (hidden, or deleted in the final text) takes no room.
@@ -1288,10 +1291,10 @@ fn layout_box(
}
if let Some(pin) = ctx.pin.as_ref().filter(|pin| in_cell && pin.story == story && float.outside_cell && page_anchored(float)) {
let page = PageFrame { sect: &pin.sect, origin: (0.0, 0.0), number: pin.number, mirror: pin.mirror };
pinned.insert(oi, float_rect(Some(page), (0.0, width), y, w, h, float));
pinned.insert(oi, float_rect(Some(page), (0.0, width), para_top, w, h, float));
continue;
}
let r = float_rect(frame, (0.0, width), y, w, h, float);
let r = float_rect(frame, (0.0, width), para_top, w, h, float);
let outline = ctx.wrap_outline(o);
excl.extend(wrap_area(r, float, outline.as_deref().map(|v| v.as_slice())));
floats.insert(oi, r);
@@ -1317,7 +1320,7 @@ fn layout_box(
prev_end = push_para(&mut items, story, &path, &pl, 0, pl.lines.len(), 0.0, y + lead, width, edges);
// Pictures, shapes and text boxes: drawn, their areas and text boxes' text.
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 at = ObjFrame { page: frame, col: (0.0, width), para_y: para_top, 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, spin, wrap, .. } = placed
+57
View File
@@ -5009,6 +5009,63 @@ fn floats_in_headers_and_table_cells_are_drawn() {
assert!(c[0].x > 72.0 && c[0].x < 72.0 + 234.0, "inside the first cell: {:?}", c[0]);
}
#[test]
fn paragraph_relative_box_floats_start_above_space_before() {
use wordcraft_doc::{
PartKind,
para::{Anchor, Float, Wrap},
};
for context in ["header", "footer", "cell", "textbox"] {
let gap = |before| {
let mut d = Document::from_text("Body");
let mut p = wordcraft_doc::Paragraph::with_text("Anchor", Default::default());
p.props.space_before = Some(before);
let float = Float { wrap: Wrap::BehindText, v_rel: Anchor::Paragraph, y: 7.0, ..Default::default() };
p.insert_object(0, rect_shape(37.0, 9.0, float), &Default::default()).unwrap();
let story = match context {
"header" | "footer" => {
let kind = if context == "header" { PartKind::Header } else { PartKind::Footer };
let id = d.add_part(kind, vec![wordcraft_doc::para_block(p)]);
if context == "header" {
d.last_section.headers.default = Some(id);
} else {
d.last_section.footers.default = Some(id);
}
StoryRef::Part(id)
}
"cell" => {
let mut t = Table::new(1, 1, 468.0);
t.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(p)];
d.insert_block(StoryRef::Body, &Path::top(1), wordcraft_doc::Block::Table(t)).unwrap();
StoryRef::Body
}
_ => {
let id = text_box(&mut d, 0, "", 200.0, 100.0, page_float(100.0, 200.0));
d.parts.get_mut(&id).unwrap().blocks = vec![wordcraft_doc::para_block(p)];
StoryRef::Part(id)
}
};
let l = lay(&d);
let page = &l.pages[0];
let items = match context {
"header" => &page.header,
"footer" => &page.footer,
_ => &page.items,
};
let y = items
.iter()
.find_map(|i| match i {
Placed::Lines { story: s, y, path, .. } if *s == story && (context != "cell" || path.0.len() > 1) => Some(*y),
_ => None,
})
.unwrap();
let r = shapes(items).into_iter().find(|r| (r.w - 37.0).abs() < 0.01).unwrap();
r.y - y
};
assert!((gap(24.0) - gap(0.0) + 24.0).abs() < 0.01, "{context}: float must stay above the space before");
}
}
/// Inside a text box whose shape style gives its text a colour (`a:fontRef`, typically white on
/// the accent fill), automatic text takes that colour instead of black; text with a colour of
/// its own keeps it (Word, observed with synthetic documents).
+4 -1
View File
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long