mirror of
https://github.com/storytold/figcraft.git
synced 2026-11-13 03:44:26 +00:00
In Figma's data a component set holds one text property per variant (plus leftovers of deleted variants), and an instance lists only the ones its current variant's layers read (measured in Figma's web app). FigCraft listed every definition on every instance, and wrapped a long property name inside an 84 x 20 box so its lines painted over the rows around it. - components::instance_prop_list: prop_list keeping slots and the properties a layer of the component references (a nested instance counts, its layers and slot content are not entered). component.properties and the Properties panel use it for instances; sets, components and variants still list every definition. - prop_label cuts the name to one line with an ellipsis and shows the full name on hover. - Tests: an_instance_lists_only_the_properties_its_variant_uses, a_long_property_name_stays_on_one_line, and the pin a_set_keeps_each_named_text_definition_and_layers_read_their_own. Two tests and one setup that listed an unbound boolean on an instance now bind it to a layer. - docs/components-parity.md: the properties row, header and revision history. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Model-Effort: Opus 5.5 drove; Fable 5.1 briefed; Sonnet 5.5 wrote at medium
161 lines
9.0 KiB
Rust
161 lines
9.0 KiB
Rust
//! Component properties keep the order they were made in: renaming one never moves it (#1536),
|
|
//! and Make slot takes the name it is given (#1537).
|
|
|
|
use figcraft_doc::{NodeId, NodeKind};
|
|
use serde_json::{Value, json};
|
|
|
|
use crate::Session;
|
|
|
|
fn ex(s: &mut Session, cmd: &str, p: Value) -> Value {
|
|
s.execute(cmd, &p).unwrap_or_else(|e| panic!("{cmd} {p}: {e}"))
|
|
}
|
|
|
|
fn idv(v: &Value) -> NodeId {
|
|
NodeId(v["id"].as_u64().unwrap())
|
|
}
|
|
|
|
/// The property names `component.properties` lists for `id`, in order.
|
|
fn names(s: &mut Session, id: NodeId) -> Vec<String> {
|
|
ex(s, "component.properties", json!({"id": id}))["properties"]
|
|
.as_array()
|
|
.unwrap()
|
|
.iter()
|
|
.map(|p| p["name"].as_str().unwrap().to_string())
|
|
.collect()
|
|
}
|
|
|
|
fn rename(s: &mut Session, id: NodeId, name: &str, new_name: &str) {
|
|
ex(s, "component.editProperty", json!({"id": id, "name": name, "newName": new_name}));
|
|
}
|
|
|
|
/// A "Card" component with five frames made slots one by one ("Slot 1" … "Slot 5"). Returns
|
|
/// (card, frames).
|
|
fn card_with_slots(s: &mut Session) -> (NodeId, Vec<NodeId>) {
|
|
let f = idv(&ex(s, "frame.create", json!({"x": 0, "y": 0, "width": 500, "height": 100, "name": "Card"})));
|
|
let frames: Vec<NodeId> = (1..=5)
|
|
.map(|k| {
|
|
idv(&ex(s, "frame.create", json!({"x": (k - 1) * 100, "y": 0, "width": 90, "height": 90, "parent": f, "name": format!("Slot {k}")})))
|
|
})
|
|
.collect();
|
|
let card = idv(&ex(s, "component.create", json!({"ids": [f]})));
|
|
for fr in &frames {
|
|
ex(s, "component.makeSlot", json!({"id": fr}));
|
|
}
|
|
(card, frames)
|
|
}
|
|
|
|
#[test]
|
|
fn renaming_a_slot_keeps_its_place() {
|
|
let mut s = Session::new();
|
|
let (card, _) = card_with_slots(&mut s);
|
|
assert_eq!(names(&mut s, card), ["Slot 1", "Slot 2", "Slot 3", "Slot 4", "Slot 5"]);
|
|
rename(&mut s, card, "Slot 1", "Icon 1");
|
|
rename(&mut s, card, "Slot 2", "Icon 2");
|
|
rename(&mut s, card, "Icon 1", "Icone 1");
|
|
assert_eq!(names(&mut s, card), ["Icone 1", "Icon 2", "Slot 3", "Slot 4", "Slot 5"], "the issue's steps");
|
|
// Renamed to sort first by name, the last one still stays last.
|
|
rename(&mut s, card, "Slot 5", "A");
|
|
assert_eq!(names(&mut s, card), ["Icone 1", "Icon 2", "Slot 3", "Slot 4", "A"]);
|
|
// Undo puts the old name back in its place; a deleted property leaves the others in order.
|
|
s.undo().unwrap();
|
|
assert_eq!(names(&mut s, card), ["Icone 1", "Icon 2", "Slot 3", "Slot 4", "Slot 5"]);
|
|
ex(&mut s, "component.deleteProperty", json!({"id": card, "name": "Icon 2"}));
|
|
assert_eq!(names(&mut s, card), ["Icone 1", "Slot 3", "Slot 4", "Slot 5"]);
|
|
// The order is saved with the file.
|
|
let saved = serde_json::to_string(s.doc().unwrap()).unwrap();
|
|
let back: figcraft_doc::Document = serde_json::from_str(&saved).unwrap();
|
|
assert_eq!(back.node(card).map(|n| &n.kind), s.doc().unwrap().node(card).map(|n| &n.kind));
|
|
}
|
|
|
|
#[test]
|
|
fn every_kind_of_property_keeps_its_place_on_rename() {
|
|
let mut s = Session::new();
|
|
let f = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 200, "height": 100, "name": "Size=Small"})));
|
|
let label = idv(&ex(&mut s, "text.create", json!({"text": "Go", "x": 10, "y": 10, "parent": f})));
|
|
let body = idv(&ex(&mut s, "frame.create", json!({"x": 100, "y": 0, "width": 90, "height": 90, "parent": f, "name": "Body"})));
|
|
let icon_f = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 300, "width": 20, "height": 20, "name": "Icon"})));
|
|
let icon = idv(&ex(&mut s, "component.create", json!({"ids": [icon_f]})));
|
|
let nested = idv(&ex(&mut s, "instance.create", json!({"component": icon, "x": 60, "y": 10, "parent": f})));
|
|
let small = idv(&ex(&mut s, "component.create", json!({"ids": [f]})));
|
|
let zoom_layer = idv(&ex(&mut s, "shape.rectangle", json!({"x": 150, "y": 10, "width": 10, "height": 10, "parent": small})));
|
|
// Made in an order that isn't by name: Zoom (boolean), Label (text), Glyph (instance swap), Body (slot).
|
|
ex(&mut s, "component.addProperty", json!({"id": small, "name": "Zoom", "type": "boolean", "layer": zoom_layer}));
|
|
ex(&mut s, "component.addProperty", json!({"id": small, "name": "Label", "type": "text", "layer": label}));
|
|
ex(&mut s, "component.addProperty", json!({"id": small, "name": "Glyph", "type": "instanceSwap", "layer": nested}));
|
|
ex(&mut s, "component.makeSlot", json!({"id": body}));
|
|
assert_eq!(names(&mut s, small), ["Zoom", "Label", "Glyph", "Body"]);
|
|
for (old, new) in [("Zoom", "A zoom"), ("Label", "B label"), ("Glyph", "C glyph"), ("Body", "D body")] {
|
|
rename(&mut s, small, old, new);
|
|
}
|
|
assert_eq!(names(&mut s, small), ["A zoom", "B label", "C glyph", "D body"]);
|
|
rename(&mut s, small, "A zoom", "Zz");
|
|
assert_eq!(names(&mut s, small), ["Zz", "B label", "C glyph", "D body"]);
|
|
// In a set the shared properties keep their order on every variant, after the variant ones
|
|
// (which keep theirs when renamed too).
|
|
ex(&mut s, "component.addVariant", json!({"id": small, "property": "Size", "value": "Large"}));
|
|
let set = s.doc().unwrap().node(small).and_then(|n| n.parent).unwrap();
|
|
ex(&mut s, "component.addProperty", json!({"id": set, "name": "State", "type": "variant", "default": "Rest"}));
|
|
assert_eq!(names(&mut s, set), ["Size", "State", "Zz", "B label", "C glyph", "D body"]);
|
|
rename(&mut s, set, "Size", "Width");
|
|
rename(&mut s, set, "C glyph", "Alpha");
|
|
assert_eq!(names(&mut s, set), ["Width", "State", "Zz", "B label", "Alpha", "D body"]);
|
|
let variants: Vec<NodeId> = s.doc().unwrap().node(set).unwrap().children.clone();
|
|
for v in variants {
|
|
assert_eq!(names(&mut s, v), ["Width", "State", "Zz", "B label", "Alpha", "D body"]);
|
|
}
|
|
// An instance lists them in the same order.
|
|
let inst = idv(&ex(&mut s, "instance.create", json!({"component": set, "x": 0, "y": 600})));
|
|
assert_eq!(names(&mut s, inst), ["Width", "State", "Zz", "B label", "Alpha", "D body"]);
|
|
}
|
|
|
|
#[test]
|
|
fn make_slot_takes_the_name_typed() {
|
|
let mut s = Session::new();
|
|
let (card, frames) = card_with_slots(&mut s);
|
|
let f = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": 0, "width": 40, "height": 40, "parent": card, "name": "Frame 12"})));
|
|
let r = ex(&mut s, "component.makeSlot", json!({"id": f, "name": " Avatar "}));
|
|
assert_eq!(r["name"], "Avatar");
|
|
assert_eq!(crate::components::slot_name(s.doc().unwrap(), f).as_deref(), Some("Avatar"));
|
|
assert_eq!(names(&mut s, card).last().map(String::as_str), Some("Avatar"), "a new slot goes last");
|
|
// A name taken, or one a property can't have, is refused.
|
|
for bad in ["Slot 1", "a=b", ""] {
|
|
let other = frames[4];
|
|
ex(&mut s, "component.removeSlot", json!({"id": other}));
|
|
assert!(s.execute("component.makeSlot", &json!({"id": other, "name": bad})).is_err(), "{bad:?}");
|
|
ex(&mut s, "component.makeSlot", json!({"id": other, "name": "Slot 5"}));
|
|
}
|
|
// Files from before the order was kept list their properties by name.
|
|
let doc = s.doc_mut().unwrap();
|
|
if let Some(NodeKind::Component { prop_order, .. }) = doc.node_mut(card).map(|n| &mut n.kind) {
|
|
prop_order.clear();
|
|
}
|
|
let mut by_name = names(&mut s, card);
|
|
let listed = by_name.clone();
|
|
by_name.sort();
|
|
assert_eq!(listed, by_name);
|
|
}
|
|
|
|
/// An instance lists the properties its own variant's layers use (#2022): in Figma's data a set
|
|
/// has one text property per variant (and leftovers of deleted ones), measured: an instance of
|
|
/// variant A lists `State` and `A label`, never `B label` or a property no layer reads.
|
|
#[test]
|
|
fn an_instance_lists_only_the_properties_its_variant_uses() {
|
|
let mut s = Session::new();
|
|
let mut variants = Vec::new();
|
|
let mut texts = Vec::new();
|
|
for (k, name) in ["Type=Flag, State=Default", "Type=Flag, State=Selected"].iter().enumerate() {
|
|
let f = idv(&ex(&mut s, "frame.create", json!({"x": 0, "y": k * 60, "width": 120, "height": 40, "name": name})));
|
|
texts.push(idv(&ex(&mut s, "text.create", json!({"text": "Label", "x": 4, "y": 4, "parent": f, "name": "Label"}))));
|
|
variants.push(idv(&ex(&mut s, "component.create", json!({"ids": [f]}))));
|
|
}
|
|
let set = idv(&ex(&mut s, "component.combineVariants", json!({"ids": variants})));
|
|
ex(&mut s, "component.addProperty", json!({"id": set, "name": "Default label", "type": "text", "layer": texts[0]}));
|
|
ex(&mut s, "component.addProperty", json!({"id": set, "name": "Selected label", "type": "text", "layer": texts[1]}));
|
|
ex(&mut s, "component.addProperty", json!({"id": set, "name": "Gone label", "type": "text"}));
|
|
assert_eq!(names(&mut s, set), ["Type", "State", "Default label", "Selected label", "Gone label"], "the set keeps every definition");
|
|
let inst = idv(&ex(&mut s, "instance.create", json!({"component": variants[0], "x": 400, "y": 0})));
|
|
assert_eq!(names(&mut s, inst), ["Type", "State", "Default label"]);
|
|
ex(&mut s, "instance.setProperties", json!({"ids": [inst], "properties": {"State": "Selected"}}));
|
|
assert_eq!(names(&mut s, inst), ["Type", "State", "Selected label"]);
|
|
}
|