Merge remote-tracking branch 'origin/main' into landb/2200

This commit is contained in:
bflatastic
2026-11-12 10:51:44 +07:00
13 changed files with 229 additions and 37 deletions
+24
View File
@@ -298,6 +298,30 @@ pub fn prop_list(doc: &Document, c: NodeId) -> Vec<(String, PropDef)> {
}
}
/// The properties an instance of `component` lists: [`prop_list`] without the boolean, text and
/// instance swap properties that no layer of the component reads. A set's variants share its
/// definitions, so one a variant's layers don't bind (another variant's text, a leftover of a
/// deleted variant) stays defined, and shows on the set, but not on this variant's instances
/// (#2022). Slots stay. Walks as [`slots_of`] does: a nested instance counts, its own layers and
/// the content put into slots are not entered.
pub fn instance_prop_list(doc: &Document, component: NodeId) -> Vec<(String, PropDef)> {
let mut used: std::collections::HashSet<String> = std::collections::HashSet::new();
let mut stack = vec![component];
let mut seen = 0usize;
while let Some(c) = stack.pop() {
seen += 1;
if seen > 1_000_000 {
break;
}
let Some(n) = doc.node(c) else { continue };
used.extend(n.prop_refs.values().cloned());
if c == component || (!is_instance(doc, c) && !holds_content(n)) {
stack.extend(n.children.iter().rev().copied());
}
}
prop_list(doc, component).into_iter().filter(|(k, d)| !takes_value(d) || used.contains(k)).collect()
}
/// Whether instances store a value for the property (every kind but slots).
pub fn takes_value(d: &PropDef) -> bool {
!matches!(d, PropDef::Slot { .. })
+4 -1
View File
@@ -395,8 +395,11 @@ fn unbind_delete_and_vary(s: &mut Session, c: NodeId, i: NodeId, title: NodeId,
ex(s, "component.deleteProperty", json!({"id": c, "name": "Heading"}));
assert!(node(s, title).prop_refs.is_empty());
assert!(matches!(&node(s, i).kind, NodeKind::Instance { props, .. } if !props.contains_key("Heading")));
let desc = ex(s, "component.properties", json!({"id": i}));
// "Show dot" is still defined but no layer reads it any more: the main lists it, the instance doesn't (#2022).
let desc = ex(s, "component.properties", json!({"id": c}));
assert_eq!(desc["properties"].as_array().unwrap().len(), 1);
let desc = ex(s, "component.properties", json!({"id": i}));
assert!(desc["properties"].as_array().unwrap().is_empty());
// A variant property on a lone component makes it a component set.
let set = idv(&ex(s, "component.addProperty", json!({"id": c, "name": "Theme", "type": "variant", "default": "Light"})));
assert!(matches!(node(s, set).kind, NodeKind::ComponentSet { .. }));
+3 -2
View File
@@ -8,7 +8,7 @@ use super::{
req_string, req_targets, set_boolean_default, slot_edited, slots_of, string, takes_value, targets, variant_conflicts, variant_name,
variant_options, variant_props, variant_set,
};
use crate::components::prop_list;
use crate::components::{instance_prop_list, prop_list};
/// Whether instance `id` has a component property `name` that a `kind` variable can drive (a
/// boolean property takes a boolean variable, a text or variant property a string one).
@@ -606,7 +606,8 @@ pub(crate) fn describe_props(doc: &Document, id: NodeId) -> Value {
}
// Slots: the frames (on a main) or this instance's copy, with whether it holds its own content.
let slots = slots_of(doc, if values.is_some() { id } else { variant_set(doc, comp).unwrap_or(comp) });
for (k, d) in prop_list(doc, comp) {
let defs = if values.is_some() { instance_prop_list(doc, comp) } else { prop_list(doc, comp) };
for (k, d) in defs {
let mut v = def_json(&k, &d);
if !takes_value(&d) {
let layers: Vec<NodeId> = slots.iter().filter(|(_, n)| *n == k).map(|(l, _)| *l).collect();
+26 -1
View File
@@ -77,8 +77,9 @@ fn every_kind_of_property_keeps_its_place_on_rename() {
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"}));
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}));
@@ -133,3 +134,27 @@ fn make_slot_takes_the_name_typed() {
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"]);
}
+8 -3
View File
@@ -417,12 +417,16 @@ fn assert_slot_text(s: &mut Session, instance: NodeId, expected: &str) {
assert!(html.contains(&format!(">{expected}</p>")), "slot Label HTML: {html}");
}
fn assert_local_slot_text(s: &mut Session, instance: NodeId, expected: &str) {
fn assert_stored_slot_text(s: &Session, instance: NodeId, expected: &str) {
assert!(
matches!(&node(s, instance).kind, NodeKind::Instance { props, .. } if props.get("Text") == Some(&json!(expected))),
"instance {instance} must store its own Text value {expected:?}: {:?}",
node(s, instance).kind
);
}
fn assert_local_slot_text(s: &mut Session, instance: NodeId, expected: &str) {
assert_stored_slot_text(s, instance, expected);
let properties = props_of(s, instance);
let text = properties.iter().find(|p| p["name"] == "Text").unwrap_or_else(|| panic!("instance {instance} has no Text property: {properties:?}"));
assert_eq!(text["value"], expected, "instance {instance} Text property");
@@ -475,14 +479,15 @@ fn slot_text_properties_do_not_read_an_unbound_base_property() {
ex(&mut s, "component.addProperty", json!({"id": base, "name": "Text", "type": "text", "default": "Wrong owner"}));
let i = idv(&ex(&mut s, "instance.create", json!({"component": composition, "x": 400})));
let nested_base = kids(&s, i)[0];
assert_local_slot_text(&mut s, nested_base, "Wrong owner");
// Bound to no layer, the base's Text isn't listed on its instance (#2269); its value stays stored.
assert_stored_slot_text(&s, nested_base, "Wrong owner");
let nested = copy_of(&s, i, main_content);
assert_local_slot_text(&mut s, nested, "Default");
assert_slot_text(&mut s, nested, "Default");
ex(&mut s, "instance.setProperties", json!({"ids": [nested], "properties": {"Text": "Custom"}}));
assert_local_slot_text(&mut s, nested, "Custom");
assert_slot_text(&mut s, nested, "Custom");
assert_local_slot_text(&mut s, nested_base, "Wrong owner");
assert_stored_slot_text(&s, nested_base, "Wrong owner");
assert_slot_text(&mut s, main_content, "Default");
healthy(&s);
}