mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-24 10:08:00 -06:00
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:
+53
-29
@@ -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)?;
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user