Generalize live field bindings and use them for Studio canvas gestures

This commit is contained in:
Timothy J. Aveni
2026-09-28 11:12:11 -07:00
parent 696023b4ba
commit 0ace71eae6
13 changed files with 1766 additions and 1586 deletions
+1
View File
@@ -98,6 +98,7 @@ export const instantiateInterface = (
return {
...common,
kind: "value",
...(member.nativeField ? { nativeField: true as const } : {}),
...(member.queryRead ? { queryRead: { ...member.queryRead } } : {}),
valueType: substitution.value(member.valueType, member.displayName),
};
+2
View File
@@ -132,6 +132,8 @@ interface InterfaceMemberBase<Type = ValueType> {
export interface ValueInterfaceMember<Type = ValueType> extends InterfaceMemberBase<Type> {
kind: "value";
/** Every operation resolves directly to the same native state slot. */
nativeField?: true;
queryRead?: QueryReadContract;
valueType: Type;
}
+45
View File
@@ -615,6 +615,23 @@ const collectIdentityIndexes = (
);
}
}
if (member.kind === "value" && member.nativeField) {
const names = member.operations.map((o) => o.displayName);
if (
!isStorableType(member.valueType) ||
!names.includes("get") ||
names.some((n) => !["get", "set", "edit"].includes(n)) ||
(names.includes("set") && names.includes("edit"))
)
issue(
issues,
"invalid-state-binding",
memberPath,
"Fields require a storable value, a native getter and at most one setter or editor",
);
if (member.queryRead && member.queryRead.execution !== "native")
issue(issues, "invalid-query-contract", memberPath, "Fields cannot have package-backed query reads");
}
requireText(issues, member.id, `${memberPath}.id`, "Member ID");
requireText(issues, member.displayName, `${memberPath}.displayName`, "Member name");
if (memberIds.has(member.id)) {
@@ -1758,6 +1775,34 @@ const validateConformances = (
}
} else binding = entry.binding;
const member = operationEntry.member;
if (member.kind === "value" && member.nativeField) {
const primitive = { get: "read", set: "write", edit: "edit" }[operation.displayName];
const getter = member.operations.find((o) => o.displayName === "get");
let read: Binding | undefined;
try {
if (getter)
read = resolveForwardedBinding(
workspace,
conformance.atomId,
conformance.interfaceRevisionId,
getter.id,
).binding;
} catch {}
if (
binding.kind !== "state" ||
binding.primitive !== primitive ||
read?.kind !== "state" ||
read.primitive !== "read" ||
read.slotId !== binding.slotId
)
issue(
issues,
"invalid-state-binding",
bindingPath,
"Field operations must resolve to the same native state slot",
);
}
if (
member.kind === "value" &&
operation.displayName === "edit" &&