M7: read the page once per keystroke when measuring a text draft (#1781)

`text_edit_layout`, which the inline editor calls on every keystroke to
size the frame, interpreted the page's content four times: `text_blocks`
for the paragraphs, `text_lines` again for the untouched runs, once more
inside `rewrite_block` on the still unchanged clone, and finally for the
rewritten draft. The first three read the same, unchanged page.

It now reads the page's lines once, groups the paragraphs from them, and
hands them to the rewrite through `rewrite_block_from`, which
`rewrite_block_with_limit` also uses after reading the lines itself. The
draft is still read back once after the rewrite. On a page of 120 runs
in 80 paragraphs, 50 keystrokes in a release build took 0.87 ms each,
against 1.25 ms before (the fastest of three runs each).

No behaviour changes. The new test checks that the measured draft frame
is the extent an applied edit actually draws, for a longer row, a
shorter one and an added row, so sharing the lines can't drift from a
fresh read.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Brandon Thomas <echelon@gmail.com>
This commit is contained in:
komo1iddin
2026-10-15 02:24:55 -04:00
committed by GitHub
co-authored by Claude Opus 5.5 Brandon Thomas
parent c5c5d0d661
commit 72a46facb8
2 changed files with 48 additions and 3 deletions
+29
View File
@@ -3713,3 +3713,32 @@ fn a_subset_program_is_not_trusted_for_new_characters_and_the_error_names_the_ch
assert!(msg.contains("ő (U+0151)") && msg.contains("Load font") && !msg.contains("Japanese"), "{msg}");
assert_eq!(page_content_bytes(&doc, 0), before);
}
#[test]
fn draft_layout_matches_the_paragraph_an_applied_edit_draws() {
let content = "BT /F1 12 Tf 14 TL 72 700 Td (A first paragraph of words) Tj T* (with a second row) Tj ET \
BT /F1 12 Tf 72 600 Td (Another paragraph below) Tj ET BT /F1 12 Tf 320 700 Td (Side note) Tj ET";
let doc = text_page(content);
let block = text::text_blocks(&doc, 0).unwrap().into_iter().position(|b| b.text.starts_with("A first")).unwrap();
let source = text::text_blocks(&doc, 0).unwrap()[block].clone();
for draft in [
source.editor_text.replacen("words", "many more words than before", 1),
source.editor_text.replacen(" of words", "", 1),
format!("{}\nand a third row", source.editor_text),
] {
let layout = text::text_edit_layout(&doc, 0, block, &draft, &text::BlockStyle::default()).unwrap();
// Apply the same edit and measure every run that isn't one of the page's other runs.
let mut applied = doc.clone();
text::rewrite_block(&mut applied, 0, block, Some(&draft), &text::BlockStyle::default()).unwrap();
let others: Vec<_> =
text::text_lines(&doc, 0).unwrap().into_iter().enumerate().filter(|(i, _)| !source.lines.contains(i)).map(|(_, l)| l.rect).collect();
let drawn = text::text_lines(&applied, 0)
.unwrap()
.into_iter()
.map(|l| l.rect)
.filter(|r| !others.contains(r))
.reduce(|a, b| [a[0].min(b[0]), a[1].min(b[1]), a[2].max(b[2]), a[3].max(b[3])]);
assert_eq!(layout.draft_rect, drawn, "{draft:?}");
assert_eq!(layout.source_rect, source.rect);
}
}
+19 -3
View File
@@ -140,7 +140,10 @@ pub struct TextEditLayout {
}
pub fn text_edit_layout(doc: &Document, page: usize, block: usize, text: &str, style: &BlockStyle) -> Result<TextEditLayout, EditError> {
let original = text_blocks(doc, page)?;
// This runs on every keystroke: read the page once for the paragraphs, the untouched runs
// and the rewrite, which starts from the same unchanged page.
let lines = text_lines(doc, page)?;
let original = group_blocks(&lines);
let source = original.get(block).ok_or(EditError::Invalid("text block does not exist".into()))?;
if text == source.editor_text && *style == BlockStyle::default() {
return Ok(TextEditLayout { source_rect: source.rect, draft_rect: Some(source.rect), overflow: false });
@@ -150,13 +153,13 @@ pub fn text_edit_layout(doc: &Document, page: usize, block: usize, text: &str, s
let key =
|line: &TextLine| (line.text.clone(), line.font.clone(), line.rect.map(f64::to_bits), line.size.to_bits(), line.color.map(f64::to_bits));
let mut untouched = std::collections::BTreeMap::new();
for (index, line) in text_lines(doc, page)?.iter().enumerate() {
for (index, line) in lines.iter().enumerate() {
if !source.lines.contains(&index) {
*untouched.entry(key(line)).or_insert(0_usize) += 1;
}
}
let mut draft = doc.clone();
rewrite_block(&mut draft, page, block, Some(text), style)?;
rewrite_block_from(&mut draft, page, lines, block, Some(text), style, MAX_TYPE3_GLYPHS)?;
let mut rect: Option<[f64; 4]> = None;
for line in text_lines(&draft, page)? {
if let Some(count) = untouched.get_mut(&key(&line))
@@ -1734,6 +1737,19 @@ pub(crate) fn rewrite_block_with_limit(
max_glyphs: usize,
) -> Result<LineEdit, EditError> {
let lines = text_lines(doc, page)?;
rewrite_block_from(doc, page, lines, block, text, style, max_glyphs)
}
/// [`rewrite_block_with_limit`] with the page's [`text_lines`], read from `doc` as it is now.
fn rewrite_block_from(
doc: &mut Document,
page: usize,
lines: Vec<TextLine>,
block: usize,
text: Option<&str>,
style: &BlockStyle,
max_glyphs: usize,
) -> Result<LineEdit, EditError> {
let blocks = group_blocks(&lines);
let b = blocks.get(block).cloned().ok_or_else(|| EditError::Invalid(format!("page {} has no paragraph {}", page + 1, block + 1)))?;
let members: Vec<&TextLine> = b.lines.iter().map(|i| &lines[*i]).collect();