mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-31 05:46:46 -06:00
M3: component properties keep their order on rename; Make slot asks for the name (#1536, #1537) (#1565)
Properties were listed by name, so a rename moved one. Components now keep prop_order (the names as they were made; a rename keeps the place) and every listing follows it. Make slot opens a name field under the frame's Slot row; Enter makes the slot, Escape cancels, a taken name keeps the field open.
This commit is contained in:
@@ -526,8 +526,15 @@ fn text_styles_on_part_of_a_text_are_written_as_values() {
|
||||
fn sized() -> (Document, [NodeId; 3]) {
|
||||
let mut doc = Document::new("Sized");
|
||||
let page = doc.first_page().unwrap();
|
||||
let main =
|
||||
add(&mut doc, page, NodeKind::Component { description: String::new(), props: BTreeMap::new() }, "Card", (0.0, 0.0), (100.0, 60.0), &|_| {});
|
||||
let main = add(
|
||||
&mut doc,
|
||||
page,
|
||||
NodeKind::Component { description: String::new(), props: BTreeMap::new(), prop_order: Vec::new() },
|
||||
"Card",
|
||||
(0.0, 0.0),
|
||||
(100.0, 60.0),
|
||||
&|_| {},
|
||||
);
|
||||
add(&mut doc, main, NodeKind::Rectangle, "Box", (10.0, 10.0), (40.0, 40.0), &|n| {
|
||||
n.constraints = Constraints { horizontal: HConstraint::Scale, vertical: VConstraint::Scale };
|
||||
});
|
||||
@@ -623,10 +630,17 @@ fn an_instance_named_after_its_set_has_no_name_of_its_own() {
|
||||
fn layers_bound_like_their_mains_are_no_overrides_in_another_mode() {
|
||||
let mut doc = themed();
|
||||
let page = doc.first_page().unwrap();
|
||||
let main =
|
||||
add(&mut doc, page, NodeKind::Component { description: String::new(), props: BTreeMap::new() }, "Chip", (0.0, 300.0), (80.0, 30.0), &|n| {
|
||||
let main = add(
|
||||
&mut doc,
|
||||
page,
|
||||
NodeKind::Component { description: String::new(), props: BTreeMap::new(), prop_order: Vec::new() },
|
||||
"Chip",
|
||||
(0.0, 300.0),
|
||||
(80.0, 30.0),
|
||||
&|n| {
|
||||
n.bound.insert("fill".into(), "v-bg".into());
|
||||
});
|
||||
},
|
||||
);
|
||||
add(&mut doc, main, NodeKind::Text(TextData::new("Chip")), "Chip label", (8.0, 6.0), (40.0, 16.0), &|n| {
|
||||
n.fills = vec![solid(0.0, 0.0, 0.0)];
|
||||
n.bound.insert("fill".into(), "v-fg".into());
|
||||
@@ -662,15 +676,22 @@ fn layers_of_a_swapped_nested_instance_keep_what_they_change() {
|
||||
let other = add(
|
||||
&mut doc,
|
||||
page,
|
||||
NodeKind::Component { description: String::new(), props: BTreeMap::new() },
|
||||
NodeKind::Component { description: String::new(), props: BTreeMap::new(), prop_order: Vec::new() },
|
||||
"Other",
|
||||
(0.0, 400.0),
|
||||
(100.0, 60.0),
|
||||
&|_| {},
|
||||
);
|
||||
add(&mut doc, other, NodeKind::Ellipse { start: 0.0, sweep: std::f64::consts::TAU, inner: 0.0 }, "Dot", (10.0, 10.0), (40.0, 40.0), &|_| {});
|
||||
let row =
|
||||
add(&mut doc, page, NodeKind::Component { description: String::new(), props: BTreeMap::new() }, "Row", (0.0, 500.0), (300.0, 80.0), &|_| {});
|
||||
let row = add(
|
||||
&mut doc,
|
||||
page,
|
||||
NodeKind::Component { description: String::new(), props: BTreeMap::new(), prop_order: Vec::new() },
|
||||
"Row",
|
||||
(0.0, 500.0),
|
||||
(300.0, 80.0),
|
||||
&|_| {},
|
||||
);
|
||||
let mut g = Gen { r: Lcg(3), doc, image: String::new(), frames: Vec::new(), layers: Vec::new() };
|
||||
let thumb = g.instance(card, row, BTreeMap::new());
|
||||
g.doc.node_mut(thumb).unwrap().name = "Thumb".into();
|
||||
|
||||
@@ -76,8 +76,8 @@ impl<'d> Exporter<'d> {
|
||||
// A name used with two types is two properties (their definitions have their own ids).
|
||||
let mut seen: BTreeMap<(String, &'static str), PropDef> = BTreeMap::new();
|
||||
for v in &variants {
|
||||
let NodeKind::Component { props, .. } = &v.kind else { continue };
|
||||
for (name, d) in props {
|
||||
let NodeKind::Component { props, prop_order, .. } = &v.kind else { continue };
|
||||
for (name, d) in figcraft_doc::ordered_props(props, prop_order) {
|
||||
let Some(ty) = prop_type(d) else { continue };
|
||||
match seen.get(&(name.clone(), ty)) {
|
||||
Some(first) if first != d => self.warn("variants with different defaults for one property were written with the first variant's"),
|
||||
|
||||
@@ -109,7 +109,7 @@ impl<'d> Exporter<'d> {
|
||||
self.warn("boolean groups whose layers combine in different ways use the group's operation");
|
||||
}
|
||||
}
|
||||
NodeKind::Component { description, props } if !nested => {
|
||||
NodeKind::Component { description, props, prop_order } if !nested => {
|
||||
if let Some(set) = self.set_of(n.id) {
|
||||
// A variant: its set holds the definitions; the variant has its own copy of
|
||||
// each (pointing at the set's), and its values of the variant properties.
|
||||
@@ -136,7 +136,11 @@ impl<'d> Exporter<'d> {
|
||||
f.push(("variantPropSpecs", arr(specs)));
|
||||
}
|
||||
} else {
|
||||
let defs: Vec<Kv> = props.iter().enumerate().filter_map(|(i, (name, d))| self.prop_def(n.id, name, d, i)).collect();
|
||||
let defs: Vec<Kv> = figcraft_doc::ordered_props(props, prop_order)
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter_map(|(i, (name, d))| self.prop_def(n.id, name, d, i))
|
||||
.collect();
|
||||
if !defs.is_empty() {
|
||||
f.push(("componentPropDefs", arr(defs)));
|
||||
}
|
||||
|
||||
@@ -410,7 +410,7 @@ impl Gen {
|
||||
/// A component with a boolean and a text property, a component set, and instances of both
|
||||
/// (with property values and overrides), and one nested two levels deep.
|
||||
fn components(&mut self, page: NodeId) {
|
||||
let comp = self.node(page, NodeKind::Component { description: "A button".into(), props: BTreeMap::new() }, "Button");
|
||||
let comp = self.node(page, NodeKind::Component { description: "A button".into(), props: BTreeMap::new(), prop_order: Vec::new() }, "Button");
|
||||
let icon = self.node(comp, NodeKind::Rectangle, "Icon");
|
||||
let label = self.node(comp, NodeKind::Text(TextData::new("Label")), "Label");
|
||||
{
|
||||
@@ -449,7 +449,7 @@ impl Gen {
|
||||
}
|
||||
// A card holding an instance of the button, and an instance of the card overriding a layer of
|
||||
// the nested button.
|
||||
let card = self.node(page, NodeKind::Component { description: String::new(), props: BTreeMap::new() }, "Card");
|
||||
let card = self.node(page, NodeKind::Component { description: String::new(), props: BTreeMap::new(), prop_order: Vec::new() }, "Card");
|
||||
// A grid whose second cell holds the nested button.
|
||||
self.doc.node_mut(card).unwrap().layout = Some(AutoLayout { mode: LayoutMode::Grid, grid_columns: 2, grid_rows: 1, ..AutoLayout::default() });
|
||||
let nested = self.instance(comp, card, BTreeMap::new());
|
||||
@@ -465,7 +465,7 @@ impl Gen {
|
||||
let set = self.node(page, NodeKind::ComponentSet { description: "States".into() }, "Toggle");
|
||||
let mut variants = Vec::new();
|
||||
for name in ["State=Off", "State=On"] {
|
||||
let v = self.node(set, NodeKind::Component { description: String::new(), props: BTreeMap::new() }, name);
|
||||
let v = self.node(set, NodeKind::Component { description: String::new(), props: BTreeMap::new(), prop_order: Vec::new() }, name);
|
||||
self.node(v, NodeKind::Ellipse { start: 0.0, sweep: std::f64::consts::TAU, inner: 0.0 }, "Knob");
|
||||
variants.push(v);
|
||||
}
|
||||
@@ -1120,7 +1120,17 @@ fn grid_track_sizes_round_trip() {
|
||||
fn instance_grid_tracks_clear_the_mains_sizes() {
|
||||
let mut doc = Document::new("File");
|
||||
let page = doc.first_page().unwrap();
|
||||
let main = doc.create(page, NodeKind::Component { description: String::new(), props: BTreeMap::new() }, "Main", 0.0, 0.0, 400.0, 200.0).unwrap();
|
||||
let main = doc
|
||||
.create(
|
||||
page,
|
||||
NodeKind::Component { description: String::new(), props: BTreeMap::new(), prop_order: Vec::new() },
|
||||
"Main",
|
||||
0.0,
|
||||
0.0,
|
||||
400.0,
|
||||
200.0,
|
||||
)
|
||||
.unwrap();
|
||||
let grid = doc.create(main, NodeKind::Frame, "Grid", 0.0, 0.0, 400.0, 200.0).unwrap();
|
||||
let layout = AutoLayout {
|
||||
mode: LayoutMode::Grid,
|
||||
@@ -1327,8 +1337,17 @@ fn set_with_properties() -> (Document, NodeId) {
|
||||
}
|
||||
let mut doc = Document::new("File");
|
||||
let page = doc.first_page().unwrap();
|
||||
let glyph =
|
||||
doc.create(page, NodeKind::Component { description: String::new(), props: BTreeMap::new() }, "Glyph", 400.0, 0.0, 16.0, 16.0).unwrap();
|
||||
let glyph = doc
|
||||
.create(
|
||||
page,
|
||||
NodeKind::Component { description: String::new(), props: BTreeMap::new(), prop_order: Vec::new() },
|
||||
"Glyph",
|
||||
400.0,
|
||||
0.0,
|
||||
16.0,
|
||||
16.0,
|
||||
)
|
||||
.unwrap();
|
||||
doc.create(glyph, NodeKind::Rectangle, "Shape", 0.0, 0.0, 16.0, 16.0).unwrap();
|
||||
let set = doc.create(page, NodeKind::ComponentSet { description: String::new() }, "Button", 0.0, 0.0, 300.0, 120.0).unwrap();
|
||||
let mut variants = Vec::new();
|
||||
@@ -1337,7 +1356,9 @@ fn set_with_properties() -> (Document, NodeId) {
|
||||
("Label".to_string(), PropDef::Text { default: "Button".into() }),
|
||||
("Show icon".to_string(), PropDef::Boolean { default: true }),
|
||||
]);
|
||||
let v = doc.create(set, NodeKind::Component { description: String::new(), props }, name, 0.0, 60.0 * i as f64, 120.0, 40.0).unwrap();
|
||||
let v = doc
|
||||
.create(set, NodeKind::Component { description: String::new(), props, prop_order: Vec::new() }, name, 0.0, 60.0 * i as f64, 120.0, 40.0)
|
||||
.unwrap();
|
||||
let label = doc.create(v, NodeKind::Text(TextData::new("Button")), "Label", 0.0, 0.0, 60.0, 20.0).unwrap();
|
||||
doc.node_mut(label).unwrap().prop_refs.insert("characters".into(), "Label".into());
|
||||
// The icon is an instance placed in the variant: like the editor's, its `source` is its
|
||||
@@ -1466,7 +1487,17 @@ fn set_properties_round_trip() {
|
||||
fn an_instance_without_its_mains_maximum_size_overrides_it_with_infinity() {
|
||||
let mut doc = Document::new("File");
|
||||
let page = doc.first_page().unwrap();
|
||||
let main = doc.create(page, NodeKind::Component { description: String::new(), props: BTreeMap::new() }, "Box", 0.0, 0.0, 100.0, 40.0).unwrap();
|
||||
let main = doc
|
||||
.create(
|
||||
page,
|
||||
NodeKind::Component { description: String::new(), props: BTreeMap::new(), prop_order: Vec::new() },
|
||||
"Box",
|
||||
0.0,
|
||||
0.0,
|
||||
100.0,
|
||||
40.0,
|
||||
)
|
||||
.unwrap();
|
||||
doc.node_mut(main).unwrap().child_layout.max_w = Some(300.0);
|
||||
doc.create(page, NodeKind::Instance { component: main, props: BTreeMap::new() }, "Box", 0.0, 100.0, 100.0, 40.0).unwrap();
|
||||
let (bytes, _) = export(&doc, None).unwrap();
|
||||
|
||||
@@ -15,8 +15,17 @@ use super::*;
|
||||
fn large(screens: usize) -> Document {
|
||||
let mut d = Document::new("Large");
|
||||
let page = d.first_page().unwrap();
|
||||
let button =
|
||||
d.create(page, NodeKind::Component { description: "A button".into(), props: Default::default() }, "Button", 0.0, -80.0, 96.0, 32.0).unwrap();
|
||||
let button = d
|
||||
.create(
|
||||
page,
|
||||
NodeKind::Component { description: "A button".into(), props: Default::default(), prop_order: Vec::new() },
|
||||
"Button",
|
||||
0.0,
|
||||
-80.0,
|
||||
96.0,
|
||||
32.0,
|
||||
)
|
||||
.unwrap();
|
||||
let label = d.create(button, NodeKind::Text(TextData::new("OK")), "Label", 8.0, 8.0, 40.0, 16.0).unwrap();
|
||||
d.get_mut(label).unwrap().plugin_data.insert("agent".into(), "note".into());
|
||||
for s in 0..screens {
|
||||
|
||||
@@ -19,7 +19,17 @@ fn png() -> Vec<u8> {
|
||||
fn published() -> (Document, figcraft_doc::NodeId, String) {
|
||||
let mut d = Document::new("Kit");
|
||||
let p = d.first_page().unwrap();
|
||||
let tile = d.create(p, NodeKind::Component { description: String::new(), props: Default::default() }, "Tile", 0.0, 0.0, 10.0, 10.0).unwrap();
|
||||
let tile = d
|
||||
.create(
|
||||
p,
|
||||
NodeKind::Component { description: String::new(), props: Default::default(), prop_order: Vec::new() },
|
||||
"Tile",
|
||||
0.0,
|
||||
0.0,
|
||||
10.0,
|
||||
10.0,
|
||||
)
|
||||
.unwrap();
|
||||
let (hash, _, _) = add_image(&mut d, png()).unwrap();
|
||||
d.get_mut(tile).unwrap().fills = vec![Paint::image(hash.clone(), ImageScaleMode::Fill)];
|
||||
let snap = d.clone();
|
||||
|
||||
@@ -2970,7 +2970,7 @@ fn layer_kind(
|
||||
let mut path_box = None;
|
||||
let mut vector_source = None;
|
||||
let kind = match typ {
|
||||
"frame" if is_master => NodeKind::Component { description: String::new(), props: BTreeMap::new() },
|
||||
"frame" if is_master => NodeKind::Component { description: String::new(), props: BTreeMap::new(), prop_order: Vec::new() },
|
||||
"frame" if v.get("isVariantContainer").and_then(Value::as_bool) == Some(true) => NodeKind::ComponentSet { description: String::new() },
|
||||
"frame" => NodeKind::Frame,
|
||||
"group" => NodeKind::Group,
|
||||
|
||||
@@ -451,7 +451,16 @@ fn unchanged_penpot_text_and_file_header_are_written_back() {
|
||||
fn components_instances_and_variants_round_trip() {
|
||||
let mut doc = Document::new("Library");
|
||||
let p = page(&doc);
|
||||
let main = add(&mut doc, p, NodeKind::Component { description: String::new(), props: BTreeMap::new() }, "Button", 0.0, 0.0, 120.0, 40.0);
|
||||
let main = add(
|
||||
&mut doc,
|
||||
p,
|
||||
NodeKind::Component { description: String::new(), props: BTreeMap::new(), prop_order: Vec::new() },
|
||||
"Button",
|
||||
0.0,
|
||||
0.0,
|
||||
120.0,
|
||||
40.0,
|
||||
);
|
||||
let label = add(&mut doc, main, NodeKind::Rectangle, "Fill", 0.0, 0.0, 120.0, 40.0);
|
||||
doc.node_mut(label).unwrap().fills = vec![Paint::solid(Color::BLACK)];
|
||||
let copy = add(&mut doc, p, NodeKind::Instance { component: main, props: BTreeMap::new() }, "Button copy", 0.0, 100.0, 120.0, 40.0);
|
||||
@@ -465,7 +474,16 @@ fn components_instances_and_variants_round_trip() {
|
||||
doc.node_mut(copy).unwrap().source = Some(main);
|
||||
let set = add(&mut doc, p, NodeKind::ComponentSet { description: String::new() }, "Size", 300.0, 0.0, 200.0, 100.0);
|
||||
for (name, y) in [("size=small", 0.0), ("size=large", 50.0)] {
|
||||
add(&mut doc, set, NodeKind::Component { description: String::new(), props: BTreeMap::new() }, name, 0.0, y, 100.0, 40.0);
|
||||
add(
|
||||
&mut doc,
|
||||
set,
|
||||
NodeKind::Component { description: String::new(), props: BTreeMap::new(), prop_order: Vec::new() },
|
||||
name,
|
||||
0.0,
|
||||
y,
|
||||
100.0,
|
||||
40.0,
|
||||
);
|
||||
}
|
||||
|
||||
let (back, e, _) = roundtrip(&doc);
|
||||
|
||||
@@ -613,7 +613,7 @@ impl<'a> Ctx<'a> {
|
||||
},
|
||||
},
|
||||
"TEXT" => NodeKind::Text(text::text_data(self, j)),
|
||||
"COMPONENT" => NodeKind::Component { description: description(&self.component_meta, j), props: BTreeMap::new() },
|
||||
"COMPONENT" => NodeKind::Component { description: description(&self.component_meta, j), props: BTreeMap::new(), prop_order: Vec::new() },
|
||||
"COMPONENT_SET" => NodeKind::ComponentSet { description: description(&self.set_meta, j) },
|
||||
"SLICE" => NodeKind::Slice,
|
||||
_ => return None,
|
||||
|
||||
@@ -469,7 +469,7 @@ fn components_sets_instances_and_properties() {
|
||||
assert_eq!(set.stroke.dashes, vec![10.0, 5.0]);
|
||||
let def = named(&doc, "State=Default");
|
||||
let icon = named(&doc, "Icon");
|
||||
let NodeKind::Component { description, props } = &def.kind else { panic!() };
|
||||
let NodeKind::Component { description, props, .. } = &def.kind else { panic!() };
|
||||
assert_eq!(description, "Primary button");
|
||||
assert_eq!(props.get("Label"), Some(&PropDef::Text { default: "OK".into() }), "{props:?}");
|
||||
assert_eq!(props.get("Icon"), Some(&PropDef::InstanceSwap { default: icon.id, preferred: Vec::new() }));
|
||||
@@ -477,7 +477,7 @@ fn components_sets_instances_and_properties() {
|
||||
let NodeKind::Component { props: hover_props, .. } = &named(&doc, "State=Hover").kind else { panic!() };
|
||||
assert!(hover_props.contains_key("Label"), "variants share the set's definitions");
|
||||
assert!(
|
||||
matches!(&icon.kind, NodeKind::Component { description, props } if description == "An icon" && props.get("Show") == Some(&PropDef::Boolean { default: true }))
|
||||
matches!(&icon.kind, NodeKind::Component { description, props, .. } if description == "An icon" && props.get("Show") == Some(&PropDef::Boolean { default: true }))
|
||||
);
|
||||
let label = doc.node(def.children[0]).unwrap();
|
||||
assert_eq!(label.prop_refs.get("characters").map(String::as_str), Some("Label"));
|
||||
|
||||
Reference in New Issue
Block a user