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 \
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" })
]
}
}

@ -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,

@ -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 = "" }
]
};

@ -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: [] };

@ -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:

@ -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,

Loading…
Cancel
Save