From 23dbf5d214aa9fde6563369cc4a3c7e91297b510 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ju=CC=88rg=20Lehni?= Date: Wed, 21 Oct 2026 15:50:13 +0200 Subject: [PATCH] Threaded text: releasing a frame from a longer thread no longer duplicates its text When two or more frames stayed in the thread, the first got the whole story while the others kept their shares, so the re-flow joined those twice. The frames after the first now start empty, and the story flows through them once. --- crates/engine/src/cmd/threads.rs | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/crates/engine/src/cmd/threads.rs b/crates/engine/src/cmd/threads.rs index ea1cc5e20..9c92c278c 100644 --- a/crates/engine/src/cmd/threads.rs +++ b/crates/engine/src/cmd/threads.rs @@ -381,6 +381,16 @@ fn release(s: &mut Session, _: &Value) -> Result { refresh_bounds(t); } } + // The first frame that stays holds the whole story, and the others none, for the + // re-flow to share it out among them once. + let end_style = story.last().map_or_else(|| style.clone(), |r| r.style.clone()); + let last_para = paras.last().cloned().unwrap_or_default(); + for id in keep.iter().skip(1) { + if let Some(NodeKind::Text(t)) = d.node_mut(*id).map(|n| &mut n.kind) { + t.runs = vec![TextRun { text: String::new(), style: end_style.clone(), inline: None }]; + t.set_all_paras(last_para.clone()); + } + } if let Some(first) = keep.first() && let Some(NodeKind::Text(t)) = d.node_mut(*first).map(|n| &mut n.kind) { @@ -684,6 +694,22 @@ mod tests { assert!(s.execute("text.thread.add", &json!({"from": c, "to": b})).is_err(), "a frame already threaded"); } + /// A frame released from a thread that keeps two or more frames gives its text back to them: + /// the story flows through them once. + #[test] + fn release_from_a_longer_thread_keeps_the_story_once() { + let (mut s, a, b) = setup(); + let c = new_id(s.execute("text.create", &json!({"x": 10, "y": 100, "text": "", "area": {"width": 150, "height": 40}})).unwrap()); + s.execute("text.thread.add", &json!({"from": a, "to": c})).unwrap(); + s.execute("text.thread.add", &json!({"from": c, "to": b})).unwrap(); + assert_eq!(threads(&s), [[a, c, b]]); + s.execute("select.set", &json!({"ids": [c]})).unwrap(); + s.execute("text.thread.releaseSelection", &json!({})).unwrap(); + assert_eq!(threads(&s), [[a, b]]); + assert!(text(&s, c).is_empty()); + assert_eq!(format!("{}{}", text(&s, a), text(&s, b)), TEXT.repeat(4)); + } + #[test] fn release_and_remove() { let (mut s, a, b) = setup();