mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-14 05:24:44 +00:00
Merge pull request #7259 from dotCooCoo/fix/threaded-text-one-pass
Threaded Text: thread edits find frames and threads in one pass
This commit is contained in:
@@ -68,11 +68,8 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
fn has_thread_selection(s: &Session) -> std::result::Result<(), String> {
|
||||
has_selection(s)?;
|
||||
let st = s.active().ok_or("no document open")?;
|
||||
if st.selection.objects.iter().any(|id| st.doc.text_threads.iter().any(|t| t.contains(id))) {
|
||||
Ok(())
|
||||
} else {
|
||||
Err("no threaded text selected".into())
|
||||
}
|
||||
let selected: Ids = st.selection.objects.iter().copied().collect();
|
||||
if st.doc.text_threads.iter().flatten().any(|id| selected.contains(id)) { Ok(()) } else { Err("no threaded text selected".into()) }
|
||||
}
|
||||
|
||||
/// The text object `id` when it can be a frame of a thread: area type or type on a path.
|
||||
@@ -87,6 +84,43 @@ fn text_of(d: &Document, id: NodeId) -> Option<&TextObject> {
|
||||
thread_local! {
|
||||
/// Tests: how many threads' stories [`reflow`] copied on this thread.
|
||||
static COPIED: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
/// Tests: how many frames [`leave_threads`] passed to [`continued_marks`] on this thread.
|
||||
static MARK_VISITS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
/// Tests: how many lookups and insertions [`Ids`] made on this thread.
|
||||
static ID_LOOKS: std::cell::Cell<usize> = const { std::cell::Cell::new(0) };
|
||||
}
|
||||
|
||||
/// A set of node ids, for the threaded-text commands to test whether a frame is selected, already
|
||||
/// listed or in a thread in the same time however many ids the set holds.
|
||||
#[derive(Clone, Default)]
|
||||
struct Ids(std::collections::HashSet<NodeId>);
|
||||
|
||||
impl Ids {
|
||||
fn contains(&self, id: &NodeId) -> bool {
|
||||
#[cfg(test)]
|
||||
ID_LOOKS.with(|c| c.set(c.get() + 1));
|
||||
self.0.contains(id)
|
||||
}
|
||||
|
||||
/// Add `id` to the set; whether it was not in it already.
|
||||
fn insert(&mut self, id: NodeId) -> bool {
|
||||
#[cfg(test)]
|
||||
ID_LOOKS.with(|c| c.set(c.get() + 1));
|
||||
self.0.insert(id)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromIterator<NodeId> for Ids {
|
||||
fn from_iter<I: IntoIterator<Item = NodeId>>(ids: I) -> Self {
|
||||
Self(ids.into_iter().collect())
|
||||
}
|
||||
}
|
||||
|
||||
/// [`vectorcraft_text::thread::continued`] for the frames of a thread that [`leave_threads`] marks.
|
||||
fn continued_marks(frames: &[&TextObject]) -> Vec<bool> {
|
||||
#[cfg(test)]
|
||||
MARK_VISITS.with(|c| c.set(c.get() + frames.len()));
|
||||
vectorcraft_text::thread::continued(frames)
|
||||
}
|
||||
|
||||
/// Re-flow every thread with a frame that changed between `before` and `doc` (structural sharing
|
||||
@@ -371,10 +405,12 @@ pub(crate) fn story_of(d: &Document, id: NodeId) -> Vec<NodeId> {
|
||||
|
||||
/// `ids` followed by the other frames of the threads they are in, each once, in thread order.
|
||||
pub(crate) fn with_threads(d: &Document, ids: &[NodeId]) -> Vec<NodeId> {
|
||||
let given: Ids = ids.iter().copied().collect();
|
||||
let mut listed = given.clone();
|
||||
let mut out = ids.to_vec();
|
||||
for thread in d.text_threads.iter().filter(|t| t.iter().any(|id| ids.contains(id))) {
|
||||
for thread in d.text_threads.iter().filter(|t| t.iter().any(|id| given.contains(id))) {
|
||||
for id in thread {
|
||||
if !out.contains(id) && text_of(d, *id).is_some() {
|
||||
if text_of(d, *id).is_some() && listed.insert(*id) {
|
||||
out.push(*id);
|
||||
}
|
||||
}
|
||||
@@ -399,13 +435,14 @@ pub(crate) fn frames(d: &Document) -> Vec<(NodeId, TextObject)> {
|
||||
/// threaded and the frames before it end inside one. A threaded frame whose mark changes lays out
|
||||
/// its first line again, so its thread re-flows after the edit.
|
||||
pub(crate) fn leave_threads(d: &mut Document, was: &[(NodeId, TextObject)]) {
|
||||
let gone: Vec<(NodeId, &TextObject, Option<NodeId>)> =
|
||||
was.iter().filter(|(id, _)| text_of(d, *id).is_none()).map(|(id, t)| (*id, t, kept_type(d, *id))).collect();
|
||||
// Each frame that left, with the type that takes its place.
|
||||
let gone: std::collections::HashMap<NodeId, (&TextObject, Option<NodeId>)> =
|
||||
was.iter().filter(|(id, _)| text_of(d, *id).is_none()).map(|(id, t)| (*id, (t, kept_type(d, *id)))).collect();
|
||||
if gone.is_empty() {
|
||||
return;
|
||||
}
|
||||
let find = |id: &NodeId| gone.iter().find(|(g, _, _)| g == id);
|
||||
let mut stayed = vec![];
|
||||
let find = |id: &NodeId| gone.get(id);
|
||||
let mut changed = vec![];
|
||||
for i in 0..d.text_threads.len() {
|
||||
let thread = d.text_threads[i].clone();
|
||||
if !thread.iter().any(|id| find(id).is_some()) {
|
||||
@@ -414,28 +451,33 @@ pub(crate) fn leave_threads(d: &mut Document, was: &[(NodeId, TextObject)]) {
|
||||
let keep: Vec<NodeId> = thread
|
||||
.iter()
|
||||
.filter_map(|id| match find(id) {
|
||||
Some((_, _, kept)) => *kept,
|
||||
Some((_, kept)) => *kept,
|
||||
None => Some(*id),
|
||||
})
|
||||
.collect();
|
||||
if let (Some((_, last, None)), Some(stay)) = (thread.last().and_then(find), keep.last()) {
|
||||
if let (Some((last, None)), Some(stay)) = (thread.last().and_then(find), keep.last()) {
|
||||
take_overflow(d, *stay, last);
|
||||
}
|
||||
stayed.extend(keep.iter().copied());
|
||||
changed.push(keep.clone());
|
||||
d.text_threads[i] = keep;
|
||||
}
|
||||
d.text_threads.retain(|t| t.len() >= 2);
|
||||
let marks: Vec<(NodeId, bool)> = stayed
|
||||
.iter()
|
||||
.map(|id| {
|
||||
let on = d.text_threads.iter().find(|t| t.contains(id)).is_some_and(|t| {
|
||||
let live: Vec<(NodeId, &TextObject)> = t.iter().filter_map(|i| Some((*i, text_of(d, *i)?))).collect();
|
||||
let frames: Vec<&TextObject> = live.iter().map(|(_, f)| *f).collect();
|
||||
live.iter().zip(vectorcraft_text::thread::continued(&frames)).any(|((i, _), on)| on && i == id)
|
||||
});
|
||||
(*id, on)
|
||||
})
|
||||
.collect();
|
||||
// One pass over each changed thread: a frame left alone continues nothing, and the area type and
|
||||
// type on a path of a thread that stays take their marks in thread order.
|
||||
let mut marks: Vec<(NodeId, bool)> = vec![];
|
||||
for keep in &changed {
|
||||
if keep.len() < 2 {
|
||||
marks.extend(keep.iter().map(|id| (*id, false)));
|
||||
continue;
|
||||
}
|
||||
let live: Vec<(NodeId, &TextObject)> = keep.iter().filter_map(|i| Some((*i, text_of(d, *i)?))).collect();
|
||||
let frames: Vec<&TextObject> = live.iter().map(|(_, f)| *f).collect();
|
||||
let mut on: std::collections::HashMap<NodeId, bool> = std::collections::HashMap::new();
|
||||
for ((id, _), mark) in live.iter().zip(continued_marks(&frames)) {
|
||||
*on.entry(*id).or_default() |= mark;
|
||||
}
|
||||
marks.extend(keep.iter().map(|id| (*id, on.get(id).copied().unwrap_or(false))));
|
||||
}
|
||||
for (id, on) in marks {
|
||||
if text_of(d, id).is_some_and(|t| t.first_para_continues != on)
|
||||
&& let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind)
|
||||
@@ -450,21 +492,23 @@ pub(crate) fn leave_threads(d: &mut Document, was: &[(NodeId, TextObject)]) {
|
||||
/// thread with frames outside `art`, to the text it shows. When such a frame leaves its thread,
|
||||
/// [`leave_threads`] puts the text it has no room for after the text of the last frame that stays.
|
||||
pub(crate) fn keep_shown_text(d: &Document, art: &mut Node) {
|
||||
let mut inside = vec![];
|
||||
art.walk(&mut |n| inside.push(n.id));
|
||||
let last: Vec<NodeId> = d
|
||||
let mut inside = Ids::default();
|
||||
art.walk(&mut |n| {
|
||||
inside.insert(n.id);
|
||||
});
|
||||
let last: Ids = d
|
||||
.text_threads
|
||||
.iter()
|
||||
.filter(|t| t.iter().any(|id| !inside.contains(id)))
|
||||
.filter_map(|t| t.last().copied().filter(|id| inside.contains(id)))
|
||||
.collect();
|
||||
if !last.is_empty() {
|
||||
if !last.0.is_empty() {
|
||||
cut_to_shown(art, &last);
|
||||
}
|
||||
}
|
||||
|
||||
/// Cut the area type and type on a path `ids` in `n` to the text each one shows.
|
||||
fn cut_to_shown(n: &mut Node, ids: &[NodeId]) {
|
||||
fn cut_to_shown(n: &mut Node, ids: &Ids) {
|
||||
if ids.contains(&n.id)
|
||||
&& let NodeKind::Text(t) = &mut n.kind
|
||||
&& !matches!(t.kind, TextKind::Point)
|
||||
@@ -567,21 +611,35 @@ fn create(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
*d.node_mut(*id).ok_or(EngineError::NoNode(*id))? = f;
|
||||
}
|
||||
}
|
||||
// Existing threads containing any of these frames merge into the new one, keeping order.
|
||||
// Existing threads containing any of these frames merge into the new one, keeping order:
|
||||
// each frame takes in the first thread not yet taken in that lists it.
|
||||
let mut threads: Vec<Option<Vec<NodeId>>> = std::mem::take(&mut d.text_threads).into_iter().map(Some).collect();
|
||||
let mut listing: std::collections::HashMap<NodeId, Vec<usize>> = std::collections::HashMap::new();
|
||||
for (i, t) in threads.iter().enumerate() {
|
||||
for id in t.iter().flatten() {
|
||||
listing.entry(*id).or_default().push(i);
|
||||
}
|
||||
}
|
||||
let mut order: Vec<NodeId> = vec![];
|
||||
let mut listed = Ids::default();
|
||||
for id in &ids {
|
||||
match d.text_threads.iter().position(|t| t.contains(id)) {
|
||||
Some(i) => {
|
||||
for x in d.text_threads.remove(i) {
|
||||
if !order.contains(&x) {
|
||||
let thread = listing.get(id).and_then(|at| at.iter().find_map(|i| threads[*i].take()));
|
||||
match thread {
|
||||
Some(thread) => {
|
||||
for x in thread {
|
||||
if listed.insert(x) {
|
||||
order.push(x);
|
||||
}
|
||||
}
|
||||
}
|
||||
None if !order.contains(id) => order.push(*id),
|
||||
None => {}
|
||||
None => {
|
||||
if listed.insert(*id) {
|
||||
order.push(*id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
d.text_threads = threads.into_iter().flatten().collect();
|
||||
d.text_threads.push(order);
|
||||
// Force a re-flow of the new thread on this edit.
|
||||
touch(d, ids[0]);
|
||||
@@ -706,11 +764,12 @@ fn touch(d: &mut Document, id: NodeId) {
|
||||
}
|
||||
|
||||
fn release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let ids = selected_roots(s)?;
|
||||
let ids: Ids = selected_roots(s)?.into_iter().collect();
|
||||
s.edit("Release Threaded Text", |d, _| {
|
||||
// The released frames' text joins the rest of the story (they become empty).
|
||||
for thread in d.text_threads.clone() {
|
||||
let (out, keep): (Vec<NodeId>, Vec<NodeId>) = thread.iter().partition(|i| ids.contains(i));
|
||||
for i in 0..d.text_threads.len() {
|
||||
let thread = d.text_threads[i].clone();
|
||||
let (out, keep): (Vec<NodeId>, Vec<NodeId>) = thread.iter().partition(|id| ids.contains(id));
|
||||
if out.is_empty() {
|
||||
continue;
|
||||
}
|
||||
@@ -750,7 +809,6 @@ fn release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
t.first_para_continues = false;
|
||||
refresh_bounds(t);
|
||||
}
|
||||
let i = d.text_threads.iter().position(|t| *t == thread).unwrap_or(0);
|
||||
d.text_threads[i] = keep;
|
||||
}
|
||||
d.text_threads.retain(|t| t.len() >= 2);
|
||||
@@ -760,7 +818,7 @@ fn release(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
}
|
||||
|
||||
fn remove(s: &mut Session, _: &Value) -> Result<Value> {
|
||||
let ids = selected_roots(s)?;
|
||||
let ids: Ids = selected_roots(s)?.into_iter().collect();
|
||||
s.edit("Remove Threading", |d, _| {
|
||||
let (gone, kept): (Vec<Vec<NodeId>>, Vec<Vec<NodeId>>) =
|
||||
std::mem::take(&mut d.text_threads).into_iter().partition(|t| t.iter().any(|i| ids.contains(i)));
|
||||
|
||||
@@ -1053,6 +1053,246 @@ fn a_frame_that_starts_its_thread_after_outlining_has_its_first_line_indent() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether frame `id` of `d` is marked as continuing a paragraph.
|
||||
fn marked(d: &Document, id: NodeId) -> bool {
|
||||
text_of(d, id).unwrap().first_para_continues
|
||||
}
|
||||
|
||||
/// The marks of `thread`'s frames, each found from the frames before it: a frame continues a
|
||||
/// paragraph when the last of them with text does not end with a paragraph break. A frame left
|
||||
/// alone continues nothing.
|
||||
fn marks_by_scan(d: &Document, thread: &[NodeId]) -> Vec<bool> {
|
||||
(0..thread.len())
|
||||
.map(|k| {
|
||||
thread.len() >= 2
|
||||
&& thread[..k]
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|id| text_of(d, *id).unwrap().plain_text())
|
||||
.find(|t| !t.is_empty())
|
||||
.is_some_and(|t| !t.ends_with('\n'))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// When frames leave their threads (from the start, the middle or the end of a thread, outlined or
|
||||
/// with their type under a new id, leaving a thread with one frame), each frame that stays takes
|
||||
/// the mark found from the frames before it, and a thread that lost no frame keeps its marks.
|
||||
#[test]
|
||||
fn frames_that_stay_after_others_leave_their_threads_take_the_marks_of_their_place() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 800, "height": 600})).unwrap();
|
||||
let mut frame = |text: &str| {
|
||||
NodeId(new_id(s.execute("text.create", &json!({"x": 10, "y": 10, "text": text, "area": {"width": 150, "height": 60}})).unwrap()))
|
||||
};
|
||||
let [a1, a2, a3, a4, a5] = ["One", "", "Two\n", "Three", "Four"].map(&mut frame);
|
||||
let [b1, b2] = ["Alpha", "Beta"].map(&mut frame);
|
||||
let [c1, c2] = ["x", "y"].map(&mut frame);
|
||||
let [d1, d2, d3, d4] = ["M", "N\n", "O", "P"].map(&mut frame);
|
||||
let rect = NodeId(new_id(s.execute("shape.rectangle", &json!({"x": 400, "y": 400, "width": 20, "height": 20})).unwrap()));
|
||||
let mut d = (*s.doc().unwrap().doc).clone();
|
||||
d.text_threads = vec![vec![a1, a2, a3, a4, a5], vec![b1, b2], vec![c1, c2], vec![d1, d2, d3, d4]];
|
||||
// Every mark is the opposite of what it becomes, or (in the thread that loses no frame) wrong.
|
||||
for id in [a1, a2, a3, a4, a5, b1, b2, c1, d1, d2, d3, d4] {
|
||||
if let Some(NodeKind::Text(t)) = d.node_mut(id).map(|n| &mut n.kind) {
|
||||
t.first_para_continues = true;
|
||||
}
|
||||
}
|
||||
let shown: Vec<(NodeId, String)> = d.text_threads.iter().flatten().map(|id| (*id, text_of(&d, *id).unwrap().plain_text())).collect();
|
||||
let was = frames(&d);
|
||||
// a1, b2 and d3 become a rectangle; a4 becomes a group holding its type under a new id.
|
||||
let outline = d.node(rect).unwrap().kind.clone();
|
||||
for id in [a1, b2, d3] {
|
||||
d.node_mut(id).unwrap().kind = outline.clone();
|
||||
}
|
||||
let mut kept = d.node(a4).unwrap().clone();
|
||||
kept.id = d.alloc_id();
|
||||
let new_a4 = kept.id;
|
||||
let layer = d.parent_of(a4);
|
||||
d.remove(a4).unwrap();
|
||||
d.insert(layer, usize::MAX, Node::group(a4, vec![std::sync::Arc::new(kept)])).unwrap();
|
||||
leave_threads(&mut d, &was);
|
||||
assert_eq!(d.text_threads, vec![vec![a2, a3, new_a4, a5], vec![c1, c2], vec![d1, d2, d4]]);
|
||||
for thread in [&[a2, a3, new_a4, a5][..], &[b1], &[d1, d2, d4]] {
|
||||
let want = marks_by_scan(&d, thread);
|
||||
assert_eq!(thread.iter().map(|id| marked(&d, *id)).collect::<Vec<_>>(), want, "{thread:?}");
|
||||
}
|
||||
assert_eq!([a2, a3, new_a4, a5, b1, d1, d2, d4].map(|id| marked(&d, id)), [false, false, false, true, false, false, true, false]);
|
||||
assert_eq!([c1, c2].map(|id| marked(&d, id)), [true, false], "a thread that lost no frame keeps its marks");
|
||||
assert!(!matches!(d.node(a4).unwrap().kind, NodeKind::Text(_)), "the group in the frame's place is not type");
|
||||
for (id, before) in shown {
|
||||
let now = if id == a4 { new_a4 } else { id };
|
||||
if let Some(t) = text_of(&d, now) {
|
||||
assert_eq!(t.plain_text(), before, "frame {id:?} keeps its text");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The frames that stay in a long thread after one leaves are marked in one pass over the thread.
|
||||
#[test]
|
||||
fn a_long_thread_is_marked_in_one_pass_when_a_frame_leaves() {
|
||||
const FRAMES: usize = 256;
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 600, "height": 300})).unwrap();
|
||||
let first = NodeId(new_id(s.execute("text.create", &json!({"x": 10, "y": 10, "text": "w0", "area": {"width": 150, "height": 60}})).unwrap()));
|
||||
let rect = NodeId(new_id(s.execute("shape.rectangle", &json!({"x": 400, "y": 200, "width": 20, "height": 20})).unwrap()));
|
||||
let mut d = (*s.doc().unwrap().doc).clone();
|
||||
let layer = d.parent_of(first);
|
||||
let model = d.node(first).unwrap().clone();
|
||||
let mut thread = vec![first];
|
||||
for k in 1..FRAMES {
|
||||
let mut n = model.clone();
|
||||
n.id = d.alloc_id();
|
||||
if let NodeKind::Text(t) = &mut n.kind {
|
||||
t.runs.truncate(1);
|
||||
t.runs[0].text = format!("w{k}");
|
||||
t.first_para_continues = true;
|
||||
}
|
||||
thread.push(d.insert(layer, usize::MAX, n).unwrap());
|
||||
}
|
||||
d.text_threads = vec![thread.clone()];
|
||||
let was = frames(&d);
|
||||
let out = thread[FRAMES / 2];
|
||||
d.node_mut(out).unwrap().kind = d.node(rect).unwrap().kind.clone();
|
||||
MARK_VISITS.with(|c| c.set(0));
|
||||
leave_threads(&mut d, &was);
|
||||
let visits = MARK_VISITS.with(std::cell::Cell::get);
|
||||
let rest: Vec<NodeId> = thread.iter().copied().filter(|id| *id != out).collect();
|
||||
assert_eq!(d.text_threads, vec![rest.clone()]);
|
||||
assert!(visits <= FRAMES, "{visits} frames visited to mark a thread of {} frames", rest.len());
|
||||
assert_eq!(rest.iter().map(|id| marked(&d, *id)).collect::<Vec<_>>(), marks_by_scan(&d, &rest));
|
||||
}
|
||||
|
||||
/// `count` threads of `per` area type frames each, made in one edit, each frame holding a word.
|
||||
fn many_threads(count: usize, per: usize) -> (Session, Vec<Vec<NodeId>>) {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({"width": 600, "height": 300})).unwrap();
|
||||
let first = NodeId(new_id(s.execute("text.create", &json!({"x": 10, "y": 10, "text": "w", "area": {"width": 150, "height": 60}})).unwrap()));
|
||||
let threads = s
|
||||
.edit("Make", |d, _| {
|
||||
let layer = d.parent_of(first);
|
||||
let model = d.node(first).unwrap().clone();
|
||||
let mut threads = vec![];
|
||||
for _ in 0..count {
|
||||
let mut thread = vec![];
|
||||
for _ in 0..per {
|
||||
let mut n = model.clone();
|
||||
n.id = d.alloc_id();
|
||||
thread.push(d.insert(layer, usize::MAX, n).unwrap());
|
||||
}
|
||||
threads.push(thread);
|
||||
}
|
||||
d.text_threads = threads.clone();
|
||||
Ok(threads)
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.text_threads, threads);
|
||||
(s, threads)
|
||||
}
|
||||
|
||||
/// Select the first frame of each of `threads`.
|
||||
fn select_firsts(s: &mut Session, threads: &[Vec<NodeId>]) {
|
||||
let ids: Vec<u64> = threads.iter().map(|t| t[0].0).collect();
|
||||
s.execute("select.set", &json!({ "ids": ids })).unwrap();
|
||||
}
|
||||
|
||||
/// Reset the count of [`Ids`] lookups, run `f`, and give its result with the lookups it made.
|
||||
fn looks<T>(f: impl FnOnce() -> T) -> (T, usize) {
|
||||
ID_LOOKS.with(|c| c.set(0));
|
||||
let out = f();
|
||||
(out, ID_LOOKS.with(std::cell::Cell::get))
|
||||
}
|
||||
|
||||
/// Whether Release Selection and Remove Threading are available is found in one pass over the
|
||||
/// threads' frames, however many frames are selected.
|
||||
#[test]
|
||||
fn thread_commands_are_offered_after_one_pass_over_the_threads() {
|
||||
let (mut s, made) = many_threads(300, 3);
|
||||
let (threads, loose) = made.split_at(150);
|
||||
s.edit("Unthread", |d, _| {
|
||||
d.text_threads = threads.to_vec();
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
// 450 frames out of any thread, then the last frame of the last thread.
|
||||
let mut ids: Vec<u64> = loose.iter().flatten().map(|id| id.0).collect();
|
||||
ids.push(threads[149][2].0);
|
||||
s.execute("select.set", &json!({ "ids": ids })).unwrap();
|
||||
let (offered, n) = looks(|| has_thread_selection(&s));
|
||||
assert!(offered.is_ok());
|
||||
assert!(n <= 2 * (451 + 450), "{n} id lookups");
|
||||
s.execute("select.set", &json!({ "ids": ids[..450] })).unwrap();
|
||||
assert!(has_thread_selection(&s).is_err(), "frames out of any thread are no threaded text");
|
||||
let rect = new_id(s.execute("shape.rectangle", &json!({"x": 400, "y": 200, "width": 20, "height": 20})).unwrap());
|
||||
s.execute("select.set", &json!({"ids": [rect]})).unwrap();
|
||||
assert!(has_thread_selection(&s).is_err(), "a rectangle is no threaded text");
|
||||
}
|
||||
|
||||
/// The frames of the selected frames' threads are listed with one lookup per frame.
|
||||
#[test]
|
||||
fn the_frames_threaded_with_a_selection_are_listed_in_one_pass() {
|
||||
let (s, threads) = many_threads(150, 3);
|
||||
let firsts: Vec<NodeId> = threads.iter().map(|t| t[0]).collect();
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let (all, n) = looks(|| with_threads(d, &firsts));
|
||||
let mut want = firsts.clone();
|
||||
want.extend(threads.iter().flat_map(|t| t[1..].iter().copied()));
|
||||
assert_eq!(all, want);
|
||||
assert!(n <= 4 * (150 + 450), "{n} id lookups");
|
||||
}
|
||||
|
||||
/// The last frames of many threads copied out of them are found with a few lookups per frame and
|
||||
/// per object copied.
|
||||
#[test]
|
||||
fn the_last_frames_copied_out_of_many_threads_are_found_in_one_pass() {
|
||||
let (s, threads) = many_threads(150, 3);
|
||||
let d = &s.doc().unwrap().doc;
|
||||
let lasts: Vec<std::sync::Arc<Node>> = threads.iter().map(|t| std::sync::Arc::new(d.node(t[2]).unwrap().clone())).collect();
|
||||
let mut art = Node::group(NodeId(u64::MAX), lasts);
|
||||
let before = art.clone();
|
||||
let ((), n) = looks(|| keep_shown_text(d, &mut art));
|
||||
assert!(n <= 4 * (151 + 450), "{n} id lookups");
|
||||
assert_eq!(art, before, "each copied frame already shows all of its word");
|
||||
}
|
||||
|
||||
/// Threading one frame of each of many threads merges them in one pass, in the order of the
|
||||
/// selection.
|
||||
#[test]
|
||||
fn creating_a_thread_from_many_threads_merges_them_in_one_pass() {
|
||||
let (mut s, threads) = many_threads(150, 3);
|
||||
select_firsts(&mut s, &threads);
|
||||
let (made, n) = looks(|| s.execute("text.thread.create", &json!({})));
|
||||
made.unwrap();
|
||||
assert!(n <= 8 * (150 + 450), "{n} id lookups");
|
||||
assert_eq!(s.doc().unwrap().doc.text_threads, vec![threads.concat()]);
|
||||
}
|
||||
|
||||
/// Release Selection on one frame of each of many threads finds the selected frames with one
|
||||
/// lookup per frame.
|
||||
#[test]
|
||||
fn releasing_frames_of_many_threads_takes_one_pass() {
|
||||
let (mut s, threads) = many_threads(150, 3);
|
||||
select_firsts(&mut s, &threads);
|
||||
let (released, n) = looks(|| s.execute("text.thread.releaseSelection", &json!({})));
|
||||
released.unwrap();
|
||||
assert!(n <= 2 * 450, "{n} id lookups");
|
||||
let rest: Vec<Vec<NodeId>> = threads.iter().map(|t| t[1..].to_vec()).collect();
|
||||
assert_eq!(s.doc().unwrap().doc.text_threads, rest);
|
||||
}
|
||||
|
||||
/// Remove Threading on the last frame of each of many threads finds the selected frames with one
|
||||
/// lookup per frame.
|
||||
#[test]
|
||||
fn removing_the_threads_of_many_frames_takes_one_pass() {
|
||||
let (mut s, threads) = many_threads(150, 3);
|
||||
let lasts: Vec<u64> = threads.iter().map(|t| t[2].0).collect();
|
||||
s.execute("select.set", &json!({ "ids": lasts })).unwrap();
|
||||
let (removed, n) = looks(|| s.execute("text.thread.remove", &json!({})));
|
||||
removed.unwrap();
|
||||
assert!(n <= 2 * 450, "{n} id lookups");
|
||||
assert!(s.doc().unwrap().doc.text_threads.is_empty());
|
||||
}
|
||||
|
||||
/// The text that a thread's last frame has no room for is measured as the frame lays it out,
|
||||
/// without the first-line indent of the paragraph it continues: when the frame is outlined, the
|
||||
/// frame before it takes only that text.
|
||||
|
||||
Reference in New Issue
Block a user