ludic/tools/ui-preview/mock.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

89 lines
3 KiB
Text

# mock.ludic - a component class the studio describes: its template and styles as sent, its model the
# story's (`model`), its props what the parent's tag gives it, its state what the template has set,
# and its functions answered from the story's table (`calls`, calls.ludic)
property PvInst {
cls: string = ""
props: Val = null
sets: Val = null # what `set name = value` wrote, over the story's model
}
# what the story says of a class: its model and its function table
property PvStory {
name: string = ""
model: Val = null
calls: Val = null
}
function pv_story(pv: PvState, name: string) -> PvStory {
for i in 0 .. len(pv.stories) {
if pv.stories[i].name == name { return pv.stories[i] }
}
return null
}
@alloc_ok("a story line: once per request")
function pv_story_get(pv: mut PvState, name: string) -> PvStory {
let s = pv_story(pv, name)
if s != null { return s }
let t = new PvStory
t.name = name
push(pv.stories, t)
return t
}
@alloc_ok("a component instance: made once per instance, as a component's record is")
function pv_make(k: UiClass) -> pointer {
let m = new PvInst
m.cls = k.name
m.sets = Value.object()
return m
}
function pv_renew(p: pointer) -> void {
let m: PvInst = p
Value.clear(m.sets)
}
function pv_props(p: pointer, v: Val) -> void {
let m: PvInst = p
m.props = v
}
# the story's model, then the props, then what was set: the later wins
function pv_model(pv: PvState, p: pointer, o: Val) -> Val {
let m: PvInst = p
Value.clear(o)
let s = pv_story(pv, m.cls)
if s != null and s.model != null and Value.kind(s.model) == 6 { pv_merge(o, s.model) }
if m.props != null { pv_merge(o, m.props) }
pv_merge(o, m.sets)
return o
}
function pv_merge(o: Val, src: Val) -> void {
for i in 0 .. Value.count(src) { Value.put(o, Value.key_at(src, i), Value.at(src, i)) }
}
@alloc_ok("once per class the studio names")
function pv_class_new(name: string) -> UiClass {
let k = new UiClass
k.name = name
k.make_of = fn pv_make
k.renew = fn pv_renew
k.props = fn pv_props
k.model = fn pv_model
k.call = fn pv_call
return k
}
# a class for every component tag in a template that the studio has not sent yet: an empty one, so
# the template links, and the studio's `load` of it later fills it in place
@alloc_ok("a template's tags read: once per load")
function pv_placeholders(ui_st: mut UiState, xml: string, own: string) -> void {
let n = len(xml)
for i in 0 .. n - 1 {
if xml[i] == 60 and xml[i + 1] >= 65 and xml[i + 1] <= 90 {
var j = i + 1
while j < n and pv_name_ch(xml[j]) { j += 1 }
if j < n and xml[j] == 58 { continue } # kit:Name, a template import's own
let tag = xml[i + 1..j]
if tag != own and ui_class(ui_st, tag) == null {
let k = pv_class_new(tag)
k.xml = "<row/>"
k.xml_path = tag + ".xml"
ui_register(ui_st, k)
}
}
}
}
function pv_name_ch(c: int) -> bool { return (c >= 48 and c <= 57) or (c >= 65 and c <= 90) or (c >= 97 and c <= 122) or c == 95 }