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:
parent
fc0f3d3790
commit
6cdef20cc2
14 changed files with 46052 additions and 45244 deletions
30
examples/library/ui_remount.ludic
Normal file
30
examples/library/ui_remount.ludic
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
# ui_remount.ludic - a component that comes and goes: mounted again, it starts as new (its state back
|
||||
# to its defaults), and a thousand comings and goings hold the heap flat, because an unmounted
|
||||
# instance is put back and mounted again rather than a record, props and model made each time
|
||||
import "ludic.ui"
|
||||
import "ui_remount_parts/Counter.ludic"
|
||||
import "ui_remount_parts/Shown.ludic"
|
||||
program UiRemount {
|
||||
numbers float
|
||||
function frame(ui_st: mut UiState) -> UiNode {
|
||||
let root: UiNode = ui_nodes(ui_st, "Shown", null)
|
||||
ui_place(ui_st, root, 0.0, 0.0, 300.0, 200.0)
|
||||
return root
|
||||
}
|
||||
entry (ui_st: mut UiState) {
|
||||
var root = frame(ui_st)
|
||||
let c = root.children[0].children[1]
|
||||
ui_press(ui_st, c.children[1])
|
||||
ui_press(ui_st, c.children[1])
|
||||
root = frame(ui_st)
|
||||
let before = root.children[0].children[1].children[0].text
|
||||
var at = 0
|
||||
for i in 0 .. 2000 {
|
||||
ui_press(ui_st, root.children[0].children[0])
|
||||
root = frame(ui_st)
|
||||
if i == 99 { at = Os.heap_bytes() }
|
||||
}
|
||||
let grew = Os.heap_bytes() - at
|
||||
print(`{before} | {root.children[0].children[1].children[0].text} | heap {grew}`)
|
||||
}
|
||||
}
|
||||
0
examples/library/ui_remount_parts/Counter.lss
Normal file
0
examples/library/ui_remount_parts/Counter.lss
Normal file
9
examples/library/ui_remount_parts/Counter.ludic
Normal file
9
examples/library/ui_remount_parts/Counter.ludic
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
# Counter.ludic - a counter that starts at 0 each time it is mounted
|
||||
component Counter {
|
||||
prop label: string
|
||||
prop step: int = 1
|
||||
state count: int = 0
|
||||
doubled: int = count * 2
|
||||
function big() -> bool { return count > 3 }
|
||||
on bump() { count += step }
|
||||
}
|
||||
5
examples/library/ui_remount_parts/Counter.xml
Normal file
5
examples/library/ui_remount_parts/Counter.xml
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<div class="counter">
|
||||
<p>{label}: {count} ({doubled}){big() ? ' big' : ''}</p>
|
||||
<button on-click="bump()">+</button>
|
||||
<button on-click="set count = 0">reset</button>
|
||||
</div>
|
||||
5
examples/library/ui_remount_parts/Shown.ludic
Normal file
5
examples/library/ui_remount_parts/Shown.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# Shown.ludic - a counter shown or not: each time it comes back it is a new counter
|
||||
component Shown {
|
||||
state on: bool = true
|
||||
on flip() { on = not on }
|
||||
}
|
||||
4
examples/library/ui_remount_parts/Shown.xml
Normal file
4
examples/library/ui_remount_parts/Shown.xml
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<div>
|
||||
<button on-click="flip()">flip</button>
|
||||
<if test="{on}"><Counter label="c"/></if>
|
||||
</div>
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)) }
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -16,8 +16,9 @@ function cm_class(c: int, px: pointer, head: Node) -> void {
|
|||
} else { lss = cm_lss_inline(c, lss, dir, 0) }
|
||||
var src = `function {px}class() -> UiClass {{\n let k = new UiClass\n k.name = "{name}"\n`
|
||||
src = src + ` k.xml_path = "{xml_path}"\n k.xml = {px}xml_text()\n k.lss_path = "{lss_at}"\n k.lss = {px}lss_text()\n`
|
||||
src = src + ` k.make = fn {px}new\n k.props = fn {px}props\n k.model = fn {px}model\n k.call = fn {px}call\n return k\n}}\n`
|
||||
src = src + ` k.make = fn {px}new\n k.renew = fn {px}renew\n k.props = fn {px}props\n k.model = fn {px}model\n k.call = fn {px}call\n return k\n}}\n`
|
||||
vw_parse(src, g_cm_file[c], g_cm_line[c], head.vis)
|
||||
vw_parse(cm_renew_src(c, px, "Cmp" + name), g_cm_file[c], g_cm_line[c], head.vis)
|
||||
cm_text_fn(c, px + "xml_text", xml, head)
|
||||
cm_text_fn(c, px + "lss_text", lss, head)
|
||||
let reg = node(E_CALL)
|
||||
|
|
@ -87,3 +88,26 @@ function cm_unique(name: pointer) -> void {
|
|||
i += 1
|
||||
}
|
||||
}
|
||||
# an instance's record put back as `new` made it, for ludic.ui to mount again rather than make another:
|
||||
# each field its default, or its type's zero
|
||||
function cm_renew_src(c: int, px: pointer, rec: pointer) -> pointer {
|
||||
var src = `function {px}renew(p: pointer) -> void {{\n let s: {rec} = p\n`
|
||||
var i = 0
|
||||
while i < len(g_cm_keep[c]) {
|
||||
let f = g_cm_keep[c][i]
|
||||
if g_cm_defs[c][i] != null {
|
||||
src = src + ` s.{f} = {px}def_{f}(s)\n`
|
||||
} else {
|
||||
src = src + ` s.{f} = {cm_zero(g_cm_keep_ty[c][i])}\n`
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
return src + "}\n"
|
||||
}
|
||||
function cm_zero(ty: pointer) -> pointer {
|
||||
if ty == "int" or ty == "long" or ty == "byte" { return "0" }
|
||||
if ty == "float" { return "0.0" }
|
||||
if ty == "fixed" { return "fixed(0)" }
|
||||
if ty == "bool" { return "false" }
|
||||
return "null"
|
||||
}
|
||||
|
|
|
|||
45586
selfhost/ludicc.seed.ll
45586
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -1109,6 +1109,7 @@ function cmd_dev_test() -> int {
|
|||
controller_case("library/ui_reload", "", "ui: templates reloaded note 1 | reloaded 0 then 1 | note again 1", "ui_reload.ludic (L11: a template changed on disk is read again, and its state kept)")
|
||||
controller_case("library/ui_component", "", "counters|a: 1 (2)|+|reset|b: 10 (20) big|+|reset|outside| b margin 9; b text 16711680; outside -1; button 30 b: 0 (0)", "ui_component.ludic (L11: components as NAME.ludic + .xml + .lss - props, state, fields, events, scoped styles, @import, class passed down)")
|
||||
reject_case("rejected/component_no_template", "there is no Lonely.xml beside it", "a component needs its template beside it")
|
||||
controller_case("library/ui_remount", "", "c: 2 (4) | c: 0 (0) | heap 0", "ui_remount.ludic (a component unmounted and mounted again starts as new, and two thousand comings and goings hold the heap flat)")
|
||||
controller_case("library/ui_controls", "", "1 false 7 2 Adalove 70 1 false 8 2 Adalove 70; scrolled 40", "ui_controls.ludic (L11: focus and keys, the built-in controls, a range dragged, a box scrolled)")
|
||||
controller_case("library/ui_anim", "", "0 100 100 | 49 50 100 | 100 100 61", "ui_anim.ludic (L11: @keyframes and animation, looping and alternating, and a transition)")
|
||||
controller_case("library/ui_popover", "", "drop false | drop true | drop false | drop false | fill 60 of 200", "ui_popover.ludic (L11: a popover keeps the keys and closes on a press outside or Esc; progress fills)")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue