Use the body boundary for header margin anchors (#2485)

* T280: anchor header drawings below tall headers

* #2485: share rendered header boundary with pins and obstacles

* docs: layout-parity status line for #2485

---------

Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
Fin Warman
2026-11-01 14:11:56 -05:00
committed by GitHub
co-authored by Brandon Thomas
parent 5cd53f184a
commit e5f2885ecc
9 changed files with 322 additions and 38 deletions
+16 -8
View File
@@ -6,8 +6,8 @@ use super::page::PageFrame;
use super::placed::Placed;
use super::table_flow::below;
use super::{
Anchor, Arc, Block, Blocks, Border, Document, Float, FloatAlign, HashMap, ParaLayout, Paragraph, Path, Rect, SectionProps, StoryRef, TableFloat,
TextDirection, Wrap, WrapText, para, table,
Anchor, Arc, Block, Blocks, Border, Document, Float, FloatAlign, HashMap, ParaLayout, Paragraph, Path, Rect, StoryRef, TableFloat, TextDirection,
Wrap, WrapText, para, table,
};
/// Cell identity is independent of optional table-style formatting.
@@ -281,7 +281,8 @@ pub(crate) fn layout_box(ctx: &mut Ctx, input: BoxInput<'_>) -> BoxLayout {
continue;
}
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 };
let page =
PageFrame { sect: &pin.sect, margin_top: pin.margin_top, origin: (0.0, 0.0), number: pin.number, mirror: pin.mirror };
pinned.insert(oi, float_rect(Some(page), (0.0, width), para_top, w, h, float));
continue;
}
@@ -460,15 +461,22 @@ fn box_float_table_rect(
_ => None,
}
};
let page_y = |s: &SectionProps| match f.v_rel {
Anchor::Page => Some((0.0, s.page_h)),
Anchor::Margin => Some((s.margin_top, s.text_height())),
_ => None,
let page_y = |fr: &PageFrame| {
let s = fr.sect;
match f.v_rel {
Anchor::Page => Some((0.0, s.page_h)),
Anchor::Margin => {
let mut reference = s.clone();
reference.margin_top = fr.margin_top.unwrap_or(s.margin_top);
Some((reference.margin_top, reference.text_height()))
}
_ => None,
}
};
let h_area = frame.and_then(|fr| page_x(&fr).map(|(a, e)| (a - fr.origin.0, e))).unwrap_or((0.0, width));
// In a cell an offset from the page or margin has nothing to measure from: the table stands
// where the text is.
let (v_area, y_off) = match frame.and_then(|fr| page_y(fr.sect).map(|(a, e)| (a - fr.origin.1, e))) {
let (v_area, y_off) = match frame.and_then(|fr| page_y(&fr).map(|(a, e)| (a - fr.origin.1, e))) {
Some(area) => (area, f.y),
None if matches!(f.v_rel, Anchor::Page | Anchor::Margin) => ((y, 0.0), 0.0),
None => ((y, 0.0), f.y),
+11 -1
View File
@@ -89,6 +89,8 @@ struct ParaEnvInput<'a> {
pub(crate) struct Pin {
pub(crate) story: StoryRef,
pub(crate) sect: SectionProps,
/// Rendered header Margin boundary; absent in footers and height probes.
pub(crate) margin_top: Option<f32>,
pub(crate) number: u32,
pub(crate) mirror: bool,
pub(crate) behind: Vec<Placed>,
@@ -108,7 +110,14 @@ pub(crate) fn page_anchored(f: &Float) -> bool {
/// Run `f`, a layout of header or footer `id` for page `number` of `sect`, with the floats in its
/// table cells that are placed on the page put there (see [`Pin`]). Returns `f`'s result and those
/// floats' items, in page coordinates: behind the header's text, and in front.
pub(crate) fn with_pin<T>(ctx: &mut Ctx, id: u32, sect: &SectionProps, number: u32, f: impl FnOnce(&mut Ctx) -> T) -> (T, Vec<Placed>, Vec<Placed>) {
pub(crate) fn with_pin<T>(
ctx: &mut Ctx,
id: u32,
sect: &SectionProps,
number: u32,
margin_top: Option<f32>,
f: impl FnOnce(&mut Ctx) -> T,
) -> (T, Vec<Placed>, Vec<Placed>) {
let tables = ctx.doc.parts.get(&id).is_some_and(|p| p.blocks.iter().any(|b| matches!(**b, Block::Table(_))));
if !tables {
return (f(ctx), Vec::new(), Vec::new());
@@ -116,6 +125,7 @@ pub(crate) fn with_pin<T>(ctx: &mut Ctx, id: u32, sect: &SectionProps, number: u
let pin = Pin {
story: StoryRef::Part(id),
sect: sect.clone(),
margin_top,
number,
mirror: ctx.mirror,
behind: Vec::new(),
+18 -10
View File
@@ -3,8 +3,8 @@ use super::page::{PageBuilder, PageFrame};
use super::placed::Placed;
use super::table_flow::footer_table_wraps;
use super::{
Anchor, Arc, CharProps, Document, Float, FloatAlign, HashMap, HideDeleted, InlineObject, Level, ParaLayout, Paragraph, Path, Point, Rect,
SectionProps, Spin, StoryRef, Wrap, WrapText, contour, display, para, textbox,
Anchor, Arc, CharProps, Document, Float, FloatAlign, HashMap, HideDeleted, InlineObject, Level, ParaLayout, Paragraph, Path, Point, Rect, Spin,
StoryRef, Wrap, WrapText, contour, display, para, textbox,
};
/// Which of `p`'s objects (by index) the layout leaves out ([`para::left_out`]): anchored in hidden
@@ -80,15 +80,23 @@ pub(crate) fn float_rect(frame: Option<PageFrame>, col: (f32, f32), y0: f32, w:
};
Some((start - shift, extent))
};
let page_y = |s: &SectionProps| match float.v_rel {
Anchor::Page => Some((0.0, s.page_h)),
Anchor::Margin => Some((s.margin_top, s.page_h - s.margin_top - s.margin_bottom)),
Anchor::TopMargin | Anchor::InsideMargin => Some((0.0, s.margin_top)),
Anchor::BottomMargin | Anchor::OutsideMargin => Some((s.page_h - s.margin_bottom, s.margin_bottom)),
_ => None,
let page_y = |f: &PageFrame| {
let s = f.sect;
let top = f.margin_top.unwrap_or(s.margin_top);
match float.v_rel {
Anchor::Page => Some((0.0, s.page_h)),
Anchor::Margin => {
let mut reference = s.clone();
reference.margin_top = top;
Some((top, reference.text_height()))
}
Anchor::TopMargin | Anchor::InsideMargin => Some((0.0, s.margin_top)),
Anchor::BottomMargin | Anchor::OutsideMargin => Some((s.page_h - s.margin_bottom, s.margin_bottom)),
_ => None,
}
};
let hx = frame.and_then(|f| page_x(&f).map(|(a, e)| (a - f.origin.0, e))).unwrap_or(col);
let vy = frame.and_then(|f| page_y(f.sect).map(|(a, e)| (a - f.origin.1, e))).unwrap_or((y0, 0.0));
let vy = frame.and_then(|f| page_y(&f).map(|(a, e)| (a - f.origin.1, e))).unwrap_or((y0, 0.0));
let h_align = PageFrame::h_align(frame.as_ref(), float.h_align);
// Without a frame (a cell) `vy` has no height to align in: the object starts at the anchor
// line instead of being pushed up by half its height or more.
@@ -346,7 +354,7 @@ pub(crate) fn anchor_floats(
// The paragraph's top, and whether text was placed before it on the page.
let top = y0 - before;
let text_before = top > pb.top + 0.5 || pb.col > 0;
let frame = PageFrame { sect: pb.sect, origin: (0.0, 0.0), number: pb.number, mirror: ctx.mirror };
let frame = PageFrame { sect: pb.sect, margin_top: None, origin: (0.0, 0.0), number: pb.number, mirror: ctx.mirror };
// An object left out of the layout (hidden, or deleted in the final text) is not placed, so
// it takes no room either.
let left_out = left_out_objects(ctx.doc, p, None, ctx.opts.show_hidden, ctx.hide_deleted);
+2 -2
View File
@@ -503,7 +503,7 @@ impl<'a> Flow<'a> {
pb.para_end = Some((pb.pages.len(), pb.col, end));
// Pictures, shapes and text boxes in these lines (web view: floats placed by this piece).
let at = ObjFrame {
page: Some(PageFrame { sect: pb.sect, origin: (0.0, 0.0), number: pb.number, mirror: ctx.mirror }),
page: Some(PageFrame { sect: pb.sect, margin_top: None, origin: (0.0, 0.0), number: pb.number, mirror: ctx.mirror }),
col: (x, self.width),
para_y: if pb.web { y } else { self.anchor_y },
table_chr: None,
@@ -800,7 +800,7 @@ pub(crate) fn place_outline_para(ctx: &mut Ctx, pb: &mut PageBuilder, p: &Paragr
let mut items = Vec::new();
push_para(&mut items, (StoryRef::Body, &[block as u32]), &pl, (0, l1), (x, y), width, edges);
let at = ObjFrame {
page: Some(PageFrame { sect: pb.sect, origin: (0.0, 0.0), number: pb.number, mirror: ctx.mirror }),
page: Some(PageFrame { sect: pb.sect, margin_top: None, origin: (0.0, 0.0), number: pb.number, mirror: ctx.mirror }),
col: (x, width),
para_y: y,
table_chr: None,
+6 -6
View File
@@ -143,7 +143,7 @@ pub(crate) fn header_table_areas(ctx: &mut Ctx, pb: &PageBuilder) -> Vec<Rect> {
let Some(part) = ctx.doc.parts.get(&id) else { return Vec::new() };
let blocks = part.blocks.clone();
let x = sect.margin_left + sect.gutter;
let frame = PageFrame { sect, origin: (x, sect.header), number: page.number, mirror: ctx.mirror };
let frame = PageFrame { sect, margin_top: Some(page.body.y), origin: (x, sect.header), number: page.number, mirror: ctx.mirror };
let base = hf.defining_width(false, page.section, Some(id));
let counters = std::mem::take(&mut ctx.counters);
let boxes = std::mem::take(&mut ctx.boxes);
@@ -298,9 +298,9 @@ pub(crate) fn headers_footers(ctx: &mut Ctx, pages: &mut [Page], sections: &[(us
&& let Some(part) = ctx.doc.parts.get(&id)
{
let blocks = part.blocks.clone();
let frame = PageFrame { sect, origin: (x, sect.header), number: page.number, mirror: ctx.mirror };
let frame = PageFrame { sect, margin_top: Some(page.body.y), origin: (x, sect.header), number: page.number, mirror: ctx.mirror };
let base = hf.defining_width(false, page.section, hid);
let (mut items, behind, front) = with_pin(ctx, id, sect, page.number, |ctx| {
let (mut items, behind, front) = with_pin(ctx, id, sect, page.number, frame.margin_top, |ctx| {
with_pct_base(ctx, base, |c| layout_box(c, BoxInput::other(StoryRef::Part(id), &blocks, w, 0, Some(frame))).items)
});
for it in &mut items {
@@ -315,11 +315,11 @@ pub(crate) fn headers_footers(ctx: &mut Ctx, pages: &mut [Page], sections: &[(us
let blocks = part.blocks.clone();
let base = hf.defining_width(true, page.section, fid);
// The footer's height fixes where it sits, which page-relative floats need.
let (h, ..) = with_pin(ctx, id, sect, page.number, |ctx| {
let (h, ..) = with_pin(ctx, id, sect, page.number, None, |ctx| {
with_pct_base(ctx, base, |c| layout_box(c, BoxInput::other(StoryRef::Part(id), &blocks, w, 0, None)).height)
});
let frame = PageFrame { sect, origin: (x, sect.page_h - sect.footer - h), number: page.number, mirror: ctx.mirror };
let (mut items, behind, front) = with_pin(ctx, id, sect, page.number, |ctx| {
let frame = PageFrame { sect, margin_top: None, origin: (x, sect.page_h - sect.footer - h), number: page.number, mirror: ctx.mirror };
let (mut items, behind, front) = with_pin(ctx, id, sect, page.number, frame.margin_top, |ctx| {
with_pct_base(ctx, base, |c| layout_box(c, BoxInput::other(StoryRef::Part(id), &blocks, w, 0, Some(frame))).items)
});
for it in &mut items {
+9 -5
View File
@@ -412,10 +412,11 @@ pub(crate) fn body_top_for(ctx: &mut Ctx, sect: &SectionProps, header: Option<u3
let Some(part) = ctx.doc.parts.get(&id) else { return sect.margin_top };
let blocks = part.blocks.clone();
// Placed as `headers_footers` draws it, so page-relative floats wrap the same way.
let frame = PageFrame { sect, origin: (sect.margin_left + sect.gutter, sect.header), number: 1, mirror: ctx.mirror };
let frame = PageFrame { sect, margin_top: None, origin: (sect.margin_left + sect.gutter, sect.header), number: 1, mirror: ctx.mirror };
let track = std::mem::replace(&mut ctx.track_totals, false);
let (h, ..) =
with_pin(ctx, id, sect, 1, |ctx| layout_box(ctx, BoxInput::other(StoryRef::Part(id), &blocks, sect.text_width(), 0, Some(frame))).height);
let (h, ..) = with_pin(ctx, id, sect, 1, None, |ctx| {
layout_box(ctx, BoxInput::other(StoryRef::Part(id), &blocks, sect.text_width(), 0, Some(frame))).height
});
ctx.track_totals = track;
// Word starts the body right below a header that reaches past the top margin, no gap.
sect.margin_top.max(sect.header + h)
@@ -428,7 +429,8 @@ pub(crate) fn foot_top_for(ctx: &mut Ctx, sect: &SectionProps, footer: Option<u3
// A page total in the footer is filled in after pagination, like the header's: it doesn't
// make the body lay out again.
let track = std::mem::replace(&mut ctx.track_totals, false);
let (laid, ..) = with_pin(ctx, id, sect, 1, |ctx| layout_box(ctx, BoxInput::other(StoryRef::Part(id), &blocks, sect.text_width(), 0, None)));
let (laid, ..) =
with_pin(ctx, id, sect, 1, None, |ctx| layout_box(ctx, BoxInput::other(StoryRef::Part(id), &blocks, sect.text_width(), 0, None)));
ctx.track_totals = track;
let top = sect.page_h - sect.footer - laid.height;
// Flow-table strokes extend above their cell boxes. Floating decorations keep their wrap rules.
@@ -470,6 +472,8 @@ pub(crate) fn foot_top_for(ctx: &mut Ctx, sect: &SectionProps, footer: Option<u3
#[derive(Clone, Copy)]
pub(crate) struct PageFrame<'a> {
pub(crate) sect: &'a SectionProps,
/// Override the Margin anchor area without changing the declared margin bands.
pub(crate) margin_top: Option<f32>,
/// Page position of the box's (0, 0); (0, 0) for body text.
pub(crate) origin: (f32, f32),
/// The page's number: inside and outside are left and right on odd pages, the other way
@@ -504,5 +508,5 @@ impl PageFrame<'_> {
/// The page `pb` is filling, for placing floating objects on it (`mirror`: [`Ctx::mirror`]).
pub(crate) fn page_frame<'a>(pb: &PageBuilder<'a>, mirror: bool) -> PageFrame<'a> {
PageFrame { sect: pb.sect, origin: (0.0, 0.0), number: pb.number, mirror }
PageFrame { sect: pb.sect, margin_top: None, origin: (0.0, 0.0), number: pb.number, mirror }
}
+1 -1
View File
@@ -51,7 +51,7 @@ pub(crate) fn footer_table_wraps(ctx: &mut Ctx, pb: &PageBuilder) -> Vec<WrapAre
let story = StoryRef::Part(id);
let h = layout_box(ctx, BoxInput::other(story, &blocks, sect.text_width(), 0, None)).height;
let y = sect.page_h - sect.footer - h;
let frame = PageFrame { sect, origin: (x, y), number: page.number, mirror: ctx.mirror };
let frame = PageFrame { sect, margin_top: None, origin: (x, y), number: page.number, mirror: ctx.mirror };
let mut areas = layout_box(ctx, BoxInput::other(story, &blocks, sect.text_width(), 0, Some(frame))).page_table_wraps;
for (rect, ..) in &mut areas {
rect.x += x;
+250 -4
View File
@@ -5466,12 +5466,15 @@ fn header_cell_float_placed_on_the_page() {
let header = |d: &mut Document, outside_cell: bool| {
let mut t = Table::new(1, 2, 468.0);
let mut cp = wordcraft_doc::Paragraph::with_text("Right cell", Default::default());
// This page-centering control keeps its header within the declared margin.
cp.props.line_spacing = Some(wordcraft_doc::props::LineSpacing::Exactly(12.0));
cp.props.space_before = Some(0.0);
cp.props.space_after = Some(0.0);
let tail = wordcraft_doc::Paragraph { props: cp.props.clone(), ..Default::default() };
cp.insert_object(0, rect_shape(360.0, 288.0, wm(outside_cell)), &Default::default()).unwrap();
t.rows[0].cells[1].blocks = vec![wordcraft_doc::para_block(cp)];
let id = d.add_part(
wordcraft_doc::PartKind::Header,
vec![std::sync::Arc::new(wordcraft_doc::Block::Table(t)), wordcraft_doc::para_block(Default::default())],
);
let id =
d.add_part(wordcraft_doc::PartKind::Header, vec![std::sync::Arc::new(wordcraft_doc::Block::Table(t)), wordcraft_doc::para_block(tail)]);
d.last_section.headers.default = Some(id);
};
header(&mut d, true);
@@ -12091,3 +12094,246 @@ fn an_empty_bulleted_mark_after_a_bulleted_section_closer_draws_no_bullet() {
assert!(text, "{filler} lines above: the text closer keeps its bullet");
}
}
fn boundary_header_para() -> Paragraph {
let mut p = Paragraph::with_text("One\nTwo\nThree\nFour\nFive", Default::default());
p.props.line_spacing = Some(wordcraft_doc::props::LineSpacing::Exactly(10.0));
p.props.space_before = Some(0.0);
p.props.space_after = Some(0.0);
p
}
#[test]
fn header_boundary_pinned() {
use wordcraft_doc::para::{Anchor, Float, FloatAlign, Wrap};
for anchor in [Anchor::Margin, Anchor::Page, Anchor::TopMargin] {
for align in [None, Some(FloatAlign::Center)] {
let mut doc = Document::from_text("Body");
doc.last_section.margin_top = 60.0;
doc.last_section.header = 36.0;
let mut cell = Paragraph::with_text("Cell", Default::default());
let float = Float {
wrap: Wrap::BehindText,
h_rel: Anchor::Page,
v_rel: anchor,
v_align: align,
outside_cell: true,
x: 300.0,
y: -20.0,
..Default::default()
};
cell.insert_object(0, rect_shape(40.0, 10.0, float), &Default::default()).unwrap();
let mut table = Table::new(1, 1, 40.0);
table.rows[0].cells[0].blocks = vec![wordcraft_doc::para_block(cell)];
let header = doc.add_part(
wordcraft_doc::PartKind::Header,
vec![wordcraft_doc::para_block(boundary_header_para()), Arc::new(Block::Table(table.clone()))],
);
let footer = doc.add_part(wordcraft_doc::PartKind::Footer, vec![Arc::new(Block::Table(table))]);
doc.last_section.headers.default = Some(header);
doc.last_section.footers.default = Some(footer);
let laid = lay(&doc);
let page = &laid.pages[0];
assert!(page.body.y > doc.last_section.margin_top);
for (items, margin) in [(&page.header, page.body.y), (&page.footer, doc.last_section.margin_top)] {
let (start, extent) = match anchor {
Anchor::Margin => (margin, doc.last_section.page_h - margin - doc.last_section.margin_bottom),
Anchor::TopMargin => (0.0, doc.last_section.margin_top),
_ => (0.0, doc.last_section.page_h),
};
let expected = match align {
Some(FloatAlign::Center) => start + (extent - 10.0) / 2.0,
_ => start - 20.0,
};
let rects = shapes(items);
assert_eq!(rects.len(), 1);
assert!((rects[0].y - expected).abs() < 0.01, "{anchor:?} {align:?}: {:?}, expected {expected}, body {:?}", rects[0], page.body);
let hit = items
.iter()
.find_map(|item| match item {
Placed::Object { rect, .. } => Some(rect),
_ => None,
})
.unwrap();
assert!((hit.y - expected).abs() < 0.01);
}
}
}
}
#[test]
fn header_boundary_obstacle() {
use wordcraft_doc::para::Anchor;
use wordcraft_doc::props::{HeightRule, TableFloat};
const FLOAT_HEIGHT: f32 = 10.0;
for overlap in [false, true] {
let mut doc = Document::from_text("Body");
doc.last_section.margin_top = 60.0;
doc.last_section.header = 36.0;
let mut header_table = Table::new(1, 1, 40.0);
header_table.rows[0].props.height = Some(FLOAT_HEIGHT);
header_table.rows[0].props.height_rule = HeightRule::Exact;
header_table.props.float = Some(TableFloat { h_rel: Anchor::Page, v_rel: Anchor::Margin, x: 300.0, y: -20.0, ..Default::default() });
let id = doc
.add_part(wordcraft_doc::PartKind::Header, vec![wordcraft_doc::para_block(boundary_header_para()), Arc::new(Block::Table(header_table))]);
doc.last_section.headers.default = Some(id);
let target = lay(&doc).pages[0].body.y - 20.0;
let mut body_table = Table::new(1, 1, 40.0);
body_table.rows[0].props.height = Some(FLOAT_HEIGHT);
body_table.rows[0].props.height_rule = HeightRule::Exact;
body_table.props.float = Some(TableFloat { h_rel: Anchor::Page, v_rel: Anchor::Page, x: 300.0, y: target, overlap, ..Default::default() });
doc.body.insert(0, Arc::new(Block::Table(body_table)));
let laid = lay(&doc);
let page = &laid.pages[0];
let cell = |items: &[Placed]| {
items
.iter()
.find_map(|item| match item {
Placed::Cell { rect, .. } => Some(*rect),
_ => None,
})
.unwrap()
};
let (header, body) = (cell(&page.header), cell(&page.items));
assert!(page.body.y > doc.last_section.margin_top);
assert!((header.y - target).abs() < 0.01);
if overlap {
assert!((body.y - target).abs() < 0.01, "overlap retains requested position: {body:?}");
} else {
assert!(body.y >= header.bottom() - 0.01, "body {body:?} overlaps rendered header {header:?}");
}
}
}
#[test]
fn header_margin_tracks_body_top() {
use wordcraft_doc::para::{Anchor, Float, Wrap};
use wordcraft_doc::props::LineSpacing;
for margin in [60.0, 120.0] {
for anchor in [Anchor::Margin, Anchor::Page, Anchor::Paragraph] {
let mut d = Document::from_text("Body");
d.last_section.margin_top = margin;
d.last_section.header = 36.0;
let mut hp = Paragraph::with_text("One\nTwo\nThree\nFour\nFive", Default::default());
hp.props.line_spacing = Some(LineSpacing::Exactly(10.0));
hp.props.space_before = Some(0.0);
hp.props.space_after = Some(0.0);
let float = Float { wrap: Wrap::BehindText, h_rel: Anchor::Page, v_rel: anchor, x: 300.0, y: -20.0, ..Default::default() };
hp.insert_object(0, rect_shape(40.0, 10.0, float), &Default::default()).unwrap();
let id = d.add_part(wordcraft_doc::PartKind::Header, vec![wordcraft_doc::para_block(hp)]);
d.last_section.headers.default = Some(id);
let l = lay(&d);
let page = &l.pages[0];
let expected = match anchor {
Anchor::Margin => page.body.y - 20.0,
Anchor::Page => -20.0,
_ => 16.0,
};
assert!(
(shapes(&page.header)[0].y - expected).abs() < 0.01,
"margin {margin}, anchor {anchor:?}: {:?}, body {:?}",
shapes(&page.header),
page.body
);
}
}
}
#[test]
fn header_anchor_extents_stay() {
use wordcraft_doc::para::{Anchor, Float, FloatAlign, Wrap};
use wordcraft_doc::props::LineSpacing;
for margin in [60.0, 120.0] {
for anchor in
[Anchor::Margin, Anchor::TopMargin, Anchor::InsideMargin, Anchor::Page, Anchor::Paragraph, Anchor::BottomMargin, Anchor::OutsideMargin]
{
for align in [None, Some(FloatAlign::Start), Some(FloatAlign::Center), Some(FloatAlign::End)] {
let mut d = Document::from_text("Body");
d.last_section.margin_top = margin;
d.last_section.header = 36.0;
let mut hp = Paragraph::with_text("One\nTwo\nThree\nFour\nFive", Default::default());
hp.props.line_spacing = Some(LineSpacing::Exactly(10.0));
hp.props.space_before = Some(0.0);
hp.props.space_after = Some(0.0);
let float =
Float { wrap: Wrap::BehindText, h_rel: Anchor::Page, v_rel: anchor, v_align: align, x: 300.0, y: -20.0, ..Default::default() };
hp.insert_object(0, rect_shape(40.0, 10.0, float), &Default::default()).unwrap();
let id = d.add_part(wordcraft_doc::PartKind::Header, vec![wordcraft_doc::para_block(hp)]);
d.last_section.headers.default = Some(id);
let l = lay(&d);
let page = &l.pages[0];
if margin == 60.0 {
assert!(page.body.y > margin, "the header must displace the body");
} else {
assert!((page.body.y - margin).abs() < 0.01, "the header must fit within the declared margin");
}
let s = &d.last_section;
let (start, extent) = match anchor {
Anchor::Margin => (page.body.y, s.page_h - page.body.y - s.margin_bottom),
Anchor::TopMargin | Anchor::InsideMargin => (0.0, margin),
Anchor::Page => (0.0, s.page_h),
Anchor::BottomMargin | Anchor::OutsideMargin => (s.page_h - s.margin_bottom, s.margin_bottom),
_ => (s.header, 0.0),
};
let expected = match align {
None => start - 20.0,
Some(FloatAlign::Center) => start + (extent - 10.0) / 2.0,
Some(FloatAlign::End) => start + extent - 10.0,
_ => start,
};
let drawn = shapes(&page.header);
assert_eq!(drawn.len(), 1, "the countercontrol must place its drawing");
assert!(
(drawn[0].y - expected).abs() < 0.01,
"margin {margin}, anchor {anchor:?}, align {align:?}: {:?}, expected {expected}, body {:?}",
drawn,
page.body
);
}
}
}
}
#[test]
fn header_margin_table_area() {
use wordcraft_doc::para::{Anchor, FloatAlign};
use wordcraft_doc::props::{HeightRule, LineSpacing, TableFloat};
for margin in [60.0, 120.0] {
for align in [None, Some(FloatAlign::Center), Some(FloatAlign::End)] {
let mut d = Document::from_text("Body");
d.last_section.margin_top = margin;
d.last_section.header = 36.0;
let mut hp = Paragraph::with_text("One\nTwo\nThree\nFour\nFive", Default::default());
hp.props.line_spacing = Some(LineSpacing::Exactly(10.0));
hp.props.space_before = Some(0.0);
hp.props.space_after = Some(0.0);
let mut t = Table::new(1, 1, 40.0);
t.rows[0].props.height = Some(10.0);
t.rows[0].props.height_rule = HeightRule::Exact;
t.props.float = Some(TableFloat { h_rel: Anchor::Page, v_rel: Anchor::Margin, v_align: align, x: 300.0, y: -20.0, ..Default::default() });
let id = d.add_part(wordcraft_doc::PartKind::Header, vec![wordcraft_doc::para_block(hp), Arc::new(Block::Table(t))]);
d.last_section.headers.default = Some(id);
let l = lay(&d);
let page = &l.pages[0];
if margin == 60.0 {
assert!(page.body.y > margin, "the header must displace the body");
}
let cells: Vec<_> = page
.header
.iter()
.filter_map(|it| match it {
Placed::Cell { rect, .. } => Some(rect),
_ => None,
})
.collect();
assert_eq!(cells.len(), 1);
let extent = (d.last_section.page_h - page.body.y - d.last_section.margin_bottom).max(18.0);
let expected = match align {
Some(FloatAlign::Center) => page.body.y + (extent - 10.0) / 2.0,
Some(FloatAlign::End) => page.body.y + extent - 10.0,
_ => page.body.y - 20.0,
};
assert!((cells[0].y - expected).abs() < 0.01, "margin {margin}, align {align:?}: {:?}, expected {expected}", cells[0]);
}
}
}
File diff suppressed because one or more lines are too long