mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-25 10:17:02 -06:00
The list fills the panel down to its bottom bar and scrolls, also while a drag nears its top or
bottom edge (in the dock and in a flyout, as the Layers panel does).
Dragging in the list:
- an item (colour, text style or graphic) dropped on another of its kind lands just before or
after it, by the pointer's half, an accent line showing where, and joins that item's group;
- dropped elsewhere on a group (its header too, even collapsed) it goes into that group, and on
the ungrouped items out of its group, the section outlined; with every item grouped, a
"Drop here to take it out of its group" strip shows while one is dragged;
- a group dragged by its header goes before or after another group.
Colours and graphics still drag onto art and the canvas as before.
Engine: library.moveItem takes `before` / `after` (an item of the same kind) to place the item
in its kind's order, which the panel shows; the new library.moveGroup {group, to} moves a group.
Text styles drag as PanelDrag::LibraryItem, groups as PanelDrag::LibraryGroup, and a library
colour's Paint drag names its library item, so the panel tells it from a swatch dragged in.
Tests: reordering and moving in the engine, the commands in the library fuzz test, and panel
tests that drop styles and group headers and check the list scrolls and scrolls while dragging.
Strings translated in the 23 catalogs; docs/mcp.md and the parity row updated.
428 lines
26 KiB
Rust
428 lines
26 KiB
Rust
//! The Libraries panel's libraries (#745): making, renaming and deleting them, adding graphics,
|
||
//! colours and text styles from the selection, using them in another document, and keeping them
|
||
//! in files across sessions.
|
||
|
||
use serde_json::{Value, json};
|
||
|
||
use super::*;
|
||
|
||
fn session() -> Session {
|
||
let mut s = Session::new();
|
||
s.execute("file.new", &json!({"width": 200, "height": 200})).unwrap();
|
||
s
|
||
}
|
||
|
||
fn run(s: &mut Session, id: &str, p: Value) -> Value {
|
||
s.execute(id, &p).unwrap_or_else(|e| panic!("{id} {p}: {e}"))
|
||
}
|
||
|
||
fn objects(s: &Session) -> usize {
|
||
s.doc().unwrap().doc.layers[0].children().map_or(0, |c| c.len())
|
||
}
|
||
|
||
fn hex_of(s: &Session, id: &Value, stroke: bool) -> String {
|
||
let n = s.doc().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().clone();
|
||
let paint = if stroke { n.appearance.stroke().unwrap().paint.clone() } else { n.appearance.fill().unwrap().paint.clone() };
|
||
paint.color().unwrap().to_hex()
|
||
}
|
||
|
||
#[test]
|
||
fn libraries_are_made_renamed_and_deleted() {
|
||
let mut s = session();
|
||
assert_eq!(run(&mut s, "library.list", json!({}))["libraries"], json!([]));
|
||
let id = run(&mut s, "library.create", json!({"name": "Brand"}))["id"].as_str().unwrap().to_string();
|
||
let other = run(&mut s, "library.create", json!({"name": "Brand"}))["id"].as_str().unwrap().to_string();
|
||
assert_ne!(id, other, "two libraries of one name are two files");
|
||
run(&mut s, "library.rename", json!({"library": other, "name": "Client"}));
|
||
let list = run(&mut s, "library.list", json!({}));
|
||
let names: Vec<&str> = list["libraries"].as_array().unwrap().iter().map(|l| l["name"].as_str().unwrap()).collect();
|
||
assert_eq!(names, ["Brand", "Client"]);
|
||
assert!(list["folder"].is_null(), "no folder: libraries last for the session");
|
||
run(&mut s, "library.delete", json!({"library": "client"}));
|
||
assert_eq!(run(&mut s, "library.list", json!({}))["libraries"].as_array().unwrap().len(), 1);
|
||
assert!(s.execute("library.get", &json!({"library": "Client"})).is_err());
|
||
}
|
||
|
||
/// A library's file is named after it, and a Windows device name (`CON`, `NUL`, `COM1`…) gets a
|
||
/// `-` after it, on every platform.
|
||
#[test]
|
||
fn a_library_named_after_a_windows_device_gets_a_dash() {
|
||
let mut s = session();
|
||
let ids: Vec<String> = ["CON", "nul", "Com1", "LPT³", "Console", "COM10"]
|
||
.iter()
|
||
.map(|name| run(&mut s, "library.create", json!({"name": name}))["id"].as_str().unwrap().to_string())
|
||
.collect();
|
||
assert_eq!(ids, ["CON-", "nul-", "Com1-", "LPT³-", "Console", "COM10"]);
|
||
}
|
||
|
||
#[test]
|
||
fn a_graphic_goes_into_a_library_and_comes_out_in_another_document() {
|
||
let mut s = session();
|
||
let id = run(&mut s, "shape.ellipse", json!({"x": 10, "y": 10, "width": 40, "height": 30}))["id"].clone();
|
||
run(&mut s, "paint.setFill", json!({"color": "#cc3300", "ids": [id]}));
|
||
run(&mut s, "select.set", json!({"ids": [id]}));
|
||
// With no library yet, adding makes "My Library".
|
||
let added = run(&mut s, "library.add", json!({"kind": "graphic"}));
|
||
assert_eq!(added["existing"], false);
|
||
let lib = added["library"].as_str().unwrap().to_string();
|
||
let item = added["id"].as_str().unwrap().to_string();
|
||
let got = run(&mut s, "library.get", json!({"library": lib}));
|
||
assert_eq!(got["name"], "My Library");
|
||
let g = &got["graphics"][0];
|
||
// Its size as it draws: the 1 pt stroke included.
|
||
assert!((g["width"].as_f64().unwrap() - 41.0).abs() < 1e-6 && (g["height"].as_f64().unwrap() - 31.0).abs() < 1e-6, "{g}");
|
||
assert!(!g["thumbnail"].as_str().unwrap().is_empty(), "a thumbnail of the art");
|
||
// The same art again is the item already there.
|
||
assert_eq!(run(&mut s, "library.add", json!({"kind": "graphic"}))["existing"], true);
|
||
// Placed in another document, centred where asked, selected, one undo step.
|
||
run(&mut s, "file.new", json!({"width": 300, "height": 300}));
|
||
let placed = run(&mut s, "library.use", json!({"library": lib, "kind": "graphic", "item": item, "center": [150, 100]}));
|
||
let new_id = placed["ids"][0].clone();
|
||
let st = s.doc().unwrap();
|
||
let b = st.doc.node(NodeId(new_id.as_u64().unwrap())).unwrap().geometric_bounds().unwrap();
|
||
assert!((b.center().x - 150.0).abs() < 1e-6 && (b.center().y - 100.0).abs() < 1e-6, "{b:?}");
|
||
assert_eq!(st.selection.objects, vec![NodeId(new_id.as_u64().unwrap())]);
|
||
assert_eq!(hex_of(&s, &new_id, false), "#cc3300");
|
||
assert_eq!(objects(&s), 1);
|
||
run(&mut s, "edit.undo", json!({}));
|
||
assert_eq!(objects(&s), 0);
|
||
// A graphic is removed by id.
|
||
run(&mut s, "library.removeItem", json!({"library": lib, "kind": "graphic", "item": item}));
|
||
assert!(s.execute("library.use", &json!({"library": lib, "kind": "graphic", "item": item})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn colours_come_from_the_selection_and_paint_it() {
|
||
let mut s = session();
|
||
let lib = run(&mut s, "library.create", json!({}))["id"].as_str().unwrap().to_string();
|
||
let a = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 20, "height": 20}))["id"].clone();
|
||
run(&mut s, "paint.setFill", json!({"color": "#123456", "ids": [a]}));
|
||
run(&mut s, "paint.setStroke", json!({"color": "#abcdef", "ids": [a]}));
|
||
run(&mut s, "select.set", json!({"ids": [a]}));
|
||
assert_eq!(run(&mut s, "library.add", json!({"library": lib, "kind": "fillColor"}))["name"], "#123456");
|
||
assert_eq!(run(&mut s, "library.add", json!({"library": lib, "kind": "strokeColor"}))["name"], "#abcdef");
|
||
assert_eq!(run(&mut s, "library.add", json!({"library": lib, "kind": "fillColor"}))["existing"], true);
|
||
let b = run(&mut s, "shape.rectangle", json!({"x": 50, "y": 0, "width": 20, "height": 20}))["id"].clone();
|
||
run(&mut s, "select.set", json!({"ids": [b]}));
|
||
run(&mut s, "library.use", json!({"library": lib, "kind": "fillColor", "item": "#123456"}));
|
||
run(&mut s, "library.use", json!({"library": lib, "kind": "fillColor", "item": "#123456", "to": "stroke"}));
|
||
assert_eq!(hex_of(&s, &b, false), "#123456");
|
||
assert_eq!(hex_of(&s, &b, true), "#123456");
|
||
assert!(s.execute("library.use", &json!({"library": lib, "kind": "fillColor", "item": "#nope"})).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn text_styles_come_from_the_selected_type_and_apply_in_one_step() {
|
||
let mut s = session();
|
||
let lib = run(&mut s, "library.create", json!({}))["id"].as_str().unwrap().to_string();
|
||
assert!(s.execute("library.add", &json!({"library": lib, "kind": "charStyle"})).is_err(), "no type selected");
|
||
let t = run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "Heading", "size": 31}))["id"].clone();
|
||
run(&mut s, "select.set", json!({"ids": [t]}));
|
||
let name = run(&mut s, "library.add", json!({"library": lib, "kind": "charStyle"}))["name"].as_str().unwrap().to_string();
|
||
assert!(name.ends_with("31 pt"), "{name}");
|
||
run(&mut s, "library.add", json!({"library": lib, "kind": "paraStyle"}));
|
||
// Another document: the style is added and applied, and one undo takes both back.
|
||
run(&mut s, "file.new", json!({"width": 200, "height": 200}));
|
||
let u = run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "Body", "size": 9}))["id"].clone();
|
||
run(&mut s, "select.set", json!({"ids": [u]}));
|
||
let undo = s.doc().unwrap().history.undo.len();
|
||
let r = run(&mut s, "library.use", json!({"library": lib, "kind": "charStyle", "item": name}));
|
||
assert_eq!(r["style"], name);
|
||
assert_eq!(s.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||
let size = |s: &Session| match &s.doc().unwrap().doc.node(NodeId(u.as_u64().unwrap())).unwrap().kind {
|
||
vectorcraft_doc::NodeKind::Text(t) => t.runs[0].style.size,
|
||
_ => 0.0,
|
||
};
|
||
assert_eq!(size(&s), 31.0);
|
||
run(&mut s, "edit.undo", json!({}));
|
||
assert_eq!(size(&s), 9.0);
|
||
assert!(s.doc().unwrap().doc.char_styles.iter().all(|c| c.name != name), "the style went with the undo");
|
||
// A style of that name with other attributes in the document: the library's comes in numbered.
|
||
run(&mut s, "charStyle.new", json!({"name": name, "attrs": {"size": 5}}));
|
||
let r = run(&mut s, "library.use", json!({"library": lib, "kind": "charStyle", "item": name}));
|
||
assert_eq!(r["style"], format!("{name} 2"));
|
||
}
|
||
|
||
/// A text style that a library file names with spaces around it comes into the document with the
|
||
/// name trimmed, as New Character Style and New Paragraph Style trim a name, and applies.
|
||
#[test]
|
||
fn a_library_style_named_with_spaces_comes_in_trimmed() {
|
||
let mut s = session();
|
||
let style = |attrs: Value| json!([{"name": " Body ", "attrs": attrs}]);
|
||
let data = json!({"name": "Spaced", "charStyles": style(json!({"size": 31.0})), "paraStyles": style(json!({}))});
|
||
let lib = run(&mut s, "library.import", json!({"data": data.to_string()}))["id"].as_str().unwrap().to_string();
|
||
let t = run(&mut s, "text.create", json!({"x": 10, "y": 50, "text": "Body", "size": 9}))["id"].clone();
|
||
run(&mut s, "select.set", json!({"ids": [t]}));
|
||
for kind in ["charStyle", "paraStyle"] {
|
||
assert_eq!(run(&mut s, "library.use", json!({"library": lib, "kind": kind, "item": " Body "}))["style"], "Body", "{kind}");
|
||
}
|
||
let d = &s.doc().unwrap().doc;
|
||
assert_eq!((d.char_styles[0].name.as_str(), d.para_styles[0].name.as_str()), ("Body", "Body"));
|
||
let vectorcraft_doc::NodeKind::Text(text) = &d.node(NodeId(t.as_u64().unwrap())).unwrap().kind else { panic!("not text") };
|
||
assert_eq!((text.runs[0].style.size, text.runs[0].style.style_name.as_deref()), (31.0, Some("Body")));
|
||
}
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
#[test]
|
||
fn libraries_are_kept_in_files_across_sessions() {
|
||
let dir = std::env::temp_dir().join(format!("vc-libraries-{}", std::process::id()));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
let folder = dir.to_string_lossy().to_string();
|
||
let mut s = session();
|
||
s.libraries.set_dir(Some(folder.clone()));
|
||
let lib = run(&mut s, "library.create", json!({"name": "Kept"}))["id"].as_str().unwrap().to_string();
|
||
let id = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 10, "height": 10}))["id"].clone();
|
||
run(&mut s, "select.set", json!({"ids": [id]}));
|
||
run(&mut s, "library.add", json!({"library": lib, "kind": "graphic", "name": "Square"}));
|
||
run(&mut s, "library.add", json!({"library": lib, "kind": "fillColor"}));
|
||
let mut t = session();
|
||
t.libraries.set_dir(Some(folder.clone()));
|
||
let got = run(&mut t, "library.get", json!({"library": "Kept"}));
|
||
assert_eq!(got["graphics"][0]["name"], "Square");
|
||
assert_eq!(got["colors"].as_array().unwrap().len(), 1);
|
||
run(&mut t, "library.delete", json!({"library": "Kept"}));
|
||
let mut u = session();
|
||
u.libraries.set_dir(Some(folder));
|
||
assert_eq!(run(&mut u, "library.list", json!({}))["libraries"], json!([]), "deleting removes the file");
|
||
// A damaged file is skipped.
|
||
std::fs::create_dir_all(&dir).unwrap();
|
||
std::fs::write(dir.join("Bad.vclibrary"), b"{not json").unwrap();
|
||
let mut v = session();
|
||
v.libraries.set_dir(Some(dir.to_string_lossy().to_string()));
|
||
assert_eq!(run(&mut v, "library.list", json!({}))["libraries"], json!([]));
|
||
let _ = std::fs::remove_dir_all(&dir);
|
||
}
|
||
|
||
/// A library "Brand" holding a graphic, a colour and a character style, each added from the
|
||
/// selection → (session, the graphic's id, the colour's name, the style's name).
|
||
fn brand() -> (Session, String, String, String) {
|
||
let mut s = session();
|
||
run(&mut s, "library.create", json!({"name": "Brand"}));
|
||
let r = run(&mut s, "shape.rectangle", json!({"x": 0, "y": 0, "width": 20, "height": 20}))["id"].clone();
|
||
run(&mut s, "paint.setFill", json!({"color": "#336699", "ids": [r]}));
|
||
run(&mut s, "select.set", json!({"ids": [r]}));
|
||
let g = run(&mut s, "library.add", json!({"kind": "graphic", "name": "Logo"}))["id"].as_str().unwrap().to_string();
|
||
let c = run(&mut s, "library.add", json!({"kind": "fillColor"}))["name"].as_str().unwrap().to_string();
|
||
let t = run(&mut s, "text.create", json!({"x": 10, "y": 80, "text": "Type", "size": 18}))["id"].clone();
|
||
run(&mut s, "select.set", json!({"ids": [t]}));
|
||
let st = run(&mut s, "library.add", json!({"kind": "charStyle"}))["name"].as_str().unwrap().to_string();
|
||
(s, g, c, st)
|
||
}
|
||
|
||
/// The current library's groups as `library.get` reports them: (name, [(kind, item)]).
|
||
fn groups(s: &mut Session) -> Vec<(String, Vec<(String, String)>)> {
|
||
let got = run(s, "library.get", json!({}));
|
||
got["groups"]
|
||
.as_array()
|
||
.unwrap()
|
||
.iter()
|
||
.map(|g| {
|
||
let items = g["items"].as_array().unwrap().iter().map(|r| (r["kind"].as_str().unwrap().into(), r["item"].as_str().unwrap().into()));
|
||
(g["name"].as_str().unwrap().to_string(), items.collect())
|
||
})
|
||
.collect()
|
||
}
|
||
|
||
fn refs(v: &[(&str, &str)]) -> Vec<(String, String)> {
|
||
v.iter().map(|(k, i)| (k.to_string(), i.to_string())).collect()
|
||
}
|
||
|
||
/// Graphics added one after another get ids of their own (the third used to repeat the second's).
|
||
#[test]
|
||
fn every_graphic_gets_an_id_of_its_own() {
|
||
let mut s = session();
|
||
run(&mut s, "library.create", json!({}));
|
||
let mut ids = vec![];
|
||
for x in [0, 30, 60, 90] {
|
||
let r = run(&mut s, "shape.rectangle", json!({"x": x, "y": 0, "width": 10 + x, "height": 10}))["id"].clone();
|
||
ids.push(run(&mut s, "library.add", json!({"kind": "graphic", "ids": [r]}))["id"].as_str().unwrap().to_string());
|
||
}
|
||
assert_eq!(ids, ["graphic", "graphic-2", "graphic-3", "graphic-4"]);
|
||
}
|
||
|
||
/// Items go into user-named groups and back out (#926); deleting a group keeps its items.
|
||
#[test]
|
||
fn items_are_grouped_moved_and_ungrouped() {
|
||
let (mut s, g, c, st) = brand();
|
||
assert!(groups(&mut s).is_empty(), "no groups yet");
|
||
// A group made with items; a graphic is named by its name or its id.
|
||
let r = run(&mut s, "library.createGroup", json!({"name": "Logos", "items": [{"kind": "graphic", "item": "Logo"}]}));
|
||
assert_eq!(r["group"], "Logos");
|
||
assert_eq!(groups(&mut s), [("Logos".into(), refs(&[("graphic", &g)]))]);
|
||
// A name taken (ignoring case) gets a number.
|
||
assert_eq!(run(&mut s, "library.createGroup", json!({"name": "logos"}))["group"], "logos 2");
|
||
run(&mut s, "library.renameGroup", json!({"group": "logos 2", "name": "Palette"}));
|
||
assert!(s.execute("library.renameGroup", &json!({"group": "Palette", "name": "LOGOS"})).is_err(), "another group's name");
|
||
assert!(s.execute("library.renameGroup", &json!({"group": "Nope", "name": "X"})).is_err());
|
||
// Moving an item takes it out of the group it was in; a colour is named as fill or stroke.
|
||
run(&mut s, "library.moveItem", json!({"kind": "strokeColor", "item": c, "group": "palette"}));
|
||
run(&mut s, "library.moveItem", json!({"kind": "charStyle", "item": st, "group": "Palette"}));
|
||
let moved = run(&mut s, "library.moveItem", json!({"kind": "graphic", "item": "Logo", "group": "Palette"}));
|
||
assert_eq!((moved["item"].as_str(), moved["group"].as_str()), (Some(g.as_str()), Some("Palette")));
|
||
assert_eq!(groups(&mut s), [("Logos".into(), vec![]), ("Palette".into(), refs(&[("color", &c), ("charStyle", &st), ("graphic", &g)]))]);
|
||
// Without a group, out of its group.
|
||
assert!(run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": c}))["group"].is_null());
|
||
assert!(s.execute("library.moveItem", &json!({"kind": "fillColor", "item": c, "group": "Nope"})).is_err());
|
||
assert!(s.execute("library.moveItem", &json!({"kind": "fillColor", "item": "#nope", "group": "Palette"})).is_err());
|
||
assert!(s.execute("library.createGroup", &json!({"items": [{"kind": "graphic", "item": "nope"}]})).is_err(), "no such item");
|
||
assert_eq!(groups(&mut s).len(), 2, "a failed command makes no group");
|
||
// A deleted item leaves its group.
|
||
run(&mut s, "library.removeItem", json!({"kind": "charStyle", "item": st}));
|
||
assert_eq!(groups(&mut s)[1].1, refs(&[("graphic", &g)]));
|
||
// A deleted group's items stay, ungrouped.
|
||
assert_eq!(run(&mut s, "library.deleteGroup", json!({"group": "Palette"}))["ungrouped"], 1);
|
||
assert_eq!(run(&mut s, "library.get", json!({}))["graphics"].as_array().unwrap().len(), 1);
|
||
assert_eq!(groups(&mut s), [("Logos".into(), vec![])]);
|
||
assert_eq!(run(&mut s, "library.list", json!({}))["libraries"][0]["groups"], 1);
|
||
}
|
||
|
||
/// The names of the current library's `list` ("colors", "graphics"…), in their order.
|
||
fn order(s: &mut Session, list: &str, key: &str) -> Vec<String> {
|
||
let got = run(s, "library.get", json!({}));
|
||
got[list].as_array().unwrap().iter().map(|v| v[key].as_str().unwrap().to_string()).collect()
|
||
}
|
||
|
||
/// Dragging in the Libraries panel: an item moves just before or after another of its kind, into
|
||
/// that one's group or out of every group, and groups move to another place in the list.
|
||
#[test]
|
||
fn items_and_groups_are_reordered() {
|
||
let mut s = session();
|
||
run(&mut s, "library.create", json!({"name": "Brand"}));
|
||
for (name, color) in [("Red", "#ff0000"), ("Green", "#00ff00"), ("Blue", "#0000ff"), ("Gray", "#808080")] {
|
||
run(&mut s, "library.add", json!({"kind": "fillColor", "color": color, "name": name}));
|
||
}
|
||
// Before an item, and after one; on itself it stays.
|
||
run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Blue", "before": "Red"}));
|
||
assert_eq!(order(&mut s, "colors", "name"), ["Blue", "Red", "Green", "Gray"]);
|
||
run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Blue", "after": "Gray"}));
|
||
assert_eq!(order(&mut s, "colors", "name"), ["Red", "Green", "Gray", "Blue"]);
|
||
run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Red", "before": "Red"}));
|
||
assert_eq!(order(&mut s, "colors", "name"), ["Red", "Green", "Gray", "Blue"]);
|
||
// Into a group next to an item there: the group lists it next to that one too.
|
||
run(
|
||
&mut s,
|
||
"library.createGroup",
|
||
json!({"name": "Cool", "items": [{"kind": "fillColor", "item": "Green"}, {"kind": "fillColor", "item": "Blue"}]}),
|
||
);
|
||
let r = run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Gray", "group": "Cool", "before": "Blue"}));
|
||
assert_eq!(r["group"], "Cool");
|
||
assert_eq!(order(&mut s, "colors", "name"), ["Red", "Green", "Gray", "Blue"]);
|
||
assert_eq!(groups(&mut s), [("Cool".into(), refs(&[("color", "Green"), ("color", "Gray"), ("color", "Blue")]))]);
|
||
// Next to an ungrouped item, without a group: out of its group.
|
||
run(&mut s, "library.moveItem", json!({"kind": "fillColor", "item": "Blue", "before": "Red"}));
|
||
assert_eq!(order(&mut s, "colors", "name"), ["Blue", "Red", "Green", "Gray"]);
|
||
assert_eq!(groups(&mut s), [("Cool".into(), refs(&[("color", "Green"), ("color", "Gray")]))]);
|
||
// Wrong asks change nothing.
|
||
assert!(s.execute("library.moveItem", &json!({"kind": "fillColor", "item": "Red", "before": "Nope"})).is_err());
|
||
assert!(s.execute("library.moveItem", &json!({"kind": "fillColor", "item": "Red", "before": "Blue", "after": "Gray"})).is_err());
|
||
assert!(s.execute("library.moveItem", &json!({"kind": "charStyle", "item": "Red", "before": "Blue"})).is_err(), "not a style");
|
||
assert_eq!(order(&mut s, "colors", "name"), ["Blue", "Red", "Green", "Gray"]);
|
||
// Groups: to an index, past the end to the last place.
|
||
run(&mut s, "library.createGroup", json!({"name": "Warm"}));
|
||
run(&mut s, "library.createGroup", json!({"name": "Neutral"}));
|
||
let names = |s: &mut Session| groups(s).into_iter().map(|(n, _)| n).collect::<Vec<_>>();
|
||
assert_eq!(run(&mut s, "library.moveGroup", json!({"group": "neutral", "to": 0}))["to"], 0);
|
||
assert_eq!(names(&mut s), ["Neutral", "Cool", "Warm"]);
|
||
assert_eq!(run(&mut s, "library.moveGroup", json!({"group": "Neutral", "to": 99}))["to"], 2);
|
||
assert_eq!(names(&mut s), ["Cool", "Warm", "Neutral"]);
|
||
assert!(s.execute("library.moveGroup", &json!({"group": "Nope", "to": 0})).is_err());
|
||
assert!(s.execute("library.moveGroup", &json!({"group": "Cool", "to": -1})).is_err());
|
||
assert!(s.execute("library.moveGroup", &json!({"group": "Cool"})).is_err());
|
||
assert_eq!(groups(&mut s)[0].1, refs(&[("color", "Green"), ("color", "Gray")]), "a group keeps its items when it moves");
|
||
}
|
||
|
||
/// A library goes out to a file with its art and groups and comes back as a new library, numbered
|
||
/// when its name is taken; the graphic it brings places in a document (#926).
|
||
#[test]
|
||
fn a_library_is_exported_and_imported() {
|
||
let (mut s, g, c, _) = brand();
|
||
run(&mut s, "library.createGroup", json!({"name": "Logos", "items": [{"kind": "graphic", "item": g}, {"kind": "fillColor", "item": c}]}));
|
||
let out = run(&mut s, "library.export", json!({}));
|
||
assert_eq!(out["name"], "Brand");
|
||
let data = out["data"].as_str().unwrap().to_string();
|
||
let r = run(&mut s, "library.import", json!({"data": data}));
|
||
let counts = ["graphics", "colors", "charStyles", "paraStyles", "groups"].map(|k| r[k].as_u64().unwrap());
|
||
assert_eq!((r["name"].as_str(), counts), (Some("Brand (2)"), [1, 1, 1, 0, 1]));
|
||
let id = r["id"].as_str().unwrap().to_string();
|
||
assert_eq!(run(&mut s, "library.list", json!({}))["current"], id, "the imported library is the current one");
|
||
let a = run(&mut s, "library.get", json!({"library": "Brand"}));
|
||
let b = run(&mut s, "library.get", json!({"library": id}));
|
||
for k in ["graphics", "colors", "charStyles", "paraStyles", "groups"] {
|
||
assert_eq!(a[k], b[k], "{k}");
|
||
}
|
||
// Again: the next number; given a name, that name.
|
||
let b64 = vectorcraft_format::base64_encode(data.as_bytes());
|
||
assert_eq!(run(&mut s, "library.import", json!({"dataBase64": b64}))["name"], "Brand (3)");
|
||
assert_eq!(run(&mut s, "library.import", json!({"data": data, "name": "Client"}))["name"], "Client");
|
||
// Its graphic places in another document.
|
||
run(&mut s, "file.new", json!({"width": 100, "height": 100}));
|
||
let placed = run(&mut s, "library.use", json!({"library": id, "kind": "graphic", "item": "Logo", "center": [50, 50]}));
|
||
assert_eq!(placed["ids"].as_array().unwrap().len(), 1);
|
||
// Not a library file.
|
||
let swatches = json!({"format": "vcswatches", "name": "S"}).to_string();
|
||
for bad in [json!({"data": "{not json"}), json!({"data": "[1, 2]"}), json!({"data": swatches}), json!({"dataBase64": "%%"}), json!({})] {
|
||
assert!(s.execute("library.import", &bad).is_err(), "{bad}");
|
||
}
|
||
}
|
||
|
||
/// A file written before groups loads; one with repeated names, groups naming items that aren't
|
||
/// there or kinds this version doesn't know, and a thumbnail claiming a huge image is made safe.
|
||
#[test]
|
||
fn imported_libraries_are_made_consistent() {
|
||
let mut s = session();
|
||
let old = json!({"name": "Old", "colors": [{"name": "Red", "color": {"model": "rgb", "r": 1, "g": 0, "b": 0}}]});
|
||
let r = run(&mut s, "library.import", json!({"data": old.to_string()}));
|
||
assert_eq!((r["colors"].as_u64(), r["groups"].as_u64()), (Some(1), Some(0)));
|
||
// A PNG header claiming 100000 × 100000 pixels.
|
||
let mut png = b"\x89PNG\r\n\x1a\n\0\0\0\x0dIHDR".to_vec();
|
||
png.extend(100_000u32.to_be_bytes());
|
||
png.extend(100_000u32.to_be_bytes());
|
||
png.extend([8, 6, 0, 0, 0]);
|
||
let messy = json!({
|
||
"name": " ",
|
||
"colors": [{"name": "Red", "color": {"model": "rgb", "r": 1, "g": 0, "b": 0}}, {"name": "Red", "color": {"model": "rgb", "r": 0, "g": 1, "b": 0}}, {"name": "", "color": {"model": "rgb", "r": 0, "g": 0, "b": 1}}],
|
||
"graphics": [
|
||
{"id": "g", "name": "A", "width": 1, "height": 1, "data": "", "thumbnail": vectorcraft_format::base64_encode(&png)},
|
||
{"id": "g", "name": "B", "width": -5, "height": 1, "data": ""}
|
||
],
|
||
"groups": [
|
||
{"name": "One", "items": [{"kind": "color", "item": "Red 2"}, {"kind": "graphic", "item": "nope"}, {"kind": "brush", "item": "Red"}]},
|
||
{"name": "one", "items": [{"kind": "color", "item": "Red 2"}, {"kind": "graphic", "item": "g-2"}]}
|
||
]
|
||
});
|
||
let r = run(&mut s, "library.import", json!({"data": messy.to_string()}));
|
||
assert_eq!(r["name"], "Library", "a blank name");
|
||
let got = run(&mut s, "library.get", json!({}));
|
||
let names: Vec<&str> = got["colors"].as_array().unwrap().iter().map(|c| c["name"].as_str().unwrap()).collect();
|
||
assert_eq!(names, ["Red", "Red 2", "Color"]);
|
||
let graphics = got["graphics"].as_array().unwrap();
|
||
assert_eq!((graphics[0]["id"].as_str(), graphics[1]["id"].as_str()), (Some("g"), Some("g-2")));
|
||
assert_eq!((graphics[0]["thumbnail"].as_str(), graphics[1]["width"].as_f64()), (Some(""), Some(0.0)));
|
||
assert_eq!(
|
||
got["groups"],
|
||
json!([{"name": "One", "items": [{"kind": "color", "item": "Red 2"}]}, {"name": "one 2", "items": [{"kind": "graphic", "item": "g-2"}]}])
|
||
);
|
||
}
|
||
|
||
#[cfg(not(target_arch = "wasm32"))]
|
||
#[test]
|
||
fn a_library_is_exported_to_a_file_and_imported_from_it() {
|
||
let dir = vectorcraft_testkit::temp_dir("library-export");
|
||
let path = dir.join("Brand.vclibrary").to_string_lossy().to_string();
|
||
let (mut s, g, ..) = brand();
|
||
run(&mut s, "library.createGroup", json!({"name": "Logos", "items": [{"kind": "graphic", "item": g}]}));
|
||
assert_eq!(run(&mut s, "library.export", json!({"library": "Brand", "path": path}))["path"], path.as_str());
|
||
let mut t = session();
|
||
let r = run(&mut t, "library.import", json!({"path": path}));
|
||
assert_eq!((r["name"].as_str(), r["graphics"].as_u64(), r["groups"].as_u64()), (Some("Brand"), Some(1), Some(1)));
|
||
assert!(t.execute("library.import", &json!({"path": dir.join("missing.vclibrary").to_string_lossy()})).is_err());
|
||
// Groups are kept in the library folder's files too.
|
||
let folder = dir.join("folder").to_string_lossy().to_string();
|
||
let mut u = session();
|
||
u.libraries.set_dir(Some(folder.clone()));
|
||
run(&mut u, "library.import", json!({"path": path}));
|
||
let mut v = session();
|
||
v.libraries.set_dir(Some(folder));
|
||
assert_eq!(groups(&mut v), [("Logos".into(), refs(&[("graphic", &g)]))]);
|
||
}
|