namspacing and lowercasing ui functions. they're functions, not data constructors now. matching CG userspace ui functions
This commit is contained in:
parent
b8a396a734
commit
3fe7750290
6 changed files with 126 additions and 99 deletions
|
|
@ -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…
Add table
Add a link
Reference in a new issue