M12: measure each XFA table cell under its own same-name SOM index

cells() (behind row_height) measured every cell with size_of(), at SOM
index 0, while draw_row() draws cell i at its index among its same-named
siblings. With a short qty[0] beside a long qty[1] the row took qty[0]'s
height (38.5pt), and the long cell ran into the following rows and
content; a non-interactive cell drew its text over them.

Each cell is now measured with size_at() at SomIndices::of(..).at(i), the
index it is drawn and bound at (XFA 3.3, "Tables", p. 327; "Growable
Containers", p. 271). content_width(), group_height() and draw_group()
measured their children at index 0 too and now use the same index.
size_of() had no callers left and is removed, so every measurement names
its index.

Tests: a row with short qty[0] and long qty[1] is as tall as the long cell,
with the following rows, footer and content after it, in the positioned,
positioned-in-flow and flowed placements; a non-interactive long qty[1]
keeps its lines inside its own row. Both fail with the index forced back
to 0 (38.5pt rows).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK
This commit is contained in:
mtlcl410andClaude Opus 5.5 committed 2026-10-29 21:56:26 +08:00
1 parent 5172a1b7ea
commit 0d0162bf81
3 files changed
+112 -16

No files matched your search

+4 -3
View File
@@ -273,6 +273,8 @@ impl Layouter<'_> {
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();
// Each field is measured under the SOM index it is drawn at.
let sibs = SomIndices::of(&nodes);
let placed: Vec<(usize, Rect)> = match g.layout {
Layout::Tb => {
let mut y = 0.0;
@@ -281,7 +283,7 @@ impl Layouter<'_> {
if !k.common().presence.occupies() {
continue;
}
let (w, h) = self.size_of(k, inner.w, &ctx, depth + 1);
let (w, h) = self.size_at(k, inner.w, &ctx, depth + 1, sibs.at(i));
out.push((i, Rect::new(0.0, y, w, h)));
y += h;
}
@@ -293,14 +295,13 @@ impl Layouter<'_> {
.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 (w, h) = self.size_at(k, (inner.w - c.x.unwrap_or(0.0)).max(1.0), &ctx, depth + 1, sibs.at(i));
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());
+24 -13
View File
@@ -6,11 +6,6 @@ use super::{
};
impl Layouter<'_> {
/// [`Self::size_at`] for the first instance of `node` (one that doesn't repeat).
pub(super) fn size_of(&self, node: &Node, avail_w: f64, ctx: &Ctx, depth: usize) -> (f64, f64) {
self.size_at(node, avail_w, ctx, depth, 0)
}
/// The width and height instance `index` of `node` under `ctx` takes in `avail_w`: its own
/// size, else what its content needs (containers are measured once per width, depth and
/// instance, and measuring stops at its caps). Instances differ when the data gives each its
@@ -156,20 +151,24 @@ impl Layouter<'_> {
Node::Area(a) => (&a.children, Layout::Positioned),
_ => return avail_w,
};
let visible = children.iter().filter(|k| k.common().presence.occupies());
// Each child under the SOM index it is drawn at.
let sibs = SomIndices::of(children);
let visible = children.iter().enumerate().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();
let sum: f64 = visible.map(|(i, k)| self.size_at(k, avail_w, ctx, depth + 1, sibs.at(i)).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);
.map(|(i, k)| {
let (w, h) = self.size_at(k, avail_w, ctx, depth + 1, sibs.at(i));
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),
Layout::Tb | Layout::Table => {
visible.map(|(i, k)| self.size_at(k, avail_w, ctx, depth + 1, sibs.at(i)).0).fold(0.0, f64::max).min(avail_w)
}
}
}
@@ -308,7 +307,15 @@ impl Layouter<'_> {
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::Tb => {
let sibs = SomIndices::of(&nodes);
nodes
.iter()
.enumerate()
.filter(|(_, k)| k.common().presence.occupies())
.map(|(i, k)| self.size_at(k, inner_w, ctx, depth + 1, sibs.at(i)).1)
.sum()
}
Layout::Positioned => self.positioned_height(&nodes, inner_w, ctx, depth),
_ => self.packed_height(&nodes, inner_w, ctx, depth),
}
@@ -364,20 +371,24 @@ impl Layouter<'_> {
self.pack(children, inner_w, ctx, depth).1
}
/// Cell rectangles of a row (relative to the row's inner box) and the row height.
/// Cell rectangles of a row (relative to the row's inner box) and the row height. Each cell
/// is measured under its own SOM index among its same-named siblings (`qty[1]`, not
/// `qty[0]`), the index [`Self::draw_row`] draws it at, so a cell that grows with its value
/// takes its own value's 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;
let sibs = SomIndices::of(&row.children);
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);
let (_, ch) = self.size_at(k, w, &ctx, depth + 1, sibs.at(i));
out.push((i, Rect::new(x, 0.0, w, ch)));
h = h.max(ch);
x += w;
+84
View File
@@ -279,3 +279,87 @@ fn each_row_instance_takes_the_height_of_its_own_growing_text() {
}
}
}
/// A multi-line `qty` text field 100pt wide that grows with its text, with `access` and
/// template value `value`.
fn qty_cell(access: &str, value: &str) -> String {
format!(
r#"<field name="qty" w="100pt" minH="20pt" access="{access}"><ui><textEdit multiLine="1"/></ui><value><text>{value}</text></value></field>"#
)
}
/// [`table_form`] with two same-named cells per `item` row, `qty[0]` and `qty[1]` (`first` and
/// `second`), in two 100pt columns, and the data of row `i` holding `data[i]`.
fn same_name_cells_form(place: Place, first: &str, second: &str, data: [&str; 3]) -> String {
let mut xml = table_form(place, r#"min="1" max="-1""#, 3)
.replace(r#"columnWidths="200pt""#, r#"columnWidths="100pt 100pt""#)
.replace(r#"<field name="qty" w="200pt" h="20pt"><ui><textEdit/></ui></field>"#, &format!("{first}{second}"));
for (i, d) in data.iter().enumerate() {
xml = xml.replace(&format!("<item><qty>row {}</qty></item>", i + 1), &format!("<item>{d}</item>"));
}
xml
}
#[test]
fn a_row_is_as_tall_as_its_tallest_same_named_cell() {
// Each cell of a row is measured under its own SOM index among its same-named siblings,
// the index it is drawn and bound at (XFA 3.3, "Tables", p. 327; "Growable Containers",
// p. 271; Chapter 9, p. 336): a short `qty[0]` beside a long `qty[1]` makes the row as tall
// as the long one, and the rows and content after it start below it.
let long = "a long value that wraps over several lines of the cell".repeat(6);
let cell = qty_cell("open", "");
for place in [Place::Positioned, Place::PositionedFlow, Place::Flowed] {
let data = ["<qty>s1</qty><qty>t1</qty>", &format!("<qty>s2</qty><qty>{long}</qty>"), "<qty>s3</qty><qty>t3</qty>"];
let form = layout_xml(&same_name_cells_form(place, &cell, &cell, data)).unwrap();
let all = laid_out(&form);
let first = named(&all, "qty");
let second: Vec<&Laid> = all.iter().filter(|w| w.1.ends_with(".qty[1]")).collect();
assert_eq!(first.iter().map(|w| w.2.clone().unwrap_or_default()).collect::<Vec<_>>(), ["s1", "s2", "s3"], "{place:?}");
assert_eq!(second.len(), 3, "{place:?}");
assert_eq!(second[1].2.as_deref().map(str::trim), Some(long.trim()), "{place:?}");
let tall = second[1].3;
assert!(tall.h > 60.0, "{place:?}: the long cell {tall:?} did not grow");
// Both cells of the row take its height, and the next row starts below it.
assert!((first[1].3.h - tall.h).abs() < 0.01, "{place:?}: qty[0] {:?} beside qty[1] {tall:?}", first[1].3);
for cells in [&first, &second] {
for pair in cells.windows(2) {
let (a, b) = (pair[0].3, pair[1].3);
assert!((b.y - a.bottom()).abs() < 0.01, "{place:?}: row at {b:?} after {a:?}");
}
}
let foot = named(&all, "total")[0].3;
assert!((foot.y - second[2].3.bottom()).abs() < 0.01, "{place:?}: footer {foot:?} after {:?}", second[2].3);
if let Some(after) = named(&all, "after").first() {
assert!((after.3.y - foot.bottom()).abs() < 0.01, "{place:?}: after {:?}, footer {foot:?}", after.3);
}
}
}
#[test]
fn a_long_non_interactive_same_named_cell_stays_in_its_row() {
// A non-interactive `qty[1]` is drawn as text, not as a widget; measured under its own SOM
// index (where the data holds nothing, so its long template value is what shows), each row
// holds its lines, and none run into the row after it or the footer.
let long = "longword ".repeat(12);
for place in [Place::Positioned, Place::PositionedFlow, Place::Flowed] {
let data = ["<qty>s1</qty>", "<qty>s2</qty>", "<qty>s3</qty>"];
let form = layout_xml(&same_name_cells_form(place, &qty_cell("open", ""), &qty_cell("nonInteractive", &long), data)).unwrap();
let all = laid_out(&form);
let rows: Vec<Rect> = named(&all, "qty").iter().map(|w| w.3).collect();
assert_eq!(rows.len(), 3, "{place:?}");
let lines: Vec<f64> = form
.pages
.iter()
.flat_map(|p| p.items.iter())
.filter_map(|it| if let Item::Text(s) = it { Some(s) } else { None })
.filter(|s| s.text.contains("longword"))
.map(|s| s.baseline)
.collect();
assert!(lines.len() >= 6, "{place:?}: {lines:?}");
let per_row: Vec<usize> = rows.iter().map(|r| lines.iter().filter(|&&b| b > r.y && b <= r.bottom()).count()).collect();
assert_eq!(per_row.iter().sum::<usize>(), lines.len(), "{place:?}: lines {lines:?} outside rows {rows:?}");
assert!(per_row.iter().all(|&n| n == per_row[0] && n > 1), "{place:?}: lines per row {per_row:?}");
let foot = named(&all, "total")[0].3;
assert!((foot.y - rows[2].bottom()).abs() < 0.01, "{place:?}: footer {foot:?} after {:?}", rows[2]);
}
}