|
|
|
|
@ -3,6 +3,7 @@ import { valueToUI } from './valueToUI';
|
|
|
|
|
import { render, hitTest } from './ui';
|
|
|
|
|
import { evaluate } from './interpreter';
|
|
|
|
|
import { CGError } from './error';
|
|
|
|
|
import type { AST } from './ast';
|
|
|
|
|
import type { Env } from './env';
|
|
|
|
|
import type { Store } from './store';
|
|
|
|
|
|
|
|
|
|
@ -53,18 +54,25 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env:
|
|
|
|
|
rerender();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function recomputeDependents(changedName: string) {
|
|
|
|
|
function recomputeDependents(changedName: string, visited: Set<string> = new Set()) {
|
|
|
|
|
const toRecompute = dependents.get(changedName);
|
|
|
|
|
if (!toRecompute) return;
|
|
|
|
|
|
|
|
|
|
for (const depName of toRecompute) {
|
|
|
|
|
// Cycle detection
|
|
|
|
|
if (visited.has(depName)) {
|
|
|
|
|
console.warn(`Cycle detected ${depName} already recomputed`);
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
visited.add(depName);
|
|
|
|
|
|
|
|
|
|
const entry = store.get(depName);
|
|
|
|
|
if (entry) {
|
|
|
|
|
const newValue = evaluate(entry.body, env, source);
|
|
|
|
|
env.set(depName, newValue);
|
|
|
|
|
entry.value = newValue;
|
|
|
|
|
|
|
|
|
|
recomputeDependents(depName);
|
|
|
|
|
recomputeDependents(depName, visited);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@ -222,6 +230,20 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env:
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleEvent(event: Value) {
|
|
|
|
|
handleEventInner(event);
|
|
|
|
|
rerender();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function handleEventInner(event: Value) {
|
|
|
|
|
if (event.kind === 'constructor' && event.name === 'Batch') {
|
|
|
|
|
if (event.args.length === 1 && event.args[0].kind === 'list') {
|
|
|
|
|
for (const subEvent of event.args[0].elements) {
|
|
|
|
|
handleEventInner(subEvent);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.kind === 'constructor' && event.name === 'FocusAndClick') {
|
|
|
|
|
if (event.args.length === 2 && event.args[0].kind === 'string') {
|
|
|
|
|
const componentKey = event.args[0].value;
|
|
|
|
|
@ -253,7 +275,7 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env:
|
|
|
|
|
const path = pathList.elements.map((e: Value) => e.kind === 'string' ? e.value : '');
|
|
|
|
|
const currentValue = env.get(name);
|
|
|
|
|
if (!currentValue) return;
|
|
|
|
|
newValue = updatePath(currentValue, path, event.args[2]);
|
|
|
|
|
newValue = updatePath(currentValue, path, event.args[2], env);
|
|
|
|
|
} else {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
@ -265,11 +287,19 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env:
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
recomputeDependents(name);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
rerender();
|
|
|
|
|
if (event.kind === 'constructor' && event.name === 'Focus') {
|
|
|
|
|
if (event.args.length === 1 && event.args[0].kind === 'string') {
|
|
|
|
|
setFocus(event.args[0].value)
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (event.kind === 'constructor' && event.name === 'NoOp')
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
if (app.update.kind !== 'closure')
|
|
|
|
|
throw new Error('update must be a function');
|
|
|
|
|
|
|
|
|
|
@ -283,7 +313,6 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env:
|
|
|
|
|
const newState = evaluate(app.update.body, callEnv, source);
|
|
|
|
|
|
|
|
|
|
state = newState;
|
|
|
|
|
rerender();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
if (error instanceof CGError) {
|
|
|
|
|
console.error(error.format());
|
|
|
|
|
@ -345,7 +374,6 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env:
|
|
|
|
|
|
|
|
|
|
if (focusedComponentKey) {
|
|
|
|
|
handleComponentEvent(focusedComponentKey, event);
|
|
|
|
|
|
|
|
|
|
} else {
|
|
|
|
|
handleEvent(event);
|
|
|
|
|
}
|
|
|
|
|
@ -360,18 +388,54 @@ export function runApp(app: App, canvas: HTMLCanvasElement, source: string, env:
|
|
|
|
|
rerender();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updatePath(obj: Value, path: string[], value: Value): Value {
|
|
|
|
|
function updatePath(obj: Value, path: string[], value: Value, env: Env): Value {
|
|
|
|
|
if (path.length === 0) return value;
|
|
|
|
|
|
|
|
|
|
if (obj.kind !== 'record')
|
|
|
|
|
throw new Error('Cannot access field on non-record');
|
|
|
|
|
|
|
|
|
|
const [field, ...rest] = path;
|
|
|
|
|
|
|
|
|
|
const newFields = {
|
|
|
|
|
...obj.fields,
|
|
|
|
|
[field]: updatePath(obj.fields[field], rest, value, env)
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// Reevaluate any dependent fields
|
|
|
|
|
if (rest.length === 0 && obj.fieldMeta) {
|
|
|
|
|
const visited = new Set<string>();
|
|
|
|
|
recomputeRecordFields(field, newFields, obj.fieldMeta, visited, env);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
kind: 'record',
|
|
|
|
|
fields: {
|
|
|
|
|
...obj.fields,
|
|
|
|
|
[field]: updatePath(obj.fields[field], rest, value)
|
|
|
|
|
}
|
|
|
|
|
fields: newFields,
|
|
|
|
|
fieldMeta: obj.fieldMeta
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function recomputeRecordFields(
|
|
|
|
|
changedField: string,
|
|
|
|
|
fields: { [key: string]: Value },
|
|
|
|
|
fieldMeta: { [key: string]: { body: AST, dependencies: Set<string> } },
|
|
|
|
|
visited: Set<string>,
|
|
|
|
|
env: Env
|
|
|
|
|
) {
|
|
|
|
|
for (const [fieldName, meta] of Object.entries(fieldMeta)) {
|
|
|
|
|
if (visited.has(fieldName)) continue;
|
|
|
|
|
|
|
|
|
|
if (meta.dependencies.has(changedField)) {
|
|
|
|
|
visited.add(fieldName);
|
|
|
|
|
|
|
|
|
|
const fieldEnv: Env = new Map(env);
|
|
|
|
|
for (const [k, v] of Object.entries(fields)) {
|
|
|
|
|
fieldEnv.set(k, v);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const newValue = evaluate(meta.body, fieldEnv, '');
|
|
|
|
|
fields[fieldName] = newValue;
|
|
|
|
|
|
|
|
|
|
recomputeRecordFields(fieldName, fields, fieldMeta, visited, env);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|