ludic.ui: an unmounted component is mounted again rather than made again - its record renewed (a generated renew), its props and model kept; an action's call answers into a ring

A prompt that comes and goes as a player walks (co-op's netleak: in_get, cmp_*_new, bd_class, value_slot/put)
made a new record, props and model on every mount, and an action's call answered into a new Val (ev_call_with).
examples/library/ui_remount: two thousand comings and goings hold the heap at 0, and the counter starts at 0.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 15:25:55 +03:00
parent fc0f3d3790
commit 6cdef20cc2
14 changed files with 46052 additions and 45244 deletions

View file

@ -42,6 +42,8 @@ export state UiState {
mm_ivals: []Val = null
ev_depth: int = 0 # eval.ludic: a call's arguments, in lists kept one a depth
ev_args: []Val = new []Val
ev_ring: []Val = new []Val # eval.ludic: calls' answers while an action runs, EV_RING of them
ev_ring_at: int = 0
cv_texts: []string = new []string # units_pieces.ludic: a value's var() pieces, kept
tx_pieces: [][]string = new [][]string # eval_text.ludic: a text's pieces, a list a depth
tx_depth: int = 0
@ -117,6 +119,7 @@ export state UiState {
in_act_down: bool = false # A or Enter went down this frame, or came up
in_act_up: bool = false
in_list: []UiInst = new []UiInst
in_free: []UiInst = new []UiInst # unmounted instances, each mounted again as its class's next one
in_build: int = 0
ly_fg_a: float = 1.0
ly_lead: float = 0.0

View file

@ -8,6 +8,7 @@ export property UiClass {
lss_path: string = ""
lss: string = ""
make: fn() -> pointer = null
renew: fn(pointer) -> void = null # a record `make` made, put back as new: an instance mounted again
props: fn(pointer, Val) -> void = null
model: fn(pointer, Val) -> Val = null # fills the object it is given (the instance keeps one)
call: fn(pointer, string, Val, Val) -> Val = null # the last: where to write the answer, or null

View file

@ -51,7 +51,7 @@ function ev_call_with(ui_st: mut UiState, x: UiExpr, e: UiEnv, args: Val) -> Val
var at = e
while at != null {
if at.inst != null {
let r = at.inst.cls.call(at.inst.ptr, x.s, args, uv_hold(ui_st))
let r = at.inst.cls.call(at.inst.ptr, x.s, args, ev_into(ui_st))
if Value.kind(r) != 99 { return r }
}
at = at.parent
@ -62,5 +62,17 @@ function ev_call_with(ui_st: mut UiState, x: UiExpr, e: UiEnv, args: Val) -> Val
}
return ui_st.ui_view.call(x.s, args)
}
# where a call writes its answer: this build's pool, or while an action runs (the pool is off) the next
# of EV_RING kept slots - an answer is read long before 64 more calls come round to it
function ev_into(ui_st: mut UiState) -> Val {
let v = uv_hold(ui_st)
if v != null { return v }
let ring = ui_st.ev_ring
if len(ring) < EV_RING { push(ring, Value.null()) }
let at = ui_st.ev_ring_at % len(ring)
ui_st.ev_ring_at = at + 1
return ring[at]
}
const EV_RING: int = 64
function ui_eval_text(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> string { return vl_text(ui_eval(ui_st, x, e)) }
function ui_eval_bool(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> bool { return vl_truthy(ui_eval(ui_st, x, e)) }

View file

@ -8,6 +8,8 @@ property UiInst {
props: Val = null # its props, filled again each frame into the same object
model: Val = null # and its model, filled in place by the class's model()
}
# builds before an unmounted instance is mounted again: the tree that last named it is good for two
const IN_SPARE_AFTER: int = 2
function in_get(ui_st: mut UiState, key: string, k: UiClass) -> UiInst {
for i in 0 .. len(ui_st.in_list) {
let it = ui_st.in_list[i]
@ -16,14 +18,30 @@ function in_get(ui_st: mut UiState, key: string, k: UiClass) -> UiInst {
return it
}
}
let it = new UiInst
let it = in_reuse(ui_st, k)
it.key = key
it.cls = k
it.ptr = k.make()
it.seen = ui_st.in_build
push(ui_st.in_list, it)
return it
}
# an unmounted instance of this class, put back as new, else a new one: a prompt that comes and goes
# as the player walks is mounted again without making another record, props and model each time
function in_reuse(ui_st: mut UiState, k: UiClass) -> UiInst {
let l = ui_st.in_free
for i in 0 .. len(l) {
let it = l[i]
if it.cls == k and it.cls.renew != null and ui_st.in_build - it.seen >= IN_SPARE_AFTER {
l[i] = l[len(l) - 1]
List.pop(l)
k.renew(it.ptr)
return it
}
}
let it = new UiInst
it.cls = k
it.ptr = k.make()
return it
}
# after a build of `screen`: its instances the build did not reach are unmounted
function in_sweep(ui_st: mut UiState, screen: string) -> void {
var kept = 0
@ -32,7 +50,7 @@ function in_sweep(ui_st: mut UiState, screen: string) -> void {
if it.seen == ui_st.in_build or not lc_mine(it.key, screen) {
ui_st.in_list[kept] = it
kept += 1
}
} else { push(ui_st.in_free, it) }
}
while len(ui_st.in_list) > kept { List.pop(ui_st.in_list) }
}