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.
cg/src/runtime-compiled.ts

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();
}