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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Pq7HyCfwzyRXj4VfQAVVdv
This commit is contained in:
mtlcl410andClaude Opus 5.5 committed 2026-11-02 15:15:33 +08:00
1 parent f21fe84490
commit 31db4dbbfc
3 files changed
+84 -9

No files matched your search

+21 -6
View File
@@ -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<Item> = 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;
}
}
+6 -3
View File
@@ -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
+57
View File
@@ -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#"<subform name="s" layout="tb" w="7.5in"><field {name} w="2in" {min_h}><ui><textEdit multiLine="1"/></ui><value><text>{}</text></value></field><field name="after" w="2in" h="0.3in"/></subform>"#,
"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#"<field name="f{i}" w="7in" h="1in"/>"#)).collect();
let body = format!(
r#"<subform name="box" layout="tb" w="7.5in"><border><edge/><fill><color value="230,230,230"/></fill></border><margin topInset="0.1in" bottomInset="0.2in"/><field name="tall" w="7in" h="6in"/><subform name="s" layout="lr-tb" w="7.5in">{rows}</subform></subform>"#
);
let form = layout_xml(&one_area_form(r#"layout="tb""#, &body)).unwrap();
let rows_on: Vec<usize> =
(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<Rect> {
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 {