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>
78 lines
2 KiB
Text
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
|
|
}
|