reconcileBindings function
Route every edit of a bound property to what it is bound to — the other half of a binding, run by the editor after each mutation.
The default IS the mockup: dragging a text whose x reads slide is
editing slide, because that is the value every caller who passes
nothing will get. So a node whose value has moved off its parameter's
default moves the default, and every other node reading that parameter
follows. For an item binding the default is the FIRST item's field, which
is the rule the whole repeater rests on; the rows are then redrawn.
A binding whose source is gone — a parameter no longer declared, an item field no longer carried, a cell dragged out of the repeat it reads from — is removed here, in the same edit, rather than dropped silently at save. Returns the names of the properties whose binding was removed, so the editor can say so.
A TOKEN is the exception to "the edit reaches the source": tokens are declared by hand in a file the tool only reads, and shared by every scene of the package, so a property edited off its token's value DETACHES — the value stays, local now — rather than moving the token under every other scene. That is what a design tool does when you type over a variable, and the inspector offers the token back one click away.
Implementation
List<String> reconcileBindings(SceneDocument doc) {
var dropped = <String>[];
var listChanged = false;
for (var (node, _) in doc.walk()) {
for (var entry in node.bindings.entries.toList()) {
var prop = entry.key;
// A style is several properties, each the node's to override: an edit
// is never a detach here. Only a token that is gone, or no longer a
// style, drops it — an export's style is one the editor cannot see
// into, and it stays.
if (resolveSceneKey(node, prop)?.prop.kind == ScenePropKind.style) {
var name = switch (entry.value) {
StyleRef(:var name) => name,
_ => null,
};
if (name == null || doc.tokenNamed(name)?.isStyle != true) {
node.bindings.remove(prop);
dropped.add('${node.name}.$prop');
}
continue;
}
var current = getSceneProperty(node, prop);
// An export has no value here to be cleared or to drift from: the
// binding is the whole of it, and only an unbind or the token going
// away ends it.
if (entry.value case TokenRef(:var name)
when doc.tokenNamed(name)?.isExport == true) {
continue;
}
// A property that was cleared — a fill removed, a size set to hug —
// no longer reads anything.
if (current == null) {
node.bindings.remove(prop);
dropped.add('${node.name}.$prop');
continue;
}
switch (entry.value) {
case ParamRef(:var name):
var decl = doc.paramNamed(name);
if (decl == null || decl.kind == SceneParamKind.list) {
node.bindings.remove(prop);
dropped.add('${node.name}.$prop');
} else if (decl.defaultValue != current) {
var i = doc.params.indexOf(decl);
doc.params[i] = decl.withDefault(current);
for (var (other, _) in doc.walk()) {
if (identical(other, node)) continue;
for (var e in other.bindings.entries) {
if (e.value == entry.value) {
setSceneProperty(other, e.key, current);
}
}
}
}
case TokenRef(:var name):
var decl = doc.tokenNamed(name);
if (decl == null || decl.isStyle) {
node.bindings.remove(prop);
dropped.add('${node.name}.$prop');
// An opaque token has no value to keep: the argument goes too.
if (tokenMarkerName(current) != null) {
setSceneProperty(node, prop, null);
}
} else if (decl.hasValue && decl.value != current) {
node.bindings.remove(prop);
dropped.add('${node.name}.$prop');
}
case StyleRef():
// Handled above, under its own key.
break;
case ItemRef(:var list, :var field):
var decl = doc.paramNamed(list);
var scope = _repeatScope(doc, node);
var items = decl?.items ?? const <SceneItem>[];
if (decl == null ||
decl.kind != SceneParamKind.list ||
scope != list ||
items.isEmpty ||
!items.first.containsKey(field)) {
node.bindings.remove(prop);
dropped.add('${node.name}.$prop');
} else {
var was = items.first[field]!;
var next = _itemValue(was, current);
if (next != was) {
var i = doc.params.indexOf(decl);
doc.params[i] = decl.withDefault([
{...items.first, field: next},
...items.skip(1),
]);
listChanged = true;
}
}
}
}
}
if (listChanged) bindRepeats(doc);
return dropped;
}