diff --git a/crates/xfa/src/layout/draw.rs b/crates/xfa/src/layout/draw.rs index 69309aa0e0..1f31d74f72 100644 --- a/crates/xfa/src/layout/draw.rs +++ b/crates/xfa/src/layout/draw.rs @@ -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 = 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()); diff --git a/crates/xfa/src/layout/measure.rs b/crates/xfa/src/layout/measure.rs index 78c1ca10ec..551472bae2 100644 --- a/crates/xfa/src/layout/measure.rs +++ b/crates/xfa/src/layout/measure.rs @@ -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 = 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::() } 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; diff --git a/crates/xfa/src/table_repeat_tests.rs b/crates/xfa/src/table_repeat_tests.rs index 68c36023f3..11acfb66c0 100644 --- a/crates/xfa/src/table_repeat_tests.rs +++ b/crates/xfa/src/table_repeat_tests.rs @@ -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#"{value}"# + ) +} + +/// [`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#""#, &format!("{first}{second}")); + for (i, d) in data.iter().enumerate() { + xml = xml.replace(&format!("row {}", i + 1), &format!("{d}")); + } + 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 = ["s1t1", &format!("s2{long}"), "s3t3"]; + 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::>(), ["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 = ["s1", "s2", "s3"]; + 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 = named(&all, "qty").iter().map(|w| w.3).collect(); + assert_eq!(rows.len(), 3, "{place:?}"); + let lines: Vec = 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 = rows.iter().map(|r| lines.iter().filter(|&&b| b > r.y && b <= r.bottom()).count()).collect(); + assert_eq!(per_row.iter().sum::(), 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]); + } +}