From be826a3bcd3bb24ff8916ecb82637785c11bdb96 Mon Sep 17 00:00:00 2001 From: aiongg Date: Sat, 24 Oct 2026 08:17:01 +0000 Subject: [PATCH] Integration: a short multi-line field accepts the line it draws (#4339) The fit check required the padded height for every line; it now uses the bound the appearance applies, so a 16 pt field at 12 pt takes one line. --- crates/forms/src/appearance.rs | 7 +++++-- crates/forms/src/tests.rs | 28 ++++++++++++++++++++++++++++ 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/crates/forms/src/appearance.rs b/crates/forms/src/appearance.rs index 15584ec77..3172fd7d6 100644 --- a/crates/forms/src/appearance.rs +++ b/crates/forms/src/appearance.rs @@ -130,8 +130,11 @@ impl TextBox { // The smallest an automatic size goes ([`auto_text_size_by`]). let size = self.size.unwrap_or(4.0); if self.multiline { - let lines = wrap_with(&shown, size, self.inner_w, width_of).len() as f64; - lines * size * 1.15 <= self.inner_h + 1e-6 + // As the appearance draws it: the first baseline is 0.85 em below the padded top and + // a line is drawn while its descent (0.207 em) stays inside the border, so the + // padding below the last line is not needed: `inner_h` plus 2 pt. + let above = wrap_with(&shown, size, self.inner_w, width_of).len().saturating_sub(1) as f64; + above * size * 1.15 + 1.057 * size <= self.inner_h + 2.0 + 0.01 } else { // A line break in a single-line value is drawn as a space. let line: String = shown.chars().map(|c| if matches!(c, '\n' | '\r') { ' ' } else { c }).collect(); diff --git a/crates/forms/src/tests.rs b/crates/forms/src/tests.rs index b36657293..8d0b331b7 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -439,6 +439,34 @@ fn a_text_field_says_what_fits_it_as_its_appearance_draws_it() { assert!(tb("comb").fits(&"W".repeat(50))); } +/// #4339 review: a multi-line field is full when its appearance stops drawing lines (a line is +/// drawn while its descent stays inside the border), not when `lines x size x 1.15` outgrows the +/// padded height: a 16 pt field at 12 pt draws one line. +#[test] +fn a_short_multiline_field_takes_the_one_line_it_draws() { + let tx = |name: &str, extra: &str, h: i32| { + format!("<< /Type /Annot /Subtype /Widget /FT /Tx /T ({name}) /DA (/Helv 12 Tf 0 g) /Ff {}{extra} /Rect [50 {} 150 700] /P 3 0 R >>", flags::MULTILINE, 700 - h) + }; + let objs: Vec = vec![ + "<< /Type /Catalog /Pages 2 0 R /AcroForm 4 0 R >>".into(), + "<< /Type /Pages /Kids [3 0 R] /Count 1 /MediaBox [0 0 600 800] >>".into(), + "<< /Type /Page /Parent 2 0 R /Annots [6 0 R 7 0 R] >>".into(), + "<< /Fields [6 0 R 7 0 R] /DA (/Helv 12 Tf 0 g) /DR << /Font << /Helv 5 0 R >> >> >>".into(), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(), + tx("plain", "", 16), + tx("boxed", " /MK << /BC [0] >>", 16), + ]; + let doc = document(&objs); + let all = fields(&doc); + let tb = |name: &str| { + let f = field(&all, name); + appearance::text_box(&doc, f, &f.widgets[0]) + }; + assert!(tb("plain").fits("one line") && !tb("plain").fits("one\ntwo")); + // A 1 pt border takes 1 pt from each side: the line's descent then falls outside it. + assert!(!tb("boxed").fits("one line")); +} + /// An edit to a field that doesn't scroll keeps as much of what it inserted as fits, wherever the /// caret was, and always keeps a deletion. #[test]