diff --git a/crates/engine/src/code_connect_tests.rs b/crates/engine/src/code_connect_tests.rs
index 1ee785e6..24e03e4b 100644
--- a/crates/engine/src/code_connect_tests.rs
+++ b/crates/engine/src/code_connect_tests.rs
@@ -65,37 +65,47 @@ fn connected_instances_show_the_real_snippet() {
);
assert_eq!(code(&mut s, i, "swiftui"), "import AcmeUI\n\nCard(title: \"Say \\\"hi\\\"\", style: .plain)\n");
assert!(code(&mut s, i, "compose").contains("@Composable"), "other languages stay generated");
+ let screen = leptos_arkui_and_trees(&mut s, c, i);
+ rejects_bad_links(&mut s, c, screen);
+ disconnect_then_undo(&mut s, c, i);
+}
+
+fn leptos_arkui_and_trees(s: &mut Session, c: NodeId, i: NodeId) -> NodeId {
// Leptos: a `use` line and `view!` markup (strings with escapes in braces).
ex(
- &mut s,
+ s,
"codeConnect.set",
json!({"id": c, "language": "leptos", "importPath": "crate::ui", "template": ""}),
);
- assert_eq!(code(&mut s, i, "leptos"), "use crate::ui::Card;\n\n\n");
+ assert_eq!(code(s, i, "leptos"), "use crate::ui::Card;\n\n\n");
// ArkUI: an ArkTS import and an object-literal call.
ex(
- &mut s,
+ s,
"codeConnect.set",
json!({"id": c, "language": "arkui", "importPath": "@acme/arkui", "template": "Card({ title: {title}, dotted: {Show dot} })"}),
);
- assert_eq!(code(&mut s, i, "arkui"), "import { Card } from \"@acme/arkui\";\n\nCard({ title: \"Say \\\"hi\\\"\", dotted: false })\n");
- let v = ex(&mut s, "inspect.componentCode", json!({"id": i, "language": "arkui"}));
+ assert_eq!(code(s, i, "arkui"), "import { Card } from \"@acme/arkui\";\n\nCard({ title: \"Say \\\"hi\\\"\", dotted: false })\n");
+ let v = ex(s, "inspect.componentCode", json!({"id": i, "language": "arkui"}));
assert_eq!(v["usage"], "Card({ title: \"Say \\\"hi\\\"\", dotted: false })\n");
assert_eq!(v["connected"]["import"], "import { Card } from \"@acme/arkui\";");
assert!(s.execute("codeConnect.set", &json!({"id": c, "language": "leptos", "importPath": "crate/ui"})).is_err(), "a Rust path");
- ex(&mut s, "codeConnect.remove", json!({"id": c, "language": "leptos"}));
+ ex(s, "codeConnect.remove", json!({"id": c, "language": "leptos"}));
// inspect.componentCode: the snippet as the usage, and the link.
- let v = ex(&mut s, "inspect.componentCode", json!({"id": i, "language": "swiftui"}));
+ let v = ex(s, "inspect.componentCode", json!({"id": i, "language": "swiftui"}));
assert_eq!(v["usage"], "Card(title: \"Say \\\"hi\\\"\", style: .plain)\n");
assert_eq!(v["connected"]["import"], "import AcmeUI");
- assert_eq!(ex(&mut s, "inspect.componentCode", json!({"id": i, "language": "compose"}))["connected"], Value::Null);
+ assert_eq!(ex(s, "inspect.componentCode", json!({"id": i, "language": "compose"}))["connected"], Value::Null);
// Inside a generated layer tree, connected instances are their snippet.
- let screen = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 500, "width": 400, "height": 300, "name": "Screen"})));
- ex(&mut s, "instance.create", json!({"component": c, "x": 20, "y": 30, "parent": screen}));
- assert!(code(&mut s, screen, "react").contains("•\n"));
+ let screen = idv(&ex(s, "frame.create", json!({"x": 0, "y": 500, "width": 400, "height": 300, "name": "Screen"})));
+ ex(s, "instance.create", json!({"component": c, "x": 20, "y": 30, "parent": screen}));
+ assert!(code(s, screen, "react").contains("•\n"));
// Literal braces stay; fields left out keep their value.
- ex(&mut s, "codeConnect.set", json!({"id": c, "template": ""}));
- assert_eq!(code(&mut s, c, "react"), "import { Card } from \"@acme/ui\";\n\n\n");
+ ex(s, "codeConnect.set", json!({"id": c, "template": ""}));
+ assert_eq!(code(s, c, "react"), "import { Card } from \"@acme/ui\";\n\n\n");
+ screen
+}
+
+fn rejects_bad_links(s: &mut Session, c: NodeId, screen: NodeId) {
// Validation.
for bad in [
json!({"id": c, "template": "{title ? \"a\"}"}),
@@ -109,13 +119,16 @@ fn connected_instances_show_the_real_snippet() {
] {
assert!(s.execute("codeConnect.set", &bad).is_err(), "{bad}");
}
+}
+
+fn disconnect_then_undo(s: &mut Session, c: NodeId, i: NodeId) {
// Disconnect one language; undo brings it back.
- ex(&mut s, "codeConnect.remove", json!({"id": i, "language": "react"}));
- assert!(code(&mut s, i, "react").contains("export function Card({"));
- assert!(code(&mut s, i, "swiftui").starts_with("import AcmeUI"));
- ex(&mut s, "edit.undo", json!({}));
- assert!(code(&mut s, i, "react").starts_with("import { Card }"));
- assert_eq!(ex(&mut s, "codeConnect.remove", json!({"id": c}))["removed"], 3);
+ ex(s, "codeConnect.remove", json!({"id": i, "language": "react"}));
+ assert!(code(s, i, "react").contains("export function Card({"));
+ assert!(code(s, i, "swiftui").starts_with("import AcmeUI"));
+ ex(s, "edit.undo", json!({}));
+ assert!(code(s, i, "react").starts_with("import { Card }"));
+ assert_eq!(ex(s, "codeConnect.remove", json!({"id": c}))["removed"], 3);
assert!(s.execute("codeConnect.remove", &json!({"id": c})).is_err(), "nothing left");
}
diff --git a/crates/engine/src/derived.rs b/crates/engine/src/derived.rs
index 765e3be5..b6a001b8 100644
--- a/crates/engine/src/derived.rs
+++ b/crates/engine/src/derived.rs
@@ -1,4 +1,4 @@
-use super::*;
+use super::{Arc, BTreeSet, Document, Node, NodeId, board, cmd, components, measure, style_vars};
/// Nodes whose `Arc` differs from `before` (new or edited).
pub fn changed_nodes(doc: &Document, before: &Document) -> Vec {
diff --git a/crates/engine/src/doc_state.rs b/crates/engine/src/doc_state.rs
index 4f9509a7..a9cad9c9 100644
--- a/crates/engine/src/doc_state.rs
+++ b/crates/engine/src/doc_state.rs
@@ -1,5 +1,5 @@
use super::view_state::ViewState;
-use super::*;
+use super::{Arc, BTreeSet, Deserialize, Document, NodeId, Serialize, cmd, components, file_access, guides, library, rotation_origin, vectoredit};
/// One undo step.
#[derive(Clone, Debug)]
diff --git a/crates/engine/src/error.rs b/crates/engine/src/error.rs
index 7f2ab1d4..7f26258f 100644
--- a/crates/engine/src/error.rs
+++ b/crates/engine/src/error.rs
@@ -1,4 +1,4 @@
-use super::*;
+use super::NodeId;
#[derive(Debug, thiserror::Error)]
pub enum EngineError {
diff --git a/crates/engine/src/keymap_tests.rs b/crates/engine/src/keymap_tests.rs
index f624ecfb..bdf57c9d 100644
--- a/crates/engine/src/keymap_tests.rs
+++ b/crates/engine/src/keymap_tests.rs
@@ -95,6 +95,11 @@ fn key_names_normalise() {
#[test]
fn lookups_follow_the_platform_and_context() {
+ lookups_on_every_platform();
+ lookups_that_differ_by_platform();
+}
+
+fn lookups_on_every_platform() {
let none = Mods::default();
let shift = mods(true, false, false, false);
for p in PLATFORMS {
@@ -119,6 +124,9 @@ fn lookups_follow_the_platform_and_context() {
assert_eq!(id_for(p, Context::Text, "V", none), None);
assert_eq!(id_for(p, Context::Text, "A", mods(false, true, false, false)), None);
}
+}
+
+fn lookups_that_differ_by_platform() {
let mac = Platform::Mac;
let other = Platform::Other;
// Use as mask: ⌃⌘M on macOS, Ctrl+Alt+M elsewhere.
@@ -133,6 +141,10 @@ fn lookups_follow_the_platform_and_context() {
assert_eq!(id_for(mac, Context::Canvas, "T", mods(false, true, false, true)), Some("arrange.tidy"));
assert_eq!(id_for(other, Context::Canvas, "T", mods(true, true, true, true)), Some("arrange.tidy"));
assert_eq!(id_for(other, Context::Canvas, "T", mods(false, true, true, true)), Some("text.alignCenter"));
+ macos_tells_ctrl_from_cmd(mac, other);
+}
+
+fn macos_tells_ctrl_from_cmd(mac: Platform, other: Platform) {
// macOS tells ⌃ from ⌘: ⌃G is layout grids, ⌘G groups; ⌃C picks a colour, ⌘C copies.
assert_eq!(id_for(mac, Context::Canvas, "G", mods(false, false, false, true)), Some("view.layoutGrids"));
assert_eq!(id_for(mac, Context::Canvas, "G", mods(false, false, true, false)), Some("object.group"));
diff --git a/crates/engine/src/library_variables_tests.rs b/crates/engine/src/library_variables_tests.rs
index b4a4171a..f61bab29 100644
--- a/crates/engine/src/library_variables_tests.rs
+++ b/crates/engine/src/library_variables_tests.rs
@@ -122,6 +122,65 @@ fn publishing_collections_hides_and_versions_variables() {
assert!(empty.execute("library.publish", &json!({})).is_err());
}
+/// `bind_alias_mode_read_only_and_updates`: every edit of a copy's variable or collection is refused.
+fn assert_copies_read_only(s: &mut Session, local: &str) {
+ let coll = copy_of(s, "primary").collection;
+ for (cmd, p) in [
+ ("variable.set", json!({"variable": local, "value": "#ff0000"})),
+ ("variable.update", json!({"variable": local, "name": "mine"})),
+ ("variable.delete", json!({"variable": local})),
+ ("variable.duplicate", json!({"variable": local})),
+ ("variable.create", json!({"collection": coll, "name": "extra", "type": "number", "value": 1})),
+ ("variable.addMode", json!({"collection": coll, "name": "Mine"})),
+ ("variable.renameMode", json!({"collection": coll, "mode": "Dark", "name": "Night"})),
+ ("variable.renameCollection", json!({"collection": coll, "name": "Mine"})),
+ ("variable.deleteCollection", json!({"collection": coll})),
+ ] {
+ let e = s.execute(cmd, &p).unwrap_err();
+ assert!(e.to_string().contains("can't be edited"), "{cmd}: {e}");
+ }
+}
+
+/// `bind_alias_mode_read_only_and_updates`: saved and reopened, the file renders without the library.
+fn reopened_without_library(s: &mut Session, d: &std::path::Path, lib_path: &str, lid: &str, outer: NodeId) {
+ // Saved and reopened, the copies, records and bindings stay; the file renders without the library.
+ let user_path = path_str(&d.join("app.figcraft"));
+ ex(s, "file.saveAs", json!({"path": user_path}));
+ std::fs::remove_file(lib_path).unwrap();
+ let mut t = Session::empty();
+ ex(&mut t, "file.open", json!({"path": user_path}));
+ assert_eq!(t.doc().unwrap().libraries, s.doc().unwrap().libraries);
+ assert_eq!(fill(&t, outer), "FF0000");
+ assert!(t.execute("library.bindVariable", &json!({"library": lid, "variable": "primary", "field": "fill", "ids": [outer]})).is_err());
+}
+
+/// `bind_alias_mode_read_only_and_updates`: the library changes its variables; the copies, bindings and alias follow.
+fn library_update_follows(s: &mut Session, lib: &mut Session, local: &str, [inner, outer]: [NodeId; 2]) {
+ let modes = Default::default();
+ // The library changes its variables and publishes again.
+ ex(lib, "variable.set", json!({"variable": "primary", "mode": "Light", "value": "#ff0000"}));
+ ex(lib, "variable.set", json!({"variable": "primary", "mode": "Dark", "value": "#222222"}));
+ ex(lib, "variable.set", json!({"variable": "base", "value": "#00ffff"}));
+ ex(lib, "variable.renameMode", json!({"collection": "Theme", "mode": "Dark", "name": "Night"}));
+ republish(lib);
+ let u = ex(s, "library.updates", json!({}));
+ assert_eq!(u["count"], 3, "primary, base and the collection: {u}");
+ assert_eq!(u["libraries"][0]["variables"].as_array().map(Vec::len), Some(2));
+ let r = ex(s, "library.acceptUpdates", json!({}));
+ assert_eq!((r["variables"].as_u64(), r["collections"].as_u64()), (Some(2), Some(1)), "{r}");
+ // Same copies (same ids), new values: the bound layers and the alias follow.
+ assert_eq!(copy_of(s, "primary").id, local);
+ assert_eq!((fill(s, inner), fill(s, outer)), ("222222".into(), "FF0000".into()));
+ assert!(matches!(s.doc().unwrap().variables.resolve("brand", &modes), Some(VariableValue::Color(c)) if c.to_hex() == "00FFFF"));
+ assert_eq!(s.doc().unwrap().variables.collections[0].modes[1].name, "Night");
+ assert_eq!(ex(s, "library.updates", json!({}))["count"], 0);
+ // Undo takes the update back.
+ ex(s, "edit.undo", json!({}));
+ assert_eq!(fill(s, outer), "0000FF");
+ ex(s, "edit.redo", json!({}));
+ healthy(s);
+}
+
#[test]
fn bind_alias_mode_read_only_and_updates() {
let d = dir("flow");
@@ -180,57 +239,14 @@ fn bind_alias_mode_read_only_and_updates() {
assert!(s.execute("library.aliasVariable", &json!({"library": lid, "alias": "primary", "variable": local})).is_err(), "copies are read-only");
// The copies are read-only.
- let coll = copy_of(&s, "primary").collection;
- for (cmd, p) in [
- ("variable.set", json!({"variable": local, "value": "#ff0000"})),
- ("variable.update", json!({"variable": local, "name": "mine"})),
- ("variable.delete", json!({"variable": local})),
- ("variable.duplicate", json!({"variable": local})),
- ("variable.create", json!({"collection": coll, "name": "extra", "type": "number", "value": 1})),
- ("variable.addMode", json!({"collection": coll, "name": "Mine"})),
- ("variable.renameMode", json!({"collection": coll, "mode": "Dark", "name": "Night"})),
- ("variable.renameCollection", json!({"collection": coll, "name": "Mine"})),
- ("variable.deleteCollection", json!({"collection": coll})),
- ] {
- let e = s.execute(cmd, &p).unwrap_err();
- assert!(e.to_string().contains("can't be edited"), "{cmd}: {e}");
- }
+ assert_copies_read_only(&mut s, &local);
// Local edits still work.
ex(&mut s, "variable.set", json!({"variable": "gap", "value": 3}));
healthy(&s);
- // The library changes its variables and publishes again.
- ex(&mut lib, "variable.set", json!({"variable": "primary", "mode": "Light", "value": "#ff0000"}));
- ex(&mut lib, "variable.set", json!({"variable": "primary", "mode": "Dark", "value": "#222222"}));
- ex(&mut lib, "variable.set", json!({"variable": "base", "value": "#00ffff"}));
- ex(&mut lib, "variable.renameMode", json!({"collection": "Theme", "mode": "Dark", "name": "Night"}));
- republish(&mut lib);
- let u = ex(&mut s, "library.updates", json!({}));
- assert_eq!(u["count"], 3, "primary, base and the collection: {u}");
- assert_eq!(u["libraries"][0]["variables"].as_array().map(Vec::len), Some(2));
- let r = ex(&mut s, "library.acceptUpdates", json!({}));
- assert_eq!((r["variables"].as_u64(), r["collections"].as_u64()), (Some(2), Some(1)), "{r}");
- // Same copies (same ids), new values: the bound layers and the alias follow.
- assert_eq!(copy_of(&s, "primary").id, local);
- assert_eq!((fill(&s, inner), fill(&s, outer)), ("222222".into(), "FF0000".into()));
- assert!(matches!(s.doc().unwrap().variables.resolve("brand", &modes), Some(VariableValue::Color(c)) if c.to_hex() == "00FFFF"));
- assert_eq!(s.doc().unwrap().variables.collections[0].modes[1].name, "Night");
- assert_eq!(ex(&mut s, "library.updates", json!({}))["count"], 0);
- // Undo takes the update back.
- ex(&mut s, "edit.undo", json!({}));
- assert_eq!(fill(&s, outer), "0000FF");
- ex(&mut s, "edit.redo", json!({}));
- healthy(&s);
+ library_update_follows(&mut s, &mut lib, &local, [inner, outer]);
- // Saved and reopened, the copies, records and bindings stay; the file renders without the library.
- let user_path = path_str(&d.join("app.figcraft"));
- ex(&mut s, "file.saveAs", json!({"path": user_path}));
- std::fs::remove_file(&lib_path).unwrap();
- let mut t = Session::empty();
- ex(&mut t, "file.open", json!({"path": user_path}));
- assert_eq!(t.doc().unwrap().libraries, s.doc().unwrap().libraries);
- assert_eq!(fill(&t, outer), "FF0000");
- assert!(t.execute("library.bindVariable", &json!({"library": lid, "variable": "primary", "field": "fill", "ids": [outer]})).is_err());
+ reopened_without_library(&mut s, &d, &lib_path, &lid, outer);
// Removing the library keeps the copies in use and drops the rest.
ex(&mut s, "variable.bind", json!({"ids": [outer], "field": "cornerRadius", "variable": null}));
diff --git a/crates/engine/src/live_docs_tests.rs b/crates/engine/src/live_docs_tests.rs
index 7380cc9b..df805266 100644
--- a/crates/engine/src/live_docs_tests.rs
+++ b/crates/engine/src/live_docs_tests.rs
@@ -62,7 +62,6 @@ fn each_document_has_its_own_session() {
let ua = uid(&s);
// A new document isn't in the first one's session.
run(&mut s, "file.new", json!({}));
- let ub = uid(&s);
let off = run(&mut s, "collab.peers", json!({}));
for key in ["session", "role", "link", "url", "address", "fileName", "document", "me"] {
assert_eq!(off[key], Value::Null, "{key}: {off}");
@@ -78,8 +77,12 @@ fn each_document_has_its_own_session() {
assert!(s.live_mut().is_none() && s.active_read_only().is_none());
assert_eq!(run(&mut s, "collab.stop", json!({}))["ended"], false, "nothing to stop here");
assert_eq!(s.live_of(ua).status, Status::Live, "and the other file's session isn't stopped");
+ hosting_a_second_session(&mut s, &script, &a, ua);
+}
+
+fn hosting_a_second_session(s: &mut Session, script: &Arc>, a: &Value, ua: u64) {
// Hosting it too starts a second session; the first goes on.
- let b = run(&mut s, "collab.host", json!({}));
+ let b = run(s, "collab.host", json!({}));
assert_ne!(a["session"], b["session"]);
assert_ne!(a["link"], b["link"]);
assert_eq!((b["document"].clone(), b["sessions"].as_array().unwrap().len()), (json!(1), 2));
@@ -87,33 +90,38 @@ fn each_document_has_its_own_session() {
let sc = script.lock().unwrap();
assert_eq!((sc.hosted.len(), sc.left), (2, 0));
}
+ people_come_and_go(s, script, a, &b, ua);
+}
+
+fn people_come_and_go(s: &mut Session, script: &Arc>, a: &Value, b: &Value, ua: u64) {
+ let ub = uid(s);
// People come and go in their own session.
- let (a_id, b_id) = (id_of(&a), id_of(&b));
- feed(&mut s, &script, a_id, vec![Event::PeerJoined(peer(5, "Ada"))]);
- feed(&mut s, &script, b_id, vec![Event::PeerJoined(peer(6, "Bo")), Event::PeerJoined(peer(7, "Cy"))]);
+ let (a_id, b_id) = (id_of(a), id_of(b));
+ feed(s, script, a_id, vec![Event::PeerJoined(peer(5, "Ada"))]);
+ feed(s, script, b_id, vec![Event::PeerJoined(peer(6, "Bo")), Event::PeerJoined(peer(7, "Cy"))]);
assert_eq!((s.live_of(ua).peers.len(), s.live_of(ub).peers.len()), (1, 2));
let names = |v: &Value| v["peers"].as_array().unwrap().iter().map(|p| p["name"].as_str().unwrap().to_string()).collect::>();
let p = s.live_summary();
assert_eq!((names(&p), &p["link"]), (vec!["Bo".to_string(), "Cy".to_string()], &b["link"]));
assert_eq!((&p["sessions"][0]["peers"], &p["sessions"][1]["peers"]), (&json!(1), &json!(2)));
// The file in front decides what `collab.peers` and the commands are about.
- run(&mut s, "doc.switch", json!({"index": 0}));
- let p = run(&mut s, "collab.peers", json!({}));
+ run(s, "doc.switch", json!({"index": 0}));
+ let p = run(s, "collab.peers", json!({}));
assert_eq!((names(&p), &p["link"], &p["document"], &p["session"]), (vec!["Ada".to_string()], &a["link"], &json!(0), &a["session"]));
- assert_eq!(run(&mut s, "collab.follow", json!({"client": 5}))["follow"], 5);
+ assert_eq!(run(s, "collab.follow", json!({"client": 5}))["follow"], 5);
assert!(s.execute("collab.follow", &json!({"client": 6})).is_err(), "Bo is in the other file's session");
- run(&mut s, "doc.switch", json!({"index": 1}));
+ run(s, "doc.switch", json!({"index": 1}));
assert!(s.execute("collab.follow", &json!({"client": 5})).is_err());
assert_eq!((s.live().follow, s.live_of(ua).follow), (None, Some(5)));
// Stopping ends this file's session only.
- assert_eq!(run(&mut s, "collab.stop", json!({}))["ended"], true);
+ assert_eq!(run(s, "collab.stop", json!({}))["ended"], true);
assert_eq!(script.lock().unwrap().ended, [b_id]);
assert_eq!((s.live().status.clone(), s.live_of(ua).status.clone()), (Status::Off, Status::Live));
// Someone ending or leaving the other one doesn't bring this one back.
- feed(&mut s, &script, b_id, vec![Event::PeerJoined(peer(8, "Late"))]);
+ feed(s, script, b_id, vec![Event::PeerJoined(peer(8, "Late"))]);
assert_eq!((s.live.iter().count(), s.live_of(ua).peers.len()), (1, 1), "what a session still sent after it ended is dropped");
// Closing a file ends its session.
- run(&mut s, "file.close", json!({"index": 0, "force": true}));
+ run(s, "file.close", json!({"index": 0, "force": true}));
s.live_sync();
assert_eq!(script.lock().unwrap().ended, [b_id, a_id]);
assert_eq!(s.live.iter().count(), 0);
diff --git a/crates/engine/src/live_share_tests.rs b/crates/engine/src/live_share_tests.rs
index f486250f..452eb3e8 100644
--- a/crates/engine/src/live_share_tests.rs
+++ b/crates/engine/src/live_share_tests.rs
@@ -185,6 +185,38 @@ fn the_built_in_server_is_the_default_and_can_be_changed_or_turned_off() {
assert_eq!(old.publish_server, DEFAULT_SERVER);
}
+/// The middle of the online-sharing test: re-asking switches codes, and an offline stop stops nothing.
+fn asking_again_and_offline_stop(s: &mut Session, script: &Arc>) {
+ // Asking again only switches which code is the one to give out.
+ let r = ex(s, "collab.share", json!({"access": "view"}));
+ assert_eq!((&r["code"], &r["share"]["access"]), (&json!("4FH-J8R-2WN"), &json!("view")));
+ assert_eq!(ex(s, "collab.share", json!({"via": "online"}))["code"], "4FH-J8R-2WN", "nothing new starts");
+ assert_eq!(ex(s, "collab.share", json!({"access": "edit"}))["code"], "K7M-2QX-9TD");
+ assert_eq!(script.lock().unwrap().published.len(), 1);
+ assert!(err(s, "collab.share", json!({"via": "local"})).contains("stop sharing it first"));
+ assert!(err(s, "collab.share", json!({"access": "admin"})).contains("`access` must be"));
+ // Stopping while the server can't be reached stops nothing, and says so.
+ script.lock().unwrap().offline = true;
+ assert!(err(s, "collab.stop", json!({})).contains("couldn't stop sharing"));
+ assert_eq!(ex(s, "collab.peers", json!({}))["code"], "K7M-2QX-9TD");
+ script.lock().unwrap().offline = false;
+}
+
+/// The sharing-again step of the online-sharing test: new links and codes, nothing published twice.
+fn sharing_again(s: &mut Session, script: &Arc>) {
+ // Sharing again: new links and codes for the same file, nothing published twice.
+ let r = ex(s, "collab.share", json!({}));
+ assert_eq!((&r["code"], &r["share"]["shared"], &r["share"]["viewCode"]), (&json!("NEW-ED1-T01"), &json!(true), &json!("NEW-V1E-W01")));
+ assert!(r["share"]["editLink"].as_str().unwrap().contains("ShareeditShareedit01"));
+ {
+ let sc = script.lock().unwrap();
+ assert_eq!(sc.published.len(), 1);
+ assert_eq!(sc.asked[1], ("wss://share.example/".to_string(), "f1".to_string(), "OwnerOwnerOwnerOwner00".to_string(), vec![], true));
+ }
+ assert_eq!(ex(s, "collab.share", json!({}))["code"], "NEW-ED1-T01", "shared already: nothing more is asked");
+ assert_eq!(script.lock().unwrap().asked.len(), 2);
+}
+
#[test]
fn sharing_online_publishes_to_the_server_in_preferences_and_shows_its_code() {
let (mut s, script) = session();
@@ -212,19 +244,7 @@ fn sharing_online_publishes_to_the_server_in_preferences_and_shows_its_code() {
let (edit_link, own) = (r["share"]["editLink"].as_str().unwrap(), r["published"]["edit"].as_str().unwrap());
assert!(edit_link.contains("ShareeditShareedit00") && edit_link != own && r["link"] == own, "{edit_link}");
assert!(s.message.is_none(), "the popover shows the code: no toast about publishing");
- // Asking again only switches which code is the one to give out.
- let r = ex(&mut s, "collab.share", json!({"access": "view"}));
- assert_eq!((&r["code"], &r["share"]["access"]), (&json!("4FH-J8R-2WN"), &json!("view")));
- assert_eq!(ex(&mut s, "collab.share", json!({"via": "online"}))["code"], "4FH-J8R-2WN", "nothing new starts");
- assert_eq!(ex(&mut s, "collab.share", json!({"access": "edit"}))["code"], "K7M-2QX-9TD");
- assert_eq!(script.lock().unwrap().published.len(), 1);
- assert!(err(&mut s, "collab.share", json!({"via": "local"})).contains("stop sharing it first"));
- assert!(err(&mut s, "collab.share", json!({"access": "admin"})).contains("`access` must be"));
- // Stopping while the server can't be reached stops nothing, and says so.
- script.lock().unwrap().offline = true;
- assert!(err(&mut s, "collab.stop", json!({})).contains("couldn't stop sharing"));
- assert_eq!(ex(&mut s, "collab.peers", json!({}))["code"], "K7M-2QX-9TD");
- script.lock().unwrap().offline = false;
+ asking_again_and_offline_stop(&mut s, &script);
// Stopping: the server is asked, as the file's owner, to revoke the others' links. The file
// stays there and its owner stays in it: nothing is left, nothing is removed.
assert_eq!(ex(&mut s, "collab.stop", json!({})), json!({"ended": false, "shared": false}));
@@ -248,17 +268,7 @@ fn sharing_online_publishes_to_the_server_in_preferences_and_shows_its_code() {
);
assert!(r["published"]["owner"].is_string(), "still the owner's file on the server");
assert!(s.message.as_deref().unwrap_or_default().contains("stays on the server"));
- // Sharing again: new links and codes for the same file, nothing published twice.
- let r = ex(&mut s, "collab.share", json!({}));
- assert_eq!((&r["code"], &r["share"]["shared"], &r["share"]["viewCode"]), (&json!("NEW-ED1-T01"), &json!(true), &json!("NEW-V1E-W01")));
- assert!(r["share"]["editLink"].as_str().unwrap().contains("ShareeditShareedit01"));
- {
- let sc = script.lock().unwrap();
- assert_eq!(sc.published.len(), 1);
- assert_eq!(sc.asked[1], ("wss://share.example/".to_string(), "f1".to_string(), "OwnerOwnerOwnerOwner00".to_string(), vec![], true));
- }
- assert_eq!(ex(&mut s, "collab.share", json!({}))["code"], "NEW-ED1-T01", "shared already: nothing more is asked");
- assert_eq!(script.lock().unwrap().asked.len(), 2);
+ sharing_again(&mut s, &script);
// Stopping again revokes the new ones; then `collab.stop` on a file that isn't shared leaves
// its session like any other (the file stays on the server).
ex(&mut s, "collab.stop", json!({}));
diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs
index 6bfedcba..ced1793f 100644
--- a/crates/engine/src/prefs.rs
+++ b/crates/engine/src/prefs.rs
@@ -1,4 +1,4 @@
-use super::*;
+use super::{Arc, BTreeMap, Deserialize, Document, NodeId, Serialize, Value, cmd, live, online, plugins};
/// The smallest and largest interface size, relative to the system's display scale (1.0 is the
/// system's own size).
diff --git a/crates/engine/src/proto_conditional_tests.rs b/crates/engine/src/proto_conditional_tests.rs
index e40aea5d..b5441262 100644
--- a/crates/engine/src/proto_conditional_tests.rs
+++ b/crates/engine/src/proto_conditional_tests.rs
@@ -71,41 +71,51 @@ fn build_if_else_if_else_with_nested_actions() {
assert_eq!(shown(&s, bl[1].condition.as_ref().unwrap()), "open == false");
assert_eq!(shown(&s, bl[2].condition.as_ref().unwrap()), "label == \"Hi\"");
assert!(bl[3].condition.is_none(), "the else stays last");
+ actions_in_the_branches(&mut s, b, btn);
+ reorder_replace_and_remove(&mut s, b, btn);
+}
+
+fn actions_in_the_branches(s: &mut Session, b: NodeId, btn: NodeId) {
+ let ex = |s: &mut Session, c: &str, p: Value| s.execute(c, &p).unwrap();
// Actions inside the branches: a set variable with an expression in the if, a back in the else.
let r = ex(
- &mut s,
+ s,
"proto.action.add",
json!({"id": btn, "index": 0, "path": [1, 0, 0], "action": {"type": "setVariable", "variable": "count", "expression": "count + 1"}}),
);
assert_eq!(r["path"], json!([1, 0, 0]));
- ex(&mut s, "proto.action.add", json!({"id": btn, "index": 0, "path": [1, 3, 0], "action": {"type": "back"}}));
- ex(&mut s, "proto.action.add", json!({"id": btn, "index": 0, "path": [1, 3, 1], "action": {"type": "navigate", "dest": b}}));
- let list = actions(&s, btn);
+ ex(s, "proto.action.add", json!({"id": btn, "index": 0, "path": [1, 3, 0], "action": {"type": "back"}}));
+ ex(s, "proto.action.add", json!({"id": btn, "index": 0, "path": [1, 3, 1], "action": {"type": "navigate", "dest": b}}));
+ let list = actions(s, btn);
let Some(Action::SetVariable { variable, expr: Some(e), .. }) = action_at(&list, &[1, 0, 0]) else { panic!("{list:?}") };
- assert_eq!(variable, &var(&s, "count"));
- assert_eq!(shown(&s, e), "count + 1");
+ assert_eq!(variable, &var(s, "count"));
+ assert_eq!(shown(s, e), "count + 1");
assert_eq!(action_at(&list, &[1, 3, 0]), Some(&Action::Back));
+}
+
+fn reorder_replace_and_remove(s: &mut Session, b: NodeId, btn: NodeId) {
+ let ex = |s: &mut Session, c: &str, p: Value| s.execute(c, &p).unwrap();
// Reorder inside the else, replace one, change a condition, remove a block.
- assert_eq!(ex(&mut s, "proto.action.move", json!({"id": btn, "index": 0, "path": [1, 3, 1], "to": 0}))["path"], json!([1, 3, 0]));
- assert!(matches!(action_at(&actions(&s, btn), &[1, 3, 0]), Some(Action::Navigate { .. })));
- ex(&mut s, "proto.action.set", json!({"id": btn, "index": 0, "path": [1, 3, 1], "action": {"type": "closeOverlay"}}));
- assert_eq!(action_at(&actions(&s, btn), &[1, 3, 1]), Some(&Action::CloseOverlay));
- ex(&mut s, "proto.conditional.setCondition", json!({"id": btn, "index": 0, "path": [1], "block": 1, "expression": "not open and count < 10"}));
- assert_eq!(shown(&s, blocks(&actions(&s, btn)[1])[1].condition.as_ref().unwrap()), "(not open) and (count < 10)");
- ex(&mut s, "proto.conditional.removeBlock", json!({"id": btn, "index": 0, "path": [1], "block": 2}));
- assert_eq!(blocks(&actions(&s, btn)[1]).len(), 3);
- ex(&mut s, "proto.action.remove", json!({"id": btn, "index": 0, "path": [1, 2, 1]}));
- assert_eq!(blocks(&actions(&s, btn)[1])[2].actions, [Action::Navigate { dest: b, preserve_scroll: false }]);
+ assert_eq!(ex(s, "proto.action.move", json!({"id": btn, "index": 0, "path": [1, 3, 1], "to": 0}))["path"], json!([1, 3, 0]));
+ assert!(matches!(action_at(&actions(s, btn), &[1, 3, 0]), Some(Action::Navigate { .. })));
+ ex(s, "proto.action.set", json!({"id": btn, "index": 0, "path": [1, 3, 1], "action": {"type": "closeOverlay"}}));
+ assert_eq!(action_at(&actions(s, btn), &[1, 3, 1]), Some(&Action::CloseOverlay));
+ ex(s, "proto.conditional.setCondition", json!({"id": btn, "index": 0, "path": [1], "block": 1, "expression": "not open and count < 10"}));
+ assert_eq!(shown(s, blocks(&actions(s, btn)[1])[1].condition.as_ref().unwrap()), "(not open) and (count < 10)");
+ ex(s, "proto.conditional.removeBlock", json!({"id": btn, "index": 0, "path": [1], "block": 2}));
+ assert_eq!(blocks(&actions(s, btn)[1]).len(), 3);
+ ex(s, "proto.action.remove", json!({"id": btn, "index": 0, "path": [1, 2, 1]}));
+ assert_eq!(blocks(&actions(s, btn)[1])[2].actions, [Action::Navigate { dest: b, preserve_scroll: false }]);
// Every edit is one undo step.
assert!(s.undo().unwrap());
- assert_eq!(blocks(&actions(&s, btn)[1])[2].actions.len(), 2);
+ assert_eq!(blocks(&actions(s, btn)[1])[2].actions.len(), 2);
// Removing the if leaves the else if as the if; removing that leaves only the else: the
// conditional goes.
- let r = ex(&mut s, "proto.conditional.removeBlock", json!({"id": btn, "index": 0, "path": [1], "block": 0}));
+ let r = ex(s, "proto.conditional.removeBlock", json!({"id": btn, "index": 0, "path": [1], "block": 0}));
assert_eq!(r["removedAction"], false);
- let r = ex(&mut s, "proto.conditional.removeBlock", json!({"id": btn, "index": 0, "path": [1], "block": 0}));
+ let r = ex(s, "proto.conditional.removeBlock", json!({"id": btn, "index": 0, "path": [1], "block": 0}));
assert_eq!(r["removedAction"], true);
- assert_eq!(actions(&s, btn), [Action::Navigate { dest: b, preserve_scroll: false }]);
+ assert_eq!(actions(s, btn), [Action::Navigate { dest: b, preserve_scroll: false }]);
}
#[test]
diff --git a/crates/engine/src/selection/selection_colors.rs b/crates/engine/src/selection/selection_colors.rs
index 56bc4b35..00c7389f 100644
--- a/crates/engine/src/selection/selection_colors.rs
+++ b/crates/engine/src/selection/selection_colors.rs
@@ -11,7 +11,7 @@ use serde_json::{Value, json};
use crate::cmd::lists::{BoundList, list_binding, rebind_edited_paint};
use crate::cmd::styles::rebind_paint_alpha;
-use crate::params::*;
+use crate::params::{get, paint_of, parse, req_string, targets};
use crate::{CommandSpec, Result, Session, bad, spec};
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
diff --git a/crates/engine/src/session.rs b/crates/engine/src/session.rs
index acafd229..a31114e2 100644
--- a/crates/engine/src/session.rs
+++ b/crates/engine/src/session.rs
@@ -2,7 +2,10 @@ use super::derived::normalize;
use super::doc_state::{DocState, ENDS_PREVIEW, HistoryEntry, Interaction, PREVIEW_READ_ONLY};
use super::error::{EngineError, Result, bad};
use super::prefs::{Clipboard, Prefs, UiRequest};
-use super::*;
+use super::{
+ Arc, BatchScope, BatchStep, CommandSpec, Document, Node, NodeId, Session, Value, background, board, branches, cmd, components, figcraft_collab,
+ find_command, guard, json, library, live, params, plugins, textpreview, tools,
+};
mod access;
mod exec;
diff --git a/crates/engine/src/session/access.rs b/crates/engine/src/session/access.rs
index af9fd7f5..05bc45d5 100644
--- a/crates/engine/src/session/access.rs
+++ b/crates/engine/src/session/access.rs
@@ -1,4 +1,4 @@
-use super::*;
+use super::{DocState, Document, EngineError, NodeId, Result, Session, Value, json};
impl Session {
pub fn ds(&self) -> Result<&DocState> {
diff --git a/crates/engine/src/session/exec.rs b/crates/engine/src/session/exec.rs
index add12ac5..00798b2c 100644
--- a/crates/engine/src/session/exec.rs
+++ b/crates/engine/src/session/exec.rs
@@ -1,4 +1,7 @@
-use super::*;
+use super::{
+ Arc, CommandSpec, Document, ENDS_PREVIEW, EngineError, Node, NodeId, PREVIEW_READ_ONLY, Result, Session, Value, bad, board, branches, cmd,
+ components, find_command, guard, library, normalize, params, textpreview,
+};
/// Whether a command, or a batch wrapping one, imports a file passed as Base64 `data`.
fn command_carries_import_bytes(id: &str, params: &Value) -> bool {
diff --git a/crates/engine/src/session/new.rs b/crates/engine/src/session/new.rs
index 8790c83b..96bf0c2d 100644
--- a/crates/engine/src/session/new.rs
+++ b/crates/engine/src/session/new.rs
@@ -1,4 +1,4 @@
-use super::*;
+use super::{Arc, Clipboard, DocState, Document, Prefs, Session, background, cmd, library, live, plugins, tools};
impl Session {
/// A session with one new document.
diff --git a/crates/engine/src/session/undo.rs b/crates/engine/src/session/undo.rs
index a354ea27..5fd0ec80 100644
--- a/crates/engine/src/session/undo.rs
+++ b/crates/engine/src/session/undo.rs
@@ -1,4 +1,7 @@
-use super::*;
+use super::{
+ Arc, BatchScope, BatchStep, DocState, Document, EngineError, HistoryEntry, Interaction, NodeId, PREVIEW_READ_ONLY, Result, Session, UiRequest,
+ Value, cmd, figcraft_collab, normalize, textpreview,
+};
impl Session {
/// How many undo and redo steps the active document has (the user's own in a live session).
diff --git a/crates/engine/src/sharing/live/events.rs b/crates/engine/src/sharing/live/events.rs
index 5b6008f8..9d851cab 100644
--- a/crates/engine/src/sharing/live/events.rs
+++ b/crates/engine/src/sharing/live/events.rs
@@ -1,6 +1,9 @@
use super::host::{clean_hosting, clean_shared};
use super::types::{Event, PageInfo, Role, SessionId, Status};
-use super::*;
+use super::{
+ Access, Arc, Client, ClientMsg, DocState, Document, EngineError, Live, NodeId, PUBLISHING, Peer, Presence, Result, ServerMsg, Session, Signal,
+ ToolId, VIEW_ONLY, editing_hint, editing_of, sanitize,
+};
impl Session {
/// The active document (`None`: no document is open).
diff --git a/crates/engine/src/sharing/live/host.rs b/crates/engine/src/sharing/live/host.rs
index 685ab5c0..06f6df95 100644
--- a/crates/engine/src/sharing/live/host.rs
+++ b/crates/engine/src/sharing/live/host.rs
@@ -1,5 +1,8 @@
use super::types::{Bind, HostOptions, NetReply, NetRequest, ONLINE_OFF, PublishOptions, Role, SessionId, Share, Shared, SharedLink, Status, Via};
-use super::*;
+use super::{
+ Access, Admin, AdminReply, ClientMsg, EngineError, HostInfo, IpAddr, Live, Result, Session, Value, bad, code, display_name, get, json,
+ req_string, string,
+};
/// What a server says about its hosting, made safe to show: no control characters, short, and a
/// terms address only when it is a web page (`http(s)://`).
diff --git a/crates/engine/src/sharing/live/join.rs b/crates/engine/src/sharing/live/join.rs
index 972198ef..e08afcc6 100644
--- a/crates/engine/src/sharing/live/join.rs
+++ b/crates/engine/src/sharing/live/join.rs
@@ -1,6 +1,8 @@
use super::host::{host, hosting_request, manage_file, publish, report, server_info, share, share_answer, share_request, shared_now, unavailable};
use super::types::{Published, Role, SessionId, Share, Status, join_target};
-use super::*;
+use super::{
+ Admin, AdminReply, CommandSpec, EngineError, Live, Result, Resume, Session, Value, bad, display_name, get, json, num, req_string, spec, string,
+};
fn join(s: &mut Session, p: &Value) -> Result {
const CMD: &str = "collab.join";
diff --git a/crates/engine/src/sharing/live/sync.rs b/crates/engine/src/sharing/live/sync.rs
index f2fd3c54..3920bdca 100644
--- a/crates/engine/src/sharing/live/sync.rs
+++ b/crates/engine/src/sharing/live/sync.rs
@@ -1,6 +1,9 @@
use super::host::hosting_json;
use super::types::{NO_SESSION, SessionId, Share, SharedLink, Status};
-use super::*;
+use super::{
+ Arc, Client, EditingText, EngineError, EngineRecompute, FollowedView, Live, Result, Session, Value, ViewState, bad, busy, close, code,
+ editing_of, json,
+};
impl Session {
/// Your client id in document `uid`'s live session (`None`: it isn't in one). Refused for
diff --git a/crates/engine/src/sharing/live/types.rs b/crates/engine/src/sharing/live/types.rs
index 16a237d5..efeecac9 100644
--- a/crates/engine/src/sharing/live/types.rs
+++ b/crates/engine/src/sharing/live/types.rs
@@ -1,4 +1,7 @@
-use super::*;
+use super::{
+ Access, Admin, AdminReply, Arc, ClientMsg, Code, Document, HostInfo, IpAddr, LibraryReply, LibraryReq, NO_TEAM_LIBRARIES, Peer, PluginReply,
+ PluginReq, Presence, Resume, Serialize, ServerMsg, Signal, code,
+};
/// The server files are shared through online unless Preferences name another: the only place
/// its address is written.
diff --git a/crates/engine/src/slot_tests.rs b/crates/engine/src/slot_tests.rs
index cbf824d9..01a5db08 100644
--- a/crates/engine/src/slot_tests.rs
+++ b/crates/engine/src/slot_tests.rs
@@ -99,6 +99,28 @@ fn slots_are_frames_of_a_main_with_a_slot_property() {
healthy(&s);
}
+/// `instances_fill_slots_with_their_own_content`: resetting a slot, or the whole instance, restores the default content.
+fn reset_slot_content(s: &mut Session, [a, aslot, ph, ichip, chip]: [NodeId; 5]) {
+ // Reset: back to the main's default content.
+ ex(s, "instance.resetSlot", json!({"id": aslot}));
+ healthy(s);
+ assert!(!slot_edited(s.doc().unwrap(), aslot));
+ assert_eq!(kids(s, aslot).len(), 2);
+ assert_eq!(fill(s, copy_of(s, a, ph)), "0000FF");
+ assert!(!s.doc().unwrap().contains(ichip));
+ s.undo().unwrap();
+ assert!(s.doc().unwrap().contains(ichip));
+ healthy(s);
+ // Reset all changes resets slots too; `replace` clears the default content first.
+ ex(s, "component.resetInstance", json!({"ids": [a]}));
+ assert!(!slot_edited(s.doc().unwrap(), aslot));
+ let only = idv(&ex(s, "instance.slotInsert", json!({"id": aslot, "component": chip, "replace": true})));
+ assert_eq!(kids(s, aslot), vec![only]);
+ ex(s, "component.resetOverride", json!({"id": aslot, "group": "slot"}));
+ assert_eq!(kids(s, aslot).len(), 2);
+ healthy(s);
+}
+
#[test]
fn instances_fill_slots_with_their_own_content() {
let mut s = Session::new();
@@ -158,23 +180,7 @@ fn instances_fill_slots_with_their_own_content() {
// Layers of the instance can't move into its slot either.
let atitle = copy_of(&s, a, title);
refused(&mut s, "node.reparent", json!({"ids": [atitle], "parent": aslot}));
- // Reset: back to the main's default content.
- ex(&mut s, "instance.resetSlot", json!({"id": aslot}));
- healthy(&s);
- assert!(!slot_edited(s.doc().unwrap(), aslot));
- assert_eq!(kids(&s, aslot).len(), 2);
- assert_eq!(fill(&s, copy_of(&s, a, ph)), "0000FF");
- assert!(!s.doc().unwrap().contains(ichip));
- s.undo().unwrap();
- assert!(s.doc().unwrap().contains(ichip));
- healthy(&s);
- // Reset all changes resets slots too; `replace` clears the default content first.
- ex(&mut s, "component.resetInstance", json!({"ids": [a]}));
- assert!(!slot_edited(s.doc().unwrap(), aslot));
- let only = idv(&ex(&mut s, "instance.slotInsert", json!({"id": aslot, "component": chip, "replace": true})));
- assert_eq!(kids(&s, aslot), vec![only]);
- ex(&mut s, "component.resetOverride", json!({"id": aslot, "group": "slot"}));
- assert_eq!(kids(&s, aslot).len(), 2);
+ reset_slot_content(&mut s, [a, aslot, ph, ichip, chip]);
healthy(&s);
}
diff --git a/crates/engine/src/vector_multi_behaviour_tests.rs b/crates/engine/src/vector_multi_behaviour_tests.rs
index 2afaf9a2..ef337af8 100644
--- a/crates/engine/src/vector_multi_behaviour_tests.rs
+++ b/crates/engine/src/vector_multi_behaviour_tests.rs
@@ -95,6 +95,14 @@ fn selected_in(s: &Session, id: NodeId) -> Vec {
#[test]
fn editing_several_vectors_at_once() {
+ several_vectors_enter_edit_mode();
+ let (mut s, a, b, atl, btl) = points_across_both_layers();
+ arrows_and_drags_move_every_layer(&mut s, a, b, atl, btl);
+ corner_radius_mirroring_and_delete(&mut s, a, b, atl, btl);
+ bend_select_all_pen_and_clicks(&mut s, a, b);
+}
+
+fn several_vectors_enter_edit_mode() {
// (how edit mode is entered with two shapes selected, derivation)
let rows: Vec<(&str, &str)> = vec![
("command", "the edit command takes every selected shape"),
@@ -135,8 +143,9 @@ fn editing_several_vectors_at_once() {
crate::vectoredit::enter_targets(s.doc().unwrap(), &[a, f]).is_none(),
"silent: a frame in the selection leaves Enter to select its children (pinned)"
);
+}
- // Points across both layers.
+fn points_across_both_layers() -> (Session, NodeId, NodeId, usize, usize) {
let mut s = Session::new();
let (a, b) = (rect(&mut s, 0.0), rect(&mut s, 200.0));
ex(&mut s, "vector.edit.begin", json!({"ids": [a, b]}));
@@ -161,64 +170,74 @@ fn editing_several_vectors_at_once() {
sb.sort_unstable();
assert_eq!((editing(&s).unwrap().id, selected_in(&s, a), selected_in(&s, b)), (active, sa, sb), "{why}");
}
+ (s, a, b, atl, btl)
+}
+
+fn arrows_and_drags_move_every_layer(s: &mut Session, a: NodeId, b: NodeId, atl: usize, btl: usize) {
// Arrows nudge every selected point, one undo step for all layers.
- ex(&mut s, "vector.edit.select", json!({"vertices": [atl]}));
- ex(&mut s, "vector.edit.select", json!({"id": b, "vertices": [btl], "add": true}));
- assert_eq!((selected_in(&s, a), selected_in(&s, b)), (vec![atl], vec![btl]), "the select command picks points in a named layer");
+ ex(s, "vector.edit.select", json!({"vertices": [atl]}));
+ ex(s, "vector.edit.select", json!({"id": b, "vertices": [btl], "add": true}));
+ assert_eq!((selected_in(s, a), selected_in(s, b)), (vec![atl], vec![btl]), "the select command picks points in a named layer");
assert!(s.tool_key("Right", false, false, false).unwrap());
- assert!(near(page_pos(&s, a, atl), (1.0, 0.0)) && near(page_pos(&s, b, btl), (201.0, 0.0)), "an arrow nudges the points in every layer");
+ assert!(near(page_pos(s, a, atl), (1.0, 0.0)) && near(page_pos(s, b, btl), (201.0, 0.0)), "an arrow nudges the points in every layer");
s.undo().unwrap();
- assert!(near(page_pos(&s, a, atl), (0.0, 0.0)) && near(page_pos(&s, b, btl), (200.0, 0.0)), "one undo step for every layer");
+ assert!(near(page_pos(s, a, atl), (0.0, 0.0)) && near(page_pos(s, b, btl), (200.0, 0.0)), "one undo step for every layer");
// Dragging one selected point drags them all (one on the transform box's edge: its corners
// scale, as with points of one layer).
- let atr = point_at(&s, a, (100.0, 0.0));
- ex(&mut s, "vector.edit.select", json!({"id": a, "vertices": [atr], "add": true}));
- drag(&mut s, (100.0, 0.0), (110.0, 20.0), NONE);
+ let atr = point_at(s, a, (100.0, 0.0));
+ ex(s, "vector.edit.select", json!({"id": a, "vertices": [atr], "add": true}));
+ drag(s, (100.0, 0.0), (110.0, 20.0), NONE);
assert!(
- near(page_pos(&s, a, atl), (10.0, 20.0)) && near(page_pos(&s, a, atr), (110.0, 20.0)) && near(page_pos(&s, b, btl), (210.0, 20.0)),
+ near(page_pos(s, a, atl), (10.0, 20.0)) && near(page_pos(s, a, atr), (110.0, 20.0)) && near(page_pos(s, b, btl), (210.0, 20.0)),
"dragging one selected point moves them all"
);
s.undo().unwrap();
- assert!(near(page_pos(&s, a, atl), (0.0, 0.0)) && near(page_pos(&s, b, btl), (200.0, 0.0)), "the drag is one undo step");
- ex(&mut s, "vector.edit.select", json!({"vertices": [atl]}));
- ex(&mut s, "vector.edit.select", json!({"id": b, "vertices": [btl], "add": true}));
+ assert!(near(page_pos(s, a, atl), (0.0, 0.0)) && near(page_pos(s, b, btl), (200.0, 0.0)), "the drag is one undo step");
+ ex(s, "vector.edit.select", json!({"vertices": [atl]}));
+ ex(s, "vector.edit.select", json!({"id": b, "vertices": [btl], "add": true}));
+}
+
+fn corner_radius_mirroring_and_delete(s: &mut Session, a: NodeId, b: NodeId, atl: usize, btl: usize) {
// Corner radius, mirroring and deleting reach every layer's selected points.
- ex(&mut s, "vector.edit.cornerRadius", json!({"radius": 10}));
- assert!(net(&s, a).vertices[atl].radius == 10.0 && net(&s, b).vertices[btl].radius == 10.0, "the corner radius applies to every layer");
- ex(&mut s, "vector.edit.mirroring", json!({"mode": "angle"}));
+ ex(s, "vector.edit.cornerRadius", json!({"radius": 10}));
+ assert!(net(s, a).vertices[atl].radius == 10.0 && net(s, b).vertices[btl].radius == 10.0, "the corner radius applies to every layer");
+ ex(s, "vector.edit.mirroring", json!({"mode": "angle"}));
assert!(
- net(&s, a).vertices[atl].mirror == Some(figcraft_doc::HandleMirroring::Angle)
- && net(&s, b).vertices[btl].mirror == Some(figcraft_doc::HandleMirroring::Angle),
+ net(s, a).vertices[atl].mirror == Some(figcraft_doc::HandleMirroring::Angle)
+ && net(s, b).vertices[btl].mirror == Some(figcraft_doc::HandleMirroring::Angle),
"mirroring applies to every layer"
);
assert!(s.tool_key("Delete", false, false, false).unwrap());
- assert_eq!((net(&s, a).vertices.len(), net(&s, b).vertices.len()), (3, 3), "Delete removes the selected point in every layer");
+ assert_eq!((net(s, a).vertices.len(), net(s, b).vertices.len()), (3, 3), "Delete removes the selected point in every layer");
s.undo().unwrap();
- assert_eq!((net(&s, a).vertices.len(), net(&s, b).vertices.len()), (4, 4), "one undo step");
+ assert_eq!((net(s, a).vertices.len(), net(s, b).vertices.len()), (4, 4), "one undo step");
+}
+
+fn bend_select_all_pen_and_clicks(s: &mut Session, a: NodeId, b: NodeId) {
// The Bend tool (held with Cmd/Ctrl) bends the segment under the pointer, in its own layer.
- drag(&mut s, (250.0, 100.0), (250.0, 130.0), CMD);
- assert!(net(&s, b).segments.iter().any(|sg| !sg.is_straight()), "the bend lands on the layer under the pointer");
- assert!(net(&s, a).segments.iter().all(|sg| sg.is_straight()), "other layers keep their segments");
+ drag(s, (250.0, 100.0), (250.0, 130.0), CMD);
+ assert!(net(s, b).segments.iter().any(|sg| !sg.is_straight()), "the bend lands on the layer under the pointer");
+ assert!(net(s, a).segments.iter().all(|sg| sg.is_straight()), "other layers keep their segments");
// Select all takes every edited layer's points.
- let r = ex(&mut s, "vector.edit.selectAll", json!({}));
+ let r = ex(s, "vector.edit.selectAll", json!({}));
assert_eq!(r["count"], json!(8), "silent: select all takes the points of every edited layer (pinned)");
// The pen draws in the active layer only.
- ex(&mut s, "vector.edit.select", json!({"id": a, "vertices": []}));
- ex(&mut s, "vector.edit.tool", json!({"tool": "pen"}));
- click(&mut s, (50.0, 300.0), NONE);
- assert_eq!((net(&s, a).vertices.len(), net(&s, b).vertices.len()), (5, 4), "silent: the pen adds to the active layer (pinned)");
- ex(&mut s, "vector.edit.penEnd", json!({}));
- ex(&mut s, "vector.edit.tool", json!({"tool": "move"}));
+ ex(s, "vector.edit.select", json!({"id": a, "vertices": []}));
+ ex(s, "vector.edit.tool", json!({"tool": "pen"}));
+ click(s, (50.0, 300.0), NONE);
+ assert_eq!((net(s, a).vertices.len(), net(s, b).vertices.len()), (5, 4), "silent: the pen adds to the active layer (pinned)");
+ ex(s, "vector.edit.penEnd", json!({}));
+ ex(s, "vector.edit.tool", json!({"tool": "move"}));
// A click on empty canvas inside an edited layer's area keeps edit mode; so does one far
// away, which only clears the points (a double-click or Escape leaves).
- click(&mut s, (250.0, 50.0), NONE);
- assert!(editing(&s).is_some(), "a click inside an edited layer clears the points and stays");
- let st = ex(&mut s, "vector.edit.state", json!({}));
+ click(s, (250.0, 50.0), NONE);
+ assert!(editing(s).is_some(), "a click inside an edited layer clears the points and stays");
+ let st = ex(s, "vector.edit.state", json!({}));
assert_eq!(st["layers"].as_array().map(Vec::len), Some(2), "the state lists every edited layer");
- click(&mut s, (900.0, 900.0), NONE);
- assert!(editing(&s).is_some(), "a click away from every edited layer keeps edit mode");
+ click(s, (900.0, 900.0), NONE);
+ assert!(editing(s).is_some(), "a click away from every edited layer keeps edit mode");
s.pointer(&ev(PointerKind::DoubleClick, (900.0, 900.0), NONE)).unwrap();
- assert!(editing(&s).is_none(), "a double-click away from every edited layer leaves edit mode");
+ assert!(editing(s).is_none(), "a double-click away from every edited layer leaves edit mode");
}
#[test]
diff --git a/crates/engine/src/view_state.rs b/crates/engine/src/view_state.rs
index e162f039..54102d6b 100644
--- a/crates/engine/src/view_state.rs
+++ b/crates/engine/src/view_state.rs
@@ -1,4 +1,4 @@
-use super::*;
+use super::{Deserialize, Serialize};
/// The canvas view of a document: what's at the centre of the viewport and the zoom.
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]