From 31db4dbbfcd1225b646c0b59a5621d8ffe01c993 Mon Sep 17 00:00:00 2001 From: mtlcl410 <2368091+mtlcl410@users.noreply.github.com> Date: Mon, 2 Nov 2026 15:15:33 +0800 Subject: [PATCH] M12: a split row group keeps its section's border; an unnamed growable field grows A growable lr-tb/rl-tb subform split at a content area's bottom now breaks the flowed subforms round it there (break_frames), as a break between their children does, so a bordered or filled section keeps a closed part on each page instead of losing its border. An unnamed growable multi-line field is measured with its template text. A nameless field takes no part in data binding (XFA 3.3 p. 194), and its SOM context ends in its parent, so its data is not looked up. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv --- crates/xfa/src/layout/flow.rs | 27 +++++++++++---- crates/xfa/src/layout/measure.rs | 9 +++-- crates/xfa/src/tests.rs | 57 ++++++++++++++++++++++++++++++++ 3 files changed, 84 insertions(+), 9 deletions(-) diff --git a/crates/xfa/src/layout/flow.rs b/crates/xfa/src/layout/flow.rs index d83842c26b..cdbb461455 100644 --- a/crates/xfa/src/layout/flow.rs +++ b/crates/xfa/src/layout/flow.rs @@ -37,7 +37,7 @@ impl Layouter<'_> { self.place(h); return Ok(()); } - self.split_block(first, y, drawn) + self.split_block(first, y, drawn, flows_on) } /// Remember that the items the last page got since `start` are one field or draw at @@ -56,8 +56,11 @@ impl Layouter<'_> { /// Spread the items the last page holds from index `first` on (a block drawn from `top` to /// `end`, past the bottom of its content area) over this and the following content areas. /// Breaks fall between text, pictures and fields, never through one that fits in an area; - /// fills and lines are cut at the break. - fn split_block(&mut self, first: usize, top: f64, end: f64) -> Result<(), XfaError> { + /// fills and lines are cut at the break. A flowing subform (`flows_on`: one [`Self::flow_block`] + /// splits where it would otherwise move whole) breaks the flowed subforms round it there too + /// ([`Self::break_frames`]), so they keep their border; anything else just moves on, and + /// those subforms are drawn without theirs ([`Self::finish_frame`]). + fn split_block(&mut self, first: usize, top: f64, end: f64, flows_on: bool) -> Result<(), XfaError> { let items: Vec = match self.pages.last_mut() { Some(p) if first <= p.items.len() => p.items.split_off(first), _ => return Ok(()), @@ -139,9 +142,21 @@ impl Layouter<'_> { rest = below; seg_top = cut; first_segment = false; - self.next_area()?; - let (rect, _, _) = self.cursor(); - shift = cut - rect.y; + if flows_on { + // Each subform round it ends its part at the cut, after its bottom margin, and + // starts a new one after its top margin, where the rest goes on. A rest that fits + // an area has the top margins give way to it, as `make_room` does. + let (rect, _, _) = self.cursor(); + if let Some(c) = &mut self.cur { + c.y = (cut - shift).min(c.rect.bottom()).max(c.y); + } + let rest = (end - cut).max(0.0); + self.break_frames(if rest <= rect.h + 0.01 { rest } else { 0.0 }, false)?; + } else { + self.next_area()?; + } + let (rect, y, _) = self.cursor(); + shift = cut - if flows_on { y } else { rect.y }; dx = rect.x - origin_x; } } diff --git a/crates/xfa/src/layout/measure.rs b/crates/xfa/src/layout/measure.rs index 3e73884ff1..a6e9b48fd6 100644 --- a/crates/xfa/src/layout/measure.rs +++ b/crates/xfa/src/layout/measure.rs @@ -262,12 +262,15 @@ impl Layouter<'_> { /// The height the text of a multi-line text field takes (its value in the data, else the /// template's), for a field without a fixed height, which grows to hold its text. `ctx` - /// is the field's own (its SOM ends in the field). Zero for anything else. + /// is the field's own (its SOM ends in the field). Zero for anything else. A nameless field + /// takes no part in data binding (XFA 3.3 p. 194), and its `ctx` ends in its parent: its + /// text is the template's. fn grown_text_height(&self, f: &Field, inner_w: f64, ctx: &Ctx) -> f64 { - if !f.grows_with_text() || f.common.name.is_none() { + if !f.grows_with_text() { return 0.0; } - let value = self.data.and_then(|d| d.value_at(&data_path(self.tpl, &ctx.som))).filter(|t| !t.trim().is_empty()).or_else(|| f.value.plain()); + let bound = f.common.name.as_ref().and(self.data); + let value = bound.and_then(|d| d.value_at(&data_path(self.tpl, &ctx.som))).filter(|t| !t.trim().is_empty()).or_else(|| f.value.plain()); let Some(value) = value.filter(|t| !t.trim().is_empty()) else { return 0.0 }; // Beside a caption the text has what the caption leaves; a widget's text has 2pt of // padding on each side, a non-interactive field's (drawn as text) none, so it wraps diff --git a/crates/xfa/src/tests.rs b/crates/xfa/src/tests.rs index 36e169a435..4dca485fdf 100644 --- a/crates/xfa/src/tests.rs +++ b/crates/xfa/src/tests.rs @@ -4278,6 +4278,63 @@ fn table_rows_of_minimum_height_cells_fill_the_page_at_that_height() { assert_eq!(pages(r#"h="0.5in""#, 30).0, [20, 10]); } +/// An unnamed growable multi-line field grows to hold its text as a named one does, with a +/// minimum height or without. A nameless field takes no part in data binding (XFA 3.3 p. 194), +/// so the text it shows, and the layout measures, is the template's. +#[test] +fn an_unnamed_growable_field_grows_to_hold_its_text() { + let lay = |name: &str, min_h: &str| { + let body = format!( + r#"{}"#, + "word ".repeat(80) + ); + let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap(); + let ws = widgets(&form.pages[0]); + let field = ws.iter().find(|w| w.name != "after").map(|w| w.rect.h).unwrap(); + let after = ws.iter().find(|w| w.name == "after").map(|w| w.rect.y).unwrap(); + (field, after) + }; + for min_h in ["", r#"minH="0.3in""#] { + let named = lay(r#"name="a""#, min_h); + assert!(named.0 > 100.0, "the named field grows: {named:?}"); + let unnamed = lay("", min_h); + assert!( + (unnamed.0 - named.0).abs() < 0.01 && (unnamed.1 - named.1).abs() < 0.01, + "{min_h}: unnamed {unnamed:?}, named {named:?} (field height, next field's top)" + ); + } +} + +/// A growable `lr-tb` subform split at a page break inside a bordered, filled `tb` section breaks +/// that section with it, as a break between the section's children does: a closed part on each +/// page, its bottom margin ending the first and its top margin starting the second, no warning. +#[test] +fn a_split_lr_tb_subform_keeps_the_border_of_the_section_round_it() { + let rows: String = (0..6).map(|i| format!(r#""#)).collect(); + let body = format!( + r#"{rows}"# + ); + let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap(); + let rows_on: Vec = + (0..6).map(|i| form.pages.iter().position(|p| widgets(p).iter().any(|w| w.name == format!("f{i}"))).unwrap_or(9)).collect(); + // After 0.1in of margin and the 6in field, three 1in rows fit the 10in area. + assert_eq!(rows_on, [0, 0, 0, 1, 1, 1]); + let fills = |page: usize| -> Vec { + form.pages[page].items.iter().filter_map(|i| if let Item::Fill { rect, .. } = i { Some(*rect) } else { None }).collect() + }; + let lines = |page: usize| form.pages[page].items.iter().filter(|i| matches!(i, Item::Line { .. })).count(); + let (first, second) = (fills(0), fills(1)); + assert_eq!((first.len(), second.len()), (1, 1), "a part on each page: {:?}", form.warnings); + assert_eq!((lines(0), lines(1)), (4, 4), "each part's border is closed"); + // Page 1: the top margin, the field, three rows, the bottom margin. + assert!((first[0].y - 36.0).abs() < 0.01 && (first[0].bottom() - (36.0 + 7.2 + 432.0 + 216.0 + 14.4)).abs() < 0.01, "{:?}", first[0]); + // Page 2: the top margin again, the other three rows, the bottom margin. + assert!((second[0].y - 36.0).abs() < 0.01 && (second[0].bottom() - (36.0 + 7.2 + 216.0 + 14.4)).abs() < 0.01, "{:?}", second[0]); + let f3 = widgets(&form.pages[1]).into_iter().find(|w| w.name == "f3").map(|w| w.rect).unwrap(); + assert!((f3.y - 43.2).abs() < 0.01, "the second part starts after the top margin: {f3:?}"); + assert!(form.warnings.is_empty(), "{:?}", form.warnings); +} + /// A form with `draws` in a subform placed by position, on a letter page whose content area is a /// quarter inch in, with `master` on the page itself. fn form_of_draws(master: &str, draws: &str) -> String {