ludic/tools/ui-preview/out.ludic
Orkuncakilkaya 49b5e80af5 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>
2026-09-29 19:53:26 +03:00

78 lines
2 KiB
Text

# out.ludic - the reply stream: lines written into a buffer made once and grown when a frame needs
# more, then written and flushed whole, so a draw list makes no string per primitive
property PvOut {
p: pointer = null
n: int = 0
cap: int = 0
}
extern function fflush(f: pointer) -> int = "fflush"
function pv_room(o: PvOut, more: int) -> void {
if o.n + more + 1 <= o.cap { return }
var cap = o.cap * 2
if cap < 65536 { cap = 65536 }
while cap < o.n + more + 1 { cap = cap * 2 }
var q: pointer = null
unsafe {
q = bytes(cap)
for i in 0 .. o.n { q[i] = o.p[i] }
if o.p != null { free(o.p) }
}
o.p = q
o.cap = cap
}
function pv_byte(o: PvOut, c: int) -> void {
pv_room(o, 1)
unsafe { o.p[o.n] = c }
o.n += 1
}
# text as it is (a word of the protocol)
function pv_s(o: PvOut, s: string) -> void {
let k = len(s)
pv_room(o, k)
unsafe {
for i in 0 .. k { o.p[o.n + i] = s[i] }
}
o.n += k
}
# a space, then a word
function pv_w(o: PvOut, s: string) -> void {
pv_byte(o, 32)
pv_s(o, s)
}
# a space, then text as a JSON string: " and \ escaped, control bytes as \n \r \t or \u00XX, UTF-8 as is
function pv_q(o: PvOut, s: string) -> void {
pv_byte(o, 32)
pv_byte(o, 34)
for i in 0 .. len(s) {
let c = s[i] & 255
if c == 34 or c == 92 {
pv_byte(o, 92)
pv_byte(o, c)
} else if c == 10 {
pv_s(o, "\\n")
} else if c == 13 {
pv_s(o, "\\r")
} else if c == 9 {
pv_s(o, "\\t")
} else if c < 32 {
pv_s(o, "\\u00")
pv_byte(o, pv_hexch(c / 16))
pv_byte(o, pv_hexch(c % 16))
} else { pv_byte(o, c) }
}
pv_byte(o, 34)
}
function pv_hexch(d: int) -> int {
if d < 10 { return 48 + d }
return 87 + d
}
# the line so far ends
function pv_nl(o: PvOut) -> void { pv_byte(o, 10) }
# everything buffered goes to stdout now, and the buffer is empty again
function pv_flush(o: PvOut) -> void {
if o.n == 0 { return }
let f = file_stdout()
unsafe { file_write(f, o.p, o.n) }
fflush(f)
o.n = 0
}