namspacing and lowercasing ui functions. they're functions, not data constructors now. matching CG userspace ui functions

master
Dustin Swan 3 weeks ago
parent b8a396a734
commit 3fe7750290
Signed by: dustinswan
GPG Key ID: 30D46587E2100467

@ -1,45 +1,45 @@
centerH = parentW child \ centerH = parentW child \
childW = (measure child).width; childW = (ui.measure child).width;
Positioned { x = (parentW - childW) / 2, y = 0, child = child }; ui.positioned { x = (parentW - childW) / 2, y = 0, child = child };
centerV = parentH child \ centerV = parentH child \
childH = (measure child).height; childH = (ui.measure child).height;
Positioned { y = (parentH - childH) / 2, x = 0, child = child }; ui.positioned { y = (parentH - childH) / 2, x = 0, child = child };
center = parentW parentH child \ center = parentW parentH child \
childSize = measure child; childSize = ui.measure child;
Positioned { x = (parentW - childSize.width) / 2, y = (parentH - childSize.height) / 2, child = child }; ui.positioned { x = (parentW - childSize.width) / 2, y = (parentH - childSize.height) / 2, child = child };
# button : Record -> UI # button : Record -> ui
button = config \ button = config \
Clickable { ui.clickable {
event = config.event, event = config.event,
child = Stack { child = ui.stack {
children = [ children = [
Rect { w = 100, h = 40, color = "#eee" }, ui.rect { w = 100, h = 40, color = "#eee" },
Text { content = config.label, x = 10, y = 25, color = "#222" } ui.text { content = config.label, x = 10, y = 25, color = "#222" }
] ]
} }
}; };
box = config \ Stack { box = config \ ui.stack {
children = [ children = [
# background # background
Rect { w = config.w, h = config.h, color = config.color }, ui.rect { w = config.w, h = config.h, color = config.color },
# top border # 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 # bottom border
Positioned { x = 0, y = config.h - config.borderBottom, child = ui.positioned { x = 0, y = config.h - config.borderBottom, child =
Rect { w = config.w, h = config.borderBottom, color = config.borderColor } ui.rect { w = config.w, h = config.borderBottom, color = config.borderColor }
}, },
# left border # 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 # right border
Positioned { x = config.w - config.borderRight, y = 0, child = ui.positioned { x = config.w - config.borderRight, y = 0, child =
Rect { w = config.borderRight, h = config.h, color = config.borderColor } ui.rect { w = config.borderRight, h = config.h, color = config.borderColor }
}, },
# content # 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 \ insertChar = text pos char \
@ -57,7 +57,7 @@ deleteChar = text pos \
calcScrollOffset = text cursorPos scrollOffset inputWidth \ calcScrollOffset = text cursorPos scrollOffset inputWidth \
textBeforeCursor = slice text 0 cursorPos; textBeforeCursor = slice text 0 cursorPos;
cursorX = measureText textBeforeCursor; cursorX = ui.measureText textBeforeCursor;
(cursorX < scrollOffset (cursorX < scrollOffset
| True \ max 0 (cursorX - 20) | True \ max 0 (cursorX - 20)
| False \ | False \
@ -69,8 +69,8 @@ findPosHelper = text targetX index \
(index >= len text) (index >= len text)
| True \ len text | True \ len text
| False \ ( | False \ (
widthSoFar = measureText (slice text 0 index); widthSoFar = ui.measureText (slice text 0 index);
widthNext = measureText (slice text 0 (index + 1)); widthNext = ui.measureText (slice text 0 (index + 1));
midpoint = (widthSoFar + widthNext) / 2; midpoint = (widthSoFar + widthNext) / 2;
(targetX < midpoint (targetX < midpoint
| True \ index | True \ index
@ -81,7 +81,7 @@ findCursorPos = text clickX scrollOffset inputPadding \
adjustedX = clickX + scrollOffset - inputPadding; adjustedX = clickX + scrollOffset - inputPadding;
findPosHelper text adjustedX 0; findPosHelper text adjustedX 0;
textInput = config \ Stateful { textInput = config \ ui.stateful {
key = config.key, key = config.key,
focusable = True, focusable = True,
@ -133,29 +133,29 @@ textInput = config \ Stateful {
view = state \ view = state \
textBeforeCursor = slice state.text 0 state.cursorPos; textBeforeCursor = slice state.text 0 state.cursorPos;
cursorX = measureText textBeforeCursor; cursorX = ui.measureText textBeforeCursor;
padding = 8; padding = 8;
Clip { ui.clip {
w = config.w, w = config.w,
h = config.h, h = config.h,
child = Stack { child = ui.stack {
children = [ 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, x = 8 - state.scrollOffset,
y = 0, 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 (state.focused
| True \ Positioned { | True \ ui.positioned {
x = 8 + cursorX - state.scrollOffset, x = 8 + cursorX - state.scrollOffset,
y = 8, 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" })
] ]
} }
} }

@ -5,42 +5,42 @@ palette = config \
results = take 10 (config.search config.state.query); results = take 10 (config.search config.state.query);
padding = 0; dialogPadding = 0;
textInputHeight = 40; textInputHeight = 40;
contentWidth = windowWidth - (padding * 2); contentWidth = windowWidth - (dialogPadding * 2);
contentHeight = windowHeight - (padding * 2); contentHeight = windowHeight - (dialogPadding * 2);
listHeight = contentHeight - 40; listHeight = contentHeight - 40;
paletteRow = config \ paletteRow = config \
color = (config.selected | True \ "rgba(255,255,255,0.2)" | False \ "transparent"); color = (config.selected | True \ "rgba(255,255,255,0.2)" | False \ "transparent");
Clickable { ui.clickable {
event = config.onClick, event = config.onClick,
child = Stack { child = ui.stack {
children = [ children = [
Rect { w = config.w, h = config.h, color = color }, ui.rect { w = config.w, h = config.h, color = color },
centerV config.h ( centerV config.h (
Positioned { ui.positioned {
x = 10, x = 10,
y = 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, x = (config.viewport.width - windowWidth) / 2,
y = (config.viewport.height - windowHeight) / 2, y = (config.viewport.height - windowHeight) / 2,
child = Stack { child = ui.stack {
children = [ children = [
Rect { w = windowWidth, h = windowHeight, color = "#063351", radius = 0, strokeWidth = 1, strokeColor = "#1A5F80" }, ui.rect { w = windowWidth, h = windowHeight, color = "#063351", radius = 0, strokeWidth = 1, strokeColor = "#1A5F80" },
Padding { ui.padding {
amount = padding, amount = dialogPadding,
child = Column { child = ui.column {
gap = 0, gap = 0,
children = [ children = [
textInput { textInput {
@ -51,17 +51,17 @@ palette = config \
backgroundColor = "rgba(0,0,0,0.2)", backgroundColor = "rgba(0,0,0,0.2)",
w = contentWidth, w = contentWidth,
h = textInputHeight, 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 onKeyDown = key \ key
| ArrowUp \ config.state.focusedIndex := max 0 (config.state.focusedIndex - 1) | ArrowUp \ config.state.focusedIndex := max 0 (config.state.focusedIndex - 1)
| ArrowDown \ config.state.focusedIndex := (config.state.focusedIndex + 1) | ArrowDown \ config.state.focusedIndex := (config.state.focusedIndex + 1)
| Enter \ (\ config.onSelect (unwrapOr "" (nth config.state.focusedIndex results))) | Enter \ (\ config.onSelect (unwrapOr "" (nth config.state.focusedIndex results)))
| _ \ NoOp | _ \ noOp
}, },
Clip { ui.clip {
w = contentWidth, w = contentWidth,
h = listHeight, h = listHeight,
child = Column { child = ui.column {
gap = 1, gap = 1,
children = mapWithIndex (t i \ paletteRow { children = mapWithIndex (t i \ paletteRow {
child = t, child = t,

@ -1,5 +1,6 @@
osState = { osState = {
palette = { palette = {
visible = True,
query = "", query = "",
focusedIndex = 0, focusedIndex = 0,
}, },
@ -11,15 +12,17 @@ update = state event \ event
| _ \ state; | _ \ state;
view = state viewport \ view = state viewport \
Stack { ui.stack {
children = [ children = [
Rect { w = viewport.width, h = viewport.height, color = "#012" }, ui.rect { w = viewport.width, h = viewport.height, color = "#012" },
palette { osState.palette.visible
| True \ palette {
state = osState.palette, state = osState.palette,
search = storeSearch, search = storeSearch,
onSelect = item \ (debug "selected" item), onSelect = item \ (debug "selected" item),
viewport = viewport, viewport = viewport,
} }
| False \ text { content = "" }
] ]
}; };

@ -18,7 +18,7 @@ export function compile(ast: AST): string {
return sanitize(ast.name); return sanitize(ast.name);
case 'lambda': 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)}) })`; return `Object.assign((${params}) => ${compile(ast.body)}, { _ast: (${JSON.stringify(ast)}) })`;
case 'apply': case 'apply':
@ -36,7 +36,7 @@ export function compile(ast: AST): string {
case 'record': { case 'record': {
const fields = Object.entries(ast.fields) const fields = Object.entries(ast.fields)
.map(([k, v]) => `${sanitize(k)}: ${compile(v)}`); .map(([k, v]) => `${sanitizeName(k)}: ${compile(v)}`);
return `({${fields.join(', ')}})`; return `({${fields.join(', ')}})`;
} }
@ -48,15 +48,15 @@ export function compile(ast: AST): string {
} }
case 'record-access': case 'record-access':
return `${compile(ast.record)}.${sanitize(ast.field)}`; return `${compile(ast.record)}.${sanitizeName(ast.field)}`;
case 'record-update': case 'record-update':
const updates = Object.entries(ast.updates) 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(', ')}})`; return `({...${compile(ast.record)}, ${updates.join(', ')}})`;
case 'let': case 'let':
return `((${sanitize(ast.name)}) => return `((${sanitizeName(ast.name)}) =>
${compile(ast.body)})(${compile(ast.value)})`; ${compile(ast.body)})(${compile(ast.value)})`;
case 'match': case 'match':
@ -98,9 +98,13 @@ function sanitize(name: string): string {
if (ops[name]) return ops[name]; 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}`; if (natives.includes(name)) return `_rt.${name}`;
return sanitizeName(name);
}
function sanitizeName(name: string): string {
const reserved = [ const reserved = [
'default','class','function','return','const','let','var', 'default','class','function','return','const','let','var',
'if','else','switch','case','for','while','do','break', '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: [] }; return { condition: 'true', bindings: [] };
case 'var': case 'var':
return { condition: 'true', bindings: [`${sanitize(pattern.name)} = ${expr}`] }; return { condition: 'true', bindings: [`${sanitizeName(pattern.name)} = ${expr}`] };
case 'literal': case 'literal':
return { condition: `${expr} === ${JSON.stringify(pattern.value)}`, bindings: [] }; return { condition: `${expr} === ${JSON.stringify(pattern.value)}`, bindings: [] };

@ -22,10 +22,19 @@ export const _rt = {
gte: (a: any) => (b: any) => ({ _tag: a >= b ? 'True' : 'False' }), gte: (a: any) => (b: any) => ({ _tag: a >= b ? 'True' : 'False' }),
lte: (a: any) => (b: any) => ({ _tag: a <= b ? 'True' : 'False' }), lte: (a: any) => (b: any) => ({ _tag: a <= b ? 'True' : 'False' }),
len: (list: any[]) => list.length, ui: {
str: (x: any) => String(x), rect: (config: any) => ({ _kind: 'rect', ...config }),
slice: (s: string | any[]) => (start: number) => (end: number) => s.slice(start, end), text: (config: any) => ({ _kind: 'text', ...config }),
debug: (label: string) => (value: any) => { console.log(label, value); return value; }, 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) => { measureText: (text: string) => {
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d'); const ctx = canvas.getContext('2d');
@ -35,6 +44,17 @@ export const _rt = {
} }
return text.length * 10; // fallback 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; },
fuzzyMatch: (query: string) => (target: string) => { fuzzyMatch: (query: string) => (target: string) => {
const q = query.toLowerCase(); const q = query.toLowerCase();
const t = target.toLowerCase(); const t = target.toLowerCase();
@ -69,11 +89,11 @@ export const _rt = {
} }
}, },
measure: (ui: any): { width: number, height: number } => { measure: (ui: any): { width: number, height: number } => {
switch (ui._tag) { switch (ui._kind) {
case 'Rect': return { width: ui.w, height: ui.h }; case 'rect': return { width: ui.w, height: ui.h };
case 'Text': return { width: ui.content.length * 10, height: 20 }; // TODO case 'text': return { width: ui.content.length * 10, height: 20 }; // TODO
case 'Clip': return { width: ui.w, height: ui.h }; case 'clip': return { width: ui.w, height: ui.h };
case 'Row': { case 'row': {
let totalWidth = 0; let totalWidth = 0;
let maxHeight = 0; let maxHeight = 0;
for (const child of ui.children) { for (const child of ui.children) {
@ -85,7 +105,7 @@ export const _rt = {
return { width: totalWidth, height: maxHeight }; return { width: totalWidth, height: maxHeight };
} }
case 'Column': { case 'column': {
let totalHeight = 0; let totalHeight = 0;
let maxWidth = 0; let maxWidth = 0;
for (const child of ui.children) { for (const child of ui.children) {
@ -97,7 +117,7 @@ export const _rt = {
return { width: maxWidth, height: totalHeight }; return { width: maxWidth, height: totalHeight };
} }
case 'Padding': { case 'padding': {
const childSize = _rt.measure(ui.child); const childSize = _rt.measure(ui.child);
return { return {
width: childSize.width + ui.amount * 2, width: childSize.width + ui.amount * 2,
@ -105,7 +125,7 @@ export const _rt = {
} }
} }
case 'Stack': { case 'stack': {
let maxWidth = 0; let maxWidth = 0;
let maxHeight = 0; let maxHeight = 0;
for (const child of ui.children) { for (const child of ui.children) {
@ -117,9 +137,9 @@ export const _rt = {
return { width: maxWidth, height: maxHeight }; return { width: maxWidth, height: maxHeight };
} }
case 'Clickable': case 'clickable':
case 'Opacity': case 'opacity':
case 'Positioned': case 'positioned':
return _rt.measure(ui.child); return _rt.measure(ui.child);
default: default:

@ -1,11 +1,11 @@
// import type { UIValue } from './types' // import type { UIValue } from './types'
export function valueToUI(value: any): any { export function valueToUI(value: any): any {
if (!value || !value._tag) if (!value || !value._kind)
throw new Error(`Expected UI constructor, got: ${JSON.stringify(value)}`); throw new Error(`Expected UI constructor, got: ${JSON.stringify(value)}`);
switch(value._tag) { switch(value._kind) {
case 'Rect': case 'rect':
return { return {
kind: 'rect', kind: 'rect',
w: value.w, w: value.w,
@ -16,34 +16,34 @@ export function valueToUI(value: any): any {
strokeColor: value.strokeColor, strokeColor: value.strokeColor,
}; };
case 'Text': case 'text':
return { return {
kind: 'text', kind: 'text',
content: value.content, content: value.content,
color: value.color, color: value.color,
}; };
case 'Row': case 'row':
return { return {
kind: 'row', kind: 'row',
gap: value.gap || 0, gap: value.gap || 0,
children: value.children.map(valueToUI), children: value.children.map(valueToUI),
}; };
case 'Column': case 'column':
return { return {
kind: 'column', kind: 'column',
gap: value.gap || 0, gap: value.gap || 0,
children: value.children.map(valueToUI), children: value.children.map(valueToUI),
}; };
case 'Stack': case 'stack':
return { return {
kind: 'stack', kind: 'stack',
children: value.children.map(valueToUI), children: value.children.map(valueToUI),
}; };
case 'Positioned': case 'positioned':
return { return {
kind: 'positioned', kind: 'positioned',
x: value.x || 0, x: value.x || 0,
@ -51,21 +51,21 @@ export function valueToUI(value: any): any {
child: valueToUI(value.child), child: valueToUI(value.child),
}; };
case 'Padding': case 'padding':
return { return {
kind: 'padding', kind: 'padding',
amount: value.amount || 0, amount: value.amount || 0,
child: valueToUI(value.child), child: valueToUI(value.child),
}; };
case 'Clickable': case 'clickable':
return { return {
kind: 'clickable', kind: 'clickable',
event: value.event, event: value.event,
child: valueToUI(value.child), child: valueToUI(value.child),
}; };
case 'Clip': case 'clip':
return { return {
kind: 'clip', kind: 'clip',
w: value.w, w: value.w,
@ -73,14 +73,14 @@ export function valueToUI(value: any): any {
child: valueToUI(value.child), child: valueToUI(value.child),
}; };
case 'Opacity': case 'opacity':
return { return {
kind: 'opacity', kind: 'opacity',
opacity: value.opacity, opacity: value.opacity,
child: valueToUI(value.child), child: valueToUI(value.child),
}; };
case 'Stateful': case 'stateful':
return { return {
kind: 'stateful', kind: 'stateful',
key: value.key, key: value.key,

Loading…
Cancel
Save