From ab2db45dfcd4217343c295fcfcbcea4288a377b8 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Thu, 29 Oct 2026 13:34:40 +0800 Subject: [PATCH 1/4] M12: an XFA subformSet is transparent to layout, so its members sit where their container puts them A subformSet was always laid out top to bottom. In a positioned subform its members, which have no x or y of their own, were stacked instead of overlapping at their positions, so a two-column section pushed its second column past the page (XFA 3.3 p. 357: "The layout process treats subformSet objects as transparent"). A set now takes its container's layout; in a table or a row its members still go one under another. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01CBVyQEwtmEw3mS1MoAiwrK --- crates/xfa/src/parse.rs | 27 +++++++++++++++------------ crates/xfa/src/tests.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index 838d1730ea..b1f4f36e04 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -602,7 +602,7 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec, 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 `` 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))); @@ -618,15 +618,16 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec, depth: usize) -> PageS PageSet { areas } } -fn parse_subform(n: XmlNode, warnings: &mut Vec, depth: usize) -> Subform { +fn parse_subform(n: XmlNode, warnings: &mut Vec, depth: usize, set_layout: Option) -> Subform { let common = parse_common(n); + 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); } } @@ -637,7 +638,7 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec, depth: usize) -> Subfor let break_to_page = |name: &str| children(n, name).any(|b| b.attribute("targetType").is_none_or(|t| t == "pageArea" || t == "contentArea")); Subform { common, - layout: Layout::parse(n.attribute("layout")), + layout, children: children_out, scripts: parse_events(n), page_set, @@ -650,21 +651,23 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec, depth: usize) -> Subfor } } -fn parse_node(c: XmlNode, warnings: &mut Vec, depth: usize) -> Option { +/// `parent`: the layout of the container `c` is in, which a `subformSet` takes on. +fn parse_node(c: XmlNode, warnings: &mut Vec, depth: usize, parent: Layout) -> Option { 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 }; + Node::Subform(Box::new(parse_subform(c, warnings, depth, Some(layout)))) } "area" => Node::Area(Box::new(Area { common: parse_common(c), - 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), value: parse_value(child(c, "value"), warnings) })), @@ -742,7 +745,7 @@ pub fn parse(xml: &str) -> Result<(Template, Vec), XfaError> { }; let mut warnings = Vec::new(); 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)); diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index e424be5441..5383e98b41 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -4145,3 +4145,29 @@ fn clear_password_values_clears_static_xfa_password_fields() { let packets = read_packets(&doc).unwrap().unwrap(); assert!(!packets.xdp.contains("hunter2"), "{}", packets.xdp); } + +/// 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#""# + ) + }; + 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"); +} From a3382ca1f91769d0795f18a60b0713c3c43eb668 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Fri, 30 Oct 2026 19:34:56 +0800 Subject: [PATCH 2/4] M12: an XFA subformSet's members flow among their lr-tb parent's other children In a flowing (lr-tb/rl-tb) parent the members of a subformSet now go on along the parent's line with its other children, rather than the set being packed as one block that moves to the next line whole (XFA 3.3 p. 357: the set is transparent to layout). The members keep the set in their SOM paths. Adds a test that, in a table and in one of its rows, a set's members still go one under another (review). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv --- crates/xfa/src/layout/draw.rs | 21 ++++++++-- crates/xfa/src/layout/measure.rs | 70 ++++++++++++++++++++++++-------- crates/xfa/src/model.rs | 3 ++ crates/xfa/src/parse.rs | 5 ++- crates/xfa/src/tests.rs | 53 ++++++++++++++++++++++++ 5 files changed, 132 insertions(+), 20 deletions(-) diff --git a/crates/xfa/src/layout/draw.rs b/crates/xfa/src/layout/draw.rs index f6520c1628..551a265597 100644 --- a/crates/xfa/src/layout/draw.rs +++ b/crates/xfa/src/layout/draw.rs @@ -108,10 +108,25 @@ impl Layouter<'_> { // 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) { + 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 sib = SomIndices::of(nodes).at(i) + 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)?; } } } diff --git a/crates/xfa/src/layout/measure.rs b/crates/xfa/src/layout/measure.rs index 2fffb3dfa0..0a2b6b2f94 100644 --- a/crates/xfa/src/layout/measure.rs +++ b/crates/xfa/src/layout/measure.rs @@ -1,10 +1,14 @@ use super::common::clamp_opt; use super::field::auto_check_reserve; use super::{ - Access, Area, Ctx, ExclGroup, Face, Field, Font, Layout, Layouter, MAX_DEPTH, MAX_MEASURES, Measured, Node, Para, Placement, Rect, Subform, Ui, - Value, data_path, text, + Access, Area, Ctx, ExclGroup, Face, Field, Font, Layout, Layouter, MAX_DEPTH, MAX_MEASURES, Measured, Node, Para, Placement, Rect, SomIndices, + 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 `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). @@ -302,35 +306,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, f64) { let mut out = Vec::new(); - let (mut x, mut y, mut line_h) = (0.0, 0.0, 0.0); + 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, + ) { + if depth > MAX_DEPTH { + return; + } + let sibs = SomIndices::of(children); 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) + 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; + } 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; + 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 { diff --git a/crates/xfa/src/model.rs b/crates/xfa/src/model.rs index 0fac8db18e..df7b948088 100644 --- a/crates/xfa/src/model.rs +++ b/crates/xfa/src/model.rs @@ -428,6 +428,9 @@ pub struct Subform { /// ``: 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, /// ``: the named child (a header row) is repeated after a page break. diff --git a/crates/xfa/src/parse.rs b/crates/xfa/src/parse.rs index b1f4f36e04..e20ff3b5b1 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -646,6 +646,7 @@ fn parse_subform(n: XmlNode, warnings: &mut Vec, depth: usize, set_layou 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()), } @@ -663,7 +664,9 @@ fn parse_node(c: XmlNode, warnings: &mut Vec, depth: usize, parent: Layo // table or a row they go one under another. "subformSet" => { let layout = if matches!(parent, Layout::Table | Layout::Row) { Layout::Tb } else { parent }; - Node::Subform(Box::new(parse_subform(c, warnings, depth, Some(layout)))) + 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), diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 5383e98b41..52304dfded 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -4171,3 +4171,56 @@ fn a_subform_set_in_a_positioned_subform_places_its_members_where_they_are() { 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#" + + + + + +"#; + 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#" + + + "# + ) + }; + let body = format!( + r#" + {} + {} +"#, + 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})"); + } +} From 89b0a5bec89c76371b45c49a3fd3d10e15a191d6 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Fri, 30 Oct 2026 20:02:37 +0800 Subject: [PATCH 3/4] M12: an lr-tb XFA subform finds its children's SOM indices once, not per placed child The subform's own sibling indices are reused for the first step of each placed child, and a subformSet's are built once per set (review). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv --- crates/xfa/src/layout/draw.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/xfa/src/layout/draw.rs b/crates/xfa/src/layout/draw.rs index 551a265597..81ee8ceb8d 100644 --- a/crates/xfa/src/layout/draw.rs +++ b/crates/xfa/src/layout/draw.rs @@ -108,13 +108,21 @@ impl Layouter<'_> { // 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); + // 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 = 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 sib = SomIndices::of(nodes).at(i) + instance; + 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); From 83f21712004524d13b29e4a379aac365401b7074 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Sun, 1 Nov 2026 15:04:16 +0800 Subject: [PATCH 4/4] M12: tests for nested, empty and repeating XFA subformSets Nested sets in lr-tb and positioned subforms, an empty set (and one holding only an empty one) in lr-tb, positioned and tb subforms, a repeating member of a set, and a repeating set. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv --- crates/xfa/src/tests.rs | 119 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 119 insertions(+) diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 52304dfded..919eb0df2a 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -4224,3 +4224,122 @@ fn a_subform_set_in_a_table_or_a_row_puts_its_members_one_under_another() { 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#" + + + + + + + + +"#; + 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#" + + + + + + +"#; + 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#" + + + + +"# + ); + 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#" + + + + +"#; + 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#" + + + + +"#; + 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}"); + } +}