diff --git a/crates/xfa/src/layout/draw.rs b/crates/xfa/src/layout/draw.rs index b25e1ddb66..be53dc5aa9 100644 --- a/crates/xfa/src/layout/draw.rs +++ b/crates/xfa/src/layout/draw.rs @@ -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 = 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)?; } } } diff --git a/crates/xfa/src/layout/measure.rs b/crates/xfa/src/layout/measure.rs index 6c7e7a8f32..78f2132530 100644 --- a/crates/xfa/src/layout/measure.rs +++ b/crates/xfa/src/layout/measure.rs @@ -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, 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, + ) { + 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 { diff --git a/crates/xfa/src/model.rs b/crates/xfa/src/model.rs index daac3b97ca..9595446614 100644 --- a/crates/xfa/src/model.rs +++ b/crates/xfa/src/model.rs @@ -521,6 +521,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 4541e825e5..4bda2947c8 100644 --- a/crates/xfa/src/parse.rs +++ b/crates/xfa/src/parse.rs @@ -916,7 +916,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))); @@ -933,16 +933,18 @@ fn parse_page_set(n: XmlNode, warnings: &mut Vec, 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, 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, depth: usize, set_layout: Option) -> 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, 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, 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, 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, depth: usize) -> Option { +/// 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, 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 }; + 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), 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)); diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 89d603ce83..381146b9c8 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -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#""# + ) + }; + 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#" + + + + + +"#; + 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})"); + } +} + +/// 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}"); + } +} + /// 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.