M12: page breaks of a section's own keep the border around it, a leader is given room with its row, and what runs off a page costs its section the border

This commit is contained in:
Eric Kaden
2026-10-23 22:51:54 -04:00
parent 35716c6282
commit cafd507f29
2 changed files with 119 additions and 29 deletions
+53 -29
View File
@@ -200,8 +200,9 @@ struct Frame {
border: Option<Border>,
part: FlowMark,
parts: Vec<(usize, usize, Rect)>,
/// The flow left a part's content area other than at a break between children (an item cut
/// across pages, a table's own break): the border is left out.
/// Something inside didn't fit its content area: an item cut across areas, or one placed past
/// the area's bottom (a table row taller than an area, an overflow leader and the row after it
/// that don't fit together). The border is left out ([`Layouter::finish_frame`]).
lost: bool,
}
@@ -650,10 +651,11 @@ impl Layouter<'_> {
/// another viewer breaks such a section on a synthetic form: each one's part here ends after
/// its content and its bottom margin (the innermost first, each inside the next), the flow
/// moves to the next content area, and each starts a new part there after its top margin
/// (the outermost first). The top margins take only what the `need` points of the child that
/// comes next leave of the new area: a child that fits a fresh area but not after the margins
/// would otherwise run past its bottom, out of its border.
fn break_frames(&mut self, need: f64) -> Result<(), XfaError> {
/// (the outermost first). The top margins take only what the `need` points of what comes next
/// leave of the new area: a child that fits a fresh area but not after the margins would
/// otherwise run past its bottom, out of its border. `to_page` moves to a new page instead (a
/// child's own page break), which breaks the subforms around it the same way.
fn break_frames(&mut self, need: f64, to_page: bool) -> Result<(), XfaError> {
let (rect, mut y, _) = self.cursor();
let page = self.pages.len().saturating_sub(1);
let here: Vec<bool> = self.frames.iter().map(|f| self.on_area(&f.part)).collect();
@@ -666,7 +668,11 @@ impl Layouter<'_> {
f.parts.push((page, f.part.items, Rect::new(f.x, f.part.y, f.w, y - f.part.y)));
}
}
self.next_area()?;
if to_page {
self.new_page()?;
} else {
self.next_area()?;
}
let (rect, y, _) = self.cursor();
let mut room = (rect.bottom() - y - need).max(0.0);
for i in 0..self.frames.len() {
@@ -683,17 +689,16 @@ impl Layouter<'_> {
/// Close flowed subform `f` once its content is placed: its last part ends where the flow
/// is, and its border (edges and fill) is drawn round each part, closed on every side, under
/// that part's content. One whose flow left a content area other than at a break between its
/// children (an item of its own cut across pages) is drawn without its border, and the layout
/// says so.
/// that part's content. One holding something that didn't fit its content area ([`Frame::lost`])
/// is drawn without its border, and the layout says so.
fn finish_frame(&mut self, mut f: Frame) -> Result<(), XfaError> {
let Some(b) = f.border.take() else { return Ok(()) };
if b.fill().is_none() && b.visible_edges() == [false; 4] {
return Ok(());
}
let (area, end, _) = self.cursor();
// The bottom margin stays in the content area, as at a break; content that itself runs
// past it keeps the border round it.
// The bottom margin stays in the content area, as at a break; content that itself ran
// past it has already cost the section its border ([`Self::overflowed`]).
let end = end.min(area.bottom()).max(end - f.margin.bottom);
let page = self.pages.len().saturating_sub(1);
if !self.on_area(&f.part) {
@@ -702,7 +707,7 @@ impl Layouter<'_> {
f.parts.push((page, f.part.items, Rect::new(f.x, f.part.y, f.w, end - f.part.y)));
}
if f.lost {
self.warn_once("a bordered section whose own item a page break cuts is drawn without its border");
self.warn_once("a bordered section holding an item that doesn't fit on its page is drawn without its border");
return Ok(());
}
// The last part first: a part's place on its page stays where it was found.
@@ -744,19 +749,33 @@ impl Layouter<'_> {
c.y += dy.max(0.0);
c.placed = true;
}
let (_, y, _) = self.cursor();
self.overflowed(y);
}
/// Make room for `h`: stay, or move to the next content area / page. Returns whether a
/// break happened. Something that fits a fresh area breaks the flowed subforms it is in with
/// it ([`Self::break_frames`]), a table's rows included; one taller than an area just moves
/// on, and those subforms are drawn without their border ([`Self::finish_frame`]).
fn make_room(&mut self, h: f64) -> Result<bool, XfaError> {
/// Something was placed down to `bottom`: past the content area's bottom, the flowed subforms
/// it is inside can't hold it in a part, and are drawn without their border.
fn overflowed(&mut self, bottom: f64) {
let (rect, _, _) = self.cursor();
if bottom > rect.bottom() + 0.01 {
for f in &mut self.frames {
f.lost = true;
}
}
}
/// Make room for `h`, with `after` more drawn before it should it move (a table's overflow
/// leader): stay, or move to the next content area / page. Returns whether a break happened.
/// Something that fits a fresh area breaks the flowed subforms it is in with it
/// ([`Self::break_frames`]), a table's rows included; one taller than an area just moves on,
/// and those subforms are drawn without their border ([`Self::finish_frame`]).
fn make_room(&mut self, h: f64, after: f64) -> Result<bool, XfaError> {
let (rect, y, placed) = self.cursor();
if y + h <= rect.bottom() + 0.01 || !placed {
return Ok(false);
}
if h <= rect.h + 0.01 {
self.break_frames(h)?;
self.break_frames(h + after, false)?;
} else {
self.next_area()?;
}
@@ -769,7 +788,7 @@ impl Layouter<'_> {
fn flow_block(&mut self, node: &Node, x: f64, (w, h): (f64, f64), ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
let (rect, y, _) = self.cursor();
if y + h > rect.bottom() + 0.01 && h <= rect.h + 0.01 {
self.make_room(h)?;
self.make_room(h, 0.0)?;
}
let (rect, y, _) = self.cursor();
let first = self.pages.last().map_or(0, |p| p.items.len());
@@ -888,6 +907,7 @@ impl Layouter<'_> {
c.y = (end - shift).min(c.rect.bottom()).max(c.y);
c.placed = true;
}
self.overflowed(end - shift);
return Ok(());
}
rest = below;
@@ -914,7 +934,7 @@ impl Layouter<'_> {
let Node::Subform(sf) = node else { return Ok(()) };
self.check_measures()?;
if sf.break_before_page && self.page_has_flow() {
self.new_page()?;
self.break_frames(0.0, true)?;
}
// Data adds instances of a repeating subform (rows added in another viewer).
let instances = self.instances(node, &ctx.som);
@@ -972,7 +992,7 @@ impl Layouter<'_> {
}
}
if sf.break_after_page {
self.new_page()?;
self.break_frames(0.0, true)?;
}
Ok(())
}
@@ -989,6 +1009,12 @@ impl Layouter<'_> {
let cols = self.columns(sf, inner_w);
let leader =
sf.overflow_leader.as_ref().and_then(|name| sf.children.iter().position(|c| c.common().name.as_deref() == Some(name) && is_row(c)));
// The overflow leader starts each part after the first, before the row that moved.
let lead = leader.and_then(|li| match sf.children.get(li) {
Some(Node::Subform(l)) => Some((li, l.as_ref())),
_ => None,
});
let lead_h = lead.map_or(0.0, |(_, l)| self.row_height(l, &cols, inner_w, ctx, depth + 1));
let sibs = SomIndices::of(&sf.children);
for (ci, child) in sf.children.iter().enumerate() {
if !child.common().presence.occupies() {
@@ -1003,15 +1029,13 @@ impl Layouter<'_> {
self.warn_truncated(child, &ctx.som, instances);
for inst in 0..instances {
let h = self.row_height(row, &cols, inner_w, ctx, depth + 1);
if self.make_room(h)?
&& let Some(li) = leader
&& li != ci
&& let Some(Node::Subform(l)) = sf.children.get(li)
let led = lead.filter(|(li, _)| *li != ci);
if self.make_room(h, led.map_or(0.0, |_| lead_h))?
&& let Some((li, l)) = led
{
let lh = self.row_height(l, &cols, inner_w, ctx, depth + 1);
let (_, y, _) = self.cursor();
self.draw_row(l, &cols, Rect::new(x, y, inner_w, lh), ctx, depth + 1, sibs.at(li))?;
self.place(lh);
self.draw_row(l, &cols, Rect::new(x, y, inner_w, lead_h), ctx, depth + 1, sibs.at(li))?;
self.place(lead_h);
}
let (_, y, _) = self.cursor();
self.draw_row(row, &cols, Rect::new(x, y, inner_w, h), ctx, depth + 1, csib + inst)?;
+66
View File
@@ -2129,6 +2129,11 @@ fn a_flowed_subform_is_drawn_with_its_border_under_its_content() {
let part = form.pages[1].items.iter().find_map(|i| if let Item::Fill { rect, .. } = i { Some(*rect) } else { None }).expect("the second part");
assert!(part.y <= tall.y + 0.01 && part.bottom() >= tall.bottom() - 0.01, "the border holds the field: {part:?} {tall:?}");
assert!(form.warnings.is_empty(), "{:?}", form.warnings);
// The same field first in the section, at the top of the page: after the section's own top
// margin it runs past the bottom, so the section has no border round it, and says so.
let form = lay(&boxed(r#"<field name="tall" w="3in" h="715pt"><ui><textEdit/></ui></field>"#));
assert!(form.pages.iter().all(|p| !p.items.iter().any(|i| matches!(i, Item::Fill { .. }))), "no fill drawn");
assert_eq!(form.warnings.iter().filter(|w| w.contains("bordered section")).count(), 1, "{:?}", form.warnings);
}
/// A flowed table a page break cuts between its rows gets a closed border round its part on
@@ -2165,6 +2170,67 @@ fn a_flowed_table_cut_between_its_rows_keeps_its_border_on_each_page() {
assert!(form.warnings.is_empty(), "{:?}", form.warnings);
}
/// A flowed table's overflow leader and the row that moved are given room together on the new
/// page; a leader and row too tall for any page, and a first row taller than its page, cost the
/// table its border, with the warning, rather than a border round what runs off the page.
#[test]
fn a_tables_leader_and_oversized_rows_fit_its_border_or_lose_it() {
let row = |name: &str, h: &str| {
format!(r#"<subform name="{name}" layout="row"><draw name="{name}a" h="{h}"><value><text>{name}</text></value></draw></subform>"#)
};
let table = |rows: &str| {
format!(
r#"<subform name="body" layout="tb" w="7.5in"><subform name="grid" layout="table" columnWidths="3in" w="3in"><border><edge/><fill/></border><margin topInset="0.1in"/><overflow leader="head"/>{}{rows}</subform></subform>"#,
row("head", "0.3in")
)
};
let lay = |rows: &str| layout_xml(&one_area_form(r#"layout="tb""#, &table(rows))).unwrap();
let fills = |form: &Form, page: usize| -> Vec<Rect> {
form.pages[page].items.iter().filter_map(|i| if let Item::Fill { rect, .. } = i { Some(*rect) } else { None }).collect()
};
let small: String = (0..10).map(|i| row(&format!("r{i}"), "0.3in")).collect();
// A 695pt row after ten small ones moves; it and the 21.6pt leader leave 3.4pt of the 720pt
// page for the top margin, so the row ends at the content area's bottom, in the border.
let form = lay(&(small.clone() + &row("big", "695pt")));
assert_eq!(form.pages.len(), 2);
let part = fills(&form, 1);
assert!(part.len() == 1 && (part[0].y - 36.0).abs() < 0.01 && (part[0].bottom() - 756.0).abs() < 0.01, "{part:?}");
assert!(form.warnings.is_empty(), "{:?}", form.warnings);
// 710pt and the leader don't fit any page: no border, one warning.
let form = lay(&(small + &row("big", "710pt")));
assert!(form.pages.iter().enumerate().all(|(p, _)| fills(&form, p).is_empty()), "no fill drawn");
assert_eq!(form.warnings.iter().filter(|w| w.contains("bordered section")).count(), 1, "{:?}", form.warnings);
// A first row taller than the page: no border round it, one warning.
let form = lay(&row("tall", "800pt"));
assert!(fills(&form, 0).is_empty(), "{:?}", fills(&form, 0));
assert_eq!(form.warnings.iter().filter(|w| w.contains("bordered section")).count(), 1, "{:?}", form.warnings);
}
/// A child's own page break (breakBefore, breakAfter) breaks the flowed subform around it as a
/// break between children does: a closed part on each page, no warning.
#[test]
fn a_childs_page_break_keeps_the_border_around_it_on_each_page() {
let field = |name: &str| format!(r#"<field name="{name}" w="3in" h="0.5in"><ui><textEdit/></ui></field>"#);
let body = format!(
r#"<subform name="body" layout="tb" w="7.5in"><subform name="box" layout="tb" w="7.5in"><border><edge/><fill/></border><margin topInset="0.1in" bottomInset="0.2in"/>{}<subform name="s1" layout="tb"><breakBefore targetType="pageArea"/>{}</subform><subform name="s2" layout="tb"><breakAfter targetType="pageArea"/>{}</subform>{}</subform></subform>"#,
field("a"),
field("b"),
field("c"),
field("d")
);
let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap();
assert_eq!(form.pages.len(), 3, "a, then b and c, then d");
for (page, fields) in [(0, 1.0), (1, 2.0), (2, 1.0)] {
let fills: Vec<Rect> =
form.pages[page].items.iter().filter_map(|i| if let Item::Fill { rect, .. } = i { Some(*rect) } else { None }).collect();
let lines = form.pages[page].items.iter().filter(|i| matches!(i, Item::Line { .. })).count();
assert_eq!((fills.len(), lines), (1, 4), "page {}: one closed part", page + 1);
// The top margin, its fields, the bottom margin.
assert!((fills[0].y - 36.0).abs() < 0.01 && (fills[0].h - (7.2 + fields * 36.0 + 14.4)).abs() < 0.01, "page {}: {:?}", page + 1, fills[0]);
}
assert!(form.warnings.is_empty(), "{:?}", form.warnings);
}
#[test]
fn a_page_break_keeps_a_fields_border_with_the_field() {
// 6in of heading, then a bordered 5in field: it doesn't fit the 10in area and moves to