Merge PR #4338: M12: flowed XFA subforms are drawn with their border and fill

crates/xfa/src/tests.rs: both sides' appended tests kept.
parity/acrobat-features.toml: form.xfa-dynamic evidence and notes: both sides' insertions.
This commit is contained in:
aiongg
2026-10-24 06:24:41 +00:00
3 changed files with 387 additions and 16 deletions
+190 -14
View File
@@ -192,6 +192,8 @@ impl Ctx {
Ctx { font: self.font.apply(&c.font), para: self.para.apply(&c.para), som: self.som.clone() }
}
/// [`Self::child`] for object `c`, its SOM path extended by its name and `index` among the
/// siblings of that name (an unnamed object adds nothing).
fn with_som(&self, c: &Common, index: usize) -> Ctx {
let mut out = self.child(c);
if let Some(n) = &c.name {
@@ -214,6 +216,33 @@ struct Measured {
size: (f64, f64),
}
/// A point in the flow ([`Layouter::flow_mark`]): the page and content area, the height
/// reached there, and how many items the page had then.
#[derive(Clone, Copy, Debug)]
struct FlowMark {
page: usize,
area: usize,
content: usize,
y: f64,
items: usize,
}
/// A flowed subform being laid out ([`Layouter::frames`]): where it sits across, its margins and
/// border, its part on the content area the flow is in, and its parts on earlier ones (page
/// index, the page's first item of the part, the part's box).
struct Frame {
x: f64,
w: f64,
margin: Margin,
border: Option<Border>,
part: FlowMark,
parts: Vec<(usize, usize, Rect)>,
/// 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,
}
struct Cursor {
area: usize,
content: usize,
@@ -248,6 +277,9 @@ struct Layouter<'t> {
/// The fields and draws of the block being placed: (first item, one past the last, top,
/// bottom) on the last page. A page break keeps each whole (see `split_block`).
leaves: Vec<(usize, usize, f64, f64)>,
/// The flowed subforms being laid out, outermost first: a break between their children
/// closes their parts on one content area and opens new ones on the next.
frames: Vec<Frame>,
}
/// How many instances of a repeating subform to lay out: what the data holds once it holds
@@ -293,6 +325,7 @@ pub fn layout(tpl: &Template, data: Option<&DataNode>) -> Result<Form, XfaError>
measured: RefCell::new(HashMap::new()),
measures: Cell::new(0),
leaves: Vec::new(),
frames: Vec::new(),
};
let ctx = Ctx { font: Font::default(), para: Para::default(), som: String::new() };
l.new_page()?;
@@ -647,6 +680,8 @@ impl Layouter<'_> {
self.new_page()
}
/// The current content area, how far down it the flow is, and whether anything has been
/// placed in it; before the first page, an empty letter-size page from its top.
fn cursor(&self) -> (Rect, f64, bool) {
match &self.cur {
Some(c) => (c.rect, c.y, c.placed),
@@ -654,6 +689,111 @@ impl Layouter<'_> {
}
}
/// Where the flow is now ([`FlowMark`]); `None` before the first page.
fn flow_mark(&self) -> Option<FlowMark> {
let c = self.cur.as_ref()?;
Some(FlowMark { page: self.pages.len(), area: c.area, content: c.content, y: c.y, items: self.pages.last().map_or(0, |p| p.items.len()) })
}
/// Whether the flow is still in the content area `m` was taken in.
fn on_area(&self, m: &FlowMark) -> bool {
self.pages.len() == m.page && self.cur.as_ref().is_some_and(|c| c.area == m.area && c.content == m.content)
}
/// Whether page `page` (an index) got anything from item `at` on.
fn has_items_from(&self, page: usize, at: usize) -> bool {
self.pages.get(page).is_some_and(|p| p.items.len() > at)
}
/// Break the flowed subforms being laid out ([`Self::frames`]) across content areas, as
/// 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 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();
let filled: Vec<bool> = self.frames.iter().map(|f| self.has_items_from(page, f.part.items)).collect();
for ((f, here), filled) in self.frames.iter_mut().zip(here).zip(filled).rev() {
y = (y + f.margin.bottom).min(rect.bottom());
if !here {
f.lost = true;
} else if filled && y - f.part.y >= 0.01 {
f.parts.push((page, f.part.items, Rect::new(f.x, f.part.y, f.w, y - f.part.y)));
}
}
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() {
let Some(mark) = self.flow_mark() else { break };
let Some(f) = self.frames.get_mut(i) else { break };
f.part = mark;
// `max` before `min`: a NaN margin counts as none and leaves `room` a number.
let top = f.margin.top.max(0.0).min(room);
room = (room - top).max(0.0);
self.advance(top);
}
Ok(())
}
/// 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 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 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) {
f.lost = true;
} else if self.has_items_from(page, f.part.items) && end - f.part.y >= 0.01 {
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 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.
for (page, at, rect) in f.parts.into_iter().rev() {
self.insert_border(&b, rect, page, at)?;
}
Ok(())
}
/// Draw border `b` round `rect` on page `page` (an index), under what that page got from item
/// `at` on.
fn insert_border(&mut self, b: &Border, rect: Rect, page: usize, at: usize) -> Result<(), XfaError> {
let before = self.pages.last().map_or(0, |p| p.items.len());
self.draw_border(b, rect)?;
let drawn: Vec<Item> = match self.pages.last_mut() {
Some(p) => {
let from = before.min(p.items.len());
p.items.drain(from..).collect()
}
None => return Ok(()),
};
if let Some(target) = self.pages.get_mut(page) {
let at = at.min(target.items.len());
target.items.splice(at..at, drawn);
}
Ok(())
}
/// Move the cursor down (margins and spacing: the area still counts as empty).
fn advance(&mut self, dy: f64) {
if let Some(c) = &mut self.cur {
@@ -667,16 +807,36 @@ 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.
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);
}
self.next_area()?;
if h <= rect.h + 0.01 {
self.break_frames(h + after, false)?;
} else {
self.next_area()?;
}
Ok(true)
}
@@ -686,7 +846,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());
@@ -805,6 +965,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;
@@ -831,7 +992,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);
@@ -852,6 +1013,14 @@ impl Layouter<'_> {
continue;
}
let m = sf.common.margin;
// Its border and fill go round what it takes from here, a part on each content area.
let framed = match self.flow_mark() {
Some(part) => {
self.frames.push(Frame { x, w, margin: m, border: sf.common.border.clone(), part, parts: Vec::new(), lost: false });
true
}
None => false,
};
self.advance(m.top);
let inner_x = x + m.left;
let inner_w = (w - m.horizontal()).max(1.0);
@@ -876,9 +1045,12 @@ impl Layouter<'_> {
}
}
self.advance(m.bottom);
if framed && let Some(frame) = self.frames.pop() {
self.finish_frame(frame)?;
}
}
if sf.break_after_page {
self.new_page()?;
self.break_frames(0.0, true)?;
}
Ok(())
}
@@ -895,6 +1067,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() {
@@ -909,15 +1087,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)?;
+195
View File
@@ -2238,6 +2238,8 @@ fn repeating_rows_in_a_wrapped_container_get_the_instances_the_data_holds() {
assert_eq!(form.pages.iter().flat_map(widgets).filter(|w| w.name.starts_with("what")).count(), 1);
}
/// A multi-line field with a minimum height and no fixed one grows to hold its text, moving
/// what follows down; one with a fixed height keeps it.
#[test]
fn a_multi_line_field_without_a_height_grows_to_hold_its_text() {
let place = |data: &str| {
@@ -2363,6 +2365,199 @@ fn rounded_boxes_are_written_as_curved_paths() {
assert!(edge.starts_with("q 1 0 0 RG 2 w 37 685 m 37 738 l ") && edge.ends_with(" 179 685 l S Q") && !edge.contains(" h "), "{edge}");
}
/// A flowed subform without a fixed height gets its border and fill round all it takes, margins
/// included, drawn under its content; a flowed table too. One a page break cuts between its
/// children gets a closed border round its part on each page, its bottom margin ending the first
/// part and its top margin starting the next, as another viewer draws it on a synthetic form. One
/// holding an item that doesn't fit on its page keeps no border, and the layout says so.
#[test]
fn a_flowed_subform_is_drawn_with_its_border_under_its_content() {
let field = |name: &str| format!(r#"<field name="{name}" w="3in" h="0.5in"><ui><textEdit/></ui></field>"#);
let boxed = |inner: &str| {
format!(
r#"<subform name="box" layout="tb" w="7.5in"><border><edge thickness="1pt"/><fill><color value="230,230,230"/></fill></border><margin topInset="0.1in" bottomInset="0.2in"/>{inner}</subform>"#
)
};
let lay = |body: &str| {
layout_xml(&one_area_form(r#"layout="tb""#, &format!(r#"<subform name="body" layout="tb" w="7.5in">{body}</subform>"#))).unwrap()
};
// Two half-inch fields between a tenth and a fifth of an inch of margin: 1.3in from the top.
let form = lay(&boxed(&(field("a") + &field("b"))));
let items = &form.pages[0].items;
let fill = items.iter().position(|i| matches!(i, Item::Fill { .. })).expect("the subform's fill");
let Some(Item::Fill { rect, .. }) = items.get(fill) else { panic!("item {fill} is the fill") };
assert!((rect.y - 36.0).abs() < 0.01 && (rect.h - 1.3 * 72.0).abs() < 0.01 && (rect.w - 540.0).abs() < 0.01, "{rect:?}");
assert_eq!(items.iter().filter(|i| matches!(i, Item::Line { .. })).count(), 4, "its four edges");
let first_widget = items.iter().position(|i| matches!(i, Item::Widget(_))).unwrap();
assert!(fill < first_widget, "the fill is under the fields");
assert!(form.warnings.is_empty(), "{:?}", form.warnings);
// A flowed table's border too.
let table = r#"<subform name="grid" layout="table" columnWidths="2in 2in" w="4in"><border><edge/></border><subform name="row" layout="row"><draw name="c1" h="0.4in"><value><text>One</text></value></draw><draw name="c2" h="0.4in"><value><text>Two</text></value></draw></subform></subform>"#;
let form = lay(table);
assert_eq!(form.pages[0].items.iter().filter(|i| matches!(i, Item::Line { .. })).count(), 4, "{:?}", form.pages[0].items);
// Thirty fields: 19 fit the first page after the top margin, 11 go to the second.
let many: String = (0..30).map(|i| field(&format!("f{i}"))).collect();
let form = lay(&boxed(&many));
assert_eq!(form.pages.len(), 2);
let fills = |page: usize| -> Vec<Rect> {
form.pages[page].items.iter().filter_map(|i| if let Item::Fill { rect, .. } = i { Some(*rect) } else { None }).collect()
};
let (first, second) = (fills(0), fills(1));
assert_eq!((first.len(), second.len()), (1, 1), "a part on each page");
// Page 1: from the top (36) past 0.1in of margin and 19 fields, then 0.2in of margin.
assert!((first[0].y - 36.0).abs() < 0.01 && (first[0].bottom() - (36.0 + 7.2 + 19.0 * 36.0 + 14.4)).abs() < 0.01, "{:?}", first[0]);
// Page 2: the top margin again, 11 fields, the bottom margin.
assert!((second[0].y - 36.0).abs() < 0.01 && (second[0].bottom() - (36.0 + 7.2 + 11.0 * 36.0 + 14.4)).abs() < 0.01, "{:?}", second[0]);
let lines = |page: usize| form.pages[page].items.iter().filter(|i| matches!(i, Item::Line { .. })).count();
assert_eq!((lines(0), lines(1)), (4, 4), "each part's border is closed");
let f19 = form.pages[1].items.iter().find_map(|i| if let Item::Widget(w) = i { (w.name == "f19").then_some(w.rect) } else { None }).unwrap();
assert!((f19.y - 43.2).abs() < 0.01, "the second part starts after the top margin: {}", f19.y);
assert!(form.warnings.is_empty(), "{:?}", form.warnings);
// A bottom margin that would run past the content area stops at its edge: 39 quarter-inch
// fields end at 738, and 0.4in more would reach 766.8, past 756.
let quarter: String = (0..39).map(|i| format!(r#"<field name="q{i}" w="3in" h="0.25in"><ui><textEdit/></ui></field>"#)).collect();
let low = format!(r#"<subform name="box" layout="tb" w="7.5in"><border><edge/><fill/></border><margin bottomInset="0.4in"/>{quarter}</subform>"#);
let form = lay(&low);
let edge = form.pages[0].items.iter().find_map(|i| if let Item::Fill { rect, .. } = i { Some(rect.bottom()) } else { None }).unwrap();
assert!((edge - 756.0).abs() < 0.01, "the box ends at the content area's bottom: {edge}");
// The margins come back without a border too.
let bare = format!(r#"<subform name="box" layout="tb" w="7.5in"><margin topInset="0.1in" bottomInset="0.2in"/>{many}</subform>"#);
let form = lay(&bare);
let f19 = form.pages[1].items.iter().find_map(|i| if let Item::Widget(w) = i { (w.name == "f19").then_some(w.rect) } else { None }).unwrap();
assert!((f19.y - 43.2).abs() < 0.01, "{}", f19.y);
// A text of its own longer than a page, cut across pages: no border, one warning.
let long = format!(
r#"<draw name="long" w="7in"><font typeface="Arial" size="10pt"/><value><text>{}</text></value></draw>"#,
"A line of the long text.\n".repeat(80)
);
let form = lay(&boxed(&(field("a") + &long)));
assert!(form.pages.len() >= 2, "the long text runs onto another page");
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 field that fits a fresh page (720pt) but not after the 7.2pt top margin: the margin
// gives way, so the field ends at the content area's bottom, inside the second part.
let tall = r#"<field name="tall" w="3in" h="715pt"><ui><textEdit/></ui></field>"#;
let form = lay(&boxed(&(field("a") + tall)));
assert_eq!(form.pages.len(), 2);
let tall = form.pages[1].items.iter().find_map(|i| if let Item::Widget(w) = i { (w.name == "tall").then_some(w.rect) } else { None }).unwrap();
assert!(tall.bottom() <= 756.01, "the field stays in the content area: {tall:?}");
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);
// A section that starts near a page's bottom, where its first field doesn't fit: the field
// moves to the next page whole, and the border starts there with it, none left behind.
let spacer = r#"<field name="spacer" w="3in" h="684pt"><ui><textEdit/></ui></field>"#;
let form = lay(&format!("{spacer}{}", boxed(&(field("a") + &field("b")))));
assert_eq!(form.pages.len(), 2);
let fills = |page: usize| -> Vec<Rect> {
form.pages[page].items.iter().filter_map(|i| if let Item::Fill { rect, .. } = i { Some(*rect) } else { None }).collect()
};
assert!(fills(0).is_empty(), "no border left behind on page 1: {:?}", fills(0));
let moved = fills(1);
assert!(moved.len() == 1 && (moved[0].y - 36.0).abs() < 0.01 && (moved[0].h - (7.2 + 2.0 * 36.0 + 14.4)).abs() < 0.01, "{moved:?}");
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
/// each page, as a flowed subform does: its margins repeat, and its overflow leader starts the
/// second part after the top margin.
#[test]
fn a_flowed_table_cut_between_its_rows_keeps_its_border_on_each_page() {
let row = |name: &str, text: &str| {
format!(
r#"<subform name="{name}" layout="row"><draw name="{name}a" h="0.3in"><value><text>{text}</text></value></draw><draw name="{name}b" h="0.3in"><value><text>-</text></value></draw></subform>"#
)
};
let rows: String = (0..40).map(|i| row(&format!("r{i}"), &format!("Row {i}"))).collect();
let table = format!(
r#"<subform name="grid" layout="table" columnWidths="2in 2in" w="4in"><border><edge/><fill><color value="230,230,230"/></fill></border><margin topInset="0.1in" bottomInset="0.2in"/><overflow leader="head"/>{}{rows}</subform>"#,
row("head", "Head")
);
let body = format!(r#"<subform name="body" layout="tb" w="7.5in">{table}</subform>"#);
let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap();
assert_eq!(form.pages.len(), 2);
let fills = |page: usize| -> Vec<Rect> {
form.pages[page].items.iter().filter_map(|i| if let Item::Fill { rect, .. } = i { Some(*rect) } else { None }).collect()
};
let lines = |page: usize| form.pages[page].items.iter().filter(|i| matches!(i, Item::Line { .. })).count();
let (first, second) = (fills(0), fills(1));
assert_eq!((first.len(), second.len()), (1, 1), "a part on each page");
assert_eq!((lines(0), lines(1)), (4, 4), "each part's border is closed");
// Page 1: 0.1in of margin, the leader and 32 rows of 0.3in fill the area to its bottom.
assert!((first[0].y - 36.0).abs() < 0.01 && (first[0].bottom() - 756.0).abs() < 0.01, "{:?}", first[0]);
// Page 2: the top margin again, the leader, the other 8 rows, the bottom margin.
assert!((second[0].y - 36.0).abs() < 0.01 && (second[0].bottom() - (36.0 + 7.2 + 9.0 * 21.6 + 14.4)).abs() < 0.01, "{:?}", second[0]);
let head = form.pages[1].items.iter().find_map(|i| if let Item::Text(t) = i { (t.text == "Head").then_some(t.baseline) } else { None });
assert!(head.is_some_and(|b| b > 43.2 && b < 64.8), "the leader starts the second part after its top margin: {head:?}");
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
File diff suppressed because one or more lines are too long