You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
225 lines
6.4 KiB
TypeScript
225 lines
6.4 KiB
TypeScript
// 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) {
|
|
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;
|
|
}
|
|
|
|
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();
|
|
}
|