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:
BFlat
2026-10-30 04:49:00 +07:00
committed by GitHub
parent 3c1718bf86
commit 7d76b0c673
56 changed files with 589 additions and 135 deletions
+29 -8
View File
@@ -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();
+2 -2
View File
@@ -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"),
+6 -2
View File
@@ -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)));
}
+39 -8
View File
@@ -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();