We have UI! kind of
parent
52647a9ce1
commit
5b40e9d298
@ -0,0 +1,16 @@
|
||||
init = 0;
|
||||
|
||||
update = state event \ state + 1;
|
||||
|
||||
view = count \
|
||||
Column {
|
||||
gap = 20,
|
||||
children = [
|
||||
Clickable {
|
||||
event = "increment",
|
||||
child = Rect { w = 100, h = 40, color = "blue" }
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
{ init = init, update = update, view = view }
|
||||
@ -1,30 +1,30 @@
|
||||
import type { UIValue } from './types';
|
||||
import { render } from './ui';
|
||||
import { evaluate } from './interpreter'
|
||||
import type { Env } from './env'
|
||||
import { tokenize } from './lexer'
|
||||
import { Parser } from './parser'
|
||||
import cgCode from './counter.cg?raw';
|
||||
import { runApp } from './runtime';
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.width = 800;
|
||||
canvas.height = 600;
|
||||
document.body.appendChild(canvas);
|
||||
|
||||
const ui: UIValue = {
|
||||
kind: 'column',
|
||||
gap: 10,
|
||||
children: [
|
||||
{ kind: 'text', content: "Hello CG World", x: 0, y: 20 },
|
||||
{ kind: 'rect', w: 200, h: 50, color: 'blue' },
|
||||
{ kind: 'text', content: "YESS", x: 0, y: 20 },
|
||||
{ kind: 'row', gap: 13, children:
|
||||
[
|
||||
{ kind: 'text', content: "In a row", x: 0, y: 10 },
|
||||
{ kind: 'clickable', event: "test", child: {
|
||||
kind: 'padding', amount: 10, child: {
|
||||
kind: 'rect', w: 80, h: 30, color: '#4a90e2'
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
const tokens = tokenize(cgCode);
|
||||
const parser = new Parser(tokens);
|
||||
const ast = parser.parse();
|
||||
console.log(ast);
|
||||
|
||||
render(ui, canvas);
|
||||
const env: Env = new Map();
|
||||
const appRecord = evaluate(ast, env);
|
||||
|
||||
console.log(appRecord);
|
||||
|
||||
if (appRecord.kind !== 'record')
|
||||
throw new Error('Expected record');
|
||||
|
||||
const init = appRecord.fields.init;
|
||||
const update = appRecord.fields.update;
|
||||
const view = appRecord.fields.view;
|
||||
|
||||
runApp({ init, update, view }, canvas);
|
||||
|
||||
@ -0,0 +1,56 @@
|
||||
import type { Value } from './types';
|
||||
import { valueToUI } from './valueToUI';
|
||||
import { render, hitTest } from './ui';
|
||||
import { evaluate } from './interpreter';
|
||||
|
||||
export type App = {
|
||||
init: Value;
|
||||
update: Value; // State / Event / State
|
||||
view: Value; // State / UI
|
||||
}
|
||||
|
||||
export function runApp(app: App, canvas: HTMLCanvasElement) {
|
||||
let state = app.init;
|
||||
|
||||
function rerender() {
|
||||
if (app.view.kind !== 'closure')
|
||||
throw new Error('view must be a function');
|
||||
|
||||
const callEnv = new Map(app.view.env);
|
||||
callEnv.set(app.view.params[0], state);
|
||||
const uiValue = evaluate(app.view.body, callEnv);
|
||||
const ui = valueToUI(uiValue);
|
||||
|
||||
render(ui, canvas);
|
||||
}
|
||||
|
||||
function handleEvent(eventName: string) {
|
||||
if (app.update.kind !== 'closure')
|
||||
throw new Error('update must be a function');
|
||||
|
||||
if (app.update.params.length !== 2)
|
||||
throw new Error('update must have 2 parameters');
|
||||
|
||||
const event: Value = { kind: 'string', value: eventName };
|
||||
const callEnv = new Map(app.update.env);
|
||||
callEnv.set(app.update.params[0], state);
|
||||
callEnv.set(app.update.params[1], event);
|
||||
const newState = evaluate(app.update.body, callEnv);
|
||||
|
||||
state = newState;
|
||||
rerender();
|
||||
}
|
||||
|
||||
canvas.addEventListener('click', (e) => {
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
|
||||
const eventName = hitTest(x, y);
|
||||
if (eventName) {
|
||||
handleEvent(eventName);
|
||||
}
|
||||
});
|
||||
|
||||
rerender();
|
||||
}
|
||||
@ -0,0 +1,80 @@
|
||||
import type { Value, UIValue } from './types';
|
||||
|
||||
export function valueToUI(value: Value): UIValue {
|
||||
console.log("valueToUI", value);
|
||||
if (value.kind !== 'constructor') {
|
||||
throw new Error('UI value must be a constructor');
|
||||
}
|
||||
|
||||
if (value.args.length !== 1 || value.args[0].kind !== 'record')
|
||||
throw new Error('UI constructor must have 1 record argument');
|
||||
|
||||
const fields = value.args[0].fields;
|
||||
|
||||
switch (value.name) {
|
||||
case 'Rect': {
|
||||
const w = fields.w;
|
||||
const h = fields.h;
|
||||
const color = fields.color;
|
||||
|
||||
if (w.kind !== 'int' || h.kind !== 'int' || color.kind !== 'string')
|
||||
throw new Error('Invalid Rect fields');
|
||||
|
||||
return { kind: 'rect', w: w.value, h: h.value, color: color.value };
|
||||
}
|
||||
|
||||
case 'Text': {
|
||||
const x = fields.x;
|
||||
const y = fields.y;
|
||||
const content = fields.content;
|
||||
|
||||
if (content.kind !== 'string' || x.kind !== 'int' || y.kind !== 'int')
|
||||
throw new Error('Invalid Text fields');
|
||||
|
||||
return { kind: 'text', x: x.value, y: y.value, content: content.value };
|
||||
}
|
||||
|
||||
case 'Column': {
|
||||
const children = fields.children;
|
||||
const gap = fields.gap;
|
||||
|
||||
if (children.kind !== 'list' || gap.kind !== 'int')
|
||||
throw new Error('Invalid Column fields');
|
||||
|
||||
return { kind: 'column', gap: gap.value, children: children.elements.map(valueToUI) };
|
||||
}
|
||||
|
||||
case 'Row': {
|
||||
const children = fields.children;
|
||||
const gap = fields.gap;
|
||||
|
||||
if (children.kind !== 'list' || gap.kind !== 'int')
|
||||
throw new Error('Invalid Row fields');
|
||||
|
||||
return { kind: 'row', gap: gap.value, children: children.elements.map(valueToUI) };
|
||||
}
|
||||
|
||||
case 'Clickable': {
|
||||
const child = fields.child;
|
||||
const event = fields.event;
|
||||
|
||||
if (event.kind !== 'string')
|
||||
throw new Error('Invalid Clickable fields');
|
||||
|
||||
return { kind: 'clickable', event: event.value, child: valueToUI(child) };
|
||||
}
|
||||
|
||||
case 'Padding': {
|
||||
const child = fields.child;
|
||||
const amount = fields.amount;
|
||||
|
||||
if (amount.kind !== 'int')
|
||||
throw new Error('Invalid Padding fields');
|
||||
|
||||
return { kind: 'padding', amount: amount.value, child: valueToUI(child) };
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Unknown UI constructor: ${value.name}`);
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue