Merge #5369: M12: an XFA subformSet is transparent to layout, so its members sit where their container puts them (#5366)

# Conflicts:
#	crates/xfa/src/layout/measure.rs
#	crates/xfa/src/parse.rs
#	crates/xfa/src/tests.rs
This commit is contained in:
bflatastic committed 2026-11-09 18:39:26 +07:00
commit 9e55f61de0
5 files changed
+299 -31

No files matched your search

+26 -3
View File
@@ -163,10 +163,33 @@ impl Layouter<'_> {
self.laid_out_instances(k, &ctx.som);
}
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) {
// The sibling indices of each `subformSet` the steps go into, found once per
// set (known by where its children are); the subform's own are `sibs`.
let mut set_sibs: std::collections::HashMap<usize, SomIndices> = std::collections::HashMap::new();
for (at, r) in placed {
// Down the steps: a member of a `subformSet` takes the set's SOM path.
let (mut nodes, mut node_ctx, mut node_depth) = (s.children.as_slice(), ctx.clone(), depth + 1);
let mut target = None;
for (step, &(i, instance)) in at.iter().enumerate() {
let Some(k) = nodes.get(i) else { break };
let index = if step == 0 {
sibs.at(i)
} else {
set_sibs.entry(nodes.as_ptr() as usize).or_insert_with(|| SomIndices::of(nodes)).at(i)
};
let sib = index + instance;
match k {
Node::Subform(set) if set.set && step + 1 < at.len() => {
node_ctx = node_ctx.with_som(&set.common, sib);
nodes = &set.children;
node_depth += 1;
}
_ => target = Some((k, sib)),
}
}
if let Some((k, sib)) = target {
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)?;
self.draw_node(k, Rect::new(x, inner.y + r.y, r.w, r.h), &node_ctx, node_depth, sib)?;
}
}
}
+51 -14
View File
@@ -5,6 +5,10 @@ use super::{
Subform, Ui, Value, data_path, text,
};
/// Where [`Layouter::pack_instances`] puts one object: the (child index, instance) steps to it, and
/// its place in the packed container's box.
pub(super) type Packed = (Vec<(usize, usize)>, Rect);
impl Layouter<'_> {
/// 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
@@ -370,36 +374,69 @@ impl Layouter<'_> {
.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) {
/// Left-to-right packing: rows of children, wrapped at the available width, as (steps, place).
/// The steps are (child index, instance) from the packed container down to the object: one
/// step for a child, more for a member of a `subformSet`, which is transparent to layout (XFA
/// 3.3 p. 357), so its members go on among the container's other children. 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<Packed>, f64) {
let mut out = Vec::new();
let mut line = (0.0, 0.0, 0.0);
self.pack_into(children, inner_w, ctx, depth, som, &[], &mut line, &mut out);
(out, line.1 + line.2)
}
/// [`Self::pack_instances`] from `line` (x, y, the line's height so far), the steps to
/// `children` being `path`.
#[allow(clippy::too_many_arguments)]
fn pack_into(
&self,
children: &[Node],
inner_w: f64,
ctx: &Ctx,
depth: usize,
som: &str,
path: &[(usize, usize)],
line: &mut (f64, f64, f64),
out: &mut Vec<Packed>,
) {
if depth > MAX_DEPTH {
return;
}
let sibs = SomIndices::of(children);
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;
}
if let Node::Subform(s) = k
&& s.set
{
for instance in 0..self.instances(k, som) {
let set_ctx = ctx.with_som(&s.common, sibs.at(i).saturating_add(instance));
let at: Vec<(usize, usize)> = path.iter().copied().chain([(i, instance)]).collect();
self.pack_into(&s.children, inner_w, &set_ctx, depth + 1, &set_ctx.som, &at, line, out);
}
continue;
}
for instance in 0..self.instances(k, som) {
let (w, h) = self.size_at(k, inner_w, ctx, depth + 1, sibs.at(i).saturating_add(instance));
if x > 0.0 && x + w > inner_w + 0.05 {
y += line_h;
x = 0.0;
line_h = 0.0;
let (x, y, line_h) = line;
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.push((path.iter().copied().chain([(i, instance)]).collect(), 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)
(placed.into_iter().filter_map(|(at, r)| at.first().map(|&(i, _)| (i, r))).collect(), h)
}
fn packed_height(&self, children: &[Node], inner_w: f64, ctx: &Ctx, depth: usize) -> f64 {
+3
View File
@@ -521,6 +521,9 @@ pub struct Subform {
/// `<breakBefore targetType="pageArea">`: start on a new page.
pub break_before_page: bool,
pub break_after_page: bool,
/// Parsed from a `subformSet`: transparent to layout (XFA 3.3 p. 357), so in a flowing
/// container its members are laid out among their parent's other children.
pub set: bool,
/// Tables: the width of each column.
pub column_widths: Vec<f64>,
/// `<overflow leader="…">`: the named child (a header row) is repeated after a page break.
+21 -14
View File
@@ -916,7 +916,7 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> PageS
if content.is_empty() {
content.push(Rect::new(0.0, 0.0, width, height));
}
let items = pa.children().filter(|c| c.is_element()).filter_map(|c| parse_node(c, warnings, depth + 1)).collect();
let items = pa.children().filter(|c| c.is_element()).filter_map(|c| parse_node(c, warnings, depth + 1, Layout::Positioned)).collect();
// A page area with no `<occur>` has no limit and no minimum (a subform's default of one
// would exhaust it, and demand a page for every master).
let occur = child(pa, "occur").map(|o| parse_occur(Some(o)));
@@ -933,16 +933,18 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> PageS
}
/// A `subform` element: its own attributes, its page set if it has one, and the nodes it holds,
/// `depth` levels down (for the nesting limit).
fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Subform {
/// `depth` levels down (for the nesting limit). `set_layout` is the layout a `subformSet` takes
/// on from its container, in place of its own.
fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize, set_layout: Option<Layout>) -> Subform {
let common = parse_common(n, warnings);
let layout = set_layout.unwrap_or_else(|| Layout::parse(n.attribute("layout")));
let mut children_out = Vec::new();
let mut page_set = None;
for c in n.children().filter(|c| c.is_element()) {
match c.tag_name().name() {
"pageSet" => page_set = Some(parse_page_set(c, warnings, depth + 1)),
_ => {
if let Some(node) = parse_node(c, warnings, depth + 1) {
if let Some(node) = parse_node(c, warnings, depth + 1, layout) {
children_out.push(node);
}
}
@@ -955,7 +957,7 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Subfor
let break_to_page = |name: &str| children(n, name).any(|b| b.attribute("targetType").is_some_and(|t| t == "pageArea" || t == "contentArea"));
Subform {
common,
layout: Layout::parse(n.attribute("layout")),
layout,
children: children_out,
scripts: parse_events(n),
page_set,
@@ -963,6 +965,7 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Subfor
break_before_page: break_to_page("breakBefore")
|| child(n, "break").is_some_and(|b| b.attribute("before").is_some_and(|v| v == "pageArea" || v == "contentArea")),
break_after_page: break_to_page("breakAfter"),
set: false,
column_widths,
overflow_leader: child(n, "overflow").and_then(|o| o.attribute("leader")).map(str::to_string).filter(|s| !s.is_empty()),
}
@@ -970,22 +973,26 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Subfor
/// The template object element `c` stands for (a subform, area, field, draw or exclusion group,
/// with what it holds), or `None` for anything else and past [`MAX_DEPTH`] levels. What can't be
/// read faithfully is noted in `warnings`.
fn parse_node(c: XmlNode, warnings: &mut Vec<String>, depth: usize) -> Option<Node> {
/// read faithfully is noted in `warnings`. `parent`: the layout of the container `c` is in, which
/// a `subformSet` takes on.
fn parse_node(c: XmlNode, warnings: &mut Vec<String>, depth: usize, parent: Layout) -> Option<Node> {
if depth > MAX_DEPTH {
return None;
}
Some(match c.tag_name().name() {
"subform" => Node::Subform(Box::new(parse_subform(c, warnings, depth))),
"subform" => Node::Subform(Box::new(parse_subform(c, warnings, depth, None))),
// A set of related subforms is transparent to layout (XFA 3.3 p. 357): its members are laid out
// as its container lays out its own children, at their own positions in a positioned one. In a
// table or a row they go one under another.
"subformSet" => {
// A set of alternative subforms: lay out its members in order.
let mut sf = parse_subform(c, warnings, depth);
sf.layout = Layout::Tb;
Node::Subform(Box::new(sf))
let layout = if matches!(parent, Layout::Table | Layout::Row) { Layout::Tb } else { parent };
let mut set = parse_subform(c, warnings, depth, Some(layout));
set.set = true;
Node::Subform(Box::new(set))
}
"area" => Node::Area(Box::new(Area {
common: parse_common(c, warnings),
children: c.children().filter(|k| k.is_element()).filter_map(|k| parse_node(k, warnings, depth + 1)).collect(),
children: c.children().filter(|k| k.is_element()).filter_map(|k| parse_node(k, warnings, depth + 1, Layout::Positioned)).collect(),
})),
"field" => Node::Field(Box::new(parse_field(c, warnings))),
"draw" => Node::Draw(Box::new(Draw { common: parse_common(c, warnings), value: parse_value(child(c, "value"), warnings) })),
@@ -1271,7 +1278,7 @@ pub fn parse(xml: &str) -> Result<(Template, Vec<String>), XfaError> {
None => template,
};
let sf = children(template, "subform").next().ok_or_else(|| XfaError::Malformed("the template has no root subform".into()))?;
let mut root_sf = parse_subform(sf, &mut warnings, 0);
let mut root_sf = parse_subform(sf, &mut warnings, 0, None);
if root_sf.page_set.is_none() {
// A page set may sit beside the root subform.
root_sf.page_set = child(template, "pageSet").map(|ps| parse_page_set(ps, &mut warnings, 0));
+198
View File
@@ -5405,6 +5405,204 @@ fn page_area_uses_follow_hidden_masters_and_shrink_with_multiple_areas() {
assert!(!uses.contains_key("master"), "removed pages leave no stale occurrence metadata: {uses:?}");
}
/// A `subformSet` is transparent to layout (XFA 3.3 p. 357): in a positioned subform its members
/// sit at their own positions, side by side as a public stamp-duty form's two columns do, not one
/// under the other; in a flowed one they still go one under another.
#[test]
fn a_subform_set_in_a_positioned_subform_places_its_members_where_they_are() {
let xml = |layout: &str| {
format!(
r#"<template xmlns="http://www.xfa.org/schema/xfa-template/3.3/"><subform name="form" layout="tb">
<pageSet><pageArea name="p"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
<subform name="box" w="7in" {layout}>
<subformSet name="cols">
<subform name="left" w="3in" h="2in"><draw name="a" x="0in" y="0.5in" w="2in" h="0.3in"><value><text>left</text></value></draw></subform>
<subform name="right" w="3in" h="2in"><draw name="b" x="4in" y="0.5in" w="2in" h="0.3in"><value><text>right</text></value></draw></subform>
</subformSet>
</subform>
</subform></template>"#
)
};
let baseline = |layout: &str, text: &str| {
let form = crate::layout::layout(&crate::parse(&xml(layout)).unwrap().0, None).unwrap();
form.pages[0].items.iter().find_map(|i| if let Item::Text(s) = i { (s.text == text).then_some(s.baseline) } else { None }).unwrap()
};
assert!((baseline(r#"h="3in""#, "left") - baseline(r#"h="3in""#, "right")).abs() < 0.01, "positioned: side by side");
assert!(baseline(r#"layout="tb""#, "right") - baseline(r#"layout="tb""#, "left") > 140.0, "flowed: one under the other");
}
/// In a flowing (`lr-tb`) subform a `subformSet`'s members go on among their parent's other
/// children (XFA 3.3 p. 357): beside a 1in field in a 2in row, the first 0.6in member takes the
/// rest of that line and the second wraps, rather than the set moving to the next line whole.
/// The members keep the set in their SOM paths.
#[test]
fn a_subform_set_in_an_lr_tb_subform_flows_its_members_with_their_siblings() {
let body = r#"<subform name="row" layout="lr-tb" w="2in">
<field name="a" w="1in" h="0.3in"><ui><textEdit/></ui></field>
<subformSet name="grp" relation="ordered">
<subform name="m1" w="0.6in" h="0.3in"><field name="x" w="0.6in" h="0.3in"><ui><textEdit/></ui></field></subform>
<subform name="m2" w="0.6in" h="0.3in"><field name="y" w="0.6in" h="0.3in"><ui><textEdit/></ui></field></subform>
</subformSet>
</subform>"#;
let form = layout_xml(&one_area_form(r#"layout="tb""#, body)).unwrap();
let w = widgets(&form.pages[0]);
let at = |name: &str| w.iter().find(|w| w.name == name).map(|w| (w.som.clone(), w.rect.x, w.rect.y)).unwrap();
let (_, ax, ay) = at("a");
let (xsom, xx, xy) = at("x");
let (ysom, yx, yy) = at("y");
assert!((xy - ay).abs() < 0.5 && (xx - (ax + 72.0)).abs() < 0.5, "the first member goes on beside a: a at ({ax}, {ay}), x at ({xx}, {xy})");
assert!(yy > ay + 1.0 && (yx - ax).abs() < 0.5, "the second wraps to the next line: y at ({yx}, {yy})");
assert!(xsom.ends_with("row[0].grp[0].m1[0].x[0]") && ysom.ends_with("row[0].grp[0].m2[0].y[0]"), "{xsom} {ysom}");
}
/// In a table, and in one of its rows, a `subformSet`'s members still go one under another: the
/// set lays them out top to bottom there rather than taking the table's or the row's layout.
#[test]
fn a_subform_set_in_a_table_or_a_row_puts_its_members_one_under_another() {
let set = |name: &str| {
format!(
r#"<subformSet name="grp">
<subform name="m1" w="2in" h="0.3in"><field name="{name}1" w="2in" h="0.3in"><ui><textEdit/></ui></field></subform>
<subform name="m2" w="2in" h="0.3in"><field name="{name}2" w="2in" h="0.3in"><ui><textEdit/></ui></field></subform>
</subformSet>"#
)
};
let body = format!(
r#"<subform name="t" layout="table" columnWidths="2in 2in">
{}
<subform name="r" layout="row"><field name="c" w="2in" h="0.3in"><ui><textEdit/></ui></field>{}</subform>
</subform>"#,
set("s"),
set("q")
);
let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap();
let w = widgets(&form.pages[0]);
let at = |name: &str| w.iter().find(|w| w.name == name).map(|w| (w.rect.x, w.rect.y)).unwrap();
for (first, second) in [("s1", "s2"), ("q1", "q2")] {
let ((x1, y1), (x2, y2)) = (at(first), at(second));
assert!((x1 - x2).abs() < 0.5 && y2 > y1 + 20.0, "{second} under {first}: ({x1}, {y1}) then ({x2}, {y2})");
}
}
/// The SOM path and place of each widget of the template's field `name`, in layout order, found
/// by its SOM path (a repeated field's PDF name is made up: `x_2`).
fn widgets_named(form: &Form, name: &str) -> Vec<(String, f64, f64)> {
let tail = format!(".{name}[0]");
widgets(&form.pages[0]).iter().filter(|w| w.som.ends_with(&tail)).map(|w| (w.som.clone(), w.rect.x, w.rect.y)).collect()
}
/// #5369 review: a `subformSet` inside a `subformSet` is transparent too: in an `lr-tb` row the
/// inner set's member goes on beside the field before the outer set, the outer set's own member
/// wraps, and the field after the sets follows it. Each member keeps both sets in its SOM path.
#[test]
fn nested_subform_sets_in_an_lr_tb_subform_flow_their_members_with_their_siblings() {
let body = r#"<subform name="row" layout="lr-tb" w="2in">
<field name="a" w="1in" h="0.3in"><ui><textEdit/></ui></field>
<subformSet name="outer">
<subformSet name="inner">
<subform name="m1" w="0.6in" h="0.3in"><field name="x" w="0.6in" h="0.3in"><ui><textEdit/></ui></field></subform>
</subformSet>
<subform name="m2" w="0.6in" h="0.3in"><field name="y" w="0.6in" h="0.3in"><ui><textEdit/></ui></field></subform>
</subformSet>
<field name="b" w="0.6in" h="0.3in"><ui><textEdit/></ui></field>
</subform>"#;
let form = layout_xml(&one_area_form(r#"layout="tb""#, body)).unwrap();
let at = |name: &str| widgets_named(&form, name).into_iter().next().unwrap();
let ((_, ax, ay), (xsom, xx, xy), (ysom, yx, yy), (_, bx, by)) = (at("a"), at("x"), at("y"), at("b"));
assert!((xy - ay).abs() < 0.5 && (xx - (ax + 72.0)).abs() < 0.5, "x goes on beside a: a ({ax}, {ay}), x ({xx}, {xy})");
assert!(yy > ay + 1.0 && (yx - ax).abs() < 0.5, "y wraps: ({yx}, {yy})");
assert!((by - yy).abs() < 0.5 && (bx - (yx + 43.2)).abs() < 0.5, "b follows y: ({bx}, {by})");
assert!(xsom.ends_with("row[0].outer[0].inner[0].m1[0].x[0]") && ysom.ends_with("row[0].outer[0].m2[0].y[0]"), "{xsom} {ysom}");
}
/// #5369 review: in a positioned subform, the members of a `subformSet` nested in another sit at
/// their own positions, side by side, as the members of one set do.
#[test]
fn nested_subform_sets_in_a_positioned_subform_place_their_members_where_they_are() {
let body = r#"<subform name="box" w="7in" h="3in">
<subformSet name="outer">
<subformSet name="inner">
<subform name="l" x="0in" y="0.5in" w="3in" h="1in"><field name="p" w="1in" h="0.3in"><ui><textEdit/></ui></field></subform>
</subformSet>
<subform name="r" x="4in" y="0.5in" w="3in" h="1in"><field name="q" w="1in" h="0.3in"><ui><textEdit/></ui></field></subform>
</subformSet>
</subform>"#;
let form = layout_xml(&one_area_form(r#"layout="tb""#, body)).unwrap();
let at = |name: &str| widgets_named(&form, name).into_iter().next().unwrap();
let ((psom, px, py), (_, qx, qy)) = (at("p"), at("q"));
assert!((py - qy).abs() < 0.5 && (qx - px - 288.0).abs() < 0.5, "side by side, 4in apart: p ({px}, {py}), q ({qx}, {qy})");
assert!(psom.ends_with("box[0].outer[0].inner[0].l[0].p[0]"), "{psom}");
}
/// #5369 review: an empty `subformSet`, and one holding only an empty one, takes no room: in an
/// `lr-tb` row the field after it goes on beside the field before it, and in a positioned or a
/// `tb` subform the layout goes on as if it weren't there.
#[test]
fn an_empty_subform_set_takes_no_room() {
for (attrs, after_a) in [(r#"layout="lr-tb" w="2in""#, (72.0, 0.0)), (r#"w="7in" h="2in""#, (0.0, 0.0)), (r#"layout="tb" w="7in""#, (0.0, 21.6))]
{
let body = format!(
r#"<subform name="row" {attrs}>
<field name="a" w="1in" h="0.3in"><ui><textEdit/></ui></field>
<subformSet name="none"/>
<subformSet name="outer"><subformSet name="inner"/></subformSet>
<field name="b" w="0.6in" h="0.3in"><ui><textEdit/></ui></field>
</subform>"#
);
let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap();
let at = |name: &str| widgets_named(&form, name).into_iter().next().unwrap();
let ((_, ax, ay), (_, bx, by)) = (at("a"), at("b"));
assert!((bx - ax - after_a.0).abs() < 0.5 && (by - ay - after_a.1).abs() < 0.5, "{attrs}: a ({ax}, {ay}), b ({bx}, {by})");
}
}
/// #5369 review: a repeating member of a `subformSet` flows instance by instance among its
/// parent's other children in an `lr-tb` row, as a repeating child of the row would: the first
/// instance beside the field before the set, the others wrapping, each with its own index.
#[test]
fn a_repeating_member_of_a_subform_set_flows_instance_by_instance() {
let body = r#"<subform name="row" layout="lr-tb" w="2in">
<field name="a" w="1in" h="0.3in"><ui><textEdit/></ui></field>
<subformSet name="grp">
<subform name="m" w="0.6in" h="0.3in"><occur initial="3" max="5"/><field name="x" w="0.6in" h="0.3in"><ui><textEdit/></ui></field></subform>
</subformSet>
</subform>"#;
let form = layout_xml(&one_area_form(r#"layout="tb""#, body)).unwrap();
let (_, ax, ay) = widgets_named(&form, "a").into_iter().next().unwrap();
let x = widgets_named(&form, "x");
assert_eq!(x.len(), 3, "{x:?}");
let places: Vec<(f64, f64)> = x.iter().map(|(_, px, py)| (px - ax, py - ay)).collect();
for ((got_x, got_y), (want_x, want_y)) in places.iter().zip([(72.0, 0.0), (0.0, 21.6), (43.2, 21.6)]) {
assert!((got_x - want_x).abs() < 0.5 && (got_y - want_y).abs() < 0.5, "{places:?}");
}
for (i, (som, _, _)) in x.iter().enumerate() {
assert!(som.ends_with(&format!("row[0].grp[0].m[{i}].x[0]")), "{som}");
}
}
/// #5369 review: a repeating `subformSet` lays its members out once per instance, each instance
/// flowing on from the last in an `lr-tb` row, and each member taking its set's instance index.
#[test]
fn a_repeating_subform_set_flows_each_instance_of_its_members() {
let body = r#"<subform name="row" layout="lr-tb" w="2in">
<field name="a" w="1in" h="0.3in"><ui><textEdit/></ui></field>
<subformSet name="grp"><occur initial="2" max="3"/>
<subform name="m" w="0.6in" h="0.3in"><field name="x" w="0.6in" h="0.3in"><ui><textEdit/></ui></field></subform>
</subformSet>
</subform>"#;
let form = layout_xml(&one_area_form(r#"layout="tb""#, body)).unwrap();
let (_, ax, ay) = widgets_named(&form, "a").into_iter().next().unwrap();
let x = widgets_named(&form, "x");
assert_eq!(x.len(), 2, "{x:?}");
let places: Vec<(f64, f64)> = x.iter().map(|(_, px, py)| (px - ax, py - ay)).collect();
for ((got_x, got_y), (want_x, want_y)) in places.iter().zip([(72.0, 0.0), (0.0, 21.6)]) {
assert!((got_x - want_x).abs() < 0.5 && (got_y - want_y).abs() < 0.5, "{places:?}");
}
for (i, (som, _, _)) in x.iter().enumerate() {
assert!(som.ends_with(&format!("row[0].grp[{i}].m[0].x[0]")), "{som}");
}
}
/// The pages of `form[0].last[0].f[0]` and of the field `after` that follows `last`, when `last`
/// holds `before` ahead of `f` and `after` behind it. A breakAfter acts after its subform, so the
/// page of `after` shows whether it broke.