ludic ui-preview: ludic.ui components previewed for a studio over stdio (R5)

A prebuilt tool, bin/ludic-ui-preview (built by ludic-dev build, shipped beside ludic-lsp, run as
`ludic ui-preview [--font DIR]`): ludic.ui with a backend that records every draw call as a line,
and mock component classes the studio describes (load / model / calls). frame t runs ui_show at
interface time t; text is measured on the CPU with the game's font.json metrics; locale goes
through ludic.i18n; tree / box / rules inspect the frame. No game code, no GPU, no network. The wire
protocol is frozen as tools/ui-preview/protocol-v1.md; smoke.txt is a transcript to run.

ludic.ui gains, all additive: UiClass.make_of, UiBackend.said / emitted, UiRule.text / at (with
comment line breaks kept so rule lines count true, and a class's styles read under its .lss path),
and ui_root, ui_find, ui_rules_of, ui_rule_value, ui_building, ui_class, ui_class_load,
ui_file_forget, ui_errors_clear; ui_nine_cuts_into exported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-29 19:53:26 +03:00
parent 28f7b4666b
commit 49b5e80af5
38 changed files with 1986 additions and 16 deletions

View file

@ -0,0 +1,92 @@
# line.ludic - a request: one line of stdin, cut into words. A word is a JSON value (a string, an
# object or a list, read whole) or a run of anything but spaces (a name, a number)
# the next line without its end (a \r before it is dropped too), or null at the end of the input
@alloc_ok("a request read: the studio's input, never a frame")
function pv_read_line(b: PvOut) -> string {
b.n = 0
var c = read_char()
if c < 0 { return null }
while c >= 0 and c != 10 {
if c != 13 { pv_byte(b, c) }
c = read_char()
}
var s: pointer = null
unsafe {
s = bytes(b.n + 1)
for i in 0 .. b.n { s[i] = b.p[i] }
s[b.n] = 0
}
return s
}
@alloc_ok("a request's words: one list kept, each word a slice of the line")
function pv_words(s: string, out: []string) -> void {
List.clear(out)
let n = len(s)
var i = 0
while i < n {
while i < n and (s[i] == 32 or s[i] == 9) { i += 1 }
if i >= n { break }
var j = i
let c = s[i]
if c == 34 or c == 123 or c == 91 { j = pv_json_end(s, i) } else {
while j < n and s[j] != 32 and s[j] != 9 { j += 1 }
}
push(out, s[i..j])
i = j
}
}
# where the JSON value starting at i ends: its closing quote or bracket, strings and escapes skipped
function pv_json_end(s: string, i: int) -> int {
let n = len(s)
var depth = 0
var ins = false
var j = i
while j < n {
let c = s[j]
if ins {
if c == 92 { j += 1 } else if c == 34 {
ins = false
if depth == 0 { return j + 1 }
}
} else if c == 34 { ins = true } else if c == 123 or c == 91 { depth += 1 } else if c == 125 or c == 93 {
depth -= 1
if depth == 0 { return j + 1 }
}
j += 1
}
return n
}
# a word as text: a JSON string unquoted, anything else as it stands
@alloc_ok("a request's word read")
function pv_text(w: string) -> string {
if len(w) > 0 and w[0] == 34 { return Value.as_str(Json.parse(w)) }
return w
}
# a word as a number (0 when it is not one)
@alloc_ok("a request's word read")
function pv_float(w: string) -> float {
if len(w) == 0 { return 0.0 }
let c = w[0]
if not ((c >= 48 and c <= 57) or c == 45 or c == 46) { return 0.0 }
return Value.as_float(Json.parse(w))
}
function pv_int_of(w: string) -> int { return int(pv_float(w)) }
# a word as a JSON value (a bare word is a string)
@alloc_ok("a request's word read")
function pv_val(w: string) -> Val {
if len(w) == 0 { return Value.null() }
let c = w[0]
if c == 34 or c == 123 or c == 91 or (c >= 48 and c <= 57) or c == 45 { return Json.parse(w) }
if w == "true" { return Value.bool(1) }
if w == "false" { return Value.bool(0) }
if w == "null" { return Value.null() }
return Value.str(w)
}
# whether s holds p at `at`, compared where it stands
function pv_sat(s: string, at: int, p: string) -> bool {
if at + len(p) > len(s) { return false }
for i in 0 .. len(p) {
if s[at + i] != p[i] { return false }
}
return true
}