From cafd507f299e171cfebf0e2bb8e889e756197c5b Mon Sep 17 00:00:00 2001 From: Eric Kaden <608341+EricKaden@users.noreply.github.com> Date: Fri, 23 Oct 2026 22:51:54 -0400 Subject: [PATCH] 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 --- crates/xfa/src/layout.rs | 82 ++++++++++++++++++++++++++-------------- crates/xfa/src/tests.rs | 66 ++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+), 29 deletions(-) diff --git a/crates/xfa/src/layout.rs b/crates/xfa/src/layout.rs index a574c2bb3..4ffba1810 100644 --- a/crates/xfa/src/layout.rs +++ b/crates/xfa/src/layout.rs @@ -200,8 +200,9 @@ struct Frame { border: Option, 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 = 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 { + /// 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 { 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)?; diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index ac6366dd2..da88157f4 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -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#""#)); + 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#"{name}"#) + }; + let table = |rows: &str| { + format!( + r#"{}{rows}"#, + 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 { + 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#""#); + let body = format!( + r#"{}{}{}{}"#, + 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 = + 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