compiling. interpreting was too slow
parent
6edf592637
commit
2cd5a609bb
@ -0,0 +1,245 @@
|
|||||||
|
import type { AST, Pattern, Definition } from './ast';
|
||||||
|
import { _rt, store } from './runtime-js';
|
||||||
|
|
||||||
|
export function compile(ast: AST): string {
|
||||||
|
switch (ast.kind) {
|
||||||
|
case 'literal':
|
||||||
|
if (ast.value.kind === 'string')
|
||||||
|
return JSON.stringify(ast.value.value);
|
||||||
|
if (ast.value.kind === 'int' || ast.value.kind === 'float')
|
||||||
|
return JSON.stringify(ast.value.value);
|
||||||
|
throw new Error(`Cannot compile literal of kind ${ast.value.kind}`);
|
||||||
|
|
||||||
|
case 'variable':
|
||||||
|
return sanitize(ast.name);
|
||||||
|
|
||||||
|
case 'lambda':
|
||||||
|
const params = ast.params.map(sanitize).join(') => (');
|
||||||
|
return `((${params}) => ${compile(ast.body)})`;
|
||||||
|
|
||||||
|
case 'apply':
|
||||||
|
// Constructor
|
||||||
|
if (ast.func.kind === 'constructor') {
|
||||||
|
const ctorName = ast.func.name;
|
||||||
|
const arg = compile(ast.args[0]);
|
||||||
|
return `((_a) => _a && typeof _a === 'object' && !Array.isArray(_a) && !_a._tag
|
||||||
|
? { _tag: "${ctorName}", ..._a }
|
||||||
|
: { _tag: "${ctorName}", _0: _a })(${arg})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = ast.args.map(compile).join(')(');
|
||||||
|
return `${compile(ast.func)}(${args})`;
|
||||||
|
|
||||||
|
case 'record': {
|
||||||
|
const fields = Object.entries(ast.fields)
|
||||||
|
.map(([k, v]) => `${sanitize(k)}: ${compile(v)}`);
|
||||||
|
return `({${fields.join(', ')}})`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'list': {
|
||||||
|
const elements = ast.elements.map(e =>
|
||||||
|
'spread' in e ? `...${compile(e.spread)}` : compile(e)
|
||||||
|
);
|
||||||
|
return `[${elements.join(', ')}]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'record-access':
|
||||||
|
return `${compile(ast.record)}.${sanitize(ast.field)}`;
|
||||||
|
|
||||||
|
case 'record-update':
|
||||||
|
const updates = Object.entries(ast.updates)
|
||||||
|
.map(([k, v]) => `${sanitize(k)}: ${compile(v)}`);
|
||||||
|
return `({...${compile(ast.record)}, ${updates.join(', ')}})`;
|
||||||
|
|
||||||
|
case 'let':
|
||||||
|
return `((${sanitize(ast.name)}) =>
|
||||||
|
${compile(ast.body)})(${compile(ast.value)})`;
|
||||||
|
|
||||||
|
case 'match':
|
||||||
|
return compileMatch(ast);
|
||||||
|
|
||||||
|
case 'constructor':
|
||||||
|
return `({ _tag: "${ast.name}" })`;
|
||||||
|
/*
|
||||||
|
return `((arg) => arg && typeof arg === 'object' && !arg._tag
|
||||||
|
? { _tag: "${ast.name}", ...arg }
|
||||||
|
: { _tag: "${ast.name}", _0: arg })`;
|
||||||
|
*/
|
||||||
|
|
||||||
|
case 'rebind': {
|
||||||
|
if (ast.target.kind === 'variable') {
|
||||||
|
return `({ _tag: "Rebind", _0: "${ast.target.name}", _1: ${compile(ast.value)} })`;
|
||||||
|
} else if (ast.target.kind === 'record-access') {
|
||||||
|
let current: AST = ast.target;
|
||||||
|
const path: string[] = [];
|
||||||
|
while (current.kind === 'record-access') {
|
||||||
|
path.unshift(current.field);
|
||||||
|
current = current.record;
|
||||||
|
}
|
||||||
|
if (current.kind === 'variable') {
|
||||||
|
return `({ _tag: "Rebind", _0: "${current.name}", _1: ${JSON.stringify(path)}, _2: ${compile(ast.value)} })`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error('Invalid rebind target');
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Cannot compile ${ast.kind}`);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitize(name: string): string {
|
||||||
|
const ops: Record<string, string> = {
|
||||||
|
'add': '_rt.add', 'sub': '_rt.sub', 'mul': '_rt.mul',
|
||||||
|
'div': '_rt.div', 'mod': '_rt.mod', 'eq': '_rt.eq',
|
||||||
|
'cat': '_rt.cat', 'gt': '_rt.gt', 'lt': '_rt.lt',
|
||||||
|
'gte': '_rt.gte', 'lte': '_rt.lte', 'max': '_rt.max', 'min': '_rt.min',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (ops[name]) return ops[name];
|
||||||
|
|
||||||
|
const natives = ['measureText', 'storeSearch', 'debug', 'len', 'slice', 'str'];
|
||||||
|
if (natives.includes(name)) return `_rt.${name}`;
|
||||||
|
|
||||||
|
const reserved = [
|
||||||
|
'default','class','function','return','const','let','var',
|
||||||
|
'if','else','switch','case','for','while','do','break',
|
||||||
|
'continue','new','delete','typeof','in','this','super',
|
||||||
|
'import','export','extends','static','yield','await','async',
|
||||||
|
'try','catch','finally','throw','null','true','false'
|
||||||
|
];
|
||||||
|
if (reserved.includes(name)) return `_${name}`;
|
||||||
|
|
||||||
|
return name.replace(/-/g, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
function compileMatch(ast: AST & { kind: 'match'}): string {
|
||||||
|
const expr = compile(ast.expr);
|
||||||
|
const tmpVar = `_m${Math.floor(Math.random() * 10000)}`;
|
||||||
|
|
||||||
|
let code = `((${tmpVar}) => { `;
|
||||||
|
|
||||||
|
for (const c of ast.cases) {
|
||||||
|
const { condition, bindings } = compilePattern(c.pattern, tmpVar);
|
||||||
|
code += `if (${condition}) { `;
|
||||||
|
if (bindings.length > 0) {
|
||||||
|
code += `const ${bindings.join(', ')}; `;
|
||||||
|
}
|
||||||
|
code += `return ${compile(c.result)}; }`;
|
||||||
|
}
|
||||||
|
|
||||||
|
code += `console.error("No match for:", ${tmpVar}); throw new Error("No match"); })(${expr})`;
|
||||||
|
return code;
|
||||||
|
}
|
||||||
|
|
||||||
|
function compilePattern(pattern: Pattern, expr: string): { condition: string, bindings: string[] } {
|
||||||
|
switch (pattern.kind) {
|
||||||
|
case 'wildcard':
|
||||||
|
return { condition: 'true', bindings: [] };
|
||||||
|
|
||||||
|
case 'var':
|
||||||
|
return { condition: 'true', bindings: [`${sanitize(pattern.name)} = ${expr}`] };
|
||||||
|
|
||||||
|
case 'literal':
|
||||||
|
return { condition: `${expr} === ${JSON.stringify(pattern.value)}`, bindings: [] };
|
||||||
|
|
||||||
|
case 'constructor': {
|
||||||
|
let condition = `${expr}?._tag === "${pattern.name}"`;
|
||||||
|
const bindings: string[] = [];
|
||||||
|
pattern.args.forEach((argPattern, i) => {
|
||||||
|
const sub = compilePattern(argPattern, `${expr}._${i}`);
|
||||||
|
condition += ` && ${sub.condition}`;
|
||||||
|
bindings.push(...sub.bindings);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { condition, bindings };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'list': {
|
||||||
|
let condition = `Array.isArray(${expr}) && ${expr}.length === ${pattern.elements.length}`;
|
||||||
|
const bindings: string[] = [];
|
||||||
|
pattern.elements.forEach((elemPattern, i) => {
|
||||||
|
const sub = compilePattern(elemPattern, `${expr}[${i}]`);
|
||||||
|
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
|
||||||
|
bindings.push(...sub.bindings);
|
||||||
|
});
|
||||||
|
|
||||||
|
return { condition, bindings };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'list-spread': {
|
||||||
|
let condition = `Array.isArray(${expr}) && ${expr}.length >= ${pattern.head.length}`;
|
||||||
|
const bindings: string[] = [];
|
||||||
|
pattern.head.forEach((elemPattern, i) => {
|
||||||
|
const sub = compilePattern(elemPattern, `${expr}[${i}]`);
|
||||||
|
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
|
||||||
|
bindings.push(...sub.bindings);
|
||||||
|
});
|
||||||
|
bindings.push(`${sanitize(pattern.spread)} = ${expr}.slice(${pattern.head.length})`);
|
||||||
|
|
||||||
|
return { condition, bindings };
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'record': {
|
||||||
|
let condition = 'true';
|
||||||
|
const bindings: string[] = [];
|
||||||
|
for (const [field, fieldPattern] of Object.entries(pattern.fields)) {
|
||||||
|
const sub = compilePattern(fieldPattern, `${expr}.${sanitize(field)}`);
|
||||||
|
if (sub.condition !== 'true') condition += ` && ${sub.condition}`;
|
||||||
|
bindings.push(...sub.bindings);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { condition, bindings };
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return { condition: 'true', bindings: [] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function compileAndRun(defs: Definition[]) {
|
||||||
|
const compiledDefs: string[] = [];
|
||||||
|
|
||||||
|
for (const def of defs) {
|
||||||
|
const compiled = `const ${sanitize(def.name)} = ${compile(def.body)};`;
|
||||||
|
compiledDefs.push(compiled);
|
||||||
|
|
||||||
|
try {
|
||||||
|
new Function('_rt', compiled);
|
||||||
|
} catch (e) {
|
||||||
|
console.error(`=== BROKEN: ${def.name} ===`);
|
||||||
|
console.error(compiled);
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/*
|
||||||
|
const compiledDefs = defs.map(def =>
|
||||||
|
`const ${sanitize(def.name)} = ${compile(def.body)};`
|
||||||
|
).join('\n');
|
||||||
|
*/
|
||||||
|
|
||||||
|
const lastName = defs[defs.length - 1].name;
|
||||||
|
const defNames = defs.map(d => sanitize(d.name)).join(', ');
|
||||||
|
|
||||||
|
const code = `${compiledDefs.join('\n')}
|
||||||
|
return { ${defNames}, __result: ${sanitize(lastName)} };`;
|
||||||
|
|
||||||
|
// console.log('--- Compiled Code ---');
|
||||||
|
// console.log(code);
|
||||||
|
// console.log('=====================');
|
||||||
|
|
||||||
|
const fn = new Function('_rt', code);
|
||||||
|
const allDefs = fn(_rt);
|
||||||
|
|
||||||
|
// Populate store
|
||||||
|
for (const [name, value] of Object.entries(allDefs)) {
|
||||||
|
if (name !== '__result') {
|
||||||
|
store[name] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allDefs.__result;
|
||||||
|
}
|
||||||
@ -0,0 +1,227 @@
|
|||||||
|
// import type { UIValue } from './types';
|
||||||
|
import { valueToUI } from './valueToUI-compiled';
|
||||||
|
import { render, hitTest } from './ui';
|
||||||
|
|
||||||
|
type UIValue = any;
|
||||||
|
|
||||||
|
type App = {
|
||||||
|
init: any;
|
||||||
|
update: (state: any) => (event: any) => any;
|
||||||
|
view: (state: any) => (viewport: any) => any;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ComponentInstance = {
|
||||||
|
state: any;
|
||||||
|
update: (state: any) => (event: any) => any;
|
||||||
|
view: (state: any) => any;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) {
|
||||||
|
let state = app.init;
|
||||||
|
const componentInstances = new Map<string, ComponentInstance>();
|
||||||
|
let focusedComponentKey: string | null = null;
|
||||||
|
|
||||||
|
function setupCanvas() {
|
||||||
|
const dpr = window.devicePixelRatio || 1;
|
||||||
|
canvas.width = window.innerWidth * dpr;
|
||||||
|
canvas.height = window.innerHeight * dpr;
|
||||||
|
canvas.style.width = window.innerWidth + 'px';
|
||||||
|
canvas.style.height = window.innerHeight + 'px';
|
||||||
|
}
|
||||||
|
|
||||||
|
setupCanvas();
|
||||||
|
|
||||||
|
function setFocus(componentKey: string | null) {
|
||||||
|
if (focusedComponentKey === componentKey) return;
|
||||||
|
const oldFocus = focusedComponentKey;
|
||||||
|
focusedComponentKey = componentKey;
|
||||||
|
|
||||||
|
// Blur event to the previous
|
||||||
|
if (oldFocus && componentInstances.has(oldFocus)) {
|
||||||
|
handleComponentEvent(oldFocus, { _tag: 'Blurred' });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Focus event to the new
|
||||||
|
if (componentKey && componentInstances.has(componentKey)) {
|
||||||
|
handleComponentEvent(componentKey, { name: 'Focused' });
|
||||||
|
}
|
||||||
|
rerender();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleComponentEvent(componentKey: string, event: any) {
|
||||||
|
const instance = componentInstances.get(componentKey);
|
||||||
|
if (!instance) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const result = instance.update(instance.state)(event);
|
||||||
|
instance.state = result.state;
|
||||||
|
|
||||||
|
if (result.emit && Array.isArray(result.emit)) {
|
||||||
|
for (const e of result.emit) {
|
||||||
|
handleEvent(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rerender();
|
||||||
|
} catch(error) {
|
||||||
|
console.error('Component event error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandStateful(ui: UIValue, path: number[]): UIValue {
|
||||||
|
switch (ui.kind) {
|
||||||
|
case 'stateful': {
|
||||||
|
const fullKey = [...path, ui.key].join('.');
|
||||||
|
let instance = componentInstances.get(fullKey);
|
||||||
|
|
||||||
|
if (!instance) {
|
||||||
|
console.log('Creating stateful', fullKey);
|
||||||
|
console.log('ui.init:', ui.init);
|
||||||
|
instance = {
|
||||||
|
state: ui.init,
|
||||||
|
update: ui.update,
|
||||||
|
view: ui.view
|
||||||
|
};
|
||||||
|
componentInstances.set(fullKey, instance);
|
||||||
|
} else {
|
||||||
|
// refresh closures, pick up new values
|
||||||
|
instance.update = ui.update;
|
||||||
|
instance.view = ui.view;
|
||||||
|
}
|
||||||
|
console.log('Instance state', instance.state);
|
||||||
|
|
||||||
|
const viewResult = instance.view(instance.state);
|
||||||
|
let viewUI = valueToUI(viewResult);
|
||||||
|
|
||||||
|
if (ui.focusable) {
|
||||||
|
viewUI = {
|
||||||
|
kind: 'clickable',
|
||||||
|
child: viewUI,
|
||||||
|
event: { _tag: 'FocusAndClick', _0: fullKey }
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return expandStateful(viewUI, path);
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'stack':
|
||||||
|
case 'row':
|
||||||
|
case 'column': {
|
||||||
|
return {
|
||||||
|
...ui,
|
||||||
|
children: ui.children.map((child: UIValue, i: number) =>
|
||||||
|
expandStateful(child, [...path, i])
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
case 'clickable':
|
||||||
|
case 'padding':
|
||||||
|
case 'positioned':
|
||||||
|
case 'opacity':
|
||||||
|
case 'clip': {
|
||||||
|
return {
|
||||||
|
...ui,
|
||||||
|
child: expandStateful((ui as any).child, [...path, 0])
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// leaf nodes
|
||||||
|
return ui;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function rerender() {
|
||||||
|
const viewport = { width: window.innerWidth, height: window.innerHeight };
|
||||||
|
|
||||||
|
try {
|
||||||
|
const uiValue = app.view(state)(viewport);
|
||||||
|
const ui = valueToUI(uiValue);
|
||||||
|
const expandedUI = expandStateful(ui, []);
|
||||||
|
render(expandedUI, canvas);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Render error:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleEvent(event: any) {
|
||||||
|
if (!event || !event._tag) return;
|
||||||
|
|
||||||
|
if (event._tag === 'Batch' && event._0) {
|
||||||
|
for (const e of event._0) {
|
||||||
|
handleEvent(e);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event._tag === 'FocusAndClick') {
|
||||||
|
const componentKey = event._0;
|
||||||
|
const coords = event._1;
|
||||||
|
setFocus(componentKey);
|
||||||
|
handleComponentEvent(componentKey, { _tag: 'Clicked', _0: coords });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event._tag === 'Rebind') {
|
||||||
|
rt.rebind(event._0, event._1, event._2);
|
||||||
|
rerender();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event._tag === 'Focus') {
|
||||||
|
setFocus(event._0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event._tag === 'NoOp')
|
||||||
|
return;
|
||||||
|
|
||||||
|
state = app.update(state)(event);
|
||||||
|
rerender();
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.addEventListener('click', (e) => {
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
const x = e.clientX - rect.left;
|
||||||
|
const y = e.clientY - rect.top;
|
||||||
|
|
||||||
|
const hitResult = hitTest(x, y);
|
||||||
|
if (hitResult) {
|
||||||
|
const { event, relativeX, relativeY } = hitResult;
|
||||||
|
|
||||||
|
if (event._tag === 'FocusAndClick') {
|
||||||
|
handleEvent({
|
||||||
|
_tag: 'FocusAndClick',
|
||||||
|
_0: event._0,
|
||||||
|
_1: { x: Math.floor(relativeX), y: Math.floor(relativeY) }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
handleEvent(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('keydown', (e) => {
|
||||||
|
let event: any;
|
||||||
|
|
||||||
|
if (e.key.length === 1 && !e.ctrlKey && !e.metaKey && !e.altKey) {
|
||||||
|
event = { _tag: 'Char', _0: e.key };
|
||||||
|
} else {
|
||||||
|
event = { _tag: e.key };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (focusedComponentKey) {
|
||||||
|
handleComponentEvent(focusedComponentKey, event);
|
||||||
|
} else {
|
||||||
|
handleEvent(event);
|
||||||
|
}
|
||||||
|
e.preventDefault();
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('resize', () => {
|
||||||
|
setupCanvas();
|
||||||
|
rerender();
|
||||||
|
})
|
||||||
|
|
||||||
|
rerender();
|
||||||
|
}
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
export const store: Record<string, any> = {};
|
||||||
|
|
||||||
|
export const _rt = {
|
||||||
|
add: (a: number) => (b: number) => a + b,
|
||||||
|
sub: (a: number) => (b: number) => a - b,
|
||||||
|
mul: (a: number) => (b: number) => a * b,
|
||||||
|
div: (a: number) => (b: number) => a / b,
|
||||||
|
mod: (a: number) => (b: number) => a % b,
|
||||||
|
cat: (a: string) => (b: string) => a + b,
|
||||||
|
max: (a: number) => (b: number) => Math.max(a, b),
|
||||||
|
min: (a: number) => (b: number) => Math.min(a, b),
|
||||||
|
|
||||||
|
eq: (a: any) => (b: any) => ({ _tag: a === b ? 'True' : 'False' }),
|
||||||
|
neq: (a: any) => (b: any) => ({ _tag: a !== b ? 'True' : 'False' }),
|
||||||
|
gt: (a: any) => (b: any) => ({ _tag: a > b ? 'True' : 'False' }),
|
||||||
|
lt: (a: any) => (b: any) => ({ _tag: a < b ? 'True' : 'False' }),
|
||||||
|
gte: (a: any) => (b: any) => ({ _tag: a >= b ? 'True' : 'False' }),
|
||||||
|
lte: (a: any) => (b: any) => ({ _tag: a <= b ? 'True' : 'False' }),
|
||||||
|
|
||||||
|
len: (list: any[]) => list.length,
|
||||||
|
str: (x: any) => String(x),
|
||||||
|
slice: (s: string | any[]) => (start: number) => (end: number) => s.slice(start, end),
|
||||||
|
debug: (label: string) => (value: any) => { console.log(label, value); return value; },
|
||||||
|
measureText: (text: string) => {
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
if (ctx) {
|
||||||
|
ctx.font = '16px "SF Mono", "Monaco", "Menlo", monospace';
|
||||||
|
return Math.floor(ctx.measureText(text).width);
|
||||||
|
}
|
||||||
|
return text.length * 10; // fallback
|
||||||
|
},
|
||||||
|
storeSearch: (query: string) => {
|
||||||
|
const results: string[] = [];
|
||||||
|
const searchTerm = query.toLowerCase();
|
||||||
|
console.log("in storeSearch. query: ", searchTerm);
|
||||||
|
for (const name of Object.keys(store)) {
|
||||||
|
if (searchTerm === '' || name.toLowerCase().includes(searchTerm)) {
|
||||||
|
results.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
},
|
||||||
|
rebind: (name: string, pathOrValue: any, maybeValue?: any) => {
|
||||||
|
if (maybeValue === undefined) {
|
||||||
|
store[name] = pathOrValue;
|
||||||
|
} else {
|
||||||
|
const path = pathOrValue as string[];
|
||||||
|
let obj = store[name];
|
||||||
|
for (let i = 0; i < path.length - 1; i++) {
|
||||||
|
obj = obj[path[i]];
|
||||||
|
}
|
||||||
|
obj[path[path.length - 1]] = maybeValue;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -0,0 +1,96 @@
|
|||||||
|
// import type { UIValue } from './types'
|
||||||
|
|
||||||
|
export function valueToUI(value: any): any {
|
||||||
|
if (!value || !value._tag)
|
||||||
|
throw new Error(`Expected UI constructor, got: ${JSON.stringify(value)}`);
|
||||||
|
|
||||||
|
switch(value._tag) {
|
||||||
|
case 'Rect':
|
||||||
|
return {
|
||||||
|
kind: 'rect',
|
||||||
|
w: value.w,
|
||||||
|
h: value.h,
|
||||||
|
color: value.color,
|
||||||
|
radius: value.radius,
|
||||||
|
strokeWidth: value.strokeWidth,
|
||||||
|
strokeColor: value.strokeColor,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'Text':
|
||||||
|
return {
|
||||||
|
kind: 'text',
|
||||||
|
content: value.content,
|
||||||
|
color: value.color,
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'Row':
|
||||||
|
return {
|
||||||
|
kind: 'row',
|
||||||
|
gap: value.gap || 0,
|
||||||
|
children: value.children.map(valueToUI),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'Column':
|
||||||
|
return {
|
||||||
|
kind: 'column',
|
||||||
|
gap: value.gap || 0,
|
||||||
|
children: value.children.map(valueToUI),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'Stack':
|
||||||
|
return {
|
||||||
|
kind: 'stack',
|
||||||
|
children: value.children.map(valueToUI),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'Positioned':
|
||||||
|
return {
|
||||||
|
kind: 'positioned',
|
||||||
|
x: value.x || 0,
|
||||||
|
y: value.p || 0,
|
||||||
|
child: valueToUI(value.child),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'Padding':
|
||||||
|
return {
|
||||||
|
kind: 'padding',
|
||||||
|
amount: value.amount || 0,
|
||||||
|
child: valueToUI(value.child),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'Clickable':
|
||||||
|
return {
|
||||||
|
kind: 'clickable',
|
||||||
|
event: value.event,
|
||||||
|
child: valueToUI(value.child),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'Clip':
|
||||||
|
return {
|
||||||
|
kind: 'clip',
|
||||||
|
w: value.w,
|
||||||
|
h: value.h,
|
||||||
|
child: valueToUI(value.child),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'Opacity':
|
||||||
|
return {
|
||||||
|
kind: 'opacity',
|
||||||
|
opacity: value.opacity,
|
||||||
|
child: valueToUI(value.child),
|
||||||
|
};
|
||||||
|
|
||||||
|
case 'Stateful':
|
||||||
|
return {
|
||||||
|
kind: 'stateful',
|
||||||
|
key: value.key,
|
||||||
|
focusable: value.focusable,
|
||||||
|
init: value.init,
|
||||||
|
update: value.update,
|
||||||
|
view: value.view,
|
||||||
|
};
|
||||||
|
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown UI constructor: ${value._tag}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue