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,74 @@
# events.ludic - what happened while a request ran, kept apart from the draw list and written after
# it (protocol-v1.md, "Events"): template errors, calls, emits, sets, sounds and misses
# a template error: ludic.ui's text, its file and line split off the front when it has them
@alloc_ok("a template error: said once (ludic.ui says each once)")
function pv_err(pv: mut PvState, msg: string) -> void {
let o = pv.ev
var cut = -1
for i in 0 .. len(msg) - 1 {
if cut < 0 and msg[i] == 58 and msg[i + 1] == 32 { cut = i }
}
var file = ""
var line = 0
var rest = msg
if cut > 0 {
let head = msg[0..cut]
let colon = pv_last_colon(head)
if colon > 0 and pv_digits_only(head, colon + 1) {
file = head[0..colon]
line = pv_int_of(head[colon + 1..len(head)])
rest = msg[cut + 2..len(msg)]
} else if pv_pathish(head) {
file = head
rest = msg[cut + 2..len(msg)]
}
}
pv_s(o, "err")
pv_q(o, file)
pv_int(o, line)
pv_int(o, 0)
pv_q(o, rest)
pv_nl(o)
}
function pv_last_colon(s: string) -> int {
var at = -1
for i in 0 .. len(s) {
if s[i] == 58 { at = i }
}
return at
}
function pv_digits_only(s: string, from: int) -> bool {
if from >= len(s) { return false }
for i in from .. len(s) {
if s[i] < 48 or s[i] > 57 { return false }
}
return true
}
# a name with a dot or a slash in it, and no space: a file
function pv_pathish(s: string) -> bool {
var dot = false
for i in 0 .. len(s) {
if s[i] == 32 { return false }
if s[i] == 46 or s[i] == 47 { dot = true }
}
return dot
}
function pv_said(pv: mut PvState, msg: string) -> void { pv_err(pv, msg) }
# `emit name` ran in a template (a click's is "press", as ludic.ui keeps it)
function pv_emitted(pv: mut PvState, name: string) -> void {
pv_s(pv.ev, "emit")
pv_q(pv.ev, name)
pv_nl(pv.ev)
}
function pv_sound(pv: mut PvState, name: string) -> void {
pv_s(pv.ev, "sound")
pv_q(pv.ev, name)
pv_nl(pv.ev)
}
# a template file's text: one the studio sent (`file`), else the disk's
function pv_read(pv: PvState, path: string) -> string {
for i in 0 .. len(pv.fl_path) {
if pv.fl_path[i] == path { return pv.fl_text[i] }
}
return Fs.read_text(path)
}