mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-31 21:48:09 -06:00
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:
+190
-14
@@ -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)?;
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user