mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-15 04:49:52 +00:00
Type: story-wide panel changes follow the story's current thread
After Select All in threaded text, the panels read the frames from the list saved at Select All. A frame that had left the thread still took Paragraph, Tabs, Justification and Text Styles changes, a frame added later did not, and when the first saved frame had left, Character, OpenType and font changes went to that frame alone. text_story now reads the frames of the edited frame's thread from the document. Locked and hidden frames stay in the story.
This commit is contained in:
@@ -499,18 +499,16 @@ pub(crate) fn text_editing(app: &VectorcraftApp) -> Option<(NodeId, usize, usize
|
||||
}
|
||||
|
||||
/// The frames of the threaded story whose whole text the Type tool has selected after Select All,
|
||||
/// in thread order; `None` when its selection is in one type object.
|
||||
/// in the order of the edited frame's thread as the document holds it now; `None` when its
|
||||
/// selection is in one type object.
|
||||
pub(crate) fn text_story(app: &VectorcraftApp) -> Option<Vec<NodeId>> {
|
||||
let o = app.session.tool_options();
|
||||
let st = app.session.active()?;
|
||||
let ids: Vec<NodeId> = o
|
||||
.get("story")?
|
||||
.as_array()?
|
||||
.iter()
|
||||
.filter_map(Value::as_u64)
|
||||
.map(NodeId)
|
||||
.filter(|id| st.doc.node(*id).is_some_and(|n| matches!(n.kind, NodeKind::Text(_))))
|
||||
.collect();
|
||||
let id = NodeId(o.get("editing")?.as_u64()?);
|
||||
if o.get("story")?.as_array()?.is_empty() {
|
||||
return None;
|
||||
}
|
||||
let ids = st.doc.story_of(id);
|
||||
(ids.len() >= 2).then_some(ids)
|
||||
}
|
||||
|
||||
|
||||
@@ -1329,3 +1329,105 @@ fn select_all_in_threaded_text_formats_and_copies_the_whole_story() {
|
||||
.collect();
|
||||
assert_eq!(copied, [story]);
|
||||
}
|
||||
|
||||
/// A story threaded through three frames in order, the first `height` tall.
|
||||
fn three_frame_story(height: u32) -> (VectorcraftApp, u64, u64, u64) {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 900, "height": 300})).unwrap();
|
||||
let story = "The quick brown fox jumps over the lazy dog and keeps running far beyond the first frame. ".repeat(6);
|
||||
let area = json!({"x": 10, "y": 10, "text": story, "size": 12, "area": {"width": 150, "height": height}});
|
||||
let a = app.run("text.create", area).unwrap()["id"].as_u64().unwrap();
|
||||
let b = app.run("text.thread.add", json!({"from": a, "rect": [200, 10, 150, 60]})).unwrap()["id"].as_u64().unwrap();
|
||||
let c = app.run("text.thread.add", json!({"from": b, "rect": [400, 10, 150, 200]})).unwrap()["id"].as_u64().unwrap();
|
||||
(app, a, b, c)
|
||||
}
|
||||
|
||||
/// Select All with the Type tool in frame `id` of a story.
|
||||
fn select_all_from(app: &mut VectorcraftApp, id: u64) {
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
app.session.select_tool("type", Default::default()).unwrap();
|
||||
app.session.set_tool_option("textSelection", &json!({"editing": id, "anchor": 0, "caret": 0}));
|
||||
app.run("select.all", json!({})).unwrap();
|
||||
}
|
||||
|
||||
fn story_frame(app: &VectorcraftApp, id: u64) -> vectorcraft_doc::TextObject {
|
||||
match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => (**t).clone(),
|
||||
_ => panic!("not text"),
|
||||
}
|
||||
}
|
||||
|
||||
fn story_sizes(app: &VectorcraftApp, id: u64) -> Vec<f64> {
|
||||
story_frame(app, id).runs.iter().map(|r| r.style.size).collect()
|
||||
}
|
||||
|
||||
/// The frames that Select All saved are not the frames of the story once the first of them has
|
||||
/// left the thread: the edited frame's story takes the change.
|
||||
#[test]
|
||||
fn story_changes_follow_the_edited_frames_thread_after_the_first_saved_frame_leaves() {
|
||||
// The first frame is too short for a line, so Select All edits the second.
|
||||
let (mut app, a, b, c) = three_frame_story(1);
|
||||
select_all_from(&mut app, c);
|
||||
assert_eq!(app.session.tool_options()["story"], json!([a, b, c]));
|
||||
assert_eq!(text_editing(&app).unwrap().0, NodeId(b));
|
||||
app.run("text.thread.break", json!({"id": a, "port": "out"})).unwrap();
|
||||
assert_eq!(text_editing(&app).unwrap().0, NodeId(b));
|
||||
|
||||
assert_eq!(text_story(&app), Some(vec![NodeId(b), NodeId(c)]));
|
||||
format(&mut app, json!({"size": 9}));
|
||||
assert!(story_sizes(&app, a).iter().all(|s| (*s - 12.0).abs() < 1e-9), "the frame that left keeps its size");
|
||||
for id in [b, c] {
|
||||
assert!(story_sizes(&app, id).iter().all(|s| (*s - 9.0).abs() < 1e-9), "frame {id} takes the size");
|
||||
}
|
||||
}
|
||||
|
||||
/// A frame that left the thread after Select All takes no change, and a frame added to it does.
|
||||
#[test]
|
||||
fn story_changes_leave_out_a_frame_that_left_and_take_in_a_frame_that_joined() {
|
||||
let (mut app, a, b, c) = three_frame_story(60);
|
||||
select_all_from(&mut app, c);
|
||||
assert_eq!(text_editing(&app).unwrap().0, NodeId(a));
|
||||
app.run("text.thread.break", json!({"id": b, "port": "out"})).unwrap();
|
||||
assert_eq!(text_story(&app), Some(vec![NodeId(a), NodeId(b)]));
|
||||
let d = app.run("text.thread.add", json!({"from": a, "rect": [600, 10, 150, 60]})).unwrap()["id"].as_u64().unwrap();
|
||||
assert_eq!(text_story(&app), Some(vec![NodeId(a), NodeId(d), NodeId(b)]));
|
||||
|
||||
crate::panels::paragraph::run_on_paragraphs(&mut app, "text.setFormat", json!({"spaceBefore": 4})).unwrap();
|
||||
for id in [a, d, b] {
|
||||
assert!((story_frame(&app, id).para_at(0).space_before - 4.0).abs() < 1e-9, "frame {id} takes the space");
|
||||
}
|
||||
assert!(story_frame(&app, c).para_at(0).space_before.abs() < 1e-9, "the frame that left keeps its paragraphs");
|
||||
}
|
||||
|
||||
/// With the thread gone, the selection is in the one edited frame.
|
||||
#[test]
|
||||
fn story_changes_stay_in_the_edited_frame_once_its_thread_is_removed() {
|
||||
let (mut app, a, b, c) = three_frame_story(60);
|
||||
select_all_from(&mut app, c);
|
||||
assert_eq!(text_editing(&app).unwrap().0, NodeId(a));
|
||||
app.run("text.thread.remove", json!({})).unwrap();
|
||||
assert_eq!(text_story(&app), None);
|
||||
|
||||
crate::panels::paragraph::run_on_paragraphs(&mut app, "text.setFormat", json!({"spaceBefore": 4})).unwrap();
|
||||
assert!((story_frame(&app, a).para_at(0).space_before - 4.0).abs() < 1e-9);
|
||||
for id in [b, c] {
|
||||
assert!(story_frame(&app, id).para_at(0).space_before.abs() < 1e-9, "frame {id} keeps its paragraphs");
|
||||
}
|
||||
}
|
||||
|
||||
/// A selected story is one text: its locked and hidden frames take the change too.
|
||||
#[test]
|
||||
fn story_changes_take_in_locked_and_hidden_frames() {
|
||||
let (mut app, a, b, c) = three_frame_story(60);
|
||||
app.run("select.set", json!({"ids": [b]})).unwrap();
|
||||
app.run("object.lock", json!({})).unwrap();
|
||||
app.run("select.set", json!({"ids": [c]})).unwrap();
|
||||
app.run("object.hide", json!({})).unwrap();
|
||||
select_all_from(&mut app, a);
|
||||
assert_eq!(text_story(&app), Some(vec![NodeId(a), NodeId(b), NodeId(c)]));
|
||||
|
||||
format(&mut app, json!({"size": 9}));
|
||||
for id in [a, b, c] {
|
||||
assert_eq!(story_sizes(&app, id).iter().filter(|s| (**s - 9.0).abs() > 1e-9).count(), 0, "frame {id} takes the size");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user