M12: a non-interactive XFA field is measured at its text's horizontal scale

The layout draws a non-interactive field's text itself, condensed, but
measured its height at full width, so a condensed value reserved lines
it didn't use. Fields now measure and draw their text in one face:
scaled when the layout draws it, as designed when the forms layer does
(the over-page fallback included).
This commit is contained in:
Eric Kaden
2026-10-19 14:42:37 -04:00
parent 162a891673
commit dd782bca1d
2 changed files with 43 additions and 2 deletions
+12 -2
View File
@@ -1062,6 +1062,14 @@ impl Layouter<'_> {
}
}
/// The face a field's text is laid out in: at its horizontal scale when the layout draws
/// the text itself (a non-interactive field), as designed when the forms layer draws it in
/// a widget, whose appearances have no horizontal scaling.
fn field_text_face(f: &Field, font: &Font) -> Face {
let face = Face::of(font);
if f.access == Access::NonInteractive { face } else { face.unscaled() }
}
/// 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.
@@ -1078,7 +1086,7 @@ impl Layouter<'_> {
};
let w = (inner_w - beside - f.ui_margin.horizontal() - 4.0).max(1.0);
let font = ctx.font.apply(&f.common.font);
text::measure_height(&text::plain(value.trim_end()), w, &ctx.para, &Face::of(&font).unscaled(), &|_| String::new())
text::measure_height(&text::plain(value.trim_end()), w, &ctx.para, &Self::field_text_face(f, &font), &|_| String::new())
}
fn group_height(&self, g: &ExclGroup, inner_w: f64, ctx: &Ctx, depth: usize) -> f64 {
@@ -1583,7 +1591,9 @@ impl Layouter<'_> {
right: f.ui_margin.right + 2.0,
};
let para = Para { v_align: VAlign::Top, ..fctx.para.clone() };
let block = text::layout(&text::plain(value.trim_end()), ui.inset(&m), &para, &Face::of(&fctx.font), &|_| String::new());
// In the face it was measured in, so it takes the lines it was given.
let face = Self::field_text_face(f, &fctx.font);
let block = text::layout(&text::plain(value.trim_end()), ui.inset(&m), &para, &face, &|_| String::new());
return self.push_block(block);
}
}
+31
View File
@@ -80,6 +80,37 @@ fn horizontally_scaled_text_is_measured_and_drawn_narrower() {
assert!((half.face.width("half") - full.face.width("half") / 2.0).abs() < 1e-9);
}
#[test]
fn a_non_interactive_field_is_measured_at_the_scale_its_text_is_drawn() {
// A growing, non-interactive text field (drawn as text by the layout, not as a widget),
// then a draw below it. However condensed the field's text is, the draw sits the same
// distance below its last line: the field is as tall as the lines it shows.
// Long enough to take more than a multi-line field's three lines at either scale.
const TEXT: &str = "Remarks that run over several lines of a narrow box, and then over several more lines again, until the box has grown well past the three lines it starts with";
let gap = |scale: u32| {
let xdp = format!(
r##"<xdp:xdp xmlns:xdp="http://ns.adobe.com/xdp/"><template xmlns="http://www.xfa.org/schema/xfa-template/3.3/">
<subform name="form" layout="tb">
<pageSet><pageArea name="front"><contentArea x="0.5in" y="0.5in" w="7.5in" h="10in"/><medium short="8.5in" long="11in"/></pageArea></pageSet>
<field name="note" w="100pt" access="nonInteractive"><ui><textEdit multiLine="1"/></ui><value><text>{TEXT}</text></value><font typeface="Arial" size="10pt" fontHorizontalScale="{scale}%"/></field>
<draw name="after" w="100pt" h="20pt"><value><text>AFTER</text></value><font typeface="Arial" size="10pt"/></draw>
</subform>
</template></xdp:xdp>"##
);
let form = layout_xml(&xdp).unwrap();
let spans: Vec<&text::Span> = form.pages[0].items.iter().filter_map(|i| if let Item::Text(s) = i { Some(s) } else { None }).collect();
let after = spans.iter().find(|s| s.text.contains("AFTER")).expect("the draw").baseline;
let lines: std::collections::BTreeSet<i64> =
spans.iter().filter(|s| !s.text.contains("AFTER")).map(|s| (s.baseline * 100.0).round() as i64).collect();
let last = *lines.iter().next_back().expect("the field's lines") as f64 / 100.0;
(lines.len(), after - last)
};
let (full_lines, full_gap) = gap(100);
let (narrow_lines, narrow_gap) = gap(60);
assert!(narrow_lines < full_lines, "condensed, the text takes fewer lines: {narrow_lines} vs {full_lines}");
assert!((narrow_gap - full_gap).abs() < 0.01, "the field shrinks with its text: gap {narrow_gap} vs {full_gap}");
}
#[test]
fn horizontally_scaled_text_is_written_with_tz_inside_its_own_graphics_state() {
let (xdp, _) = scaled_text_template();