M6: a paste into a full field keeps the longest part that fits, across a change of font

This commit is contained in:
Eric Kaden
2026-10-23 22:04:52 -04:00
parent 64bdea4e61
commit 6d7e887efe
2 changed files with 62 additions and 6 deletions
+34 -6
View File
@@ -156,13 +156,41 @@ impl TextBox {
if self.fits(after) {
return (after.to_string(), p + inserted.len());
}
// The most of the insertion that fits: text only grows as it gets longer, so halve.
let (mut lo, mut hi) = (0, inserted.len());
while lo < hi {
let mid = lo + (hi - lo).div_ceil(2);
if self.fits(&with(mid)) { lo = mid } else { hi = mid - 1 }
// The longest of the insertion's first `from..=to` characters that fits, halving: in one
// font a longer text is never narrower. `None` when even `from` doesn't fit.
let longest = |from: usize, to: usize| -> Option<usize> {
if from > to || !self.fits(&with(from)) {
return None;
}
let (mut lo, mut hi) = (from, to);
while lo < hi {
let mid = lo + (hi - lo).div_ceil(2);
if self.fits(&with(mid)) { lo = mid } else { hi = mid - 1 }
}
Some(lo)
};
// The font can change once, from the form's own to Helvetica, at the first inserted
// character the form's font can't show: each side of that point is searched on its own.
let n = inserted.len();
let kept = match self.font_switch(prefix, inserted, suffix) {
Some(t) => longest(0, t.saturating_sub(1)).into_iter().chain(longest(t, n)).max(),
None => longest(0, n),
}
(with(lo), p + lo)
.unwrap_or(0);
(with(kept), p + kept)
}
/// Where an insertion changes the font [`Self::fits`] measures in: how much of `inserted`
/// (between `prefix` and `suffix`) first includes a character the form's own font can't show,
/// when everything around it can be shown in that font. `None` when the font never changes.
fn font_switch(&self, prefix: &[char], inserted: &[char], suffix: &[char]) -> Option<usize> {
let enc = self.font.as_ref().filter(|_| !self.password)?;
// As `fits` decides: a multi-line value is checked line by line, its breaks aside.
let shows = |c: &char| (self.multiline && matches!(c, '\n' | '\r')) || enc.can_encode(&c.to_string());
if !prefix.iter().chain(suffix).all(shows) {
return None;
}
inserted.iter().position(|c| !shows(c)).map(|i| i + 1)
}
}
+28
View File
@@ -472,6 +472,34 @@ fn an_edit_to_a_full_field_keeps_what_fits() {
assert!(kept.starts_with("SW") && kept.ends_with('t') && tb.fits(&kept), "{kept}");
}
/// When the field's own font can't show a character of a paste, the text from there on is drawn
/// in Helvetica, which can be narrower: the longest part that fits may come after that point, and
/// is still the one kept.
#[test]
fn an_edit_keeps_the_longest_part_that_fits_across_a_change_of_font() {
// A subset font holding only A, 1 em wide: 12 pt an A at 12 pt, against Helvetica's 8 pt.
let objs: Vec<String> = 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 (/Wide 12 Tf 0 g) /DR << /Font << /Wide 5 0 R >> >> >>".into(),
"<< /Type /Font /Subtype /Type1 /BaseFont /ABCDEF+Wide /FirstChar 65 /LastChar 65 /Widths [1000] /Encoding /WinAnsiEncoding >>".into(),
"<< /Type /Annot /Subtype /Widget /FT /Tx /T (one) /DA (/Wide 12 Tf 0 g) /Rect [50 760 150 780] /P 3 0 R >>".into(),
];
let doc = document(&objs);
let all = fields(&doc);
let f = field(&all, "one");
let tb = appearance::text_box(&doc, f, &f.widgets[0]);
// 96 pt inside: 8 A's in the subset font, or 11 characters (8.004 pt each) once a B brings
// Helvetica in.
assert!(tb.fits(&"A".repeat(8)) && !tb.fits(&"A".repeat(9)));
let pasted = format!("{}B{}", "A".repeat(10), "A".repeat(30));
let (kept, caret) = tb.fitted_edit("", &pasted);
assert_eq!(kept, format!("{}B", "A".repeat(10)), "the 11 characters that fit in Helvetica, not the 8 before the B");
assert!(!tb.fits(&format!("{}BA", "A".repeat(10))), "and no more");
assert_eq!(caret, 11);
}
/// Issue #1323: a text field without a format keeps a whitespace-only value as typed; only an
/// empty value clears it.
#[test]