mirror of
https://github.com/storytold/wordcraft.git
synced 2026-10-13 23:17:49 +00:00
R22: paragraph-relative floats start above the space before (#941)
Word positions a floating object "relative to paragraph" (and to its line) from the paragraph's start above its space before, below the previous paragraph's space after. WordCraft measured from the first line, so floats in spaced paragraphs sat lower by the space before. Measured in Word for Windows: 10 synthetic cases. Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
55bfc33418
commit
672522b2ed
4 files changed
+72
-9
No files matched your search
@@ -155,6 +155,7 @@ Ranked, to beta (detail and remaining milestones in [docs/roadmap.md](docs/roadm
|
||||
| 2026-10-12 | List indents like Word's when a numbered style has indents of its own: the style giving the list (and styles based on it) beat the level's indents, and a paragraph with its list switched off drops the styles' indents (27 cases measured in Word for Windows); NIH's Heading 3s now sit at their 0.9" |
|
||||
| 2026-10-12 | Keep with next like Word (20 cases measured in Word for Windows): a kept paragraph moves to the next page only when it would fit on this one by itself, otherwise it breaks as usual; chains of kept paragraphs move together |
|
||||
| 2026-10-12 | An empty paragraph that only carries a continuous section break takes no room, as in Word (10 cases measured in Word for Windows): text after it follows right on and balanced columns don't count it |
|
||||
| 2026-10-12 | Floating objects positioned relative to their paragraph start above its space before, as in Word (10 cases measured in Word for Windows) |
|
||||
| 2026-10-11 | Hide White Space (#645): `view.hideWhiteSpace` (View › Show checkbox, or double-click the gap between pages) collapses margins, headers and footers between pages to a thin line in Print Layout; screen only, page count unchanged |
|
||||
| 2026-10-11 | Fonts: Palatino Linotype and Book Antiqua fall back to Palatino (macOS) or TeX Gyre Pagella / P052 / URW Palladio (Linux) before generic serif (#670) |
|
||||
|---|---|
|
||||
|
||||
@@ -2449,6 +2449,7 @@ fn anchor_floats(
|
||||
pb: &mut PageBuilder,
|
||||
p: &Paragraph,
|
||||
y0: f32,
|
||||
before: f32,
|
||||
excl_mark: usize,
|
||||
label: &Option<(String, Level)>,
|
||||
) -> (HashMap<usize, Rect>, Arc<ParaLayout>) {
|
||||
@@ -2466,7 +2467,8 @@ fn anchor_floats(
|
||||
if left_out(oi) {
|
||||
continue;
|
||||
}
|
||||
let r = float_rect(Some(frame), (col_x, width), y0, w, h, float);
|
||||
// Positions relative to the paragraph start above its space before (Word for Windows).
|
||||
let r = float_rect(Some(frame), (col_x, width), y0 - before, w, h, float);
|
||||
rects.insert(oi, r);
|
||||
let outline = ctx.wrap_outline(o);
|
||||
if let Some(area) = wrap_area(r, float, outline.as_deref().map(|v| v.as_slice())) {
|
||||
@@ -2621,9 +2623,9 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
let excl_mark = pb.excl.len();
|
||||
let guess = pb.y + space_before;
|
||||
let label = ctx.next_label(p);
|
||||
let (mut float_rects, mut pl) = anchor_floats(ctx, pb, p, guess, excl_mark, &label);
|
||||
// The anchor paragraph's top the floats were placed from.
|
||||
let mut anchor_y = guess;
|
||||
let (mut float_rects, mut pl) = anchor_floats(ctx, pb, p, guess, space_before, excl_mark, &label);
|
||||
// The anchor paragraph's top (above its space before) the floats were placed from.
|
||||
let mut anchor_y = pb.y;
|
||||
let spot = |pb: &PageBuilder| (pb.pages.len(), pb.col);
|
||||
let placed_at = spot(pb);
|
||||
if pl.rp.page_break_before && !pb.at_top() && !pb.web {
|
||||
@@ -2643,6 +2645,7 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
}
|
||||
}
|
||||
pb.y += before;
|
||||
let mut applied_before = before;
|
||||
// Keep lines together: if it doesn't fit but would on an empty column, move it. Keep with
|
||||
// next alone moves a paragraph only when it would fit here by itself (Word for Windows): one
|
||||
// that doesn't breaks as usual, its last lines going over with the next paragraph.
|
||||
@@ -2652,13 +2655,14 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
if pb.y + need > pb.bottom && !pb.at_top() && need <= pb.uncapped_bottom() - pb.top {
|
||||
pb.advance(block, body_top);
|
||||
pb.y += pl.rp.space_before.min(0.0);
|
||||
applied_before = pl.rp.space_before.min(0.0);
|
||||
}
|
||||
}
|
||||
// A different top only matters when there is something to wrap around.
|
||||
if spot(pb) != placed_at || ((pb.y - guess).abs() > 0.01 && !pb.excl.is_empty()) {
|
||||
let y0 = pb.y;
|
||||
(float_rects, pl) = anchor_floats(ctx, pb, p, y0, excl_mark, &label);
|
||||
anchor_y = y0;
|
||||
(float_rects, pl) = anchor_floats(ctx, pb, p, y0, applied_before, excl_mark, &label);
|
||||
anchor_y = y0 - applied_before;
|
||||
}
|
||||
let mut n = pl.lines.len();
|
||||
let mut l0 = 0;
|
||||
@@ -2747,7 +2751,7 @@ fn place_para_in_flow(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragraph, block:
|
||||
pb.advance(block, body_top);
|
||||
if l0 == 0 {
|
||||
let y0 = pb.y;
|
||||
(float_rects, pl) = anchor_floats(ctx, pb, p, y0, excl_mark, &label);
|
||||
(float_rects, pl) = anchor_floats(ctx, pb, p, y0, 0.0, excl_mark, &label);
|
||||
anchor_y = y0;
|
||||
n = pl.lines.len();
|
||||
}
|
||||
|
||||
@@ -2342,8 +2342,10 @@ fn float_moved_to_a_new_page_sits_at_its_paragraph_top() {
|
||||
let l = lay(&d);
|
||||
let r = shapes(&l.pages[1].items)[0];
|
||||
let top = first_line_top(&l.pages[1].items);
|
||||
// The space before counts once: the picture and the text both start below it.
|
||||
assert!((r.y - top).abs() < 0.5, "picture at {} but its paragraph's text starts at {top}", r.y);
|
||||
// The picture sits at the paragraph's start, above its space before, as in Word (`plan/word`
|
||||
// R22); the text starts below the space before.
|
||||
let body_top = d.last_section.margin_top;
|
||||
assert!((r.y - body_top).abs() < 0.5 && top >= body_top, "picture at {} but its paragraph starts at {body_top}", r.y);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -4426,3 +4428,58 @@ fn an_empty_continuous_section_mark_takes_no_room() {
|
||||
let new_page = tops("", 1, SectionStart::NextPage);
|
||||
assert_eq!(new_page[1].0, 1, "a next-page section starts a page: {new_page:?}");
|
||||
}
|
||||
|
||||
/// A floating object positioned relative to its paragraph is measured from the paragraph's start
|
||||
/// above its space before, measured in Word for Windows (`plan/word` R22, 10 cases: 0, 10 and 20
|
||||
/// pt before, offsets 0 and −7.2 pt; the previous paragraph's space after still counts). The SARP
|
||||
/// form's field boxes sat a space-before too low, over their labels.
|
||||
#[test]
|
||||
fn paragraph_relative_floats_start_above_the_space_before() {
|
||||
let shape_top = |before: f32, prev_after: f32, y: f32| {
|
||||
let float = wordcraft_doc::para::Float {
|
||||
wrap: wordcraft_doc::para::Wrap::InFrontOfText,
|
||||
h_rel: wordcraft_doc::para::Anchor::Column,
|
||||
v_rel: wordcraft_doc::para::Anchor::Paragraph,
|
||||
y,
|
||||
..Default::default()
|
||||
};
|
||||
let shape = InlineObject::Shape {
|
||||
kind: wordcraft_doc::para::ShapeKind::Rectangle,
|
||||
w: 20.0,
|
||||
h: 20.0,
|
||||
fill: None,
|
||||
stroke: None,
|
||||
stroke_width: 1.0,
|
||||
float,
|
||||
story: None,
|
||||
freeform: None,
|
||||
effects: Default::default(),
|
||||
extra: Default::default(),
|
||||
};
|
||||
let plain = ParaProps { space_before: Some(0.0), space_after: Some(prev_after), ..Default::default() };
|
||||
let mut first = wordcraft_doc::Paragraph::with_text("First", Default::default());
|
||||
first.props = plain;
|
||||
let mut p = wordcraft_doc::Paragraph::with_text("Text ", Default::default());
|
||||
p.text.push(wordcraft_doc::para::OBJ);
|
||||
p.objects = vec![shape];
|
||||
p.runs = vec![wordcraft_doc::Run { len: p.text.len(), props: Default::default() }];
|
||||
p.props = ParaProps { space_before: Some(before), space_after: Some(0.0), ..Default::default() };
|
||||
let mut d = Document::from_text("");
|
||||
d.body = vec![wordcraft_doc::para_block(first), wordcraft_doc::para_block(p)];
|
||||
let l = lay(&d);
|
||||
let first_bottom = {
|
||||
let c = l.caret(&Pos::body(0, 0)).unwrap();
|
||||
c.top + c.height
|
||||
};
|
||||
let top = l.pages[0].items.iter().find_map(|i| if let Placed::Shape { rect, .. } = i { Some(rect.y) } else { None }).unwrap();
|
||||
(top, first_bottom)
|
||||
};
|
||||
for before in [0.0, 10.0, 20.0] {
|
||||
for y in [0.0, -7.2] {
|
||||
let (top, first_bottom) = shape_top(before, 0.0, y);
|
||||
assert!((top - (first_bottom + y)).abs() < 1.0, "{before} pt before, offset {y}: {top} vs {}", first_bottom + y);
|
||||
}
|
||||
}
|
||||
let (top, first_bottom) = shape_top(0.0, 10.0, 0.0);
|
||||
assert!((top - (first_bottom + 10.0)).abs() < 1.0, "below the previous paragraph's space after: {top} vs {first_bottom}");
|
||||
}
|
||||
@@ -82,6 +82,7 @@ font can't match Word's metrics.
|
||||
| 2026-10-12 | trivial | List indents and style indents combine like Word's: a numbered style's own indents (and those of styles based on it) beat the list level's, its base styles' don't, the paragraph's own list beats every style; a paragraph that switches its list off (`numId` 0) drops the styles' indents |
|
||||
| 2026-10-12 | trivial | Keep with next: a paragraph that doesn't fit on the page anyway breaks as usual (its last lines go over with the next paragraph) instead of moving whole; a chain of kept paragraphs moves together |
|
||||
| 2026-10-12 | trivial | Continuous section breaks: an empty paragraph carrying only the break takes no room (text after it follows on; balanced columns ignore it) |
|
||||
| 2026-10-12 | trivial | Floats relative to a paragraph (or its line) are measured from the paragraph's start above its space before |
|
||||
| 2026-10-11 | trivial | Chinese documents (#534): indents in characters, a page-anchored floating table after a full page moves to the next page, no blank page from a page break at the bottom of a full page |
|
||||
| 2026-10-11 | trivial | East Asian list number formats (一、二、三, 第一章, ①); list labels whose characters come from two fonts draw each part with its own font (the digits of "1、" no longer show as other letters); a list level without indents leaves the label at the paragraph edge (#534) |
|
||||
| 2026-10-11 | trivial | Vertical sections: `w:textDirection` in `w:sectPr`, upright East Asian glyphs (#491) |
|
||||
|
||||
Reference in new issue
Block a user