From b6b0bbab08b3ad3aef07cb0d911fc4255808ec7e Mon Sep 17 00:00:00 2001 From: aiongg <67543387+aiongg@users.noreply.github.com> Date: Sat, 24 Oct 2026 09:37:35 -0400 Subject: [PATCH] fix(forms): a multiline field rejects a glyph wider than the field (#4447) TextBox::fits checked only the height of a wrapped multi-line value. Long words break by character, so a single character wider than the box still overflows it, and the appearance reports Overflow::Wide for a value fits had accepted. The multi-line branch now also requires every wrapped line to fit the width. --- crates/forms/src/appearance.rs | 7 +++++-- crates/forms/src/tests.rs | 26 ++++++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/crates/forms/src/appearance.rs b/crates/forms/src/appearance.rs index 3172fd7d6c..da25c211c0 100644 --- a/crates/forms/src/appearance.rs +++ b/crates/forms/src/appearance.rs @@ -133,8 +133,11 @@ impl TextBox { // 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 + // Long words break by character, so a line is wider than the box only when it is a + // single character wider than the box, which the appearance still cuts at the sides. + let lines = wrap_with(&shown, size, self.inner_w, width_of); + let above = lines.len().saturating_sub(1) as f64; + above * size * 1.15 + 1.057 * size <= self.inner_h + 2.0 + 0.01 && lines.iter().all(|l| width_of(l, size) <= self.inner_w + 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 9af1bc5732..54bc137abd 100644 --- a/crates/forms/src/tests.rs +++ b/crates/forms/src/tests.rs @@ -471,6 +471,32 @@ fn a_short_multiline_field_takes_the_one_line_it_draws() { assert!(!tb("boxed").fits("one line")); } +/// A multi-line field narrower than one glyph: wrapping can't break inside a character, so the +/// appearance cuts that character at the sides (`Overflow::Wide`), and `fits` must say so too. +#[test] +fn a_multiline_field_rejects_a_glyph_wider_than_the_field() { + // 12 x 40 pt with no border: 8 pt inside the 2 pt padding. At 12 pt Helvetica a W is 11.3 pt + // wide and an i 2.7 pt. + 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] >>".into(), + "<< /Fields [6 0 R] /DA (/Helv 12 Tf 0 g) /DR << /Font << /Helv 5 0 R >> >> >>".into(), + "<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica /Encoding /WinAnsiEncoding >>".into(), + format!( + "<< /Type /Annot /Subtype /Widget /FT /Tx /T (thin) /V (W) /DA (/Helv 12 Tf 0 g) /Ff {} /Rect [50 660 62 700] /P 3 0 R >>", + flags::MULTILINE + ), + ]; + let doc = document(&objs); + let all = fields(&doc); + let f = field(&all, "thin"); + let tb = appearance::text_box(&doc, f, &f.widgets[0]); + assert!(tb.fits("i") && tb.fits("i\ni")); + assert!(matches!(appearance::value_overflow(&doc, f, &f.widgets[0]), Some(appearance::Overflow::Wide { .. }))); + assert!(!tb.fits("W") && !tb.fits("iW")); +} + /// 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]