mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 11:47:21 -06:00
Threaded text: Convert To Point Type on a frame no longer duplicates its text
Converting a frame of threaded text to point type left the frame's text in the point type, and the re-flow also added that text to the story, as it does for a frame that was deleted. The other frames then held it a second time. A frame that is still in the document as point type now leaves its thread with the text it holds. The story flows through the other frames without that text, so each character is held once. A thread left with one frame is no longer a thread, and undo restores the thread. Docs: docs/mcp.md, docs/target-app-parity.md.
This commit is contained in:
@@ -81,7 +81,8 @@ thread_local! {
|
||||
}
|
||||
|
||||
/// Re-flow every thread with a frame that changed between `before` and `doc` (structural sharing
|
||||
/// makes "changed" a pointer comparison). Deleted frames' text flows into the remaining ones.
|
||||
/// makes "changed" a pointer comparison). Deleted frames' text flows into the remaining ones; a
|
||||
/// frame that is now point type leaves its thread with the text it holds.
|
||||
pub(crate) fn reflow(before: &Document, doc: &mut Document) {
|
||||
let threads = std::mem::take(&mut doc.text_threads);
|
||||
let mut kept = vec![];
|
||||
@@ -116,6 +117,8 @@ pub(crate) fn reflow(before: &Document, doc: &mut Document) {
|
||||
sources.push(t.clone());
|
||||
live.push(*id);
|
||||
}
|
||||
// A frame converted to point type keeps the text it holds and leaves the thread.
|
||||
(None, Some(_)) if matches!(doc.node(*id).map(|n| &n.kind), Some(NodeKind::Text(_))) => {}
|
||||
(None, Some(old)) => {
|
||||
story.extend(old.runs.iter().cloned());
|
||||
later_wins.push(false);
|
||||
@@ -714,6 +717,55 @@ mod tests {
|
||||
assert_eq!(format!("{}{}", text(&s, a), text(&s, b)), TEXT.repeat(4));
|
||||
}
|
||||
|
||||
/// A thread of `n` frames (2 or 3), each showing part of the story: area type `a` of [`setup`],
|
||||
/// for 3 an area type frame that is empty until it is threaded, then the rectangle `b`.
|
||||
fn thread_of(n: usize) -> (Session, Vec<u64>) {
|
||||
let (mut s, a, b) = setup();
|
||||
let frames = if n == 3 {
|
||||
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();
|
||||
vec![a, c, b]
|
||||
} else {
|
||||
s.execute("text.thread.add", &json!({"from": a, "to": b})).unwrap();
|
||||
vec![a, b]
|
||||
};
|
||||
assert_eq!(threads(&s), std::slice::from_ref(&frames));
|
||||
assert!(frames.iter().all(|id| !text(&s, *id).is_empty()), "each frame shows part of the story");
|
||||
(s, frames)
|
||||
}
|
||||
|
||||
/// Convert To Point Type on a frame of threaded text takes it out of its thread with the text
|
||||
/// it holds, and the other frames keep the text they hold: the story holds each character once.
|
||||
/// A thread left with one frame is no thread. Undo threads the frame again.
|
||||
#[test]
|
||||
fn a_frame_converted_to_point_type_leaves_its_thread_with_its_text() {
|
||||
// Soft wraps of the point type become line breaks, so its characters are compared without
|
||||
// white space.
|
||||
let squeeze = |t: &str| t.split_whitespace().collect::<String>();
|
||||
for n in [2, 3] {
|
||||
for k in 0..n {
|
||||
let (mut s, frames) = thread_of(n);
|
||||
let shown: Vec<String> = frames.iter().map(|id| text(&s, *id)).collect();
|
||||
let id = frames[k];
|
||||
s.execute("type.convertToPointType", &json!({"ids": [id]})).unwrap();
|
||||
let NodeKind::Text(t) = &s.doc().unwrap().doc.node(NodeId(id)).unwrap().kind else { panic!("not text") };
|
||||
assert!(matches!(t.kind, TextKind::Point), "{n} frames, frame {k}: point type");
|
||||
assert_eq!(squeeze(&text(&s, id)), squeeze(&shown[k]), "{n} frames, frame {k}: the point type keeps its text");
|
||||
for (f, before) in frames.iter().zip(&shown).filter(|(f, _)| **f != id) {
|
||||
assert_eq!(text(&s, *f), *before, "{n} frames, frame {k}: frame {f} keeps its text");
|
||||
}
|
||||
let rest: Vec<u64> = frames.iter().copied().filter(|f| *f != id).collect();
|
||||
assert_eq!(threads(&s), if rest.len() >= 2 { vec![rest] } else { vec![] }, "{n} frames, frame {k}");
|
||||
let all: String = frames.iter().map(|f| text(&s, *f)).collect();
|
||||
assert_eq!(squeeze(&all), squeeze(&TEXT.repeat(4)), "{n} frames, frame {k}: each character once");
|
||||
s.execute("edit.undo", &json!({})).unwrap();
|
||||
assert_eq!(threads(&s), std::slice::from_ref(&frames));
|
||||
assert_eq!(frames.iter().map(|f| text(&s, *f)).collect::<Vec<_>>(), shown, "{n} frames, frame {k}: undo");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn release_and_remove() {
|
||||
let (mut s, a, b) = setup();
|
||||
|
||||
@@ -43,7 +43,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Convert To Point Type",
|
||||
["Type"],
|
||||
None,
|
||||
"{ids?|id?} area type → point type; soft line wraps become line breaks → {ids}",
|
||||
"{ids?|id?} area type → point type; soft line wraps become line breaks, and a frame of threaded text leaves its thread with the text it holds → {ids}",
|
||||
has_selection,
|
||||
to_point
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user