ludic.ui, ludic.i18n: frame_keeps and birth_leaks to 0 - :nth-child's a and b read once per argument, a border-image's miss its own function; typing, the dev dump, parsing, the pointer ring's making, Ln's counted line and the plural header declared

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 16:46:00 +03:00
parent 23d204b9fc
commit f8825dc55a
16 changed files with 67 additions and 23 deletions

View file

@ -5,6 +5,7 @@ export const PL_ABOVE_ONE: int = 1 # French, Portuguese (0 and 1 are singular)
export const PL_ONE_FORM: int = 2 # Japanese, Chinese
export const PL_SLAVIC: int = 3 # Russian, Ukrainian: one, few, many
@alloc_ok("on a language change: its plural rule read from the header")
function plural_rule_of(h: string) -> int {
let k = i18n_find(h, len(h), 0, "plural=", 7)
if k < 0 { return PL_NOT_ONE }
@ -56,6 +57,7 @@ export function Ln(i18n_st: I18nState, one: string, many: string, n: int) -> str
return count_in(t, n)
}
@alloc_ok("a counted line (Ln) made when the game asks for one, outside a frame; its caller keeps it")
function count_in(t: string, n: int) -> string {
let m = len(t)
var r = ""

View file

@ -12,6 +12,7 @@ function an_now(ui_st: UiState) -> float {
return float(ui_st.an_frame) / 60.0
}
# @keyframes NAME { from {...} 50% {...} to {...} }
@alloc_ok("a stylesheet being read: once per component class (uc_load)")
function an_read(ui_st: mut UiState, name: string, body: string, err: []string) -> void {
let k = new UiKeyframes
k.name = tpl_words(name)

View file

@ -26,14 +26,8 @@ function el_url(s: string) -> string {
function el_border_image(ui_st: mut UiState, n: UiNode, s: string) -> void {
let m = mm_of(ui_st)
var at = mm_find(m, MM_BIMG, s)
if at < 0 {
var close = 0
while close < len(s) and s[close] != 41 { close += 1 }
at = mm_add(m, MM_BIMG, s)
if at < 0 { return }
m.texts[at] = el_url(s[0..Math.min(close + 1, len(s))])
m.lists[at] = lss_split(s[Math.min(close + 1, len(s))..len(s)], 47)
}
if at < 0 { at = el_bimg_read(m, s) }
if at < 0 { return }
n.bi_src = m.texts[at]
let rest = m.lists[at]
n.bi_slice = el_num(tw(ui_st, rest[0]))
@ -95,3 +89,14 @@ function el_secs(ui_st: mut UiState, s: string) -> float {
}
return 0.0
}
@alloc_ok("a memo miss: once per border-image value, bounded by MM_CAP")
function el_bimg_read(m: UiMemo, s: string) -> int {
var close = 0
while close < len(s) and s[close] != 41 { close += 1 }
let at = mm_add(m, MM_BIMG, s)
if at < 0 { return -1 }
m.texts[at] = el_url(s[0..Math.min(close + 1, len(s))])
m.lists[at] = lss_split(s[Math.min(close + 1, len(s))..len(s)], 47)
return at
}

View file

@ -55,6 +55,8 @@ export state UiState {
pe_evs: []UiPointer = pe_evs__new() # pointer_events.ludic: an event's record and its Value, a ring
pe_vals: []Val = pe_vals__new()
pe_at: int = 0
nth_a: int = 0 # select_nth.ludic: the a and b just read
nth_b: int = 0
tt_src: string = null # tooltip.ludic: the title tt_text was translated from
fit_out: []float = new []float # draw_content.ludic: an image's box, kept
r3n_cut: []float = new []float # render3d_nine.ludic: a nine-slice's edges, kept

View file

@ -68,6 +68,7 @@ function ct_drag(ui_st: mut UiState, root: UiNode, key: string, x: float, y: flo
if n != null { ct_press(ui_st, n, x, y) }
}
# typing into a focused text field, or the key a key field is waiting for
@alloc_ok("on a key typed into a field, not a frame")
function ct_typed(ui_st: mut UiState, n: UiNode) -> void {
let t = ct_type(n)
if t == "key" { ct_key_typed(ui_st, n) }

View file

@ -37,6 +37,7 @@ function ct_num_step(ui_st: mut UiState, n: UiNode, d: int) -> void {
ct_num_set(ui_st, n, base + ct_num_step_of(n) * float(d))
}
# what was typed this frame, the characters a number can hold; Backspace takes one back
@alloc_ok("on a key typed into a number field, not a frame")
function ct_num_typed(ui_st: mut UiState, n: UiNode) -> void {
if ui_st.fc_key != n.key or (ui_st.in_now.typed == "" and not ui_st.in_now.backspace) { return }
if ui_st.ct_num_key != n.key {

View file

@ -31,6 +31,7 @@ function ct_value_text(n: UiNode) -> string {
return s + ui_attr_text(n, "unit")
}
# a number with `places` digits after the point, rounded
@alloc_ok("a memo miss (ct_value_shown): once per value a range shows")
function ct_fixed(f: float, places: int) -> string {
var k = 1
for i in 0 .. Math.max(places, 0) { k = k * 10 }

View file

@ -11,6 +11,7 @@ export function ui_dump(n: UiNode, depth: int) -> string {
return out
}
# tag#id.class.class, or the kind of a node the template did not name
@alloc_ok("developer builds only, once, under LUDIC_UI_DUMP")
function dp_name(n: UiNode) -> string {
var s = n.tag
if s == "" { s = `({n.kind})` }
@ -21,6 +22,7 @@ function dp_name(n: UiNode) -> string {
return s
}
# the computed styles that differ from an element's defaults
@alloc_ok("developer builds only, once, under LUDIC_UI_DUMP")
function dp_style(n: UiNode) -> string {
var s = ""
if n.size > 0.0 { s = s + ` font {int(n.size)}` }

View file

@ -18,6 +18,7 @@ function fb_keep(ui_st: mut UiState, screen: string, root: UiNode) -> void {
ui_kept(ui_st.fb_roots, root)
}
# the box of the element with that id (or key) on any screen shown, in screen pixels; null if none
@alloc_ok("a question the game asks now and then (an anchor, a screen's box), never per frame")
export function ui_box(ui_st: UiState, id: string) -> UiBox {
for i in 0 .. len(ui_st.fb_roots) {
let n = fb_find(ui_st.fb_roots[i], id)

View file

@ -28,6 +28,7 @@ function lk_in(words: string, w: string) -> bool {
return false
}
# `any` finds an unexported one too, to say why it cannot be used
@alloc_ok("a template being linked: once per component class (uc_load)")
function lk_find(f: UiFile, tag: string, any: bool) -> UiTpl {
var colon = -1
for i in 0 .. len(tag) {

View file

@ -1,11 +1,13 @@
# pointer_ring.ludic - a pointer event's record and the Value its handler is given, made once
# events are made once and taken in turn: more than a frame fires before its handlers have run
const PE_RING: int = 16
@alloc_ok("once: made with the state")
function pe_evs__new() -> []UiPointer {
let xs = new []UiPointer
for i in 0 .. PE_RING { push(xs, new UiPointer) }
return xs
}
@alloc_ok("once: made with the state")
function pe_vals__new() -> []Val {
let xs = new []Val
for i in 0 .. PE_RING {

View file

@ -77,7 +77,7 @@ function sel_pseudo_ok(ui_st: mut UiState, p: string, arg: string, n: UiNode) ->
if p == "checked" { return ui_attr_on(n, "checked") }
if p == "active" { return fr_active(ui_st, n) }
if p == "capturing" { return ui_st.ct_capture != "" and n.key == ui_st.ct_capture }
if p == "nth-child" { return sel_nth(arg, n.index + 1) }
if p == "nth-child" { return sel_nth(ui_st, arg, n.index + 1) }
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `:{p}: there is no such pseudo-class here`)
return false

View file

@ -1,21 +1,43 @@
# select_nth.ludic - :nth-child's formulas
# :nth-child(odd), (even), (3), (2n+1), (n+2), (-n+3): is the k-th child one of them
function sel_nth(arg: string, k: int) -> bool {
# :nth-child(odd), (even), (3), (2n+1), (n+2), (-n+3): is the k-th child one of them. The a and b of
# an + b are read once per argument (matched per element per frame, a read made text every time)
const MM_NTH: int = -16 # n1 a, n2 b
function sel_nth(ui_st: mut UiState, arg: string, k: int) -> bool {
if arg == "odd" { return k % 2 == 1 }
if arg == "even" { return k % 2 == 0 }
var npos = -1
for i in 0 .. len(arg) {
if arg[i] == 110 { npos = i }
}
if npos < 0 { return string(k) == arg }
var a = 1
let head: string = arg[0..npos]
if head == "-" { a = -1 } else if len(head) > 0 { a = int(el_num(head)) }
var b = 0
var tail = tpl_words(arg[npos + 1..len(arg)])
if len(tail) > 0 and tail[0] == 43 { tail = tpl_words(tail[1..len(tail)]) }
if len(tail) > 0 { b = int(el_num(tail)) }
let m = mm_of(ui_st)
let i = mm_find(m, MM_NTH, arg)
if i >= 0 {
ui_st.nth_a = int(m.n1[i])
ui_st.nth_b = int(m.n2[i])
} else { sel_nth_read(ui_st, m, arg) }
let a = ui_st.nth_a
let b = ui_st.nth_b
if a == 0 { return k == b }
let d = k - b
return d % a == 0 and d / a >= 0
}
@alloc_ok("a memo miss: once per :nth-child argument, bounded by MM_CAP")
function sel_nth_read(ui_st: mut UiState, m: UiMemo, arg: string) -> void {
var npos = -1
for i in 0 .. len(arg) {
if arg[i] == 110 { npos = i }
}
var a = 0
var b = 0
if npos < 0 { b = int(el_num(arg)) } else {
a = 1
let head: string = arg[0..npos]
if head == "-" { a = -1 } else if len(head) > 0 { a = int(el_num(head)) }
var tail = tpl_words(arg[npos + 1..len(arg)])
if len(tail) > 0 and tail[0] == 43 { tail = tpl_words(tail[1..len(tail)]) }
if len(tail) > 0 { b = int(el_num(tail)) }
}
ui_st.nth_a = a
ui_st.nth_b = b
let j = mm_add(m, MM_NTH, arg)
if j >= 0 {
m.n1[j] = float(a)
m.n2[j] = float(b)
}
}

View file

@ -13,6 +13,7 @@ function sel_attr(r: UiRd, s: UiSel) -> void {
}
if not rd_op(r, "]") { rd_fail(r, "']'") }
}
@alloc_ok("a stylesheet being read: once per component class (uc_load)")
function sel_pseudo(r: UiRd, s: UiSel) -> void {
r.i += 1
let name = sel_name(r)

View file

@ -27,6 +27,7 @@ function lss_file(ui_st: mut UiState, src: string, path: string) -> UiFile {
f.sheet = sheet_of(f)
return f
}
@alloc_ok("a stylesheet being read: once per component class (uc_load)")
function lss_text(ui_st: mut UiState, f: UiFile, src: string) -> void {
let err = new []string
lss_block(ui_st, f, lss_strip(src), null, err)

View file

@ -16,6 +16,7 @@ export property UiTpl {
raw: string = "" # a <style>'s text, which is not a template's
where: string = "" # file:line, for an error
}
@alloc_ok("a template being read: once per component class (uc_load)")
function tpl_read(ui_st: mut UiState, x: Xml, file: string) -> UiTpl {
let t = new UiTpl
t.tag = Xml.tag(x)