From 6edf5926378919bb9d3d0cfcc481c3d3afbb1201 Mon Sep 17 00:00:00 2001 From: Dustin Swan Date: Sat, 7 Feb 2026 23:47:11 -0700 Subject: [PATCH] Adding center and box, more color options to UI stuff, working on command palette styling --- src/builtins.ts | 19 +++++++ src/os.cg | 126 +++++++++++++++++++++++++++---------------- src/types.ts | 2 +- src/ui-components.cg | 69 +++++++++++++++++------- src/ui.ts | 11 ++-- src/valueToUI.ts | 8 ++- 6 files changed, 164 insertions(+), 71 deletions(-) diff --git a/src/builtins.ts b/src/builtins.ts index deef558..cbc527c 100644 --- a/src/builtins.ts +++ b/src/builtins.ts @@ -1,4 +1,6 @@ import type { Value } from './types' +import { measure } from './ui' +import { valueToUI } from './valueToUI' const measureCanvas = document.createElement('canvas'); const measureCtx = measureCanvas.getContext('2d')!; @@ -463,6 +465,23 @@ export const builtins: { [name: string]: Value } = { } }, + 'measure': { + kind: 'native', + name: 'measure', + arity: 1, + fn: (uiValue) => { + const ui = valueToUI(uiValue); + const size = measure(ui); + return { + kind: 'record', + fields: { + width: { kind: 'int', value: size.width }, + height: { kind: 'int', value: size.height } + } + }; + } + }, + 'debug': { kind: 'native', name: 'debug', diff --git a/src/os.cg b/src/os.cg index 60939b0..4fd1fcd 100644 --- a/src/os.cg +++ b/src/os.cg @@ -1,61 +1,95 @@ osState = { + palette = { query = "", - focusedPaletteIndex = 0 + focusedIndex = 0, + windowHeight = 400, + windowWidth = 600, + } }; -init = { - test = "" -}; +init = {}; update = state event \ event | _ \ state; -listRow = child selected \ -Clickable { - event = osState.query := "", +listRow = config \ + color = (config.selected | True \ "rgba(255,255,255,0.2)" | False \ "transparent"); + + Clickable { + event = config.onClick, child = Stack { - children = [ - Rect { w = 300, h = 40, strokeWidth = 2, strokeColor = "black", color = "pink" } - ] + children = [ + Rect { w = config.w, h = config.h, color = color }, + centerV config.h ( + Positioned { + x = 10, + y = 0, + child = Text { content = config.child, color = "white" } + } + ) + ] } -}; + }; -# listRow = child selected \ -# pre = (selected | True \ "*" | _ \ ""); -# Text { content = pre & " " & child }; +palette = state viewport \ + results = take 10 (storeSearch osState.palette.query); -view = state viewport \ - _ = debug "focusedPalletIndex" osState.focusedPaletteIndex; - - Positioned { - x = 30, - y = 30, - child = Column { - gap = 10, - children = [ - textInput { - key = "query", - initialValue = osState.query, - initialFocus = True, - w = 300, - h = 40, - onChange = text \ osState.query := text, - onKeyDown = key \ key - | ArrowUp \ osState.focusedPaletteIndex := max 0 (osState.focusedPaletteIndex - 1) - | ArrowDown \ osState.focusedPaletteIndex := (osState.focusedPaletteIndex + 1) - | _ \ NoOp - }, - Clip { - w = 300, - h = 300, - child = Column { - gap = 10, - # children = mapWithIndex (t i \ Text { content = str i & " " & t }) (storeSearch osState.query) - children = mapWithIndex (t i \ listRow t (osState.focusedPaletteIndex == i)) (storeSearch osState.query) + state = osState.palette; + + padding = 0; + + textInputHeight = 40; + contentWidth = state.windowWidth - (padding * 2); + contentHeight = state.windowHeight - (padding * 2); + listHeight = contentHeight - 40; + + Positioned { + x = (viewport.width - state.windowWidth) / 2, + y = (viewport.height - state.windowHeight) / 2, + + child = Stack { + children = [ + Rect { w = state.windowWidth, h = state.windowHeight, color = "#063351", radius = 0, strokeWidth = 1, strokeColor = "#1A5F80" }, + Padding { + amount = padding, + child = Column { + gap = 0, + children = [ + textInput { + key = "query", + initialValue = osState.palette.query, + initialFocus = True, + color = "white", + backgroundColor = "rgba(0,0,0,0.2)", + w = contentWidth, + h = textInputHeight, + onChange = text \ Batch [osState.palette.query := text, osState.palette.focusedIndex := 0], + onKeyDown = key \ key + | ArrowUp \ osState.palette.focusedIndex := max 0 (osState.palette.focusedIndex - 1) + | ArrowDown \ osState.palette.focusedIndex := (osState.palette.focusedIndex + 1) + | _ \ NoOp + }, + Clip { + w = contentWidth, + h = listHeight, + child = Column { + gap = 1, + children = mapWithIndex (t i \ listRow { child = t, w = contentWidth, h = textInputHeight, selected = (osState.palette.focusedIndex == i), onClick = NoOp }) results + } + } + ] + } } - } - ] - } - }; + ] + } + }; + +view = state viewport \ + Stack { + children = [ + Rect { w = viewport.width, h = viewport.height, color = "#012" }, + palette state viewport + ] + }; os = { init = init, update = update, view = view } diff --git a/src/types.ts b/src/types.ts index 68c4399..151357b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -54,7 +54,7 @@ export type NativeFunction = { export type UIValue = | { kind: 'rect', w: number, h: number, color?: string, strokeColor?: string, strokeWidth?: number, radius?: number } - | { kind: 'text', content: string } + | { kind: 'text', content: string, color?: string } | { kind: 'row', children: UIValue[], gap: number } | { kind: 'column', children: UIValue[], gap: number } | { kind: 'clickable', child: UIValue, event: Value } diff --git a/src/ui-components.cg b/src/ui-components.cg index 106835a..5070286 100644 --- a/src/ui-components.cg +++ b/src/ui-components.cg @@ -1,17 +1,48 @@ +centerH = parentW child \ + childW = (measure child).width; + 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 }; + +center = parentW parentH child \ + childSize = measure child; + Positioned { x = (parentW - childSize.width) / 2, y = (parentH - childSize.height) / 2, child = child }; + # button : Record -> UI button = config \ - Clickable { - event = config.event, - child = Stack { - children = [ - Rect { w = 100, h = 40, color = "#eee" }, - Text { content = config.label, x = 10, y = 25, color = "#222" } - ] - } - }; + Clickable { + event = config.event, + child = Stack { + children = [ + Rect { w = 100, h = 40, color = "#eee" }, + Text { content = config.label, x = 10, y = 25, color = "#222" } + ] + } + }; + +box = config \ Stack { + children = [ + # background + Rect { w = config.w, h = config.h, color = config.color }, + # top border + 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 } + }, + # left border + 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 } + }, + # content + Positioned { x = config.paddingLeft, y = config.paddingTop, child = config.child } + ]}; insertChar = text pos char \ - # _ = debug "insertChar" char; before = slice text 0 pos; after = slice text pos (len text); before & char & after; @@ -66,33 +97,33 @@ textInput = config \ Stateful { update = state event \ event | ArrowLeft \ newCursorPos = max 0 (state.cursorPos - 1); - newScroll = calcScrollOffset state.text newCursorPos state.scrollOffset 284; + newScroll = calcScrollOffset state.text newCursorPos state.scrollOffset config.w; newState = state.{ text = state.text, cursorPos = newCursorPos, scrollOffset = newScroll }; { state = newState, emit = [config.onKeyDown] } | ArrowRight \ newCursorPos = min (len state.text) (state.cursorPos + 1); - newScroll = calcScrollOffset state.text newCursorPos state.scrollOffset 284; + newScroll = calcScrollOffset state.text newCursorPos state.scrollOffset config.w; newState = state.{ text = state.text, cursorPos = newCursorPos, scrollOffset = newScroll }; { state = newState, emit = [config.onKeyDown] } | Backspace \ newText = deleteChar state.text state.cursorPos; newCursorPos = max 0 (state.cursorPos - 1); - newScroll = calcScrollOffset newText newCursorPos state.scrollOffset 284; + newScroll = calcScrollOffset newText newCursorPos state.scrollOffset config.w; newState = state.{ text = newText, cursorPos = newCursorPos, scrollOffset = newScroll }; { state = newState, emit = [config.onChange newText, config.onKeyDown] } | Char c \ newText = insertChar state.text state.cursorPos c; newCursorPos = state.cursorPos + 1; - newScroll = calcScrollOffset newText newCursorPos state.scrollOffset 284; + newScroll = calcScrollOffset newText newCursorPos state.scrollOffset config.w; newState = state.{ text = newText, cursorPos = newCursorPos, scrollOffset = newScroll }; { state = newState, emit = [config.onChange newText, config.onKeyDown] } | Clicked coords \ newCursorPos = findCursorPos state.text coords.x state.scrollOffset 8; - newScroll = calcScrollOffset state.text newCursorPos state.scrollOffset 284; + newScroll = calcScrollOffset state.text newCursorPos state.scrollOffset config.w; newState = state.{ text = state.text, cursorPos = newCursorPos, scrollOffset = newScroll }; { state = newState, emit = [] } @@ -110,19 +141,19 @@ textInput = config \ Stateful { h = config.h, child = Stack { children = [ - Rect { w = config.w, h = config.h, color = "rgba(240,240,240,0.9)", radius = 4 }, + Rect { w = config.w, h = config.h, color = config.backgroundColor, radius = 0 }, Positioned { x = 8 - state.scrollOffset, - y = 8, - child = Positioned { x = 0, y = 17, child = Text { content = state.text } } + y = 0, + child = Positioned { x = 0, y = 12, child = Text { content = state.text, color = config.color } } }, (state.focused | True \ Positioned { x = 8 + cursorX - state.scrollOffset, y = 8, - child = Rect { w = 2, h = 24, color = "black" } + child = Rect { w = 2, h = 24, color = config.color } } | _ \ Rect { w = 0, h = 0, color = "transparent" }) ] diff --git a/src/ui.ts b/src/ui.ts index c853246..a9e4a1c 100644 --- a/src/ui.ts +++ b/src/ui.ts @@ -31,6 +31,9 @@ function renderUI(ui: UIValue, ctx: CanvasRenderingContext2D, x: number, y: numb if (ui.radius && ui.radius > 0) { const r = Math.min(ui.radius, ui.w / 2, ui.h / 2); + const inset = ui.strokeWidth ? ui.strokeWidth / 2 : 0; + // TODO + ctx.beginPath(); ctx.moveTo(x + r, y); ctx.lineTo(x + ui.w - r, y); @@ -56,7 +59,8 @@ function renderUI(ui: UIValue, ctx: CanvasRenderingContext2D, x: number, y: numb if (ui.strokeColor && ui.strokeWidth) { ctx.strokeStyle = ui.strokeColor; ctx.lineWidth = ui.strokeWidth; - ctx.strokeRect(x, y, ui.w, ui.h);; + const inset = ui.strokeWidth / 2; + ctx.strokeRect(x + inset, y + inset, ui.w - ui.strokeWidth, ui.h - ui.strokeWidth); } } @@ -64,8 +68,9 @@ function renderUI(ui: UIValue, ctx: CanvasRenderingContext2D, x: number, y: numb } case 'text': - ctx.fillStyle = 'black'; + ctx.fillStyle = ui.color || 'black'; ctx.font = '16px "SF Mono", "Monaco", "Menlo", "Consolas", "Courier New", monospace'; + ctx.textBaseline = 'top'; ctx.fillText(ui.content, x, y); break; @@ -129,7 +134,7 @@ function renderUI(ui: UIValue, ctx: CanvasRenderingContext2D, x: number, y: numb } } -function measure(ui: UIValue): { width: number, height: number } { +export function measure(ui: UIValue): { width: number, height: number } { switch (ui.kind) { case 'rect': return { width: ui.w, height: ui.h }; diff --git a/src/valueToUI.ts b/src/valueToUI.ts index 3148146..07514aa 100644 --- a/src/valueToUI.ts +++ b/src/valueToUI.ts @@ -29,12 +29,16 @@ export function valueToUI(value: Value): UIValue { } case 'Text': { - const content = fields.content; + const { content, color } = fields; if (content.kind !== 'string') throw new Error('Invalid Text fields'); - return { kind: 'text', content: content.value }; + return { + kind: 'text', + content: content.value, + color: color && color.kind === 'string' ? color.value : undefined + }; } case 'Column': {