fix(edit): wrap added text once per write, not once per line

Writing an added text item re-wrapped the whole text inside the loop over its
lines to learn the line count, so multiline text cost time and temporary
memory quadratic in its length. The wrapped lines are computed once.
This commit is contained in:
aiongg
2026-10-22 21:16:21 +00:00
parent cb1ddc899b
commit f1a89a4842
2 changed files with 21 additions and 2 deletions
+10 -2
View File
@@ -235,9 +235,17 @@ pub fn lines(t: &AddedText) -> Vec<String> {
wrapped(t).into_iter().map(|(line, _)| line).collect()
}
#[cfg(test)]
thread_local! {
/// How often [`wrapped`] laid a text out on this thread, for the test that keeps it out of per-line loops.
pub(crate) static WRAP_CALLS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
}
/// [`lines`], each with whether its paragraph runs right to left (its first strong character
/// does).
fn wrapped(t: &AddedText) -> Vec<(String, bool)> {
#[cfg(test)]
WRAP_CALLS.with(|c| c.set(c.get().saturating_add(1)));
let width = (t.rect[2] - t.rect[0]).max(t.size);
let mut out = Vec::new();
for para in t.text.split('\n') {
@@ -780,7 +788,8 @@ fn draw(doc: &mut Document, c: &Content, view: [f64; 6], taken: &Dict, existing:
let r = text_rect(t);
let [cr, cg, cb] = t.color.map(|v| v.clamp(0.0, 1.0));
out.extend(format!("BT /{name} {} Tf {} {} {} rg\n", n(t.size), n(cr), n(cg), n(cb)).bytes());
for (i, line) in lines(t).iter().enumerate() {
let all = lines(t);
for (i, line) in all.iter().enumerate() {
let w = t.family.width(line, t.size, t.bold, t.italic);
let x = match t.align {
Align::Left | Align::Justify => r[0],
@@ -790,7 +799,6 @@ fn draw(doc: &mut Document, c: &Content, view: [f64; 6], taken: &Dict, existing:
// Baseline: 0.8 em below the line top.
let y = r[3] - (i as f64 * 1.2 + 0.95) * t.size;
// Justified: word spacing makes every line but the last fill the box.
let all = lines(t);
let spaces = line.matches(' ').count();
let tw =
if t.align == Align::Justify && i + 1 < all.len() && spaces > 0 { ((r[2] - r[0]) - w).max(0.0) / spaces as f64 } else { 0.0 };
+11
View File
@@ -6355,3 +6355,14 @@ fn reading_keeps_dotted_leader_entries_whole_and_in_order() {
]
);
}
#[test]
fn added_text_is_wrapped_once_per_write() {
let mut doc = fixture();
let lines: Vec<String> = (0..40).map(|i| format!("line {i}")).collect();
let text = AddedText { rect: [72.0, 100.0, 300.0, 700.0], text: lines.join("\n"), size: 10.0, align: Align::Justify, ..AddedText::default() };
added::WRAP_CALLS.with(|c| c.set(0));
add_content(&mut doc, 0, &Content::Text(text)).unwrap();
let calls = added::WRAP_CALLS.with(std::cell::Cell::get);
assert!(calls <= 4, "the text was wrapped {calls} times for 40 lines");
}