refactor(xfa): split layout.rs into layout/

New modules: items, helpers, pages, flow, measure, draw, field.
This commit is contained in:
aiongg
2026-10-26 22:18:50 +00:00
parent 4e278cb5fc
commit 3da49321f2
8 changed files with 1955 additions and 1916 deletions
+14 -1916
View File
File diff suppressed because it is too large Load Diff
+211
View File
@@ -0,0 +1,211 @@
use super::items::{Action, Item, Rounded};
use super::*;
pub(super) fn collect_values(nodes: &[Node], out: &mut HashMap<String, String>, depth: usize) {
if depth > MAX_DEPTH {
return;
}
for n in nodes {
match n {
Node::Field(f) => {
if let Some(id) = &f.common.id {
out.insert(id.clone(), f.value.plain().unwrap_or_default());
}
}
Node::Subform(s) => {
collect_values(&s.children, out, depth + 1);
if let Some(ps) = &s.page_set {
for a in &ps.areas {
collect_values(&a.items, out, depth + 1);
}
}
}
Node::Area(a) => collect_values(&a.children, out, depth + 1),
Node::ExclGroup(_) | Node::Draw(_) => {}
}
}
}
pub(super) fn clamp_opt(v: f64, min: Option<f64>, max: Option<f64>) -> f64 {
let mut v = v;
if let Some(m) = min {
v = v.max(m);
}
if let Some(m) = max
&& m > 0.0
{
v = v.min(m);
}
v.max(0.0)
}
/// Does `node` flow (may be split across pages) rather than being placed as one block?
pub(super) fn splittable(node: &Node) -> bool {
matches!(node, Node::Subform(s) if matches!(s.layout, Layout::Tb | Layout::Table) && s.common.h.is_none())
}
/// Top and bottom of an item a page break must not cut (text, a picture, a field); `None`
/// for fills and lines, which are cut at the break.
pub(super) fn solid_extent(item: &Item) -> Option<(f64, f64)> {
match item {
Item::Text(s) => Some((s.baseline - s.face.size * 0.8, s.baseline + s.face.size * 0.2)),
Item::Image { rect, .. } => Some((rect.y, rect.bottom())),
Item::Widget(w) => Some((w.rect.y, w.rect.bottom())),
Item::Fill { .. } | Item::Line { .. } | Item::Rounded(_) => None,
}
}
/// `item` as the part above `cut` and the part below: solid items go whole to the side their
/// top is on, fills and lines crossing the cut are cut in two.
pub(super) fn divide(item: Item, cut: f64) -> (Option<Item>, Option<Item>) {
if let Some((top, _)) = solid_extent(&item) {
return if top < cut - 0.01 { (Some(item), None) } else { (None, Some(item)) };
}
match item {
Item::Fill { rect, color } => {
if rect.bottom() <= cut + 0.01 {
(Some(Item::Fill { rect, color }), None)
} else if rect.y >= cut - 0.01 {
(None, Some(Item::Fill { rect, color }))
} else {
let above = Rect::new(rect.x, rect.y, rect.w, cut - rect.y);
let below = Rect::new(rect.x, cut, rect.w, rect.bottom() - cut);
(Some(Item::Fill { rect: above, color }), Some(Item::Fill { rect: below, color }))
}
}
Item::Line { from, to, width, color, dashed } => {
let (lo, hi) = if from.1 <= to.1 { (from, to) } else { (to, from) };
if lo.1 >= cut {
(None, Some(Item::Line { from, to, width, color, dashed }))
} else if hi.1 <= cut {
(Some(Item::Line { from, to, width, color, dashed }), None)
} else {
// lo.1 < cut < hi.1, so the divisor is positive.
let t = (cut - lo.1) / (hi.1 - lo.1);
let mid = (lo.0 + (hi.0 - lo.0) * t, cut);
(Some(Item::Line { from: lo, to: mid, width, color, dashed }), Some(Item::Line { from: mid, to: hi, width, color, dashed }))
}
}
Item::Rounded(r) => {
if r.rect.bottom() <= cut + 0.01 {
(Some(Item::Rounded(r)), None)
} else if r.rect.y >= cut - 0.01 {
(None, Some(Item::Rounded(r)))
} else {
// Each part keeps the corners on its side, rounded no more than its own height
// allows, and is open where it was cut.
let [tl, tr, br, bl] = r.radii;
let above = Rect::new(r.rect.x, r.rect.y, r.rect.w, cut - r.rect.y);
let below = Rect::new(r.rect.x, cut, r.rect.w, r.rect.bottom() - cut);
let fit = |v: f64, h: f64| v.min(h.max(0.0));
let upper = Rounded { rect: above, radii: [fit(tl, above.h), fit(tr, above.h), 0.0, 0.0], open_bottom: true, ..(*r).clone() };
let lower = Rounded { rect: below, radii: [0.0, 0.0, fit(br, below.h), fit(bl, below.h)], open_top: true, ..*r };
(Some(Item::Rounded(Box::new(upper))), Some(Item::Rounded(Box::new(lower))))
}
}
other => (Some(other), None),
}
}
/// `item` moved by (`dx`, `dy`).
pub(super) fn moved(mut item: Item, dx: f64, dy: f64) -> Item {
match &mut item {
Item::Fill { rect, .. } | Item::Image { rect, .. } => {
rect.x += dx;
rect.y += dy;
}
Item::Rounded(r) => {
r.rect.x += dx;
r.rect.y += dy;
}
Item::Line { from, to, .. } => {
*from = (from.0 + dx, from.1 + dy);
*to = (to.0 + dx, to.1 + dy);
}
Item::Text(s) => {
s.x += dx;
s.baseline += dy;
}
Item::Widget(w) => {
w.rect.x += dx;
w.rect.y += dy;
}
}
item
}
pub(super) fn is_row(node: &Node) -> bool {
matches!(node, Node::Subform(s) if s.layout == Layout::Row)
}
/// The Acrobat date pattern for an XFA picture clause such as `date{YYYY-MM-DD}`.
pub(crate) fn date_pattern(picture: Option<&str>) -> String {
picture.and_then(date_letters).unwrap_or_else(|| "yyyy-mm-dd".into())
}
/// What scripts read as a field's `formattedValue` when its display picture is `picture`: the
/// ISO date `raw` shown in a date picture (`date{YYYYMMDD}` turns `2026-10-20` into
/// `20261020`; of several alternatives, the first). `None` for other kinds of picture, for a
/// clause this can't read or show (month and weekday names) and for a value that isn't an ISO
/// date: the raw value stands then.
pub fn formatted_value(picture: &str, raw: &str) -> Option<String> {
let first = picture.split('|').next()?.trim();
let rest = first.strip_prefix("date")?;
if !rest.starts_with(['{', '(']) {
return None;
}
let pattern = date_pattern(Some(first));
// A picture with parts this can't show yet (month or weekday names, `MMM` or `EEE`) would
// leave them as letters: the raw value stands then.
let shows = |t: &String| !t.chars().all(|c| c.is_ascii_alphabetic()) || matches!(t.as_str(), "yyyy" | "yy" | "mm" | "m" | "dd" | "d");
if !crate::data::date_tokens(&pattern).iter().all(shows) {
return None;
}
// Only the ISO date's own shape (`2026-10-20`): `2026-1-2` is not one, and stands.
let b = raw.as_bytes();
let iso = b.len() == 10 && b.iter().enumerate().all(|(i, c)| if i == 4 || i == 7 { *c == b'-' } else { c.is_ascii_digit() });
if !iso {
return None;
}
let shown = iso_to_pattern(raw, &pattern);
(shown != raw).then_some(shown)
}
/// The native PDF action a button's click script amounts to (a reset, print, save or link),
/// else the script itself to run; `None` without a click script.
pub(super) fn button_action(scripts: &[Script]) -> Option<Action> {
let s = scripts.iter().find(|s| s.activity == "click")?;
let t = &s.text;
// Only a single-statement idiom becomes a native PDF action; anything else is run by the
// XFA scripting engine.
let one = t.trim().trim_end_matches(';');
let single = !one.contains(';') && !one.contains('\n');
if !single {
return (!t.trim().is_empty()).then(|| Action::Script(t.clone()));
}
if t.contains("resetData") {
Some(Action::Reset)
} else if t.contains("host.print") || t.contains("execMenuItem(\"Print\")") {
Some(Action::Print)
} else if t.contains("SaveAs") || t.contains("execMenuItem(\"Save\")") {
Some(Action::SaveAs)
} else if let Some(i) = t.find("launchURL(") {
let rest = &t[i + "launchURL(".len()..];
let q = rest.find(['"', '\''])?;
let quote = rest[q..].chars().next()?;
let body = &rest[q + 1..];
let end = body.find(quote)?;
Some(Action::Url(body[..end].to_string()))
} else if t.trim().is_empty() {
None
} else {
Some(Action::Script(t.clone()))
}
}
/// A PDF field name: no periods (they separate hierarchy levels), no control characters.
pub(super) fn clean_name(name: &str) -> String {
let s: String = name.chars().filter(|c| !c.is_control()).map(|c| if c == '.' { '_' } else { c }).collect();
let s = s.trim().to_string();
if s.is_empty() { "field".into() } else { s }
}
+324
View File
@@ -0,0 +1,324 @@
use super::common::clean_name;
use super::items::{Item, Rounded};
use super::*;
impl Layouter<'_> {
// ───────────────────────────────────────────────────────────────────────── drawing
pub(super) fn draw_row(&mut self, row: &Subform, cols: &[f64], rect: Rect, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
if depth > MAX_DEPTH {
return Ok(());
}
if let Some(b) = &row.common.border {
self.draw_border(b, rect)?;
}
let ctx = ctx.with_som(&row.common, sib);
let inner = rect.inset(&row.common.margin);
let (cells, _) = self.cells(row, cols, inner.w, &ctx, depth);
let sibs = SomIndices::of(&row.children);
for (i, r) in cells {
if let Some(k) = row.children.get(i) {
// Cells stretch to the row height.
self.draw_node(k, Rect::new(inner.x + r.x, inner.y, r.w, inner.h), &ctx, depth + 1, sibs.at(i))?;
}
}
Ok(())
}
pub(super) fn draw_positioned_child(&mut self, k: &Node, inner: Rect, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
let c = k.common();
if !c.presence.occupies() {
return Ok(());
}
let (w, h) = self.size_of(k, (inner.w - c.x.unwrap_or(0.0)).max(1.0), ctx, depth + 1);
let (x, y) = c.top_left(w, h);
self.draw_node(k, Rect::new(inner.x + x, inner.y + y, w, h), ctx, depth + 1, sib)
}
pub(super) fn draw_node(&mut self, node: &Node, rect: Rect, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
if depth > MAX_DEPTH {
return Ok(());
}
let c = node.common();
if c.presence == Presence::Invisible {
return Ok(());
}
match node {
Node::Draw(d) => {
let start = self.item_count();
self.draw_draw(d, rect, ctx)?;
// Text and pictures stay with their border; a bare line or rectangle is cut.
if matches!(d.value, Value::Text(_) | Value::Rich(_) | Value::Image { .. }) {
self.keep_whole(start, rect);
}
Ok(())
}
Node::Field(f) => {
let start = self.item_count();
self.draw_field(f, rect, ctx, None, sib)?;
self.keep_whole(start, rect);
Ok(())
}
Node::ExclGroup(g) => self.draw_group(g, rect, ctx, depth, sib),
Node::Area(a) => {
let ctx = ctx.with_som(&a.common, sib);
let sibs = SomIndices::of(&a.children);
for (i, k) in a.children.iter().enumerate() {
self.draw_positioned_child(k, rect, &ctx, depth + 1, sibs.at(i))?;
}
Ok(())
}
Node::Subform(s) => {
// Every drawn instance spends budget, as a flowed one does, so nested repeats that
// place nothing still stop.
self.measures.set(self.measures.get().saturating_add(1));
self.check_measures()?;
if let Some(b) = &s.common.border {
self.draw_border(b, rect)?;
}
let parent_ctx = ctx;
let ctx = ctx.with_som(&s.common, sib);
let inner = rect.inset(&s.common.margin);
let sibs = SomIndices::of(&s.children);
match s.layout {
Layout::Positioned => {
for (i, k) in s.children.iter().enumerate() {
self.draw_positioned_child(k, inner, &ctx, depth + 1, sibs.at(i))?;
}
}
Layout::Tb => {
let mut y = inner.y;
for (i, k) in s.children.iter().enumerate() {
if !k.common().presence.occupies() {
continue;
}
let (w, h) = self.size_of(k, inner.w, &ctx, depth + 1);
// A repeating subform here (a table in an area of a positioned page,
// say) gets its instances, as in a flowed one, one under another.
for instance in 0..self.instances(k, &ctx.som) {
self.draw_node(k, Rect::new(inner.x, y, w, h), &ctx, depth + 1, sibs.at(i) + instance)?;
y += h;
}
}
}
Layout::LrTb | Layout::RlTb => {
// A repeating subform here (rows of fields under a heading) gets its
// instances, as in the other layouts.
let (placed, _) = self.pack_instances(&s.children, inner.w, &ctx, depth, &ctx.som);
for (i, instance, r) in placed {
if let Some(k) = s.children.get(i) {
let x = if s.layout == Layout::RlTb { inner.right() - r.x - r.w } else { inner.x + r.x };
self.draw_node(k, Rect::new(x, inner.y + r.y, r.w, r.h), &ctx, depth + 1, sibs.at(i) + instance)?;
}
}
}
Layout::Table => {
let cols = self.columns(s, inner.w);
let mut y = inner.y;
for (i, k) in s.children.iter().enumerate() {
if !k.common().presence.occupies() {
continue;
}
let csib = sibs.at(i);
if let Node::Subform(r) = k
&& r.layout == Layout::Row
{
let h = self.row_height(r, &cols, inner.w, &ctx, depth + 1);
self.draw_row(r, &cols, Rect::new(inner.x, y, inner.w, h), &ctx, depth + 1, csib)?;
y += h;
} else {
let (w, h) = self.size_of(k, inner.w, &ctx, depth + 1);
self.draw_node(k, Rect::new(inner.x, y, w, h), &ctx, depth + 1, csib)?;
y += h;
}
}
}
Layout::Row => {
let cols = self.columns(s, inner.w);
self.draw_row(s, &cols, rect, parent_ctx, depth, sib)?;
}
}
Ok(())
}
}
}
/// Draw border `b` around `rect`: its fill, then each shown edge as a line inside the box, or
/// the whole box as one rounded shape when its corners are rounded.
pub(super) fn draw_border(&mut self, b: &Border, rect: Rect) -> Result<(), XfaError> {
let vis = b.visible_edges();
// Rounded corners: the box is one shape, filled and edged along its curves, when its
// edges are all alike (all shown and the same, or all hidden). Edges that differ keep
// square corners.
let radii = b.radii(rect.w, rect.h);
let alike = b.edges.iter().all(|e| *e == b.edges[0]);
if radii.iter().any(|r| *r > 0.0) && (vis == [true; 4] && alike || vis == [false; 4]) {
let e = b.edges[0];
let edge = vis[0].then(|| {
let color = if e.stroke == Stroke::Relief { [0.55, 0.55, 0.55] } else { e.color.0 };
(e.thickness.clamp(0.1, 20.0), color, matches!(e.stroke, Stroke::Dashed | Stroke::Dotted))
});
let shape = Rounded {
rect,
radii,
inverted: b.corners.map(|c| c.inverted),
fill: b.fill().map(|f| f.0),
edge,
open_top: false,
open_bottom: false,
};
return self.push(Item::Rounded(Box::new(shape)));
}
if let Some(fill) = b.fill() {
self.push(Item::Fill { rect, color: fill.0 })?;
}
let (x0, y0, x1, y1) = (rect.x, rect.y, rect.right(), rect.bottom());
// Top, right, bottom, left; lines sit inside the box.
let segs = [((x0, y0), (x1, y0)), ((x1, y0), (x1, y1)), ((x0, y1), (x1, y1)), ((x0, y0), (x0, y1))];
for (i, seg) in segs.iter().enumerate() {
if !vis[i] {
continue;
}
let e = b.edges[i];
let t = e.thickness.clamp(0.1, 20.0);
let (dx, dy) = match i {
0 => (0.0, t / 2.0),
1 => (-t / 2.0, 0.0),
2 => (0.0, -t / 2.0),
_ => (t / 2.0, 0.0),
};
let color = if e.stroke == Stroke::Relief { [0.55, 0.55, 0.55] } else { e.color.0 };
self.push(Item::Line {
from: (seg.0.0 + dx, seg.0.1 + dy),
to: (seg.1.0 + dx, seg.1.1 + dy),
width: t,
color,
dashed: matches!(e.stroke, Stroke::Dashed | Stroke::Dotted),
})?;
}
Ok(())
}
pub(super) fn draw_value_text(&mut self, v: &Value, rect: Rect, font: &Font, para: &Para) -> Result<(), XfaError> {
let rich = match v {
Value::Text(t) => text::plain(t),
Value::Rich(r) => r.clone(),
_ => return Ok(()),
};
let face = Face::of(font);
// Text that doesn't fit the height it was given (the template's face is narrower than
// the standard one drawn in its place, so it wraps to more lines) is drawn smaller,
// down to 70%, rather than over what lies below.
let fits = |k: f64| text::measure_height(&rich, rect.w / k, para, &face, &|id, max| self.embed(id, max)) <= rect.h / k + 1.0;
let scale = if rect.h < 4.0 || fits(1.0) { 1.0 } else { [0.95, 0.9, 0.85, 0.8, 0.75].into_iter().find(|k| fits(*k)).unwrap_or(0.7) };
let mut block = text::layout(&rich, Rect::new(rect.x, rect.y, rect.w / scale, rect.h / scale), para, &face, &|id, max| self.embed(id, max));
if scale < 1.0 {
for s in &mut block.spans {
s.x = rect.x + (s.x - rect.x) * scale;
s.baseline = rect.y + (s.baseline - rect.y) * scale;
s.face.size *= scale;
}
}
self.push_block(block)
}
fn draw_draw(&mut self, d: &Draw, rect: Rect, ctx: &Ctx) -> Result<(), XfaError> {
if let Some(b) = &d.common.border {
self.draw_border(b, rect)?;
}
let ctx = ctx.child(&d.common);
let inner = rect.inset(&d.common.margin);
match &d.value {
Value::Text(_) | Value::Rich(_) => self.draw_value_text(&d.value, inner, &ctx.font, &ctx.para),
Value::Image { content_type, data } => self.draw_image(data, content_type, inner),
Value::Line { slope_up, edge } => {
if !edge.visible() {
return Ok(());
}
let (from, to) = if *slope_up {
((inner.x, inner.bottom()), (inner.right(), inner.y))
} else {
((inner.x, inner.y), (inner.right(), inner.bottom()))
};
self.push(Item::Line { from, to, width: edge.thickness.clamp(0.1, 20.0), color: edge.color.0, dashed: edge.stroke == Stroke::Dashed })
}
Value::Rectangle(b) => self.draw_border(b, inner),
Value::Empty => Ok(()),
}
}
fn draw_group(&mut self, g: &ExclGroup, rect: Rect, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
if let Some(b) = &g.common.border {
self.draw_border(b, rect)?;
}
let ctx = ctx.with_som(&g.common, sib);
// The group's data node holds the selected button's on value.
let selected: Option<String> =
self.data.and_then(|d| d.text_at(&data_path(self.tpl, &ctx.som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty());
let inner = rect.inset(&g.common.margin);
let group = self.unique_name(g.common.name.as_deref().unwrap_or("group"));
let nodes: Vec<Node> = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect();
let placed: Vec<(usize, Rect)> = match g.layout {
Layout::Tb => {
let mut y = 0.0;
let mut out = Vec::new();
for (i, k) in nodes.iter().enumerate() {
if !k.common().presence.occupies() {
continue;
}
let (w, h) = self.size_of(k, inner.w, &ctx, depth + 1);
out.push((i, Rect::new(0.0, y, w, h)));
y += h;
}
out
}
Layout::Positioned => nodes
.iter()
.enumerate()
.filter(|(_, k)| k.common().presence.occupies())
.map(|(i, k)| {
let c = k.common();
let (w, h) = self.size_of(k, (inner.w - c.x.unwrap_or(0.0)).max(1.0), &ctx, depth + 1);
let (x, y) = c.top_left(w, h);
(i, Rect::new(x, y, w, h))
})
.collect(),
_ => self.pack(&nodes, inner.w, &ctx, depth).0,
};
let sibs = SomIndices::of(&nodes);
for (i, r) in placed {
if let Some(f) = g.fields.get(i) {
let on = f.items.first().cloned().filter(|s| !s.trim().is_empty()).unwrap_or_else(|| (i + 1).to_string());
let tip = f.tooltip.clone().or_else(|| g.tooltip.clone());
let chosen = selected.as_deref() == Some(on.as_str());
self.draw_field(f, Rect::new(inner.x + r.x, inner.y + r.y, r.w, r.h), &ctx, Some((&group, &on, tip, chosen)), sibs.at(i))?;
}
}
Ok(())
}
/// A picture scaled to fit `inner`, keeping its shape, from its top left.
pub(super) fn draw_image(&mut self, data: &[u8], content_type: &str, inner: Rect) -> Result<(), XfaError> {
self.image_bytes = self.image_bytes.saturating_add(data.len());
if self.image_bytes > MAX_IMAGE_BYTES {
return Err(XfaError::TooLarge("the form's pictures are too large together to lay out".into()));
}
let fitted = match crate::image::size(data) {
Some((iw, ih)) if iw > 0 && ih > 0 && inner.w > 0.0 && inner.h > 0.0 => {
let k = (inner.w / iw as f64).min(inner.h / ih as f64);
Rect::new(inner.x, inner.y, iw as f64 * k, ih as f64 * k)
}
_ => inner,
};
self.push(Item::Image { rect: fitted, data: Arc::new(data.to_vec()), content_type: content_type.to_string() })
}
/// A field name made from `base` that no earlier field of the form has: the first keeps it,
/// later ones get `_2`, `_3`, ….
pub(super) fn unique_name(&mut self, base: &str) -> String {
let base = clean_name(base);
let n = self.names.entry(base.clone()).or_insert(0);
*n += 1;
if *n == 1 { base } else { format!("{base}_{n}") }
}
}
+305
View File
@@ -0,0 +1,305 @@
use super::common::{button_action, date_pattern};
use super::items::{BorderShape, Item, Widget, WidgetBorder, WidgetKind};
use super::*;
impl Layouter<'_> {
/// Draw field `f` at `rect`: its border (a button's is its widget's box), its caption, then its
/// widget, or its value as plain text when it is non-interactive, a barcode, or a text field
/// taller than its content area. `radio` is set for a group's radio button; `sib` is its SOM index.
pub(super) fn draw_field(
&mut self,
f: &Field,
rect: Rect,
ctx: &Ctx,
radio: Option<(&str, &str, Option<String>, bool)>,
sib: usize,
) -> Result<(), XfaError> {
if let Some(b) = &f.common.border
&& f.ui != Ui::Button
{
self.draw_border(b, rect)?;
}
let fctx = ctx.child(&f.common);
let inner = rect.inset(&f.common.margin);
let (mut ui, mut cap_rect) = (inner, None);
if let Some((placement, reserve)) = self.caption_reserve(f, inner.w, ctx) {
let reserve = auto_check_reserve(f, placement, inner.w, Some(inner.h)).unwrap_or(reserve);
let r = reserve.max(0.0);
match placement {
Placement::Top => {
cap_rect = Some(Rect::new(inner.x, inner.y, inner.w, r.min(inner.h)));
ui = Rect::new(inner.x, inner.y + r, inner.w, inner.h - r);
}
Placement::Bottom => {
cap_rect = Some(Rect::new(inner.x, inner.bottom() - r.min(inner.h), inner.w, r.min(inner.h)));
ui = Rect::new(inner.x, inner.y, inner.w, inner.h - r);
}
Placement::Right => {
cap_rect = Some(Rect::new(inner.right() - r.min(inner.w), inner.y, r.min(inner.w), inner.h));
ui = Rect::new(inner.x, inner.y, inner.w - r, inner.h);
}
_ => {
cap_rect = Some(Rect::new(inner.x, inner.y, r.min(inner.w), inner.h));
ui = Rect::new(inner.x + r, inner.y, inner.w - r, inner.h);
}
}
}
if let (Some(cr), Some(cap)) = (cap_rect, &f.caption)
&& cap.presence == Presence::Visible
{
let font = ctx.font.apply(&cap.font);
let para = ctx.para.apply(&cap.para);
self.draw_value_text(&cap.value, cr, &font, &para)?;
}
let som_path = || {
if fctx.som.is_empty() {
format!("{}[{sib}]", f.common.name.as_deref().unwrap_or("field"))
} else {
format!("{}.{}[{sib}]", fctx.som, f.common.name.as_deref().unwrap_or("field"))
}
};
match f.ui {
Ui::Unknown => {
self.warn_once("a field of an unknown kind is left blank");
return Ok(());
}
Ui::ImageEdit => {
// The picture the data holds (base64, as Designer saves it) or the template's.
let field_name = f.common.name.clone().unwrap_or_else(|| "an image field".into());
let text: Vec<u8> = self
.data
.and_then(|d| d.text_at(&data_path(self.tpl, &som_path())))
.map(|t| t.bytes().filter(|b| !b.is_ascii_whitespace()).collect())
.unwrap_or_default();
let from_data = if text.is_empty() {
None
} else if text.len() > MAX_DATA_IMAGE / 3 * 4 + 4 {
self.warn_once(format!("{field_name}: the picture in the data is larger than 32 MB and is left out"));
None
} else {
match base64::engine::general_purpose::STANDARD.decode(&text) {
Ok(d) if crate::image::kind(&d).is_some() => Some(d),
Ok(_) => {
self.warn_once(format!("{field_name}: the picture in the data is not a JPEG, PNG, GIF, BMP or TIFF and is left out"));
None
}
Err(_) => {
self.warn_once(format!("{field_name}: the data holds no readable picture (not base64) and is left out"));
None
}
}
};
let box_ = ui.inset(&f.ui_margin);
match (&from_data, &f.value) {
(Some(d), _) => self.draw_image(d, "", box_)?,
(None, Value::Image { content_type, data }) => self.draw_image(data, content_type, box_)?,
_ => {}
}
// Then the field itself, which clicking gives a new picture.
}
Ui::Barcode => {
// The value as text, where the bars would be.
self.warn_once("barcode fields show their value as text, not as bars");
let value = self
.data
.and_then(|d| d.text_at(&data_path(self.tpl, &som_path())))
.map(str::to_string)
.or_else(|| f.value.plain())
.unwrap_or_default();
if !value.trim().is_empty() {
let mut para = fctx.para.clone();
para.h_align = HAlign::Center;
self.draw_value_text(&Value::Text(value), ui.inset(&f.ui_margin), &fctx.font, &para)?;
}
return Ok(());
}
_ => {}
}
if f.access == Access::NonInteractive && f.ui != Ui::Button {
// Shown as text, not as a field.
return self.draw_value_text(&f.value, ui.inset(&f.ui_margin), &fctx.font, &fctx.para);
}
// A text field that grew past what a content area holds can't be one widget: its text
// is drawn as lines the page breaks fall between, and stays what the data holds.
if f.grows_with_text() && rect.h > self.cursor().0.h + 0.01 {
let value =
self.data.and_then(|d| d.value_at(&data_path(self.tpl, &som_path()))).filter(|t| !t.trim().is_empty()).or_else(|| f.value.plain());
if let Some(value) = value.filter(|t| !t.trim().is_empty()) {
let name = f.common.name.clone().unwrap_or_else(|| "a text field".into());
self.warn_once(format!("{name}: its text is longer than a page; it is shown across pages and can't be edited here"));
let m = Margin {
top: f.ui_margin.top + 2.0,
bottom: f.ui_margin.bottom + 2.0,
left: f.ui_margin.left + 2.0,
right: f.ui_margin.right + 2.0,
};
let para = Para { v_align: VAlign::Top, ..fctx.para.clone() };
// In the face it was measured in, so it takes the lines it was given.
let face = Self::field_text_face(f, &fctx.font);
let block = text::layout(&text::plain(value.trim_end()), ui.inset(&m), &para, &face, &|_, _| String::new());
return self.push_block(block);
}
}
let face = match (&f.ui, &f.caption) {
// Buttons show their caption in the caption's font.
(Ui::Button, Some(cap)) => Face::of(&ctx.font.apply(&cap.font)).unscaled(),
_ => Face::of(&fctx.font).unscaled(),
};
let h_align = match (&f.ui, &f.caption) {
(Ui::Button, Some(cap)) => ctx.para.apply(&cap.para).h_align,
_ => fctx.para.h_align,
};
let name = self.unique_name(f.common.name.as_deref().unwrap_or("field"));
let som = som_path();
let read_only = matches!(f.access, Access::ReadOnly | Access::Protected);
// The data's value wins over the template's default.
let data_value: Option<String> =
self.data.and_then(|d| d.value_at(&data_path(self.tpl, &som))).map(|t| t.trim().to_string()).filter(|t| !t.is_empty());
// A choice list's default is a saved value too.
let template_default = f.value.plain().map(|v| f.saved_value(v.trim())).filter(|v| !v.is_empty());
let plain_value = data_value.clone().or_else(|| template_default.clone());
let (kind, rect, border, value, tooltip) = match f.ui {
Ui::CheckButton => {
let s = f.check_size.unwrap_or(10.0).min(ui.w.max(1.0)).min(ui.h.max(1.0));
// Without a caption the box sits at the right of the content area, as if a
// caption on its left took the rest, so the paragraph's alignment has no room to
// act on; beside a caption that alignment places it. The box's own margin doesn't
// move it.
let x = match (cap_rect, fctx.para.h_align) {
(None, _) | (Some(_), HAlign::Right) => ui.right() - s,
(Some(_), HAlign::Center) => ui.x + (ui.w - s) / 2.0,
(Some(_), HAlign::Left | HAlign::Justify) => ui.x,
};
let y = match fctx.para.v_align {
VAlign::Top => ui.y,
VAlign::Middle => ui.y + (ui.h - s) / 2.0,
VAlign::Bottom => ui.bottom() - s,
};
let r = Rect::new(x, y, s, s);
let on = f.items.first().cloned().filter(|s| !s.trim().is_empty()).unwrap_or_else(|| "1".into());
let (kind, tip, checked) = match radio {
Some((group, on_value, tip, chosen)) => (
WidgetKind::Radio { group: group.to_string(), on: on_value.to_string(), round: f.check_round },
tip.or_else(|| f.tooltip.clone()),
chosen,
),
None => (
WidgetKind::CheckBox { on: on.clone(), round: f.check_round },
f.tooltip.clone(),
plain_value.as_deref().is_some_and(|v| is_on(v, &on)),
),
};
let border = widget_border(f.ui_border.as_ref(), true);
(kind, r, border, checked.then(|| on.clone()), tip)
}
Ui::Button => {
let caption = f.caption.as_ref().and_then(|c| c.value.plain()).unwrap_or_default().trim().to_string();
// A button's box is its field border, around the whole field (its margins pad
// the caption, not the box); without one it is invisible (a link over text, say).
let border = match &f.common.border {
Some(b) => widget_border(Some(b), false),
None => WidgetBorder { color: None, fill: None, shape: BorderShape::None, width: 0.0 },
};
(WidgetKind::Button { caption }, rect, border, None, f.tooltip.clone())
}
Ui::DateTimeEdit => {
let pattern = date_pattern(f.picture.as_deref());
// The data holds the date in the field's data picture, or else as an ISO date; the
// field shows it in its own pattern.
let data_pattern = data_date_letters(f);
let shown = plain_value
.as_ref()
.map(|v| if data_value.is_some() { iso_to_pattern(&data_to_iso(v, data_pattern.as_deref()), &pattern) } else { v.clone() });
(WidgetKind::Date(pattern), ui, widget_border(f.ui_border.as_ref(), false), shown, f.tooltip.clone())
}
Ui::ChoiceList => {
let options: Vec<(String, String)> = f
.items
.iter()
.enumerate()
.map(|(i, shown)| (f.item_values.get(i).cloned().unwrap_or_else(|| shown.clone()), shown.clone()))
.collect();
// The data holds saved values (several on separate lines); shown text is taken
// too, as a user typing into an editable list leaves it.
let value = plain_value.as_ref().map(|v| f.saved_value(v));
let kind = WidgetKind::Choice { options, list_box: f.choice.list_box, multi: f.choice.multi, editable: f.choice.editable };
(kind, ui, widget_border(f.ui_border.as_ref(), false), value, f.tooltip.clone())
}
Ui::Signature => (WidgetKind::Signature, ui, widget_border(f.ui_border.as_ref(), false), None, f.tooltip.clone()),
Ui::ImageEdit => {
let none = WidgetBorder { color: None, fill: None, shape: BorderShape::None, width: 0.0 };
(WidgetKind::Image, ui, none, None, f.tooltip.clone())
}
Ui::PasswordEdit => (WidgetKind::Password, ui, widget_border(f.ui_border.as_ref(), false), plain_value, f.tooltip.clone()),
_ => (WidgetKind::Text, ui, widget_border(f.ui_border.as_ref(), false), plain_value, f.tooltip.clone()),
};
if rect.w < 1.0 || rect.h < 1.0 {
return Ok(());
}
let action = if f.ui == Ui::Button { button_action(&f.scripts) } else { None };
let items: Vec<String> = match &kind {
WidgetKind::CheckBox { on, .. } => vec![on.clone(), f.items.get(1).cloned().unwrap_or_default()],
WidgetKind::Radio { on, .. } => vec![on.clone()],
_ => Vec::new(),
};
self.fields += 1;
self.push(Item::Widget(Box::new(Widget {
rect,
kind,
name,
som,
tooltip,
face,
h_align,
multiline: f.multiline,
max_chars: f.max_chars,
read_only,
border,
value,
default: template_default,
action,
items,
no_scroll: f.no_scroll,
})))
}
}
/// The room a caption without a reserve takes beside a check box: all the box leaves of a
/// content area `w` wide (and `h` tall, when known), the box at the size it can be drawn there.
/// `None` for any other field, or a caption with a reserve of its own.
pub(super) fn auto_check_reserve(f: &Field, placement: Placement, w: f64, h: Option<f64>) -> Option<f64> {
if f.ui != Ui::CheckButton || f.caption.as_ref().is_none_or(|c| c.reserve.is_some()) {
return None;
}
let s = f.check_size.unwrap_or(10.0).min(w.max(1.0)).min(h.map_or(f64::INFINITY, |h| h.max(1.0)));
let rest = match (placement, h) {
(Placement::Top | Placement::Bottom, Some(h)) => h - s,
(Placement::Top | Placement::Bottom, None) => return None,
_ => w - s,
};
Some(rest.max(0.0))
}
/// The widget's own border from the `<ui>` border element. Without one, a check box gets a
/// solid box, as Designer draws it, and any other field has no box of its own.
fn widget_border(b: Option<&Border>, check: bool) -> WidgetBorder {
let Some(b) = b else {
if !check {
return WidgetBorder { color: None, fill: None, shape: BorderShape::None, width: 0.0 };
}
return WidgetBorder { color: Some([0.0, 0.0, 0.0]), fill: Some([1.0, 1.0, 1.0]), shape: BorderShape::Full, width: 0.5 };
};
let vis = b.visible_edges();
let shape = match vis {
[false, false, false, false] => BorderShape::None,
[false, false, true, false] => BorderShape::Underline,
_ => BorderShape::Full,
};
let edge = (0..4).find(|&i| vis[i]).map(|i| b.edges[i]).unwrap_or_default();
WidgetBorder {
color: (shape != BorderShape::None).then_some(edge.color.0),
fill: b.fill().map(|c| c.0),
shape,
width: edge.thickness.clamp(0.1, 12.0),
}
}
+310
View File
@@ -0,0 +1,310 @@
use super::common::{clamp_opt, divide, is_row, moved, solid_extent, splittable};
use super::items::Item;
use super::*;
impl Layouter<'_> {
/// Place `node` as one block of size (`w`, `h`) at the cursor: in this content area when it fits,
/// in the next when it fits there, and across as many as it takes when it is taller than a
/// content area (a positioned or fixed-height subform holding more than a page).
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, 0.0)?;
}
let (rect, y, _) = self.cursor();
let first = self.pages.last().map_or(0, |p| p.items.len());
self.leaves.clear();
self.draw_node(node, Rect::new(x, y, w, h), ctx, depth, sib)?;
let page_h = self.pages.last().map_or(f64::INFINITY, |p| p.height);
// What was drawn, when it is lower than the block's own height says (content that
// outgrows a fixed height) and ends off the page.
let drawn = self
.pages
.last()
.and_then(|p| p.items.get(first..))
.unwrap_or_default()
.iter()
.filter_map(solid_extent)
.map(|e| e.1)
.fold(y + h, f64::max);
if y + h <= rect.bottom() + 0.01 && drawn <= page_h + 0.01 {
self.leaves.clear();
self.place(h);
return Ok(());
}
self.split_block(first, y, drawn)
}
/// Remember that the items the last page got since `start` are one field or draw at
/// `rect`, to be kept together by a page break.
pub(super) fn keep_whole(&mut self, start: usize, rect: Rect) {
let end = self.pages.last().map_or(0, |p| p.items.len());
if end > start {
self.leaves.push((start, end, rect.y, rect.bottom()));
}
}
pub(super) fn item_count(&self) -> usize {
self.pages.last().map_or(0, |p| p.items.len())
}
/// Spread the items the last page holds from index `first` on (a block drawn from `top` to
/// `end`, past the bottom of its content area) over this and the following content areas.
/// Breaks fall between text, pictures and fields, never through one that fits in an area;
/// fills and lines are cut at the break.
fn split_block(&mut self, first: usize, top: f64, end: f64) -> Result<(), XfaError> {
let items: Vec<Item> = match self.pages.last_mut() {
Some(p) if first <= p.items.len() => p.items.split_off(first),
_ => return Ok(()),
};
// Each item with the extent of the field or draw it belongs to: such a leaf moves
// whole (its border with its text), unless it is taller than a content area.
let area_h = self.cursor().0.h;
let mut extents: Vec<Option<(f64, f64)>> = vec![None; items.len()];
for (start, end, top, bottom) in std::mem::take(&mut self.leaves) {
if bottom - top <= area_h + 0.01
&& let Some(slots) = start.checked_sub(first).and_then(|s| extents.get_mut(s..end.saturating_sub(first)))
{
slots.fill(Some((top, bottom)));
}
}
let mut rest: Vec<(Item, Option<(f64, f64)>)> = items.into_iter().zip(extents).collect();
let origin_x = self.cursor().0.x;
// An item's place on the current page is its drawn place moved by (dx, -shift).
let (mut dx, mut shift) = (0.0, 0.0);
let mut seg_top = top;
let mut first_segment = true;
loop {
let (rect, _, placed) = self.cursor();
let limit = rect.bottom() + shift;
let mut cut = limit;
if rect.h < 1.0 {
// No room to split into: leave the rest here.
cut = f64::INFINITY;
} else {
// The lowest break at or above the bottom that crosses no solid item: going
// through them from the lowest top up, a break moved to an item's top can't
// cross one already passed.
let mut solid: Vec<(f64, f64)> =
rest.iter().filter_map(|(item, kept)| kept.or_else(|| solid_extent(item))).filter(|e| e.0 < limit - 0.01).collect();
solid.sort_by(|a, b| b.0.total_cmp(&a.0));
for (t, b) in solid {
if t < cut - 0.01 && b > cut + 0.01 {
cut = t;
}
}
if cut <= seg_top + 0.01 {
// An item taller than what is left. Start it in a fresh area if this one
// already holds something; otherwise cut through it.
cut = if first_segment && placed && seg_top > rect.y + 0.01 { seg_top } else { limit };
}
}
let mut here = Vec::new();
let mut below = Vec::new();
for (item, kept) in rest {
if let Some((top, _)) = kept {
if top < cut - 0.01 {
here.push(item)
} else {
below.push((item, kept))
}
continue;
}
match divide(item, cut) {
(Some(a), None) => here.push(a),
(None, Some(b)) => below.push((b, None)),
(Some(a), Some(b)) => {
here.push(a);
below.push((b, None));
}
(None, None) => {}
}
}
if let Some(p) = self.pages.last_mut() {
p.items.extend(here.into_iter().map(|it| moved(it, dx, -shift)));
}
if below.is_empty() {
if let Some(c) = &mut self.cur {
c.y = (end - shift).min(c.rect.bottom()).max(c.y);
c.placed = true;
}
self.overflowed(end - shift);
return Ok(());
}
rest = below;
seg_top = cut;
first_segment = false;
self.next_area()?;
let (rect, _, _) = self.cursor();
shift = cut - rect.y;
dx = rect.x - origin_x;
}
}
fn page_has_flow(&self) -> bool {
self.cur.as_ref().is_some_and(|c| c.placed || c.content > 0)
}
// ───────────────────────────────────────────────────────────────────────── flow
/// Flow `node` (a subform; anything else is ignored) into the content areas.
pub(super) fn flow_subform(&mut self, node: &Node, avail: Option<(f64, f64)>, ctx: &Ctx, depth: usize, sib: usize) -> Result<(), XfaError> {
if depth > MAX_DEPTH {
return Ok(());
}
let Node::Subform(sf) = node else { return Ok(()) };
self.check_measures()?;
if sf.break_before_page && self.page_has_flow() {
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);
self.warn_truncated(node, &ctx.som, instances);
let ctx_parent = ctx;
for i in 0..instances {
// Every instance spends budget, so nested repeats that place nothing still stop.
self.measures.set(self.measures.get().saturating_add(1));
self.check_measures()?;
let ctx = ctx_parent.with_som(&sf.common, sib + i);
let (rect, _, _) = self.cursor();
let (x, w) = avail.unwrap_or((rect.x, rect.w));
let w = clamp_opt(sf.common.w.unwrap_or(w), sf.common.min_w, sf.common.max_w).min(w.max(1.0));
if !matches!(sf.layout, Layout::Tb | Layout::Table) || sf.common.h.is_some() {
// Not splittable: one block.
let (_, h) = self.size_of(node, w, ctx_parent, depth);
self.flow_block(node, x, (w, h), ctx_parent, depth, sib + i)?;
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);
match sf.layout {
Layout::Table => self.flow_table(sf, inner_x, inner_w, &ctx, depth)?,
_ => {
let sibs = SomIndices::of(&sf.children);
for (ci, child) in sf.children.iter().enumerate() {
if !child.common().presence.occupies() {
continue;
}
let csib = sibs.at(ci);
if let Node::Subform(_) = child
&& splittable(child)
{
self.flow_subform(child, Some((inner_x, inner_w)), &ctx, depth + 1, csib)?;
continue;
}
let (cw, ch) = self.size_of(child, inner_w, &ctx, depth + 1);
self.flow_block(child, inner_x, (cw, ch), &ctx, depth + 1, csib)?;
}
}
}
self.advance(m.bottom);
if framed && let Some(frame) = self.frames.pop() {
self.finish_frame(frame)?;
}
}
if sf.break_after_page {
self.break_frames(0.0, true)?;
}
Ok(())
}
pub(super) fn columns(&self, sf: &Subform, inner_w: f64) -> Vec<f64> {
if !sf.column_widths.is_empty() {
return sf.column_widths.clone();
}
let cells = sf.children.iter().filter_map(|c| if let Node::Subform(r) = c { Some(r.children.len()) } else { None }).max().unwrap_or(1).max(1);
vec![inner_w / cells as f64; cells]
}
fn flow_table(&mut self, sf: &Subform, x: f64, inner_w: f64, ctx: &Ctx, depth: usize) -> Result<(), XfaError> {
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() {
continue;
}
let csib = sibs.at(ci);
if let Node::Subform(row) = child
&& row.layout == Layout::Row
{
// Rows added as data (in another viewer) repeat the row.
let instances = self.instances(child, &ctx.som);
self.warn_truncated(child, &ctx.som, instances);
for inst in 0..instances {
let h = self.row_height(row, &cols, inner_w, ctx, depth + 1);
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 (_, y, _) = self.cursor();
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)?;
self.place(h);
}
continue;
}
if splittable(child)
&& let Node::Subform(_) = child
{
self.flow_subform(child, Some((x, inner_w)), ctx, depth + 1, csib)?;
continue;
}
let (cw, ch) = self.size_of(child, inner_w, ctx, depth + 1);
self.flow_block(child, x, (cw, ch), ctx, depth + 1, csib)?;
}
Ok(())
}
// ───────────────────────────────────────────────────────────────────────── sizing
/// The size a node takes when given `avail_w` of width. Containers are measured once per
/// (node, width, depth): measuring a width-less container measures its content twice (for
/// its width, then its height), which would otherwise be exponential in the nesting.
/// How many instances of `node` to lay out under the container at `parent_som`: a
/// repeating subform's `occur` and data instances ([`instance_count`]); one for anything else.
pub(super) fn instances(&self, node: &Node, parent_som: &str) -> usize {
let Node::Subform(sf) = node else { return 1 };
let from_data = match (&sf.common.name, self.data) {
(Some(name), Some(d)) if sf.occur.max != Some(1) => d.count(&data_path(self.tpl, parent_som), name),
_ => 0,
};
instance_count(&sf.occur, from_data)
}
/// The data holds more instances of `node` than `laid_out`: say so, since calculations
/// then see only those.
fn warn_truncated(&mut self, node: &Node, parent_som: &str, laid_out: usize) {
let Node::Subform(sf) = node else { return };
let from_data = match (&sf.common.name, self.data) {
(Some(name), Some(d)) if sf.occur.max != Some(1) => d.count(&data_path(self.tpl, parent_som), name),
_ => 0,
};
if from_data > laid_out {
self.warn_once(format!(
"{}: the data holds {from_data} rows; only the first {laid_out} are laid out (calculations see those)",
sf.common.name.as_deref().unwrap_or("a subform")
));
}
}
}
+145
View File
@@ -0,0 +1,145 @@
use super::*;
/// How far below its baseline an underline is drawn, in the text's size: an underline is the
/// level line pushed just before its span, this far below it (the PDF writer tells it apart from
/// a rule by this).
pub(crate) const UNDERLINE_DROP: f64 = 0.1;
#[derive(Clone, Debug, PartialEq)]
pub enum Item {
Fill {
rect: Rect,
color: [f64; 3],
},
Line {
from: (f64, f64),
to: (f64, f64),
width: f64,
color: [f64; 3],
dashed: bool,
},
/// A box with rounded (or inverted) corners: its fill, and its edge drawn inside it.
Rounded(Box<Rounded>),
Text(Span),
Image {
rect: Rect,
data: Arc<Vec<u8>>,
content_type: String,
},
Widget(Box<Widget>),
}
/// A box with rounded corners ([`Item::Rounded`]).
#[derive(Clone, Debug, PartialEq)]
pub struct Rounded {
pub rect: Rect,
/// Top-left, top-right, bottom-right and bottom-left radii, each at most half the box's
/// shorter side.
pub radii: [f64; 4],
/// Corners whose curve goes into the box rather than around it, in the same order.
pub inverted: [bool; 4],
pub fill: Option<[f64; 3]>,
/// The edge: its width, colour and whether it is dashed.
pub edge: Option<(f64, [f64; 3], bool)>,
/// A page break cut the box here: that side has no edge, and its corners are square.
pub open_top: bool,
pub open_bottom: bool,
}
#[derive(Clone, Debug, PartialEq)]
pub enum WidgetKind {
Text,
/// A text field whose characters are shown as asterisks.
Password,
/// A drop-down (`list_box` false) or list box: `options` are (saved value, shown text).
Choice {
options: Vec<(String, String)>,
list_box: bool,
multi: bool,
editable: bool,
},
/// A signature field, to be signed in the app.
Signature,
/// An image field: a push button that picks a picture (the picture itself is page content).
Image,
/// A date field with its Acrobat format pattern (`yyyy-mm-dd`).
Date(String),
CheckBox {
on: String,
round: bool,
},
Radio {
group: String,
on: String,
round: bool,
},
Button {
caption: String,
},
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BorderShape {
None,
Full,
Underline,
}
#[derive(Clone, Debug, PartialEq)]
pub struct WidgetBorder {
pub color: Option<[f64; 3]>,
pub fill: Option<[f64; 3]>,
pub shape: BorderShape,
pub width: f64,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Action {
Reset,
Print,
SaveAs,
Url(String),
/// Any other click script: run by the XFA scripting engine.
Script(String),
}
/// An interactive field to create as an AcroForm widget.
#[derive(Clone, Debug, PartialEq)]
pub struct Widget {
pub rect: Rect,
pub kind: WidgetKind,
/// The PDF field name (unique in the form, no periods).
pub name: String,
/// The template's scripting object model path, for data round trips.
pub som: String,
pub tooltip: Option<String>,
pub face: Face,
pub h_align: HAlign,
pub multiline: bool,
pub max_chars: Option<usize>,
pub read_only: bool,
pub border: WidgetBorder,
/// The field's value: saved (bound) values for choice lists, newline-separated when several.
pub value: Option<String>,
/// The template's default value (what a reset restores); data values are not defaults.
pub default: Option<String>,
pub action: Option<Action>,
/// Check boxes: the on and off values of the data; radio buttons: the on value.
pub items: Vec<String>,
/// A text field that doesn't scroll: it takes no more text than it shows (`DoNotScroll`).
pub no_scroll: bool,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Page {
pub width: f64,
pub height: f64,
pub items: Vec<Item>,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct Form {
pub pages: Vec<Page>,
pub fields: usize,
pub warnings: Vec<String>,
}
+366
View File
@@ -0,0 +1,366 @@
use super::field::auto_check_reserve;
use super::common::clamp_opt;
use super::*;
impl Layouter<'_> {
/// The width and height `node` takes in `avail_w`: its own size, else what its content needs
/// (containers are measured once per width and depth, and measuring stops at its caps).
pub(super) fn size_of(&self, node: &Node, avail_w: f64, ctx: &Ctx, depth: usize) -> (f64, f64) {
let c = node.common();
let spent = self.measures.get().saturating_add(1);
self.measures.set(spent);
if depth > MAX_DEPTH || spent > MAX_MEASURES {
return (c.w.unwrap_or(avail_w), c.h.unwrap_or(0.0));
}
// Only containers recurse; their addresses are stable for the whole layout (the
// template is borrowed, the root and page areas are held by the layouter).
let key = match node {
Node::Subform(s) => Some((&**s as *const Subform as usize, avail_w.to_bits(), depth)),
Node::Area(a) => Some((&**a as *const Area as usize, avail_w.to_bits(), depth)),
_ => None,
};
if let Some(key) = key
&& let Ok(cache) = self.measured.try_borrow()
&& let Some(m) = cache.get(&key)
&& m.font == ctx.font
&& m.para == ctx.para
{
return m.size;
}
let size = self.measure(node, avail_w, ctx, depth);
if let Some(key) = key
&& let Ok(mut cache) = self.measured.try_borrow_mut()
{
cache.insert(key, Measured { font: ctx.font.clone(), para: ctx.para.clone(), size });
}
size
}
/// The size (width, height) of `node` within `avail_w`, uncached ([`Self::size_of`] caches it).
/// Without a width, a container, a text draw or a button is as wide as its content (at most
/// `avail_w`) and anything else takes `avail_w`; without a height, it is as tall as its content.
fn measure(&self, node: &Node, avail_w: f64, ctx: &Ctx, depth: usize) -> (f64, f64) {
let c = node.common();
// The node's own SOM: its repeating children count their instances under it, and a
// growing text field finds its value at it. Measurements are cached per template node,
// so instance 0 stands for all of them.
let ctx = ctx.with_som(c, 0);
let som = ctx.som.clone();
// A container without a width is as wide as its content (a radio group beside its
// question, say), and so is a text label or a button (as wide as its caption), within
// what is available; any other field without one takes what is available.
let w = match c.w {
Some(w) => w,
None => match node {
Node::Subform(_) | Node::ExclGroup(_) | Node::Area(_) => {
let inner = (avail_w - c.margin.horizontal()).max(1.0);
(self.content_width(node, inner, &ctx, depth) + c.margin.horizontal()).min(avail_w)
}
Node::Draw(d) => self.natural_width(&d.value, &ctx.font, &ctx.para).map_or(avail_w, |t| (t + c.margin.horizontal()).min(avail_w)),
Node::Field(f) if f.ui == Ui::Button => f
.caption
.as_ref()
.and_then(|cap| self.natural_width(&cap.value, &ctx.font.apply(&cap.font), &ctx.para.apply(&cap.para)))
.map_or(avail_w, |t| (t + c.margin.horizontal() + f.ui_margin.horizontal()).min(avail_w)),
Node::Field(_) => avail_w,
},
};
let w = clamp_opt(w, c.min_w, c.max_w);
let h = match c.h {
Some(h) => h,
None => {
let inner_w = (w - c.margin.horizontal()).max(1.0);
let content = match node {
Node::Draw(d) => self.value_height(&d.value, inner_w, &ctx),
Node::Field(f) => self.field_height(f, inner_w, &ctx),
Node::ExclGroup(g) => self.group_height(g, inner_w, &ctx, depth),
Node::Area(a) => self.positioned_height(&a.children, inner_w, &ctx, depth),
Node::Subform(s) => match s.layout {
Layout::Positioned => self.positioned_height(&s.children, inner_w, &ctx, depth),
// Each instance of a repeating child takes its height again.
Layout::Tb => s
.children
.iter()
.filter(|k| k.common().presence.occupies())
.map(|k| self.size_of(k, inner_w, &ctx, depth + 1).1 * self.instances(k, &som) as f64)
.sum(),
Layout::LrTb | Layout::RlTb => self.pack_instances(&s.children, inner_w, &ctx, depth, &som).1,
Layout::Table => {
let cols = self.columns(s, inner_w);
s.children
.iter()
.filter(|k| k.common().presence.occupies())
.map(|k| match k {
Node::Subform(r) if r.layout == Layout::Row => self.row_height(r, &cols, inner_w, &ctx, depth + 1),
_ => self.size_of(k, inner_w, &ctx, depth + 1).1,
})
.sum()
}
Layout::Row => self.row_height(s, &self.columns(s, inner_w), inner_w, &ctx, depth + 1),
},
};
content + c.margin.vertical()
}
};
// A flowed subform is as tall as what it holds: other viewers do not stretch one to
// the `minH` a designer left on it.
let min_h = match node {
Node::Subform(s) if c.h.is_none() && s.layout != Layout::Positioned => None,
_ => c.min_h,
};
(w, clamp_opt(h, min_h, c.max_h))
}
/// The natural width of a container's content within `avail_w`.
fn content_width(&self, node: &Node, avail_w: f64, ctx: &Ctx, depth: usize) -> f64 {
let group: Vec<Node>;
let (children, layout): (&[Node], Layout) = match node {
Node::Subform(s) => {
if s.layout == Layout::Table && !s.column_widths.is_empty() {
return s.column_widths.iter().sum::<f64>().min(avail_w);
}
(&s.children, s.layout)
}
Node::ExclGroup(g) => {
// Fields are leaves: measuring these copies caches nothing.
group = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect();
(&group, g.layout)
}
Node::Area(a) => (&a.children, Layout::Positioned),
_ => return avail_w,
};
let visible = children.iter().filter(|k| k.common().presence.occupies());
match layout {
Layout::LrTb | Layout::RlTb | Layout::Row => {
let sum: f64 = visible.map(|k| self.size_of(k, avail_w, ctx, depth + 1).0).sum();
if sum <= avail_w + 0.05 { sum } else { avail_w }
}
Layout::Positioned => visible
.map(|k| {
let (w, h) = self.size_of(k, avail_w, ctx, depth + 1);
k.common().top_left(w, h).0 + w
})
.fold(0.0, f64::max)
.min(avail_w),
Layout::Tb | Layout::Table => visible.map(|k| self.size_of(k, avail_w, ctx, depth + 1).0).fold(0.0, f64::max).min(avail_w),
}
}
/// How wide text `v` is unwrapped, in `font` and `para`, with a hair to spare so that laying
/// it out at that width keeps it on its lines; `None` for what isn't text, or text that draws
/// nothing.
fn natural_width(&self, v: &Value, font: &Font, para: &Para) -> Option<f64> {
let rich = match v {
Value::Text(t) => text::plain(t),
Value::Rich(r) => r.clone(),
_ => return None,
};
// No text (a bordered spacer or rule): nothing to size to, so it keeps what it has room for.
let w = text::natural_width(&rich, para, &Face::of(font), &|id, max| self.embed(id, max));
(w > 0.0).then_some(w + 0.01)
}
/// How tall value `v` is laid out `w` wide: text wrapped in the context's font and paragraph,
/// an image at its aspect ratio, and nothing for lines, rectangles and empty values.
fn value_height(&self, v: &Value, w: f64, ctx: &Ctx) -> f64 {
match v {
Value::Text(t) => text::measure_height(&text::plain(t), w, &ctx.para, &Face::of(&ctx.font), &|id, max| self.embed(id, max)),
Value::Rich(r) => text::measure_height(r, w, &ctx.para, &Face::of(&ctx.font), &|id, max| self.embed(id, max)),
Value::Image { data, .. } => crate::image::size(data).map_or(w, |(iw, ih)| if iw > 0 { w * ih as f64 / iw as f64 } else { w }),
Value::Line { .. } | Value::Rectangle(_) | Value::Empty => 0.0,
}
}
/// Where field `f`'s caption goes and how much room it takes from the field's box, or `None`
/// when it has no caption that takes room (a button's is drawn inside its box).
pub(super) fn caption_reserve(&self, f: &Field, inner_w: f64, ctx: &Ctx) -> Option<(Placement, f64)> {
let cap = f.caption.as_ref()?;
if !cap.presence.occupies() || f.ui == Ui::Button {
return None;
}
if cap.value.is_empty() && cap.reserve.is_none() {
return None;
}
// A check box has no text for an inline caption to run with: it is not shown.
if f.ui == Ui::CheckButton && cap.placement == Placement::Inline {
return None;
}
let placement = if cap.placement == Placement::Inline { Placement::Left } else { cap.placement };
let font = ctx.font.apply(&cap.font);
let para = ctx.para.apply(&cap.para);
let reserve = cap.reserve.unwrap_or_else(|| match placement {
Placement::Top | Placement::Bottom => {
let rich = match &cap.value {
Value::Rich(r) => r.clone(),
other => text::plain(&other.plain().unwrap_or_default()),
};
text::measure_height(&rich, inner_w, &para, &Face::of(&font), &|id, max| self.embed(id, max))
}
_ => (Face::of(&font).width(&cap.value.plain().unwrap_or_default()) + 2.0).min(inner_w * 0.6),
});
Some((placement, reserve))
}
/// The height of field `f`'s content within `inner_w` (inside its margins): its widget (a check
/// box, a button's caption, or one line of text or three when multi-line, more for a text field
/// that grows) plus a caption above or below it, or the taller of the two for one beside it.
fn field_height(&self, f: &Field, inner_w: f64, ctx: &Ctx) -> f64 {
let font = ctx.font.apply(&f.common.font);
let lines = if f.multiline { 3.0 } else { 1.0 };
let ui = match f.ui {
Ui::CheckButton => f.check_size.unwrap_or(10.0).max(text::line_height(font.size)),
// A button shows its caption: as tall as the caption's text in its own font.
Ui::Button if f.caption.as_ref().is_some_and(|c| !c.value.is_empty()) => {
let cap = f.caption.as_ref().map_or(0.0, |c| {
let rich = match &c.value {
Value::Rich(r) => r.clone(),
other => text::plain(&other.plain().unwrap_or_default()),
};
let w = (inner_w - f.ui_margin.horizontal()).max(1.0);
text::measure_height(&rich, w, &ctx.para.apply(&c.para), &Face::of(&ctx.font.apply(&c.font)), &|id, max| self.embed(id, max))
});
cap + f.ui_margin.vertical()
}
_ => (text::line_height(font.size) * lines).max(self.grown_text_height(f, inner_w, ctx)) + 4.0 + f.ui_margin.vertical(),
};
match self.caption_reserve(f, inner_w, ctx) {
Some((Placement::Top | Placement::Bottom, r)) => ui + r,
Some((placement, r)) => {
// A check box's caption without a reserve is drawn in all the box leaves.
let w = match auto_check_reserve(f, placement, inner_w, None) {
Some(auto) => auto,
None => inner_w - r,
};
let cap = f.caption.as_ref().map(|c| {
let rich = match &c.value {
Value::Rich(r) => r.clone(),
other => text::plain(&other.plain().unwrap_or_default()),
};
text::measure_height(&rich, w.max(1.0), &ctx.para.apply(&c.para), &Face::of(&ctx.font.apply(&c.font)), &|id, max| {
self.embed(id, max)
})
});
ui.max(cap.unwrap_or(0.0))
}
None => ui,
}
}
/// The face a field's text is laid out in: at its horizontal scale when the layout draws
/// the text itself (a non-interactive field), as designed when the forms layer draws it in
/// a widget, whose appearances have no horizontal scaling.
pub(super) fn field_text_face(f: &Field, font: &Font) -> Face {
let face = Face::of(font);
if f.access == Access::NonInteractive { face } else { face.unscaled() }
}
/// The height the text of a multi-line text field takes (its value in the data, else the
/// template's), for a field without a fixed height, which grows to hold its text. `ctx`
/// is the field's own (its SOM ends in the field). Zero for anything else.
fn grown_text_height(&self, f: &Field, inner_w: f64, ctx: &Ctx) -> f64 {
if !f.grows_with_text() || f.common.name.is_none() {
return 0.0;
}
let value = self.data.and_then(|d| d.value_at(&data_path(self.tpl, &ctx.som))).filter(|t| !t.trim().is_empty()).or_else(|| f.value.plain());
let Some(value) = value.filter(|t| !t.trim().is_empty()) else { return 0.0 };
// Beside a caption the text has what the caption leaves; a widget's text has 2pt of
// padding on each side, a non-interactive field's (drawn as text) none, so it wraps
// where it is drawn.
let beside = match self.caption_reserve(f, inner_w, ctx) {
Some((Placement::Top | Placement::Bottom, _)) | None => 0.0,
Some((_, r)) => r,
};
let padding = if f.access == Access::NonInteractive { 0.0 } else { 4.0 };
let w = (inner_w - beside - f.ui_margin.horizontal() - padding).max(1.0);
let font = ctx.font.apply(&f.common.font);
text::measure_height(&text::plain(value.trim_end()), w, &ctx.para, &Self::field_text_face(f, &font), &|_, _| String::new())
}
/// The height of an exclusion group's fields at `inner_w`, by the group's layout: stacked
/// top to bottom, at their own positions, or packed in rows.
fn group_height(&self, g: &ExclGroup, inner_w: f64, ctx: &Ctx, depth: usize) -> f64 {
let nodes: Vec<Node> = g.fields.iter().map(|f| Node::Field(Box::new(f.clone()))).collect();
match g.layout {
Layout::Tb => nodes.iter().filter(|k| k.common().presence.occupies()).map(|k| self.size_of(k, inner_w, ctx, depth + 1).1).sum(),
Layout::Positioned => self.positioned_height(&nodes, inner_w, ctx, depth),
_ => self.packed_height(&nodes, inner_w, ctx, depth),
}
}
fn positioned_height(&self, children: &[Node], inner_w: f64, ctx: &Ctx, depth: usize) -> f64 {
children
.iter()
.filter(|k| k.common().presence.occupies())
.map(|k| {
let c = k.common();
let (w, h) = self.size_of(k, (inner_w - c.x.unwrap_or(0.0)).max(1.0), ctx, depth + 1);
c.top_left(w, h).1 + h
})
.fold(0.0, f64::max)
}
/// Left-to-right packing: rows of children, wrapped at the available width, as (child,
/// instance, place). A repeating subform is packed once per instance it has under the
/// container at `som`.
pub(super) fn pack_instances(&self, children: &[Node], inner_w: f64, ctx: &Ctx, depth: usize, som: &str) -> (Vec<(usize, usize, Rect)>, f64) {
let mut out = Vec::new();
let (mut x, mut y, mut line_h) = (0.0, 0.0, 0.0);
for (i, k) in children.iter().enumerate() {
if !k.common().presence.occupies() {
continue;
}
let (w, h) = self.size_of(k, inner_w, ctx, depth + 1);
for instance in 0..self.instances(k, som) {
if x > 0.0 && x + w > inner_w + 0.05 {
y += line_h;
x = 0.0;
line_h = 0.0;
}
out.push((i, instance, Rect::new(x, y, w, h)));
x += w;
line_h = line_h.max(h);
}
}
(out, y + line_h)
}
/// [`Self::pack_instances`] for children that don't repeat (the fields of a group).
pub(super) fn pack(&self, children: &[Node], inner_w: f64, ctx: &Ctx, depth: usize) -> (Vec<(usize, Rect)>, f64) {
let (placed, h) = self.pack_instances(children, inner_w, ctx, depth, "");
(placed.into_iter().map(|(i, _, r)| (i, r)).collect(), h)
}
fn packed_height(&self, children: &[Node], inner_w: f64, ctx: &Ctx, depth: usize) -> f64 {
self.pack(children, inner_w, ctx, depth).1
}
/// Cell rectangles of a row (relative to the row's inner box) and the row height.
pub(super) fn cells(&self, row: &Subform, cols: &[f64], inner_w: f64, ctx: &Ctx, depth: usize) -> (Vec<(usize, Rect)>, f64) {
let ctx = ctx.child(&row.common);
let mut out = Vec::new();
let mut col = 0usize;
let mut x = 0.0;
let mut h: f64 = 0.0;
for (i, k) in row.children.iter().enumerate() {
if !k.common().presence.occupies() {
continue;
}
let span = k.common().col_span.max(1);
let w = if col < cols.len() { cols.iter().skip(col).take(span).sum::<f64>() } else { k.common().w.unwrap_or((inner_w - x).max(1.0)) };
let (_, ch) = self.size_of(k, w, &ctx, depth + 1);
out.push((i, Rect::new(x, 0.0, w, ch)));
h = h.max(ch);
x += w;
col += span;
}
let h = match row.common.h {
Some(rh) => rh,
None => h + row.common.margin.vertical(),
};
(out, clamp_opt(h, row.common.min_h, row.common.max_h))
}
pub(super) fn row_height(&self, row: &Subform, cols: &[f64], inner_w: f64, ctx: &Ctx, depth: usize) -> f64 {
self.cells(row, cols, inner_w, ctx, depth).1
}
}
+280
View File
@@ -0,0 +1,280 @@
use super::items::{Item, Page, UNDERLINE_DROP};
use super::*;
impl Layouter<'_> {
pub(super) fn warn_once(&mut self, msg: impl Into<String>) {
let m = msg.into();
if self.warned.insert(m.clone()) && self.warnings.len() < 100 {
self.warnings.push(m);
}
}
fn page_no(&self) -> usize {
self.pages.len()
}
/// The text an embedded field `id` shows, at most `max` bytes of a field value (the page
/// number and count are a few bytes; the text layout cuts them).
pub(super) fn embed(&self, id: &str, max: usize) -> String {
let v = match self.tpl.page_roles.iter().find(|(i, _)| i == id).map(|(_, r)| *r) {
Some(PageRole::Number) => self.page_no().to_string(),
Some(PageRole::Count) => PAGE_COUNT_MARK.to_string(),
None => self.values.get(id).map(|v| text::prefix(v, max).to_string()).unwrap_or_default(),
};
#[cfg(test)]
EMBED_COPIED.with(|c| c.set(c.get().saturating_add(v.len() as u64)));
v
}
/// `TooLarge` once the measurement budget is spent (sizes measured after that are guesses).
pub(super) fn check_measures(&self) -> Result<(), XfaError> {
if self.measures.get() > MAX_MEASURES {
return Err(XfaError::TooLarge("the form is nested too deeply to lay out".into()));
}
Ok(())
}
pub(super) fn push(&mut self, item: Item) -> Result<(), XfaError> {
self.check_measures()?;
self.items += 1;
if self.items > MAX_ITEMS {
return Err(XfaError::TooLarge("the form has too many objects to lay out".into()));
}
if let Some(p) = self.pages.last_mut() {
p.items.push(item);
}
Ok(())
}
pub(super) fn push_block(&mut self, b: Block) -> Result<(), XfaError> {
for s in b.spans {
if s.face.underline {
let w = s.face.width(&s.text);
let y = s.baseline + s.face.size * UNDERLINE_DROP;
self.push(Item::Line { from: (s.x, y), to: (s.x + w, y), width: (s.face.size * 0.06).max(0.3), color: s.face.color, dashed: false })?;
}
if !s.text.trim().is_empty() {
self.push(Item::Text(s))?;
}
}
Ok(())
}
// ───────────────────────────────────────────────────────────────────────── pages
pub(super) fn new_page(&mut self) -> Result<(), XfaError> {
let idx =
(0..self.areas.len()).find(|&i| self.areas[i].occur_max.is_none_or(|m| self.uses[i] < m)).unwrap_or(self.areas.len().saturating_sub(1));
self.new_page_of(idx)
}
/// Once the body has been laid out, give every page area that has been used fewer times than
/// its `occur min` the pages it still needs, in template order, after the body's pages.
pub(super) fn satisfy_page_area_minimums(&mut self) -> Result<(), XfaError> {
let areas = Rc::clone(&self.areas);
for (idx, area) in areas.iter().enumerate() {
while self.uses.get(idx).is_some_and(|u| *u < area.occur_min) {
self.new_page_of(idx)?;
}
}
Ok(())
}
/// Start a page from page area `idx`.
fn new_page_of(&mut self, idx: usize) -> Result<(), XfaError> {
if self.pages.len() >= MAX_PAGES {
return Err(XfaError::TooLarge(format!("the form lays out to more than {MAX_PAGES} pages")));
}
let areas = Rc::clone(&self.areas);
let Some(area) = areas.get(idx) else {
return Err(XfaError::Malformed("the template has no page area".into()));
};
if let Some(u) = self.uses.get_mut(idx) {
*u += 1;
}
self.pages.push(Page { width: area.width, height: area.height, items: Vec::new() });
let content = area.content.first().copied().unwrap_or(Rect::new(0.0, 0.0, area.width, area.height));
self.cur = Some(Cursor { area: idx, content: 0, rect: content, y: content.y, placed: false });
// Master page content: positioned draws and fields.
let ctx = Ctx { font: Font::default(), para: Para::default(), som: area.name.clone().map(|n| format!("{n}[0]")).unwrap_or_default() };
let page_rect = Rect::new(0.0, 0.0, area.width, area.height);
let sibs = SomIndices::of(&area.items);
for (i, item) in area.items.iter().enumerate() {
self.draw_positioned_child(item, page_rect, &ctx, 1, sibs.at(i))?;
}
Ok(())
}
/// Move to the next content area, or the next page.
pub(super) fn next_area(&mut self) -> Result<(), XfaError> {
if let Some(c) = &self.cur
&& let Some(area) = self.areas.get(c.area)
&& let Some(next) = area.content.get(c.content + 1).copied()
{
let i = c.content + 1;
self.cur = Some(Cursor { area: c.area, content: i, rect: next, y: next.y, placed: false });
return Ok(());
}
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.
pub(super) fn cursor(&self) -> (Rect, f64, bool) {
match &self.cur {
Some(c) => (c.rect, c.y, c.placed),
None => (Rect::new(0.0, 0.0, 612.0, 792.0), 0.0, false),
}
}
/// Where the flow is now ([`FlowMark`]); `None` before the first page.
pub(super) 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.
pub(super) 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.
pub(super) 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).
pub(super) fn advance(&mut self, dy: f64) {
if let Some(c) = &mut self.cur {
c.y += dy.max(0.0);
}
}
/// Move past something that was placed.
pub(super) fn place(&mut self, dy: f64) {
if let Some(c) = &mut self.cur {
c.y += dy.max(0.0);
c.placed = true;
}
let (_, y, _) = self.cursor();
self.overflowed(y);
}
/// 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.
pub(super) 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`]).
pub(super) 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 + after, false)?;
} else {
self.next_area()?;
}
Ok(true)
}
}