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