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

271 lines
8.1 KiB
TypeScript

import { render, hitTest } from './ui';
type UIValue = any;
type ComponentInstance = {
state: any;
update: (state: any) => (event: any) => any;
view: (state: any) => any;
};
export function runAppCompiled(canvas: HTMLCanvasElement, store: any) {
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, { _tag: 'Focused' });
}
// Notify ancestors
if (componentKey) {
for (const key of componentInstances.keys()) {
if (key !== componentKey && componentKey.startsWith(key + '.')) {
handleComponentEvent(key, { _tag: 'ChildFocused' });
}
}
}
}
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);
}
}
} catch(error) {
console.error('Component event error:', error);
}
}
function expandStateful(ui: UIValue, path: number[], renderedKeys: Set<string>): UIValue {
switch (ui.kind) {
case 'stateful': {
const fullKey = [...path, ui.key].join('.');
renderedKeys.add(fullKey);
let instance = componentInstances.get(fullKey);
const isNew = !instance;
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;
}
if (ui.autoFocus?._tag === 'True' && isNew) {
setFocus(fullKey);
}
const viewResult = instance.view(instance.state);
let viewUI = viewResult;
if (ui.focusable) {
viewUI = {
kind: 'clickable',
child: viewUI,
event: { _tag: 'FocusAndClick', _0: fullKey }
};
}
return expandStateful(viewUI, [...path, ui.key], renderedKeys);
}
case 'stack':
case 'row':
case 'column': {
return {
...ui,
children: ui.children.map((child: UIValue, i: number) =>
expandStateful(child, [...path, i], renderedKeys)
)
}
}
case 'clickable':
case 'padding':
case 'positioned':
case 'opacity':
case 'clip': {
return {
...ui,
child: expandStateful((ui as any).child, [...path, 0], renderedKeys)
};
}
default:
// leaf nodes
return ui;
}
}
function rerender() {
const renderedKeys = new Set<string>();
try {
const expandedUI = expandStateful(store.os, [], renderedKeys);
// clean up unrendered instances
for (const key of componentInstances.keys()) {
if (!renderedKeys.has(key)) {
componentInstances.delete(key);
}
}
if (focusedComponentKey && !renderedKeys.has(focusedComponentKey)) {
const parts = focusedComponentKey.split('.');
focusedComponentKey = null;
while (parts.length > 0) {
parts.pop();
const ancestor = parts.join('.');
if (ancestor && renderedKeys.has(ancestor)) {
focusedComponentKey = ancestor;
break;
}
}
}
render(expandedUI, canvas);
} catch (error) {
console.error('Render error:', error);
}
}
function handleEvent(event: any) {
// Thunk
if (typeof event === 'function') {
handleEvent(event(null));
return;
}
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') {
store.rebind(event._0, event._1, event._2);
return;
}
if (event._tag === 'Focus') {
setFocus(event._0);
return;
}
if (event._tag === 'NoOp')
return;
}
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);
}
}
rerender();
});
window.addEventListener('keydown', (e) => {
const event = {
_tag: 'Key',
_0: {
key: e.key,
ctrl: { _tag: e.ctrlKey ? 'True' : 'False' },
meta: { _tag: e.metaKey ? 'True' : 'False' },
alt: { _tag: e.altKey ? 'True' : 'False' },
shift: { _tag: e.shiftKey ? 'True' : 'False' },
printable: { _tag: e.key.length === 1 ? 'True' : 'False' }
}
};
if (focusedComponentKey) {
// send to focused component
handleComponentEvent(focusedComponentKey, event);
// bubble up to ancestors
for (const key of componentInstances.keys()) {
if (key !== focusedComponentKey && focusedComponentKey.startsWith(key + '.')) {
handleComponentEvent(key, event);
}
}
}
e.preventDefault();
rerender();
});
let resizeRAF = 0;
window.addEventListener('resize', () => {
cancelAnimationFrame(resizeRAF);
resizeRAF = requestAnimationFrame(() => {
setupCanvas();
store.viewport = { width: window.innerWidth, height: window.innerHeight };
rerender();
});
});
rerender();
}