diff --git a/index.html b/index.html index 602484a..7a1c06a 100644 --- a/index.html +++ b/index.html @@ -33,7 +33,7 @@ canvas {
- + diff --git a/src/ast.ts b/src/ast.ts index a3d64df..3ad6079 100644 --- a/src/ast.ts +++ b/src/ast.ts @@ -17,7 +17,7 @@ export type Variable = { kind: 'variable' name: string line?: number - column: number + column?: number start?: number } @@ -148,24 +148,6 @@ export type Definition = { start?: number } -export type TypeDef = { - kind: 'typedef' - name: string - variants: Array<{ name: string, args: string[] }> - line?: number - column?: number - start?: number -} - -export type Import = { - kind: 'import' - module: string - items: string[] | 'all' - line?: number - column?: number - start?: number -} - export type Rebind = { kind: 'rebind' target: AST @@ -189,8 +171,6 @@ export type AST = | List | ListSpread | Definition - | TypeDef - | Import | Rebind export function prettyPrint(ast: AST, indent = 0): string { diff --git a/src/cg/03-ui-components.cg b/src/cg/03-ui-components.cg index 8e2647d..e84a184 100644 --- a/src/cg/03-ui-components.cg +++ b/src/cg/03-ui-components.cg @@ -53,21 +53,21 @@ box = config \ ui.stack { children = [ -# background + # background ui.rect { w = c.w, h = c.h, color = c.color }, -# top border + # top border ui.rect { w = c.w, h = c.borderTop, color = c.borderColor }, -# bottom border + # bottom border ui.positioned { x = 0, y = c.h - c.borderBottom, child = ui.rect { w = c.w, h = c.borderBottom, color = c.borderColor } }, -# left border + # left border ui.rect { w = c.borderLeft, h = c.h, color = c.borderColor }, -# right border + # right border ui.positioned { x = c.w - c.borderRight, y = 0, child = ui.rect { w = c.borderRight, h = c.h, color = c.borderColor } }, -# content + # content ui.positioned { x = c.paddingLeft, y = c.paddingTop, child = c.child } ] }; @@ -160,7 +160,7 @@ textInput = config \ ui.stateful { | Focused \ { state = state.{ focused = True }, emit = [] } | Blurred \ { state = state.{ focused = False }, emit = [] } - | Key { key = k } \ { state = state, emit = [\ config.onKeyDown k ] } + # | Key { key = k } \ { state = state, emit = [\ config.onKeyDown k ] } | _ \ { state = state, emit = [] }, view = state \ diff --git a/src/cg/04-test.cg b/src/cg/04-test.cg deleted file mode 100644 index 6b6cc40..0000000 --- a/src/cg/04-test.cg +++ /dev/null @@ -1,43 +0,0 @@ -# standard library testing - -list = [1, 2, 3, 4, 5]; -add1 = x \ x + 1; -mapped = map add1 list; -isEven = x \ x % 2 == 0; -sumList = list \ fold add 0 list; -filtered = filter isEven mapped; -summed = sumList mapped; -listLen = len list; -reversed = reverse list; -take3 = take 3 list; -drop3 = drop 3 list; -list2 = [6, 7, 8]; -zipped = zipWith add list list2; -anded = and True False; -ored = or True False; -notted = not False; -ranged = range 3 14; -anyEven = any isEven list; -allEven = all isEven list; -flattened = flatten [[1, 2, 3], [4, 5, 6]]; -contains3 = contains 3 list; -findEven = find isEven list; - -stdlibTestResult = { - filtered = filtered, - summed = summed, - listLen = listLen, - reversed = reversed, - take3 = take3, - drop3 = drop3, - zipped = zipped, - anded = anded, - ored = ored, - notted = notted, - ranged = ranged, - anyEven = anyEven, - allEven = allEven, - flattened = flattened, - contains3 = contains3, - findEven = findEven -}; diff --git a/src/cg/05-palette.cg b/src/cg/05-palette.cg index aaaa79d..4667749 100644 --- a/src/cg/05-palette.cg +++ b/src/cg/05-palette.cg @@ -1,4 +1,5 @@ -# TODO: Section labels - flat list with Section/Item types, +# TODO +# Section labels - flat list with Section/Item types, # skip sections in keyboard nav, when scrolling with keyboard # itemHeight # items = [ @@ -30,14 +31,17 @@ # | None \ currentIndex); +paletteState = { + query = "" +}; palette = config \ focusedIndex = 0; windowHeight = 400; windowWidth = 600; - results = take 8 (config.search config.state.query); - # results = config.search config.state.query; # once you get scrolling.. + results = take 8 (config.search paletteState.query); + # results = config.search paletteState.query; # once you get scrolling.. dialogPadding = 0; @@ -64,62 +68,74 @@ palette = config \ } }; - ui.positioned { - x = (config.viewport.width - windowWidth) / 2, - y = (config.viewport.height - windowHeight) / 2, - - child = ui.stack { - children = [ - ui.rect { w = windowWidth, h = windowHeight, color = "#063351", radius = 0, strokeWidth = 1, strokeColor = "#1A5F80" }, - ui.padding { - amount = dialogPadding, - child = ui.column { - gap = 0, - children = [ - textInput { - key = "palette-query", - initialValue = config.state.query, - initialFocus = True, - color = "white", - backgroundColor = "rgba(0,0,0,0.2)", - w = contentWidth, - h = textInputHeight, - onChange = text \ batch [config.state.query := text, config.state.focusedIndex := 0], - onKeyDown = key \ key - | "ArrowUp" \ config.state.focusedIndex := max 0 (config.state.focusedIndex - 1) - | "ArrowDown" \ config.state.focusedIndex := min (len results - 1) (config.state.focusedIndex + 1) - | "Enter" \ (\ config.onSelect (unwrapOr "" (nth config.state.focusedIndex results))) - | _ \ noOp - }, - - ui.clip { - w = contentWidth, - h = listHeight, - child = ui.column { - gap = 1, - children = [ - box { - w = contentWidth, - h = 30, - color = "transparent", - paddingLeft = 6, - paddingTop = 8, - child = ui.text { content = "Store values", color = "#bbb" }, - }, - - ...(mapWithIndex (t i \ paletteRow { - child = t, - w = contentWidth, - h = textInputHeight, - selected = (config.state.focusedIndex == i), - onClick = \ config.onSelect t - }) results) - ] - } + ui.stateful { + key = "palette", + focusable = False, + + init = { + focusedIndex = 0, + }, + + update = state event \ event + | Key { printable = True } \ { state = state.{ focusedIndex = 0 }, emit = [] } + | Key { key = "ArrowUp" } \ { state = state.{ focusedIndex = max 0 (state.focusedIndex - 1) }, emit = [] } + | Key { key = "ArrowDown" } \ { state = state.{ focusedIndex = min (len results - 1) (state.focusedIndex + 1) }, emit = [] } + | Key { key = "Enter" } \ _ = debug "Enter" []; { state = state, emit = [config.onSelect (unwrapOr "" (nth state.focusedIndex results))] } + | e \ _ = debug "event" e; { state = state, emit = [] }, + + view = state \ + ui.positioned { + x = (config.viewport.width - windowWidth) / 2, + y = (config.viewport.height - windowHeight) / 2, + + child = ui.stack { + children = [ + ui.rect { w = windowWidth, h = windowHeight, color = "#063351", radius = 0, strokeWidth = 1, strokeColor = "#1A5F80" }, + ui.padding { + amount = dialogPadding, + child = ui.column { + gap = 0, + children = [ + textInput { + key = "palette-query", + initialValue = "", + initialFocus = True, + color = "white", + backgroundColor = "rgba(0,0,0,0.2)", + w = contentWidth, + h = textInputHeight, + onChange = text \ batch [paletteState.query := text], + }, + + ui.clip { + w = contentWidth, + h = listHeight, + child = ui.column { + gap = 1, + children = [ + box { + w = contentWidth, + h = 30, + color = "transparent", + paddingLeft = 6, + paddingTop = 8, + child = ui.text { content = "Store values", color = "#bbb" }, + }, + + ...(mapWithIndex (t i \ paletteRow { + child = t, + w = contentWidth, + h = textInputHeight, + selected = (state.focusedIndex == i), + onClick = \ config.onSelect t + }) results) + ] + } + } + ] } - ] - } + } + ] } - ] - } - }; + } + }; diff --git a/src/cg/10-os.cg b/src/cg/10-os.cg index 269a185..32ecace 100644 --- a/src/cg/10-os.cg +++ b/src/cg/10-os.cg @@ -1,8 +1,6 @@ osState = { palette = { - visible = True, - query = "", - focusedIndex = 0, + visible = True }, inspector = { visible = False, @@ -37,8 +35,9 @@ inspect = item \ init = {}; update = state event \ event - | Key { key = "p", meta = True } \ osState.palette.visible := not (osState.palette.visible) - | _ \ state; + | Key { key = "p", meta = True } \ + { state = state, emit = [osState.palette.visible := not (osState.palette.visible)] } + | _ \ { state = state, emit = [] }; view = state viewport \ ui.stack { @@ -55,13 +54,13 @@ view = state viewport \ # keep palette at the end so it's on top osState.palette.visible | True \ palette { - state = osState.palette, + # state = osState.palette, + # query = osState.palette.query, search = storeSearch, onSelect = onSelect, viewport = viewport, } | False \ empty, - ] }; diff --git a/src/compiler.ts b/src/compiler.ts index ccb2e8c..4893254 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -85,10 +85,7 @@ export function compile(ast: AST, useStore = true, bound = new Set()): s const path = getPath(ast.target); const value = compile(ast.value, useStore, bound); - if (!useStore || !rootName) { - const target = compile(ast.target, useStore, bound); - return `(() => { ${target} = ${value}; return { _tag: "Rerender" }; })()`; - } + if (!rootName) throw new Error('Rebind target must be a variable'); if (path.length === 0) { return `({ _tag: "Rebind", _0: "${rootName}", _1: ${value} })`; @@ -105,15 +102,15 @@ export function compile(ast: AST, useStore = true, bound = new Set()): s function sanitize(name: string, useStore = true): string { const ops: Record = { - 'add': '_rt.add', 'sub': '_rt.sub', 'mul': '_rt.mul', - 'div': '_rt.div', 'mod': '_rt.mod', 'eq': '_rt.eq', - 'cat': '_rt.cat', 'gt': '_rt.gt', 'lt': '_rt.lt', - 'gte': '_rt.gte', 'lte': '_rt.lte', 'max': '_rt.max', 'min': '_rt.min', + 'add': '_rt.add', 'sub': '_rt.sub', 'mul': '_rt.mul', 'div': '_rt.div', + 'mod': '_rt.mod', 'pow': '_rt.pow', + 'eq': '_rt.eq', 'gt': '_rt.gt', 'lt': '_rt.lt', 'gte': '_rt.gte', 'lte': '_rt.lte', + 'cat': '_rt.cat', 'max': '_rt.max', 'min': '_rt.min', }; if (ops[name]) return ops[name]; - const natives = ['eval', 'measure', 'measureText', 'storeSearch', 'getSource', 'debug', 'nth', 'len', 'slice', 'join', 'chars', 'split', 'str', 'redefine', 'undefine', 'stateful', 'batch', 'noOp', 'rerender', 'focus', 'ui']; + const natives = ['eval', 'measure', 'measureText', 'storeSearch', 'getSource', 'debug', 'nth', 'len', 'slice', 'join', 'chars', 'split', 'str', 'redefine', 'undefine', 'batch', 'noOp', 'focus', 'ui']; if (natives.includes(name)) return `_rt.${name}`; if (useStore) { diff --git a/src/parser.ts b/src/parser.ts index 825b3ea..6a1cf88 100644 --- a/src/parser.ts +++ b/src/parser.ts @@ -165,7 +165,7 @@ export class Parser { let expr = this.parseInfix(); // Rebind - if (this.current().kind == 'colon-equals') { + if (this.current().kind === 'colon-equals') { const token = this.current(); this.advance(); const value = this.parseExpressionNoMatch(); diff --git a/src/runtime-compiled.ts b/src/runtime-compiled.ts index 6575027..e73f2ff 100644 --- a/src/runtime-compiled.ts +++ b/src/runtime-compiled.ts @@ -60,7 +60,6 @@ export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) { handleEvent(e); } } - rerender(); } catch(error) { console.error('Component event error:', error); } @@ -102,7 +101,7 @@ export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) { event: { _tag: 'FocusAndClick', _0: fullKey } }; } - return expandStateful(viewUI, path, renderedKeys); + return expandStateful(viewUI, [...path, ui.key], renderedKeys); } case 'stack': @@ -186,7 +185,6 @@ export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) { if (event._tag === 'Rebind') { rt.rebind(event._0, event._1, event._2); - rerender(); return; } @@ -198,8 +196,13 @@ export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) { if (event._tag === 'NoOp') return; - state = app.update(state)(event); - rerender(); + const result = app.update(state)(event); + state = result.state; + if (result.emit && Array.isArray(result.emit)) { + for (const e of result.emit) { + handleEvent(e); + } + } } canvas.addEventListener('click', (e) => { @@ -221,6 +224,8 @@ export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) { handleEvent(event); } } + + rerender(); }); window.addEventListener('keydown', (e) => { @@ -236,20 +241,37 @@ export function runAppCompiled(app: App, canvas: HTMLCanvasElement, rt: any) { } }; - // always send to OS first - handleEvent(event); + console.log("keydown", e) + console.log("componentInstances", componentInstances) + console.log("focusedComponentKey", focusedComponentKey) 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); + } + } } + // OS root + handleEvent(event); + e.preventDefault(); + rerender(); }); + let resizeRAF = 0; window.addEventListener('resize', () => { - setupCanvas(); - rerender(); - }) + cancelAnimationFrame(resizeRAF); + resizeRAF = requestAnimationFrame(() => { + setupCanvas(); + rerender(); + }); + }); rerender(); } diff --git a/src/runtime-js.ts b/src/runtime-js.ts index 2186b77..f910068 100644 --- a/src/runtime-js.ts +++ b/src/runtime-js.ts @@ -14,7 +14,8 @@ export const _rt = { mul: (a: number) => (b: number) => a * b, div: (a: number) => (b: number) => a / b, mod: (a: number) => (b: number) => a % b, - cat: (a: string) => (b: string) => a + b, + pow: (a: number) => (b: number) => a ** b, + cat: (a: any) => (b: any) => Array.isArray(a) ? [...a, ...b] : a + b, max: (a: number) => (b: number) => Math.max(a, b), min: (a: number) => (b: number) => Math.min(a, b), @@ -110,7 +111,6 @@ export const _rt = { batch: (events: any[]) => ({ _tag: 'Batch', _0: events }), noOp: { _tag: 'NoOp' }, - rerender: { _tag: 'Rerender' }, focus: (key: string) => ({ _tag: 'Focus', _0: key }), nth: (i: number) => (xs: any[] | string) => i >= 0 && i < xs.length @@ -142,6 +142,7 @@ export const _rt = { return printed; }, rebind: (name: string, pathOrValue: any, maybeValue?: any) => { + console.log("rebind", store, name, pathOrValue, maybeValue); if (maybeValue === undefined) { store[name] = pathOrValue; } else { @@ -202,12 +203,12 @@ export const _rt = { const defs = parser.parse(); const ast = defs[0].body; // validate free vars - const free = freeVars(ast); - const allowed = new Set([ - ...Object.keys(store), - ...Object.keys(_rt), - 'True' - ]) + // const free = freeVars(ast); + // const allowed = new Set([ + // ...Object.keys(store), + // ...Object.keys(_rt), + // 'True' + // ]) const compiled = compile(defs[0].body); const fn = new Function('_rt', 'store', `return ${compiled}`); diff --git a/src/ui.ts b/src/ui.ts index ee90913..5867c85 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -213,7 +213,9 @@ export function measure(ui: UIValue): { width: number, height: number } { } export function hitTest(x: number, y: number): { event: any, relativeX: number, relativeY: number } | null { - for (const region of clickRegions) { + for (let i = clickRegions.length - 1; i >= 0; i--) { + const region = clickRegions[i]; + if (x >= region.x && x < region.x + region.width && y >= region.y && y < region.y + region.height) { return { diff --git a/src/valueToUI-compiled.ts b/src/valueToUI-compiled.ts index b69940b..40dc5d4 100644 --- a/src/valueToUI-compiled.ts +++ b/src/valueToUI-compiled.ts @@ -1,5 +1,3 @@ -// import type { UIValue } from './types' - export function valueToUI(value: any): any { if (!value || !value._kind) throw new Error(`Expected UI constructor, got: ${JSON.stringify(value)}`); @@ -92,6 +90,6 @@ export function valueToUI(value: any): any { }; default: - throw new Error(`Unknown UI constructor: ${value._tag}`); + throw new Error(`Unknown UI constructor: ${value._kind}`); } }