diff --git a/src/cg/03-ui-components.cg b/src/cg/03-ui-components.cg index 5070286..ecb0916 100644 --- a/src/cg/03-ui-components.cg +++ b/src/cg/03-ui-components.cg @@ -1,45 +1,45 @@ centerH = parentW child \ - childW = (measure child).width; - Positioned { x = (parentW - childW) / 2, y = 0, child = child }; + childW = (ui.measure child).width; + ui.positioned { x = (parentW - childW) / 2, y = 0, child = child }; centerV = parentH child \ - childH = (measure child).height; - Positioned { y = (parentH - childH) / 2, x = 0, child = child }; + childH = (ui.measure child).height; + ui.positioned { y = (parentH - childH) / 2, x = 0, child = child }; center = parentW parentH child \ - childSize = measure child; - Positioned { x = (parentW - childSize.width) / 2, y = (parentH - childSize.height) / 2, child = child }; + childSize = ui.measure child; + ui.positioned { x = (parentW - childSize.width) / 2, y = (parentH - childSize.height) / 2, child = child }; -# button : Record -> UI +# button : Record -> ui button = config \ - Clickable { + ui.clickable { event = config.event, - child = Stack { + child = ui.stack { children = [ - Rect { w = 100, h = 40, color = "#eee" }, - Text { content = config.label, x = 10, y = 25, color = "#222" } + ui.rect { w = 100, h = 40, color = "#eee" }, + ui.text { content = config.label, x = 10, y = 25, color = "#222" } ] } }; -box = config \ Stack { +box = config \ ui.stack { children = [ # background - Rect { w = config.w, h = config.h, color = config.color }, + ui.rect { w = config.w, h = config.h, color = config.color }, # top border - Rect { w = config.w, h = config.borderTop, color = config.borderColor }, + ui.rect { w = config.w, h = config.borderTop, color = config.borderColor }, # bottom border - Positioned { x = 0, y = config.h - config.borderBottom, child = - Rect { w = config.w, h = config.borderBottom, color = config.borderColor } + ui.positioned { x = 0, y = config.h - config.borderBottom, child = + ui.rect { w = config.w, h = config.borderBottom, color = config.borderColor } }, # left border - Rect { w = config.borderLeft, h = config.h, color = config.borderColor }, + ui.rect { w = config.borderLeft, h = config.h, color = config.borderColor }, # right border - Positioned { x = config.w - config.borderRight, y = 0, child = - Rect { w = config.borderRight, h = config.h, color = config.borderColor } + ui.positioned { x = config.w - config.borderRight, y = 0, child = + ui.rect { w = config.borderRight, h = config.h, color = config.borderColor } }, # content - Positioned { x = config.paddingLeft, y = config.paddingTop, child = config.child } + ui.positioned { x = config.paddingLeft, y = config.paddingTop, child = config.child } ]}; insertChar = text pos char \ @@ -57,7 +57,7 @@ deleteChar = text pos \ calcScrollOffset = text cursorPos scrollOffset inputWidth \ textBeforeCursor = slice text 0 cursorPos; - cursorX = measureText textBeforeCursor; + cursorX = ui.measureText textBeforeCursor; (cursorX < scrollOffset | True \ max 0 (cursorX - 20) | False \ @@ -69,8 +69,8 @@ findPosHelper = text targetX index \ (index >= len text) | True \ len text | False \ ( - widthSoFar = measureText (slice text 0 index); - widthNext = measureText (slice text 0 (index + 1)); + widthSoFar = ui.measureText (slice text 0 index); + widthNext = ui.measureText (slice text 0 (index + 1)); midpoint = (widthSoFar + widthNext) / 2; (targetX < midpoint | True \ index @@ -81,7 +81,7 @@ findCursorPos = text clickX scrollOffset inputPadding \ adjustedX = clickX + scrollOffset - inputPadding; findPosHelper text adjustedX 0; -textInput = config \ Stateful { +textInput = config \ ui.stateful { key = config.key, focusable = True, @@ -133,29 +133,29 @@ textInput = config \ Stateful { view = state \ textBeforeCursor = slice state.text 0 state.cursorPos; - cursorX = measureText textBeforeCursor; + cursorX = ui.measureText textBeforeCursor; padding = 8; - Clip { + ui.clip { w = config.w, h = config.h, - child = Stack { + child = ui.stack { children = [ - Rect { w = config.w, h = config.h, color = config.backgroundColor, radius = 0 }, + ui.rect { w = config.w, h = config.h, color = config.backgroundColor, radius = 0 }, - Positioned { + ui.positioned { x = 8 - state.scrollOffset, y = 0, - child = Positioned { x = 0, y = 12, child = Text { content = state.text, color = config.color } } + child = ui.positioned { x = 0, y = 12, child = ui.text { content = state.text, color = config.color } } }, (state.focused - | True \ Positioned { + | True \ ui.positioned { x = 8 + cursorX - state.scrollOffset, y = 8, - child = Rect { w = 2, h = 24, color = config.color } + child = ui.rect { w = 2, h = 24, color = config.color } } - | _ \ Rect { w = 0, h = 0, color = "transparent" }) + | _ \ ui.rect { w = 0, h = 0, color = "transparent" }) ] } } diff --git a/src/cg/05-palette.cg b/src/cg/05-palette.cg index b5d1b80..d21a390 100644 --- a/src/cg/05-palette.cg +++ b/src/cg/05-palette.cg @@ -5,42 +5,42 @@ palette = config \ results = take 10 (config.search config.state.query); - padding = 0; + dialogPadding = 0; textInputHeight = 40; - contentWidth = windowWidth - (padding * 2); - contentHeight = windowHeight - (padding * 2); + contentWidth = windowWidth - (dialogPadding * 2); + contentHeight = windowHeight - (dialogPadding * 2); listHeight = contentHeight - 40; paletteRow = config \ color = (config.selected | True \ "rgba(255,255,255,0.2)" | False \ "transparent"); - Clickable { + ui.clickable { event = config.onClick, - child = Stack { + child = ui.stack { children = [ - Rect { w = config.w, h = config.h, color = color }, + ui.rect { w = config.w, h = config.h, color = color }, centerV config.h ( - Positioned { + ui.positioned { x = 10, y = 10, - child = Text { content = config.child, color = "white" } + child = ui.text { content = config.child, color = "white" } } ) ] } }; - Positioned { + ui.positioned { x = (config.viewport.width - windowWidth) / 2, y = (config.viewport.height - windowHeight) / 2, - child = Stack { + child = ui.stack { children = [ - Rect { w = windowWidth, h = windowHeight, color = "#063351", radius = 0, strokeWidth = 1, strokeColor = "#1A5F80" }, - Padding { - amount = padding, - child = Column { + 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 { @@ -51,17 +51,17 @@ palette = config \ backgroundColor = "rgba(0,0,0,0.2)", w = contentWidth, h = textInputHeight, - onChange = text \ Batch [config.state.query := text, config.state.focusedIndex := 0], + 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 := (config.state.focusedIndex + 1) | Enter \ (\ config.onSelect (unwrapOr "" (nth config.state.focusedIndex results))) - | _ \ NoOp + | _ \ noOp }, - Clip { + ui.clip { w = contentWidth, h = listHeight, - child = Column { + child = ui.column { gap = 1, children = mapWithIndex (t i \ paletteRow { child = t, diff --git a/src/cg/10-os.cg b/src/cg/10-os.cg index 1fbe5e8..c77991d 100644 --- a/src/cg/10-os.cg +++ b/src/cg/10-os.cg @@ -1,5 +1,6 @@ osState = { palette = { + visible = True, query = "", focusedIndex = 0, }, @@ -11,15 +12,17 @@ update = state event \ event | _ \ state; view = state viewport \ - Stack { + ui.stack { children = [ - Rect { w = viewport.width, h = viewport.height, color = "#012" }, - palette { - state = osState.palette, - search = storeSearch, - onSelect = item \ (debug "selected" item), - viewport = viewport, - } + ui.rect { w = viewport.width, h = viewport.height, color = "#012" }, + osState.palette.visible + | True \ palette { + state = osState.palette, + search = storeSearch, + onSelect = item \ (debug "selected" item), + viewport = viewport, + } + | False \ text { content = "" } ] }; diff --git a/src/compiler.ts b/src/compiler.ts index 143869f..68f20b0 100644 --- a/src/compiler.ts +++ b/src/compiler.ts @@ -18,7 +18,7 @@ export function compile(ast: AST): string { return sanitize(ast.name); case 'lambda': - const params = ast.params.map(sanitize).join(') => ('); + const params = ast.params.map(sanitizeName).join(') => ('); return `Object.assign((${params}) => ${compile(ast.body)}, { _ast: (${JSON.stringify(ast)}) })`; case 'apply': @@ -36,7 +36,7 @@ export function compile(ast: AST): string { case 'record': { const fields = Object.entries(ast.fields) - .map(([k, v]) => `${sanitize(k)}: ${compile(v)}`); + .map(([k, v]) => `${sanitizeName(k)}: ${compile(v)}`); return `({${fields.join(', ')}})`; } @@ -48,15 +48,15 @@ export function compile(ast: AST): string { } case 'record-access': - return `${compile(ast.record)}.${sanitize(ast.field)}`; + return `${compile(ast.record)}.${sanitizeName(ast.field)}`; case 'record-update': const updates = Object.entries(ast.updates) - .map(([k, v]) => `${sanitize(k)}: ${compile(v)}`); + .map(([k, v]) => `${sanitizeName(k)}: ${compile(v)}`); return `({...${compile(ast.record)}, ${updates.join(', ')}})`; case 'let': - return `((${sanitize(ast.name)}) => + return `((${sanitizeName(ast.name)}) => ${compile(ast.body)})(${compile(ast.value)})`; case 'match': @@ -98,9 +98,13 @@ function sanitize(name: string): string { if (ops[name]) return ops[name]; - const natives = ['measure', 'measureText', 'storeSearch', 'getSource', 'debug', 'len', 'slice', 'str', 'redefine']; + const natives = ['measure', 'measureText', 'storeSearch', 'getSource', 'debug', 'len', 'slice', 'str', 'redefine', 'stateful', 'batch', 'noOp', 'rerender', 'focus', 'ui']; if (natives.includes(name)) return `_rt.${name}`; + return sanitizeName(name); +} + +function sanitizeName(name: string): string { const reserved = [ 'default','class','function','return','const','let','var', 'if','else','switch','case','for','while','do','break', @@ -138,7 +142,7 @@ function compilePattern(pattern: Pattern, expr: string): { condition: string, bi return { condition: 'true', bindings: [] }; case 'var': - return { condition: 'true', bindings: [`${sanitize(pattern.name)} = ${expr}`] }; + return { condition: 'true', bindings: [`${sanitizeName(pattern.name)} = ${expr}`] }; case 'literal': return { condition: `${expr} === ${JSON.stringify(pattern.value)}`, bindings: [] }; diff --git a/src/runtime-js.ts b/src/runtime-js.ts index 6c3e3a1..531583e 100644 --- a/src/runtime-js.ts +++ b/src/runtime-js.ts @@ -22,19 +22,39 @@ export const _rt = { gte: (a: any) => (b: any) => ({ _tag: a >= b ? 'True' : 'False' }), lte: (a: any) => (b: any) => ({ _tag: a <= b ? 'True' : 'False' }), + ui: { + rect: (config: any) => ({ _kind: 'rect', ...config }), + text: (config: any) => ({ _kind: 'text', ...config }), + stack: (config: any) => ({ _kind: 'stack', ...config }), + row: (config: any) => ({ _kind: 'row', ...config }), + column: (config: any) => ({ _kind: 'column', ...config }), + padding: (config: any) => ({ _kind: 'padding', ...config }), + positioned: (config: any) => ({ _kind: 'positioned', ...config }), + clickable: (config: any) => ({ _kind: 'clickable', ...config }), + clip: (config: any) => ({ _kind: 'clip', ...config }), + opacity: (config: any) => ({ _kind: 'opacity', ...config }), + stateful: (config: any) => ({ _kind: 'stateful', ...config }), + measure: (config: any) => ({ _kind: 'measure', ...config }), + measureText: (text: string) => { + const canvas = document.createElement('canvas'); + const ctx = canvas.getContext('2d'); + if (ctx) { + ctx.font = '16px "SF Mono", "Monaco", "Menlo", monospace'; + return Math.floor(ctx.measureText(text).width); + } + return text.length * 10; // fallback + }, + }, + + batch: (events: any[]) => ({ _tag: 'Batch', _0: events }), + noOp: { _tag: 'NoOp' }, + rerender: { _tag: 'Rerender' }, + focus: (key: string) => ({ _tag: 'Focus', _0: key }), + len: (list: any[]) => list.length, str: (x: any) => String(x), slice: (s: string | any[]) => (start: number) => (end: number) => s.slice(start, end), debug: (label: string) => (value: any) => { console.log(label, value); return value; }, - measureText: (text: string) => { - const canvas = document.createElement('canvas'); - const ctx = canvas.getContext('2d'); - if (ctx) { - ctx.font = '16px "SF Mono", "Monaco", "Menlo", monospace'; - return Math.floor(ctx.measureText(text).width); - } - return text.length * 10; // fallback - }, fuzzyMatch: (query: string) => (target: string) => { const q = query.toLowerCase(); const t = target.toLowerCase(); @@ -69,11 +89,11 @@ export const _rt = { } }, measure: (ui: any): { width: number, height: number } => { - switch (ui._tag) { - case 'Rect': return { width: ui.w, height: ui.h }; - case 'Text': return { width: ui.content.length * 10, height: 20 }; // TODO - case 'Clip': return { width: ui.w, height: ui.h }; - case 'Row': { + switch (ui._kind) { + case 'rect': return { width: ui.w, height: ui.h }; + case 'text': return { width: ui.content.length * 10, height: 20 }; // TODO + case 'clip': return { width: ui.w, height: ui.h }; + case 'row': { let totalWidth = 0; let maxHeight = 0; for (const child of ui.children) { @@ -85,7 +105,7 @@ export const _rt = { return { width: totalWidth, height: maxHeight }; } - case 'Column': { + case 'column': { let totalHeight = 0; let maxWidth = 0; for (const child of ui.children) { @@ -97,7 +117,7 @@ export const _rt = { return { width: maxWidth, height: totalHeight }; } - case 'Padding': { + case 'padding': { const childSize = _rt.measure(ui.child); return { width: childSize.width + ui.amount * 2, @@ -105,7 +125,7 @@ export const _rt = { } } - case 'Stack': { + case 'stack': { let maxWidth = 0; let maxHeight = 0; for (const child of ui.children) { @@ -117,9 +137,9 @@ export const _rt = { return { width: maxWidth, height: maxHeight }; } - case 'Clickable': - case 'Opacity': - case 'Positioned': + case 'clickable': + case 'opacity': + case 'positioned': return _rt.measure(ui.child); default: diff --git a/src/valueToUI-compiled.ts b/src/valueToUI-compiled.ts index 377bfa4..a7965a3 100644 --- a/src/valueToUI-compiled.ts +++ b/src/valueToUI-compiled.ts @@ -1,11 +1,11 @@ // import type { UIValue } from './types' export function valueToUI(value: any): any { - if (!value || !value._tag) + if (!value || !value._kind) throw new Error(`Expected UI constructor, got: ${JSON.stringify(value)}`); - switch(value._tag) { - case 'Rect': + switch(value._kind) { + case 'rect': return { kind: 'rect', w: value.w, @@ -16,34 +16,34 @@ export function valueToUI(value: any): any { strokeColor: value.strokeColor, }; - case 'Text': + case 'text': return { kind: 'text', content: value.content, color: value.color, }; - case 'Row': + case 'row': return { kind: 'row', gap: value.gap || 0, children: value.children.map(valueToUI), }; - case 'Column': + case 'column': return { kind: 'column', gap: value.gap || 0, children: value.children.map(valueToUI), }; - case 'Stack': + case 'stack': return { kind: 'stack', children: value.children.map(valueToUI), }; - case 'Positioned': + case 'positioned': return { kind: 'positioned', x: value.x || 0, @@ -51,21 +51,21 @@ export function valueToUI(value: any): any { child: valueToUI(value.child), }; - case 'Padding': + case 'padding': return { kind: 'padding', amount: value.amount || 0, child: valueToUI(value.child), }; - case 'Clickable': + case 'clickable': return { kind: 'clickable', event: value.event, child: valueToUI(value.child), }; - case 'Clip': + case 'clip': return { kind: 'clip', w: value.w, @@ -73,14 +73,14 @@ export function valueToUI(value: any): any { child: valueToUI(value.child), }; - case 'Opacity': + case 'opacity': return { kind: 'opacity', opacity: value.opacity, child: valueToUI(value.child), }; - case 'Stateful': + case 'stateful': return { kind: 'stateful', key: value.key,