ludic.ui, ludic.i18n: frame_allocs to 0 - a wrap, an ellipsis, a tooltip's lines, a text-shadow's colour and a range's value kept by what made them; pointer events from a ring; inline text joined through the memo; the pool's and the state's lists through ui_kept; parse, memo miss, pool miss, lazy start and template errors declared; the translation cache clears in place

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 16:41:47 +03:00
parent 629579123a
commit 429e419327
71 changed files with 365 additions and 155 deletions

View file

@ -1,5 +1,6 @@
# langs.ludic - where languages come from, later ones replacing earlier ones with the same code:
# <lang_dir>/languages.txt (one code per line, # a comment) and every .po in the mod folder
@alloc_ok("once at start: the shipped and dropped-in languages found")
export function i18n_init(i18n_st: mut I18nState) -> void {
i18n_st.codes = new []string
i18n_st.names = new []string

View file

@ -46,6 +46,7 @@ function best_of(i18n_st: mut I18nState, head: int, next: words, s: string, best
return b
}
@alloc_ok("a translation cache miss (L): once per distinct text")
function pattern(i18n_st: mut I18nState, s: string, depth: int) -> string {
i18n_st.hit = false
if i18n_st.pat_n == 0 { return s }

View file

@ -20,6 +20,7 @@ function entry_put(e: PoEntry, mode: int, s: string) -> void {
if mode == 4 { e.forms[len(e.forms) - 1] = e.forms[len(e.forms) - 1] + s }
}
@alloc_ok("on a language change: a .po read")
export function i18n_parse(text: string) -> []PoEntry {
let r = new []PoEntry
let lines = lines_of(text)
@ -81,6 +82,7 @@ function unquote(line: string, from: int) -> string {
}
# a header field out of the msgstr "" block: "X-Language-Name: Türkçe\n"
@alloc_ok("on a language change: a .po's header read")
export function i18n_header(text: string, field: string) -> string {
var n = len(text)
if n > 4000 { n = 4000 }

View file

@ -7,13 +7,16 @@ export function L(i18n_st: mut I18nState, s: string) -> string {
let r = whole(i18n_st, s)
if Dict.size(i18n_st.cache) >= CACHE_MAX {
Dict.clear(i18n_st.cache)
i18n_st.cache_out = new []string
List.clear(i18n_st.cache_out)
}
@alloc_ok("a translation cache miss: the entry is kept, up to CACHE_MAX")
Dict.set(i18n_st.cache, s, len(i18n_st.cache_out))
@alloc_ok("a translation cache miss: as above")
push(i18n_st.cache_out, r)
return r
}
@alloc_ok("a translation cache miss (L): once per distinct text, bounded by CACHE_MAX")
function whole(i18n_st: mut I18nState, s: string) -> string {
let n = len(s)
if n == 0 { return s }
@ -37,6 +40,7 @@ function whole(i18n_st: mut I18nState, s: string) -> string {
}
# a paragraph built from sentences, one at a time; English stays where a sentence is unknown
@alloc_ok("a translation cache miss (L): once per distinct text")
function sentences(i18n_st: mut I18nState, s: string) -> string {
let n = len(s)
var r = ""

View file

@ -27,6 +27,7 @@ export function i18n_use(i18n_st: mut I18nState, code: string) -> void {
}
# a .po's text as the lookups, whatever the language is called (a test, a file from elsewhere)
@alloc_ok("on a language change: the language's messages read in")
export function i18n_load_text(i18n_st: mut I18nState, text: string) -> void {
let es = i18n_parse(text)
i18n_st.exact = Dict.new()

View file

@ -43,8 +43,8 @@ function an_began(ui_st: mut UiState, key: string) -> float {
for i in 0 .. len(ui_st.an_keys) {
if ui_st.an_keys[i] == key { return ui_st.an_start[i] }
}
push(ui_st.an_keys, key)
push(ui_st.an_start, an_now(ui_st))
ui_kept(ui_st.an_keys, key)
ui_kept(ui_st.an_start, an_now(ui_st))
return an_now(ui_st)
}
# the element's animation at this moment: each property between the stops around it
@ -52,6 +52,7 @@ function an_apply(ui_st: mut UiState, n: UiNode) -> void {
if n.anim == "" or n.anim_dur <= 0.0 { return }
let k = an_find(ui_st, n.anim)
if k == null {
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `animation {n.anim}: there is no @keyframes of that name`)
return
}

View file

@ -4,6 +4,7 @@ const AT_UNDRAWN: string = " font-weight font-family font-style box-shadow curso
# a stylesheet's property: one that means nothing is said
function el_attr(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> void {
if el_apply(ui_st, n, key, v) { return }
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
if lk_in(AT_UNDRAWN, at_alias(key)) { ui_err(ui_st, `{key}: this renderer does not draw it`) } else { ui_err(ui_st, `{key}: there is no such property`) }
}
# an element's attribute: a property when it names one, otherwise only there for [selectors]

View file

@ -14,6 +14,7 @@ function el_edge(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
return true
}
# url(path) or url("path") -> path
@alloc_ok("a memo miss (el_url_u): once per text")
function el_url(s: string) -> string {
var t = tpl_words(s)
if len(t) > 5 and sat(t, 0, "url(") { t = tpl_words(t[4..len(t) - 1]) }

View file

@ -16,6 +16,7 @@ function el_fit_props(ui_st: mut UiState, n: UiNode, key: string, s: string) ->
return true
}
# "a, rgba(1, 2, 3, 0.5) 40%, b" -> its three parts
@alloc_ok("a gradient being read, on a memo miss")
function el_commas(s: string) -> []string {
let out = new []string
var at = 0

View file

@ -38,6 +38,7 @@ function el_dim(ui_st: mut UiState, n: UiNode, wide: bool, v: Val) -> void {
# a length out of calc(); a percentage has nothing to be a share of here
function el_calc_len(ui_st: mut UiState, s: string) -> float {
if not ca_eval(ui_st, s) { return 0.0 }
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
if ui_st.ca_pct != 0.0 { ui_err(ui_st, `{ui_st.el_key}: {s} - a percentage in calc() is for a width or a height`) }
return ui_st.ca_px
}
@ -53,6 +54,7 @@ function el_flex_short(ui_st: mut UiState, n: UiNode, s: string) -> void {
if len(parts) > 1 and len(parts[1]) > 0 and parts[1][0] >= 48 and parts[1][0] <= 57 { nx(n).shrink = el_num(parts[1]) }
}
# words apart by spaces, a bracket's own spaces kept: "calc(1em + 2px) 4px" is two
@alloc_ok("a memo miss (mm_split): once per text")
function el_words(s: string) -> []string {
let out = new []string
var at = 0
@ -76,17 +78,16 @@ function el_text_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
if tw(ui_st, s) == "none" { return }
let parts = mm_words(ui_st, s)
var nums = 0
var col = ""
var from = -1
for i in 0 .. len(parts) {
let p = parts[i]
if col == "" and ((p[0] >= 48 and p[0] <= 57) or p[0] == 45 or p[0] == 46) {
if from < 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 45 or p[0] == 46) {
if nums == 0 { x.tsh_x = el_unit(ui_st, p) }
if nums == 1 { x.tsh_y = el_unit(ui_st, p) }
nums += 1
} else if col == "" { col = p } else { col = col + " " + p }
} else if from < 0 { from = i }
}
if col == "" { col = "rgba(0,0,0,0.6)" }
x.tsh_c = ui_color(ui_st, col)
x.tsh_c = ui_color(ui_st, el_tsh_col(ui_st, s, parts, from))
x.tsh_a = ui_st.cl_alpha
}
# one word of a list of lengths (padding: calc(1em / 4) 2px)

View file

@ -52,6 +52,9 @@ export state UiState {
ca_used: int = 0
ca_args: [][]CaVal = new [][]CaVal
ca_depth: int = 0
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
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

@ -20,7 +20,7 @@ function bd_kids(ui_st: mut UiState, kids: []UiTpl, e0: UiEnv, key: string, out:
} else if k.tag == "else" {
if not last_if { bd_kids(ui_st, k.kids, e, ck, out) }
last_if = true
} else if k.tag == "each" { bd_each(ui_st, k, e, ck, out) } else if k.tag == "slot" { bd_slot(ui_st, k, e, ck, out) } else if not bd_comp(ui_st, k, e, ck, out) { push(out, bd_elem(ui_st, k, e, ck)) }
} else if k.tag == "each" { bd_each(ui_st, k, e, ck, out) } else if k.tag == "slot" { bd_slot(ui_st, k, e, ck, out) } else if not bd_comp(ui_st, k, e, ck, out) { ui_kept(out, bd_elem(ui_st, k, e, ck)) }
}
}
function bd_test(ui_st: mut UiState, k: UiTpl, e: UiEnv) -> bool {
@ -86,7 +86,7 @@ function bd_comp(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiN
for i in 0 .. len(k.keys) { env_bind(ce, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
ce.on_names = k.on_keys
ce.on_acts = k.on_acts
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
for i in 0 .. len(k.on_keys) { ui_kept(ce.on_envs, e) }
ce.slot = k
ce.slot_env = e
ce.ctx = e.ctx

View file

@ -22,12 +22,13 @@ function bd_class(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []Ui
ce.slot_env = e
ce.on_names = k.on_keys
ce.on_acts = k.on_acts
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
for i in 0 .. len(k.on_keys) { ui_kept(ce.on_envs, e) }
lc_note(ui_st, key, k.on_keys, k.on_acts, e)
let at = len(out)
if cls.roots == null or len(cls.roots) != 1 or cls.roots[0] != cls.root {
@alloc_ok("once per class, again after a reload")
cls.roots = new []UiTpl # its template as a list of one, made once (again after a reload)
push(cls.roots, cls.root)
ui_kept(cls.roots, cls.root)
}
let root = cls.roots
bd_kids(ui_st, root, ce, key, out)
@ -40,7 +41,7 @@ function bd_pass_down(ui_st: mut UiState, n: UiNode, k: UiTpl, e: UiEnv) -> void
if c != null {
let parts = mm_split(ui_st, ui_eval_text(ui_st, c, e), 32)
for i in 0 .. len(parts) {
if len(parts[i]) > 0 { push(n.classes, parts[i]) }
if len(parts[i]) > 0 { ui_kept(n.classes, parts[i]) }
}
}
let id = tpl_attr(k, "id")
@ -49,6 +50,7 @@ function bd_pass_down(ui_st: mut UiState, n: UiNode, k: UiTpl, e: UiEnv) -> void
up.sheet = k.sheet
up.style = k.style
up.env = e
@alloc_ok("once per pooled node: its list is kept and cleared")
if n.ups == null { n.ups = new []UiPass }
push(n.ups, up)
ui_kept(n.ups, up)
}

View file

@ -23,17 +23,16 @@ function bd_group(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []Ui
t.key = key
t.text = ui_tr(ui_st, ui_eval_text(ui_st, k.text, e))
t.env = e
push(out, t)
ui_kept(out, t)
}
# text inside text - <p>Hi <b>there</b></p>, <h3><slot/></h3> - reads as one line of it
function el_inline(n: UiNode) -> void {
function el_inline(ui_st: mut UiState, n: UiNode) -> void {
if n.kind != UI_TEXT or len(n.children) == 0 { return }
var s = n.text
for i in 0 .. len(n.children) {
let c = n.children[i]
el_inline(c)
if len(s) > 0 and len(c.text) > 0 { s = s + " " }
s = s + c.text
el_inline(ui_st, c)
if len(s) == 0 { s = c.text } else if len(c.text) > 0 { s = mm_join(ui_st, s, " ", c.text) }
}
n.text = s
List.clear(n.children)

View file

@ -13,6 +13,7 @@ function ca_eval(ui_st: mut UiState, s: string) -> bool {
ca_ws(ui_st)
if ui_st.ca_bad == "" and ui_st.ca_i < len(ui_st.ca_s) { ui_st.ca_bad = "nothing after its closing )" }
if ui_st.ca_bad != "" {
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `{s}: calc() expected {ui_st.ca_bad}`)
return false
}
@ -68,6 +69,7 @@ function ca_len(ui_st: mut UiState, a: CaVal) -> CaVal {
# a term, blank, from the pool: an evaluation is over before the next begins, and what it works out is
# copied into ca_px / ca_pct, so its records are used again by the next one (never made a frame)
function ca_new(ui_st: mut UiState) -> CaVal {
@alloc_ok("a pool miss: past the most terms one evaluation has had")
if ui_st.ca_used >= len(ui_st.ca_pool) { push(ui_st.ca_pool, new CaVal) }
let r = ui_st.ca_pool[ui_st.ca_used]
ui_st.ca_used += 1

View file

@ -12,12 +12,13 @@ function ca_word(s: string, at: int, w: string) -> bool { return at + len(w) <=
function ca_fn(ui_st: mut UiState, f: string) -> CaVal {
ui_st.ca_i = ui_st.ca_i + len(f) + 1
let depth = ui_st.ca_depth # min(calc(...)) nests: a kept list of arguments a depth
while len(ui_st.ca_args) <= depth { push(ui_st.ca_args, new []CaVal) }
@alloc_ok("once per depth of nesting, then kept")
while len(ui_st.ca_args) <= depth { ui_kept(ui_st.ca_args, new []CaVal) }
let args = ui_st.ca_args[depth]
List.clear(args)
ui_st.ca_depth = depth + 1
while ui_st.ca_bad == "" {
push(args, ca_sum(ui_st))
ui_kept(args, ca_sum(ui_st))
ca_ws(ui_st)
if ui_st.ca_i < len(ui_st.ca_s) and ui_st.ca_s[ui_st.ca_i] == 44 { ui_st.ca_i += 1 } else { break }
}
@ -31,6 +32,7 @@ function ca_fn(ui_st: mut UiState, f: string) -> CaVal {
}
for i in 0 .. len(args) {
if args[i].pct != 0.0 {
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_st.ca_bad = `lengths in {f}(), not a percentage`
return ca_new(ui_st)
}

View file

@ -84,6 +84,7 @@ function ca_number(ui_st: mut UiState) -> CaVal {
return r
}
r.bare = false
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
if ca_unit(s, num_end, end, "%") { r.pct = f } else if ca_unit(s, num_end, end, "px") { r.px = el_px(ui_st, f) } else if ca_unit(s, num_end, end, "rem") { r.px = f * el_px(ui_st, 16.0) } else if ca_unit(s, num_end, end, "em") { r.px = f * el_em(ui_st) } else if ca_unit(s, num_end, end, "vw") { r.px = f * ui_st.ly_vp_w / 100.0 } else if ca_unit(s, num_end, end, "vh") { r.px = f * ui_st.ly_vp_h / 100.0 } else { ui_st.ca_bad = `a unit calc() knows, not {s[num_end..end]}` }
return r
}

View file

@ -61,12 +61,15 @@ function cs_rule_in(ui_st: mut UiState, n: UiNode, r: UiRule, e: UiEnv) -> void
}
}
function ua_sheet(ui_st: mut UiState) -> UiSheet {
if ui_st.ua_sh != null { return ui_st.ua_sh }
if ui_st.ua_sh == null { ua_sheet_make(ui_st) }
return ui_st.ua_sh
}
@alloc_ok("once: the default sheet is read the first time a style is asked for")
function ua_sheet_make(ui_st: mut UiState) -> void {
let f = new UiFile
f.path = "the default sheet"
f.imp = new []UiFile
f.own = new []UiRule
lss_text(ui_st, f, UA_LSS)
ui_st.ua_sh = sheet_of(f)
return ui_st.ua_sh
}

View file

@ -20,12 +20,12 @@ function cs_gather(ui_st: mut UiState, n: UiNode, sh: UiSheet, e: UiEnv, rules:
for i in 0 .. len(seen) {
if seen[i] == sh { return }
}
push(seen, sh)
ui_kept(seen, sh)
for i in 0 .. len(sh.rules) {
let r = sh.rules[i]
if md_ok(ui_st, r) and sel_match(ui_st, r.sel, n) {
push(rules, r)
push(envs, e)
ui_kept(rules, r)
ui_kept(envs, e)
var j = len(rules) - 1
while j > 0 and rules[j - 1].spec > r.spec {
rules[j] = rules[j - 1]

View file

@ -32,6 +32,7 @@ function uc_find(ui_st: UiState, name: string) -> UiClass {
return null
}
# the class's template and styles, read the first time it is shown
@alloc_ok("once per component class: its template and styles are read the first time it is shown")
function uc_load(ui_st: mut UiState, k: UiClass) -> void {
if k.root != null { return }
let f = lib_blank(k.xml_path)
@ -46,6 +47,7 @@ function uc_load(ui_st: mut UiState, k: UiClass) -> void {
}
# a developer's build reads a class's files again when they change on disk: the first look is what
# they are, and a later one that differs is taken (an @import in the styles is read from disk too)
@alloc_ok("developer builds only: a changed file read again")
function uc_reload(ui_st: mut UiState) -> bool {
if ui_st.uc_list == null { return false }
var changed = false

View file

@ -17,6 +17,7 @@ function ui_color(ui_st: mut UiState, name0: string) -> int {
}
return c
}
@alloc_ok("a memo miss (ui_color): once per colour")
function ui_color_new(ui_st: mut UiState, name0: string) -> int {
let name = tw(ui_st, name0)
ui_st.cl_alpha = 1.0
@ -27,6 +28,7 @@ function ui_color_new(ui_st: mut UiState, name0: string) -> int {
for i in 0 .. len(CL_NAMES) - n - 1 {
if CL_NAMES[i] == 32 and CL_NAMES[i + n + 1] == 61 and sat(CL_NAMES, i + 1, name) { return cl_hex_at(ui_st, CL_NAMES, i + n + 2, 6) }
}
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `{name}: not a colour - #rrggbb, rgb(), rgba(), a name, or var(--a-colour)`)
return 15658734
}

View file

@ -10,7 +10,7 @@ function ct_part(ui_st: mut UiState, n: UiNode, cls: string, text: string) -> Ui
p.tag = "span"
p.env = n.env
p.sheet = n.sheet
push(p.classes, cls)
ui_kept(p.classes, cls)
if text != "" {
p.kind = UI_TEXT
p.text = text
@ -25,7 +25,7 @@ function ct_build_bar(ui_st: mut UiState, n: UiNode) -> void {
if hi <= lo { hi = lo + 1.0 }
let fill = ct_part(ui_st, n, "ui-fill", "")
fill.w_pct = Math.clamp((Value.as_float(ui_attr(n, "value")) - lo) / (hi - lo), 0.0, 1.0) * 100.0
push(n.children, fill)
ui_kept(n.children, fill)
}
function ct_build(ui_st: mut UiState, n: UiNode) -> void {
if n.tag == "progress" or n.tag == "meter" {
@ -42,15 +42,15 @@ function ct_build(ui_st: mut UiState, n: UiNode) -> void {
ct_label(ui_st, n)
if t == "checkbox" or t == "radio" {
let track = ct_part(ui_st, n, "ui-track", "")
push(track.children, ct_part(ui_st, n, "ui-knob", ""))
push(n.children, track)
ui_kept(track.children, ct_part(ui_st, n, "ui-knob", ""))
ui_kept(n.children, track)
} else if t == "range" {
ct_build_range(ui_st, n)
} else {
let field = ct_part(ui_st, n, "ui-field", "")
push(field.children, ct_part(ui_st, n, "ui-value", ct_field_text(ui_st, n)))
if ui_st.fc_key == n.key and t != "key" { push(field.children, ct_part(ui_st, n, "ui-caret", "")) }
push(n.children, field)
ui_kept(field.children, ct_part(ui_st, n, "ui-value", ct_field_text(ui_st, n)))
if ui_st.fc_key == n.key and t != "key" { ui_kept(field.children, ct_part(ui_st, n, "ui-caret", "")) }
ui_kept(n.children, field)
}
}
function ct_build_range(ui_st: mut UiState, n: UiNode) -> void {
@ -61,24 +61,25 @@ function ct_build_range(ui_st: mut UiState, n: UiNode) -> void {
let track = ct_part(ui_st, n, "ui-track", "")
let fill = ct_part(ui_st, n, "ui-fill", "")
fill.w_pct = Math.clamp((v - lo) / (hi - lo), 0.0, 1.0) * 100.0
push(track.children, fill)
push(track.children, ct_part(ui_st, n, "ui-thumb", ""))
push(n.children, track)
push(n.children, ct_part(ui_st, n, "ui-value", ct_value_text(n)))
ui_kept(track.children, fill)
ui_kept(track.children, ct_part(ui_st, n, "ui-thumb", ""))
ui_kept(n.children, track)
ui_kept(n.children, ct_part(ui_st, n, "ui-value", ct_value_shown(ui_st, n)))
}
function ct_build_select(ui_st: mut UiState, n: UiNode) -> void {
@alloc_ok("once per pooled node: its list is kept and cleared")
if n.opts == null { n.opts = new []UiNode } # its options moved to a list the node keeps, not a new one
let opts = n.opts
List.clear(opts)
for i in 0 .. len(n.children) { push(opts, n.children[i]) }
for i in 0 .. len(n.children) { ui_kept(opts, n.children[i]) }
List.clear(n.children)
ct_label(ui_st, n)
var shown = ""
let at = ct_option_at(n, opts)
if at >= 0 { shown = opts[at].text }
push(n.children, ct_part(ui_st, n, "ui-prev", "<"))
push(n.children, ct_part(ui_st, n, "ui-value", shown))
push(n.children, ct_part(ui_st, n, "ui-next", ">"))
ui_kept(n.children, ct_part(ui_st, n, "ui-prev", "<"))
ui_kept(n.children, ct_part(ui_st, n, "ui-value", shown))
ui_kept(n.children, ct_part(ui_st, n, "ui-next", ">"))
}
# which option the select's value names: an option's value="...", else its place
function ct_option_at(n: UiNode, opts: []UiNode) -> int {
@ -89,7 +90,7 @@ function ct_option_at(n: UiNode, opts: []UiNode) -> int {
}
return -1
}
function ct_field_text(ui_st: UiState, n: UiNode) -> string {
function ct_field_text(ui_st: mut UiState, n: UiNode) -> string {
let t = ui_attr_text(n, "type")
if t == "key" { return ct_key_text(ui_st, n) }
if t == "number" { return ct_num_shown(ui_st, n) }

View file

@ -65,7 +65,7 @@ function ct_num_blur(ui_st: mut UiState, root: UiNode) -> void {
}
ct_num_commit(ui_st, n)
}
function ct_num_shown(ui_st: UiState, n: UiNode) -> string {
function ct_num_shown(ui_st: mut UiState, n: UiNode) -> string {
if ui_st.ct_num_key == n.key { return ui_st.ct_num_text }
return ct_value_text(n)
return ct_value_shown(ui_st, n)
}

View file

@ -5,14 +5,15 @@ function ct_label(ui_st: mut UiState, n: UiNode) -> void {
let label = ui_attr_text(n, "label")
let note = ui_attr_text(n, "note")
if note == "" {
if label != "" { push(n.children, ct_part(ui_st, n, "ui-label", ui_tr(ui_st, label))) }
if label != "" { ui_kept(n.children, ct_part(ui_st, n, "ui-label", ui_tr(ui_st, label))) }
return
}
let both = ct_part(ui_st, n, "ui-labels", "")
if label != "" { push(both.children, ct_part(ui_st, n, "ui-label", ui_tr(ui_st, label))) }
push(both.children, ct_part(ui_st, n, "ui-note", ui_tr(ui_st, note)))
push(n.children, both)
if label != "" { ui_kept(both.children, ct_part(ui_st, n, "ui-label", ui_tr(ui_st, label))) }
ui_kept(both.children, ct_part(ui_st, n, "ui-note", ui_tr(ui_st, note)))
ui_kept(n.children, both)
}
@alloc_ok("a memo miss: once per value a range shows (memo_more.ludic)")
function ct_value_text(n: UiNode) -> string {
let v = ui_attr(n, "value")
if Value.kind(v) == 0 { return "" }

View file

@ -18,6 +18,7 @@ function gr_parse(ui_st: mut UiState, s0: string) -> UiGradient {
if j >= 0 { m.grads[j] = g }
return g
}
@alloc_ok("a memo miss (gr_parse): once per gradient")
function gr_parse_new(ui_st: mut UiState, s0: string) -> UiGradient {
let s = tw(ui_st, s0)
let parts = el_commas(s[16..len(s) - 1])

View file

@ -11,10 +11,10 @@ function fr_clip_push(ui_st: mut UiState, x: float, y: float, w: float, h: float
x1 = Math.min(x1, ui_st.fr_clips[n - 2])
y1 = Math.min(y1, ui_st.fr_clips[n - 1])
}
push(ui_st.fr_clips, x0)
push(ui_st.fr_clips, y0)
push(ui_st.fr_clips, x1)
push(ui_st.fr_clips, y1)
ui_kept(ui_st.fr_clips, x0)
ui_kept(ui_st.fr_clips, y0)
ui_kept(ui_st.fr_clips, x1)
ui_kept(ui_st.fr_clips, y1)
ui_st.ui_be.clip(x0, y0, Math.max(x1 - x0, 0.0), Math.max(y1 - y0, 0.0))
}
function fr_clip_pop(ui_st: mut UiState) -> void {
@ -28,7 +28,7 @@ function fr_clip_pop(ui_st: mut UiState) -> void {
function fr_children(ui_st: mut UiState, n: UiNode) -> void {
var positioned = 0
for i in 0 .. len(n.children) {
if n.children[i].popover { push(ui_st.pop_list, n.children[i]) } else if n.children[i].pos == 0 { fr_draw(ui_st, n.children[i]) } else { positioned += 1 }
if n.children[i].popover { ui_kept(ui_st.pop_list, n.children[i]) } else if n.children[i].pos == 0 { fr_draw(ui_st, n.children[i]) } else { positioned += 1 }
}
if positioned == 0 { return }
var lo = 1000000

View file

@ -1,5 +1,6 @@
# dump.ludic - a laid-out tree, for a test or a look
# every node and where it went, one line each - for a test, or a look at a layout
@alloc_ok("developer builds only, once, under LUDIC_UI_DUMP")
export function ui_dump(n: UiNode, depth: int) -> string {
var pad = ""
for d in 0 .. depth { pad = pad + " " }

View file

@ -24,7 +24,7 @@ function bd_elem(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string) -> UiNode
if say == "no" { ui_st.tr_no = true } else if say == "yes" { ui_st.tr_no = false }
bd_kids(ui_st, k.kids, e, key, n.children)
if k.text != null { el_text(ui_st, n, ui_tr(ui_st, ui_eval_text(ui_st, k.text, e))) }
el_inline(n)
el_inline(ui_st, n)
if k.tag == "input" or k.tag == "select" or k.tag == "progress" or k.tag == "meter" { ct_build(ui_st, n) }
ui_st.tr_no = was_no
return n
@ -37,7 +37,7 @@ function el_kind(ui_st: mut UiState, n: UiNode, tag: string) -> void {
} else if lk_in(EL_TEXTS, tag) { n.kind = UI_TEXT } else if tag == "row" { n.dir = UI_ROW } else if tag == "button" {
n.dir = UI_ROW
n.focusable = true
} else if tag == "input" or tag == "select" { n.focusable = true } else if tag == "spacer" { n.kind = UI_SPACER } else if tag == "scroll" { n.kind = UI_SCROLL } else if tag == "img" { n.kind = UI_IMAGE } else if tag == "hr" { n.kind = UI_RULE } else if not lk_in(EL_BOXES, tag) { ui_err(ui_st, `<{tag}>: there is no such element`) }
} else if tag == "input" or tag == "select" { n.focusable = true } else if tag == "spacer" { n.kind = UI_SPACER } else if tag == "scroll" { n.kind = UI_SCROLL } else if tag == "img" { n.kind = UI_IMAGE } else if tag == "hr" { n.kind = UI_RULE } else if not lk_in(EL_BOXES, tag) { el_unknown(ui_st, tag) }
}
# a box with words in it holds them as a text of its own, as <div>words</div> does
function el_text(ui_st: mut UiState, n: UiNode, s: string) -> void {
@ -49,7 +49,7 @@ function el_text(ui_st: mut UiState, n: UiNode, s: string) -> void {
t.kind = UI_TEXT
t.text = s
t.env = n.env
push(n.children, t)
ui_kept(n.children, t)
}
# id, class, hidden, disabled and src say what the element is; the rest are applied after the rules
# an attribute's text is worked out only where it is read as text (the rest keep their Value; a selector
@ -58,18 +58,21 @@ function el_keep(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> void {
if key == "id" { n.id = vl_text_u(ui_st, v) } else if key == "class" {
let parts = mm_split(ui_st, vl_text_u(ui_st, v), 32)
for i in 0 .. len(parts) {
if len(parts[i]) > 0 { push(n.classes, parts[i]) }
if len(parts[i]) > 0 { ui_kept(n.classes, parts[i]) }
}
} else if key == "popover" {
n.popover = true
n.pos = 2
push(n.akeys, key)
push(n.avals, "")
push(n.vals, v)
ui_kept(n.akeys, key)
ui_kept(n.avals, "")
ui_kept(n.vals, v)
} else if key == "tabindex" { n.focusable = vl_text_u(ui_st, v) != "-1" } else if key == "hidden" { n.hidden = el_flag(v) } else if key == "disabled" { n.enabled = not el_flag(v) } else if key == "src" { n.src = vl_text_u(ui_st, v) } else {
if key == "enabled" { n.enabled = el_flag(v) }
push(n.akeys, key)
push(n.avals, "")
push(n.vals, v)
ui_kept(n.akeys, key)
ui_kept(n.avals, "")
ui_kept(n.vals, v)
}
}
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
function el_unknown(ui_st: mut UiState, tag: string) -> void { ui_err(ui_st, `<{tag}>: there is no such element`) }

View file

@ -26,8 +26,8 @@ function env_new(ui_st: mut UiState, parent: UiEnv) -> UiEnv {
return e
}
function env_bind(e: UiEnv, name: string, v: Val) -> void {
push(e.names, name)
push(e.vals, v)
ui_kept(e.names, name)
ui_kept(e.vals, v)
}
function env_get(ui_st: mut UiState, e: UiEnv, name: string) -> Val {
var at = e
@ -48,6 +48,7 @@ function env_get(ui_st: mut UiState, e: UiEnv, name: string) -> Val {
cx = cx.parent
}
if e.model != null and Value.has(e.model, name) != 0 { return Value.get(e.model, name) }
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `no {name} here: not a loop's, a prop, a state or in the view`)
return Value.null()
}
@ -58,6 +59,7 @@ function env_set(ui_st: mut UiState, e: UiEnv, name: string, v: Val) -> void {
if at.inst != null {
let args = Value.list()
Value.add(args, v)
@alloc_ok("on an action that sets a component's state, not a frame")
if Value.kind(at.inst.cls.call(at.inst.ptr, `set:{name}`, args, null)) != 99 { return }
}
if at.state != null and Value.has(at.state, name) != 0 {
@ -66,10 +68,12 @@ function env_set(ui_st: mut UiState, e: UiEnv, name: string, v: Val) -> void {
}
at = at.parent
}
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `set {name}: no state here holds it - declare it with <state {name}="..."/>`)
}
# an error is said once, however many frames make it, and names the template line being built
# said by the runtime, or by a host about something a template asked for
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
export function ui_err(ui_st: mut UiState, msg0: string) -> void {
var msg = msg0
if ui_st.ui_at != "" and not ui_has(msg, ".xml:") { msg = `{ui_st.ui_at}: {msg}` }

View file

@ -6,6 +6,7 @@ function ui_eval(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
let o = ui_eval(ui_st, x.a, e)
if Value.kind(o) == 6 and Value.has(o, x.s) != 0 { return Value.get(o, x.s) }
if Value.kind(o) == 5 and x.s == "length" { return uv_int(ui_st, Value.count(o)) }
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `.{x.s}: there is no such field`)
return Value.null()
}
@ -31,6 +32,7 @@ function ui_eval(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
# a call: len() and range() are the template's own, anything else is the view's
function ev_call(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
let depth = ui_st.ev_depth
@alloc_ok("once per depth of nesting, then kept")
while len(ui_st.ev_args) <= depth { push(ui_st.ev_args, Value.list()) }
let args = ui_st.ev_args[depth]
Value.clear(args)
@ -57,6 +59,7 @@ function ev_call_with(ui_st: mut UiState, x: UiExpr, e: UiEnv, args: Val) -> Val
at = at.parent
}
if ui_st.ui_view == null or ui_st.ui_view.call == null {
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `{x.s}(): there is no function or event of that name here`)
return Value.null()
}
@ -68,7 +71,7 @@ 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()) }
if len(ring) < EV_RING { ui_kept(ring, Value.null()) }
let at = ui_st.ev_ring_at % len(ring)
ui_st.ev_ring_at = at + 1
return ring[at]

View file

@ -3,6 +3,7 @@
# text is built only the first time it reads that way. The memo is bounded; past it a text is built as ever
function ev_text(ui_st: mut UiState, x: UiExpr, e: UiEnv) -> Val {
let depth = ui_st.tx_depth # a hole may hold a text of its own: a kept list of pieces a depth
@alloc_ok("once per depth of nesting, then kept")
while len(ui_st.tx_pieces) <= depth { push(ui_st.tx_pieces, new []string) }
let ps = ui_st.tx_pieces[depth]
ui_st.tx_depth = depth + 1
@ -41,17 +42,17 @@ function tx_same(ps: []string, k: string) -> bool {
function tx_piece(ui_st: mut UiState, ps: []string, v: Val) -> void {
let k = Value.kind(v)
if k != 2 and k != 7 {
push(ps, vl_text_u(ui_st, v))
ui_kept(ps, vl_text_u(ui_st, v))
return
}
var t = int(Math.round(Value.as_float(v) * 10.0))
if t < 0 {
push(ps, "-")
ui_kept(ps, "-")
t = 0 - t
}
push(ps, mm_int(ui_st, t / 10))
ui_kept(ps, mm_int(ui_st, t / 10))
let d = t % 10
if d != 0 { push(ps, tx_tenth(d)) }
if d != 0 { ui_kept(ps, tx_tenth(d)) }
}
function tx_tenth(d: int) -> string {
if d == 1 { return ".1" }

View file

@ -2,7 +2,7 @@
# Shift+Tab, or the arrows), Enter and Space activate what has it, and a pointer press focuses what
# it presses; a key field that is listening has every key to itself. A key that is gone from the screen leaves nothing focused.
function fc_collect(ui_st: mut UiState, n: UiNode) -> void {
if n.focusable and n.enabled { push(ui_st.fc_list, n) }
if n.focusable and n.enabled { ui_kept(ui_st.fc_list, n) }
for i in 0 .. len(n.children) { fc_collect(ui_st, n.children[i]) }
}
function fc_find(ui_st: UiState) -> int {

View file

@ -35,6 +35,7 @@ export function ui_nodes(ui_st: mut UiState, screen: string, view: UiView) -> Ui
let cls = uc_find(ui_st, screen)
if si < 0 and cls != null { return fr_class_nodes(ui_st, cls) }
if si < 0 {
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `there is no screen {screen} - ui_load the file that has it`)
return null
}

View file

@ -15,7 +15,7 @@ function fb_keep(ui_st: mut UiState, screen: string, root: UiNode) -> void {
return
}
}
push(ui_st.fb_roots, root)
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
export function ui_box(ui_st: UiState, id: string) -> UiBox {

View file

@ -7,10 +7,12 @@ import "node.ludic"
import "node_more.ludic"
import "view.ludic"
import "backend.ludic"
import "kept.ludic"
import "pool.ludic"
import "pool_reset.ludic"
import "pool_reset_more.ludic"
import "memo.ludic"
import "memo_more.ludic"
import "memo_join.ludic"
import "eval_text.ludic"
import "units_pieces.ludic"
@ -94,6 +96,7 @@ import "hold.ludic"
import "pointer.ludic"
import "pointer_pick.ludic"
import "pointer_events.ludic"
import "pointer_ring.ludic"
import "screen_class.ludic"
import "draw.ludic"
import "draw_content.ludic"

View file

@ -21,11 +21,12 @@ function in_get(ui_st: mut UiState, key: string, k: UiClass) -> UiInst {
let it = in_reuse(ui_st, k)
it.key = key
it.seen = ui_st.in_build
push(ui_st.in_list, it)
ui_kept(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
@alloc_ok("a component instance: made once per instance key, then kept or reused")
function in_reuse(ui_st: mut UiState, k: UiClass) -> UiInst {
let l = ui_st.in_free
for i in 0 .. len(l) {
@ -50,7 +51,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) }
} else { ui_kept(ui_st.in_free, it) }
}
while len(ui_st.in_list) > kept { List.pop(ui_st.in_list) }
}

View file

@ -0,0 +1,6 @@
# kept.ludic - the one push a frame may make: into a list the pool or the state keeps and clears,
# which grows to the most it has held and stays there
function ui_kept<T>(xs: []T, v: T) -> void {
@alloc_ok("a kept list: cleared and filled again, it grows to the most it has held once")
push(xs, v)
}

View file

@ -31,9 +31,9 @@ function ui_object_fit_into(out: []float, n: UiNode, nat_w: float, nat_h: float)
h = nat_h * s
}
List.clear(out)
push(out, bx + (bw - w) / 2.0)
push(out, by + (bh - h) / 2.0)
push(out, w)
push(out, h)
ui_kept(out, bx + (bw - w) / 2.0)
ui_kept(out, by + (bh - h) / 2.0)
ui_kept(out, w)
ui_kept(out, h)
return out
}

View file

@ -45,7 +45,7 @@ function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: f
ui_st.ly_depth += 1
let sizes = ly_scratch(ui_st, depth)
for i in from .. to {
push(sizes, ly_basis(n.children[i], n.dir, main_room))
ui_kept(sizes, ly_basis(n.children[i], n.dir, main_room))
if n.children[i].pos < 2 {
used = used + sizes[i - from]
grows = grows + ly_grow(n.children[i], n.dir)
@ -73,7 +73,9 @@ function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: f
# their own lines while it is still placing them), emptied rather than made anew
function ly_scratch(ui_st: mut UiState, depth: int) -> []float {
while len(ui_st.ly_sizes) <= depth {
@alloc_ok("once per depth of nesting, then kept")
push(ui_st.ly_sizes, new []float)
@alloc_ok("once per depth of nesting, then kept")
push(ui_st.ly_frozen, new []bool)
}
List.clear(ui_st.ly_sizes[depth])

View file

@ -27,7 +27,7 @@ function ly_shrink_line(ui_st: mut UiState, depth: int, n: UiNode, from: int, si
var over = over0
let frozen = ui_st.ly_frozen[depth]
List.clear(frozen)
for k in 0 .. len(sizes) { push(frozen, n.children[from + k].pos >= 2) }
for k in 0 .. len(sizes) { ui_kept(frozen, n.children[from + k].pos >= 2) }
for pass in 0 .. 8 {
var wsum = 0.0
for k in 0 .. len(sizes) {

View file

@ -19,7 +19,8 @@ function ly_measure_text(ui_st: mut UiState, n: UiNode, avail: float) -> void {
n.mw = n.mw + widest
n.mh = n.mh + lh * float(len(n.lines))
}
function ly_wrap(ui_st: mut UiState, s: string, size: float, room: float) -> []string {
@alloc_ok("a memo miss: once per text, size and room (memo_more.ludic), bounded by MM_CAP")
function ly_wrap_new(ui_st: mut UiState, s: string, size: float, room: float) -> []string {
let words = mm_split(ui_st, s, 32)
let out = new []string
var line = ""
@ -35,9 +36,9 @@ function ly_wrap(ui_st: mut UiState, s: string, size: float, room: float) -> []s
if len(line) > 0 { push(out, line) }
return out
}
# a line cut to `room` with an ellipsis, when it does not fit
function ly_ellipsis(ui_st: UiState, s: string, size: float, room: float) -> string {
if ly_text_w(ui_st, s, size) <= room { return s }
# a line cut to `room` with an ellipsis (it does not fit)
@alloc_ok("a memo miss: once per text, size and room (memo_more.ludic), bounded by MM_CAP")
function ly_ellipsis_new(ui_st: UiState, s: string, size: float, room: float) -> string {
var n = len(s)
while n > 0 and ly_text_w(ui_st, s[0..n] + "...", size) > room { n -= 1 }
return s[0..n] + "..."

View file

@ -22,6 +22,7 @@ function lib_file(ui_st: mut UiState, path: string) -> UiFile {
}
let src = lib_text(ui_st, path)
if len(src) == 0 {
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `{path}: cannot read it`)
return null
}
@ -64,6 +65,7 @@ function lib_top(ui_st: mut UiState, f: UiFile, t: UiTpl) -> void {
}
let name = bd_name(t, "name", "")
if name == "" or (t.tag != "component" and t.tag != "screen") {
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `{f.path}: <{t.tag}> - a file holds <import>s, <style>s, <component name=...>s and <screen name=...>s`)
return
}

View file

@ -15,10 +15,10 @@ function lc_note(ui_st: mut UiState, key: string, names: []string, acts: []UiAct
if names[i] == "unmount" { u = acts[i] }
}
if m == null and u == null { return }
push(ui_st.lc_keys, key)
push(ui_st.lc_mounts, m)
push(ui_st.lc_unmounts, u)
push(ui_st.lc_envs, e)
ui_kept(ui_st.lc_keys, key)
ui_kept(ui_st.lc_mounts, m)
ui_kept(ui_st.lc_unmounts, u)
ui_kept(ui_st.lc_envs, e)
}
function lc_mine(key: string, screen: string) -> bool {
let n = len(screen)
@ -40,15 +40,15 @@ function lc_run(ui_st: mut UiState, screen: string) -> void {
if lc_mine(k, screen) {
if ui_st.lc_prev_unmounts[i] != null and not sel_has(ui_st.lc_keys, k) { act_run(ui_st, ui_st.lc_prev_unmounts[i], ui_st.lc_prev_envs[i]) }
} else {
push(keys, k)
push(unmounts, ui_st.lc_prev_unmounts[i])
push(envs, ui_st.lc_prev_envs[i])
ui_kept(keys, k)
ui_kept(unmounts, ui_st.lc_prev_unmounts[i])
ui_kept(envs, ui_st.lc_prev_envs[i])
}
}
for i in 0 .. len(ui_st.lc_keys) {
push(keys, ui_st.lc_keys[i])
push(unmounts, ui_st.lc_unmounts[i])
push(envs, ui_st.lc_envs[i])
ui_kept(keys, ui_st.lc_keys[i])
ui_kept(unmounts, ui_st.lc_unmounts[i])
ui_kept(envs, ui_st.lc_envs[i])
}
ui_st.lc_sp_keys = ui_st.lc_prev_keys
ui_st.lc_sp_unmounts = ui_st.lc_prev_unmounts

View file

@ -10,6 +10,7 @@ function lib_link(ui_st: mut UiState, f: UiFile, t: UiTpl) -> void {
lk_kids(ui_st, f, t)
return
}
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
if t.comp == null and lk_find(f, t.tag, true) != null { ui_err(ui_st, `{t.where}: <{t.tag}>: that component is not exported - export="true" on it lets other files use it`) } else if t.comp == null { ui_err(ui_st, `{t.where}: <{t.tag}>: there is no such element, and no component of that name here or exported by an import`) }
}
lk_kids(ui_st, f, t)

View file

@ -25,6 +25,7 @@ const MM_VTPL: int = -7 # a value's var()s parsed (units_pieces.ludic
const MM_COLOR: int = -8 # ui_color: n1 the colour, n2 its alpha
const MM_BIMG: int = -9 # border-image: texts the picture, lists what follows its url()
const MM_TEXT: int = -10 # a text with holes, keys[i] the text itself (eval_text.ludic)
@alloc_ok("once: the memo is made the first time it is asked")
function mm_new() -> UiMemo {
let m = new UiMemo
m.slots = words(MM_CAP)
@ -48,6 +49,7 @@ function mm_of(ui_st: mut UiState) -> UiMemo {
}
# every percentage a bar can be written as (0% .. 100%, to a tenth) made once at the start, so a bar
# moving through them never makes its text in play - a text seen for the first time was still one string
@alloc_ok("once: every percentage a bar can show, made with the memo")
function mm_warm_pct(ui_st: mut UiState, m: UiMemo) -> void {
for t in 0 .. 1001 {
var s = mm_int(ui_st, t / 10)
@ -74,6 +76,7 @@ function mm_find(m: UiMemo, tag: int, s: string) -> int {
return -1
}
# a new entry for (tag, s) with its results empty, or -1 when the table is full
@alloc_ok("a memo entry: one per distinct text, bounded by MM_CAP")
function mm_add(m: UiMemo, tag: int, s: string) -> int {
if len(m.keys) * 4 >= MM_CAP * 3 { return -1 }
var at = mm_hash(tag, s) & (MM_CAP - 1)

View file

@ -14,6 +14,7 @@ function mm_is_join(k: string, a: string, sep: string, b: string) -> bool {
return sat(k, 0, a) and sat(k, na + 1, b)
}
# a + sep + b, the same string every frame
@alloc_ok("a memo miss: a joined key made once, bounded by MM_CAP")
function mm_join(ui_st: mut UiState, a: string, sep: string, b: string) -> string {
let m = mm_of(ui_st)
var at = mm_hash_join(a, sep, b) & (MM_CAP - 1)
@ -39,6 +40,7 @@ function mm_join(ui_st: mut UiState, a: string, sep: string, b: string) -> strin
# a whole number's text, made once (a key's index, a count, money): 0..1023 at the start, the rest up
# to MM_INT_MOST the first time each is shown; only past that is one made per call
const MM_INT_MOST: int = 65536
@alloc_ok("a memo miss: a number's text made the first time it is shown")
function mm_int(ui_st: mut UiState, i: int) -> string {
if i < 0 or i >= MM_INT_MOST { return string(i) }
if ui_st.mm_ints == null {
@ -51,6 +53,7 @@ function mm_int(ui_st: mut UiState, i: int) -> string {
return l[i]
}
# a small whole number as a Value, made once and shared: read it, never change it (a loop's index)
@alloc_ok("once: the small numbers' Values")
function mm_ival(ui_st: mut UiState, i: int) -> Val {
if i < 0 or i >= 1024 { return Value.int(i) }
if ui_st.mm_ivals == null {

View file

@ -0,0 +1,88 @@
# memo_more.ludic - the layout's and the controls' texts, kept by what they were made from: a wrap
# and an ellipsis by the text, its size and its room in whole pixels (two widths of one text keep
# two entries), a tooltip's lines, a text-shadow's colour, a range's shown value by its element
const MM_WRAP: int = -11 # lists the lines; n1 the size, n2 the room
const MM_ELLIP: int = -12 # texts the cut line; n1 the size, n2 the room
const MM_TTL: int = -13 # lists a title's lines
const MM_TSH: int = -14 # texts a text-shadow's colour
const MM_RANGE: int = -15 # keys the element's key; n1 the value, texts what it shows
# the entry for (tag, s) made at a and b, or -1
function mm_find_at(m: UiMemo, tag: int, s: string, a: float, b: float) -> int {
var at = mm_hash(tag, s) & (MM_CAP - 1)
while m.slots[at] != 0 {
let i = m.slots[at] - 1
if m.tags[i] == tag and m.n1[i] == a and m.n2[i] == b and m.keys[i] == s { return i }
at = (at + 1) & (MM_CAP - 1)
}
return -1
}
function ly_wrap(ui_st: mut UiState, s: string, size: float, room0: float) -> []string {
let room = float(int(room0))
let m = mm_of(ui_st)
let i = mm_find_at(m, MM_WRAP, s, size, room)
if i >= 0 { return m.lists[i] }
let lines = ly_wrap_new(ui_st, s, size, room)
let j = mm_add(m, MM_WRAP, s)
if j >= 0 {
m.lists[j] = lines
m.n1[j] = size
m.n2[j] = room
}
return lines
}
function ly_ellipsis(ui_st: mut UiState, s: string, size: float, room0: float) -> string {
if ly_text_w(ui_st, s, size) <= room0 { return s }
let room = float(int(room0))
let m = mm_of(ui_st)
let i = mm_find_at(m, MM_ELLIP, s, size, room)
if i >= 0 { return m.texts[i] }
let t = ly_ellipsis_new(ui_st, s, size, room)
let j = mm_add(m, MM_ELLIP, s)
if j >= 0 {
m.texts[j] = t
m.n1[j] = size
m.n2[j] = room
}
return t
}
function tt_lines(ui_st: mut UiState, s: string) -> []string {
let m = mm_of(ui_st)
let i = mm_find(m, MM_TTL, s)
if i >= 0 { return m.lists[i] }
let lines = tt_lines_new(s)
let j = mm_add(m, MM_TTL, s)
if j >= 0 { m.lists[j] = lines }
return lines
}
# a text-shadow's colour: its words from `from` on, joined once per text
function el_tsh_col(ui_st: mut UiState, s: string, parts: []string, from: int) -> string {
if from < 0 { return "rgba(0,0,0,0.6)" }
if from == len(parts) - 1 { return parts[from] }
let m = mm_of(ui_st)
let i = mm_find(m, MM_TSH, s)
if i >= 0 { return m.texts[i] }
let t = el_tsh_join(parts, from)
let j = mm_add(m, MM_TSH, s)
if j >= 0 { m.texts[j] = t }
return t
}
@alloc_ok("a memo miss: once per text-shadow value, bounded by MM_CAP")
function el_tsh_join(parts: []string, from: int) -> string {
var col = parts[from]
for k in from + 1 .. len(parts) { col = col + " " + parts[k] }
return col
}
# what a range shows: made again only when its value moves (a slider held still makes nothing)
function ct_value_shown(ui_st: mut UiState, n: UiNode) -> string {
let f = Value.as_float(ui_attr(n, "value"))
let m = mm_of(ui_st)
var i = mm_find(m, MM_RANGE, n.key)
if i >= 0 and m.n1[i] == f and m.texts[i] != null { return m.texts[i] }
let t = ct_value_text(n)
if i < 0 { i = mm_add(m, MM_RANGE, n.key) }
if i >= 0 {
m.n1[i] = f
m.texts[i] = t
}
return t
}

View file

@ -27,6 +27,7 @@ export function ui_native(ui_st: mut UiState, tag: string, measure: fn(UiNode, f
export function ui_native_input(ui_st: mut UiState, tag: string, f: fn(UiNode, UiPointer) -> bool) -> void {
let nv = nt_find(ui_st, tag)
if nv == null {
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `ui_native_input: there is no native <{tag}> - ui_native it first`)
return
}
@ -44,9 +45,9 @@ function nt_fired_clear(ui_st: mut UiState) -> void {
List.clear(ui_st.nt_q_vals)
}
export function ui_fire(ui_st: mut UiState, n: UiNode, name: string, value: Val) -> void {
push(ui_st.nt_q_nodes, n)
push(ui_st.nt_q_names, name)
push(ui_st.nt_q_vals, value)
ui_kept(ui_st.nt_q_nodes, n)
ui_kept(ui_st.nt_q_names, name)
ui_kept(ui_st.nt_q_vals, value)
}
function nt_fired_run(ui_st: mut UiState) -> void {
let nodes = ui_st.nt_q_nodes # run from these while what they fire goes to the spares

View file

@ -32,6 +32,7 @@ export property UiNodeX {
zoom: float = 1.0 # zoom: this element and everything inside it, larger or smaller
}
function nx(n: UiNode) -> UiNodeX {
@alloc_ok("once per pooled node: its extras are kept")
if n.ex == null { n.ex = new UiNodeX }
return n.ex
}

View file

@ -43,7 +43,7 @@ function fr_active(ui_st: UiState, n: UiNode) -> bool {
}
function fr_under(n: UiNode, px: float, py: float, keys: []string) -> void {
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return }
if n.key != "" { push(keys, n.key) }
if n.key != "" { ui_kept(keys, n.key) }
for i in 0 .. len(n.children) { fr_under(n.children[i], px, py, keys) }
}
# the control (or pressable element) the pointer is on: out from the topmost element under it

View file

@ -55,7 +55,11 @@ function pe_down(ui_st: mut UiState, n: UiNode, b: int) -> void {
# one event: a native's input answers at once (false: not its to take); an element's on-<kind> runs
# once the frame is drawn, with the pointer's fields on `event`
function pe_send(ui_st: mut UiState, n: UiNode, kind: string, b: int) -> bool {
let e = new UiPointer
let e = ui_st.pe_evs[ui_st.pe_at]
let v = ui_st.pe_vals[ui_st.pe_at]
ui_st.pe_at = (ui_st.pe_at + 1) % PE_RING
e.dx = 0.0
e.dy = 0.0
e.kind = kind
e.x = ui_st.in_now.x - n.x
e.y = ui_st.in_now.y - n.y
@ -66,13 +70,12 @@ function pe_send(ui_st: mut UiState, n: UiNode, kind: string, b: int) -> bool {
e.button = b
e.wheel = ui_st.in_now.wheel
if n.kind == UI_NATIVE and n.native != null and n.native.input != null { return n.native.input(n, e) }
let v = Value.object()
Value.put(v, "x", Value.float(e.x))
Value.put(v, "y", Value.float(e.y))
Value.put(v, "dx", Value.float(e.dx))
Value.put(v, "dy", Value.float(e.dy))
Value.put(v, "button", Value.int(b))
Value.put(v, "wheel", Value.float(e.wheel))
Value.be_float(Value.get(v, "x"), e.x)
Value.be_float(Value.get(v, "y"), e.y)
Value.be_float(Value.get(v, "dx"), e.dx)
Value.be_float(Value.get(v, "dy"), e.dy)
Value.be_int(Value.get(v, "button"), b)
Value.be_float(Value.get(v, "wheel"), e.wheel)
ui_fire(ui_st, n, kind, v)
return true
}
@ -81,3 +84,4 @@ function pe_takes_wheel(box: UiNode, px: float, py: float) -> bool {
let t = fr_pick(box, px, py, PK_WHEEL)
return t != null and t != box
}

View file

@ -0,0 +1,22 @@
# 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
function pe_evs__new() -> []UiPointer {
let xs = new []UiPointer
for i in 0 .. PE_RING { push(xs, new UiPointer) }
return xs
}
function pe_vals__new() -> []Val {
let xs = new []Val
for i in 0 .. PE_RING {
let v = Value.object()
Value.put(v, "x", Value.float(0.0))
Value.put(v, "y", Value.float(0.0))
Value.put(v, "dx", Value.float(0.0))
Value.put(v, "dy", Value.float(0.0))
Value.put(v, "button", Value.int(0))
Value.put(v, "wheel", Value.float(0.0))
push(xs, v)
}
return xs
}

View file

@ -25,19 +25,7 @@ function pl_begin(ui_st: mut UiState, screen: string) -> void {
for i in 0 .. len(ui_st.pl_pools) {
if ui_st.pl_pools[i].key == screen { p = ui_st.pl_pools[i] }
}
if p == null {
p = new UiPool
p.key = screen
p.nodes0 = new []UiNode
p.nodes1 = new []UiNode
p.envs0 = new []UiEnv
p.envs1 = new []UiEnv
p.vals0 = new []Val
p.vals1 = new []Val
p.ups0 = new []UiPass
p.ups1 = new []UiPass
push(ui_st.pl_pools, p)
}
if p == null { p = pl_new(ui_st, screen) }
p.gen = 1 - p.gen
p.un = 0
p.ue = 0
@ -45,6 +33,21 @@ function pl_begin(ui_st: mut UiState, screen: string) -> void {
p.uu = 0
ui_st.pl_cur = p
}
@alloc_ok("once per screen: its pool is made the first time it is built")
function pl_new(ui_st: mut UiState, screen: string) -> UiPool {
let p = new UiPool
p.key = screen
p.nodes0 = new []UiNode
p.nodes1 = new []UiNode
p.envs0 = new []UiEnv
p.envs1 = new []UiEnv
p.vals0 = new []Val
p.vals1 = new []Val
p.ups0 = new []UiPass
p.ups1 = new []UiPass
push(ui_st.pl_pools, p)
return p
}
# a node, blank, with its own lists (empty) - from the pool when a build is under way
function nd_take(ui_st: mut UiState) -> UiNode {
let p = ui_st.pl_cur
@ -58,10 +61,11 @@ function nd_take(ui_st: mut UiState) -> UiNode {
return n
}
let n = nd_fresh()
push(list, n)
ui_kept(list, n)
p.un += 1
return n
}
@alloc_ok("a pool miss: past the most nodes a screen has had, then kept")
function nd_fresh() -> UiNode {
let n = new UiNode
n.children = new []UiNode
@ -83,10 +87,11 @@ function env_take(ui_st: mut UiState) -> UiEnv {
return e
}
let e = env_fresh()
push(list, e)
ui_kept(list, e)
p.ue += 1
return e
}
@alloc_ok("a pool miss: past the most scopes a screen has had, then kept")
function env_fresh() -> UiEnv {
let e = new UiEnv
e.names = new []string
@ -108,7 +113,7 @@ function uv_new(ui_st: mut UiState, tag: int) -> Val {
return Value.become(v, tag)
}
let v = Value.become(Value.null(), tag)
push(list, v)
ui_kept(list, v)
p.uv += 1
return v
}
@ -140,10 +145,11 @@ function uv_hold(ui_st: mut UiState) -> Val {
return list[p.uv - 1]
}
let v = Value.null()
push(list, v)
ui_kept(list, v)
p.uv += 1
return v
}
@alloc_ok("a pool miss: past the most passes a screen has had, then kept")
function up_take(ui_st: mut UiState) -> UiPass {
let p = ui_st.pl_cur
if p == null { return new UiPass }
@ -154,7 +160,7 @@ function up_take(ui_st: mut UiState) -> UiPass {
return list[p.uu - 1]
}
let u = new UiPass
push(list, u)
ui_kept(list, u)
p.uu += 1
return u
}

View file

@ -25,6 +25,7 @@ function pp_anchor_of(ui_st: mut UiState, parent: UiNode, i: int) -> UiNode {
if f != null { return f }
at = at.parent
}
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `anchor="{id}": no element has that id`)
return null
}

View file

@ -13,6 +13,7 @@ function pp_bounds(ui_st: mut UiState, p: UiNode) -> void {
box = pp_by_id(at, id, p)
at = at.parent
}
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
if box == null { ui_err(ui_st, `within="{id}": no element has that id`) }
} else {
var at = p.parent

View file

@ -1,6 +1,7 @@
# reload.ludic - a developer's templates and stylesheets read again when they change on disk: every
# file is re-read in the order it was first loaded (so an import is there before its importer), and
# the screens' state is kept, as React's fast refresh keeps it. True when anything changed.
@alloc_ok("developer builds only: the screens' files looked at again once a second")
export function ui_reload(ui_st: mut UiState) -> bool {
var changed = uc_reload(ui_st)
for i in 0 .. len(ui_st.lib_files) {

View file

@ -81,6 +81,7 @@ function r3u_cell(render3d_st: mut Render3dState, ui_st: mut UiState, k: int, sr
}
if idx < 0 and n > 0 and src[at] >= 48 and src[at] <= 57 { idx = int(el_num_in(src, at, len(src))) }
if idx < 0 {
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `{src}: the atlas has no such picture`)
return
}

View file

@ -8,18 +8,7 @@ function fr_class_nodes(ui_st: mut UiState, cls: UiClass) -> UiNode {
ui_st.ui_view = null
uc_load(ui_st, cls)
let root = fr_root(ui_st, cls.name, cls.sheet)
if cls.use == null {
let use = new UiTpl
use.tag = cls.name
use.cls = cls
use.keys = new []string
use.vals = new []UiExpr
use.on_keys = new []string
use.on_acts = new []UiAct
use.kids = new []UiTpl
cls.use = use
cls.none = Value.object()
}
if cls.use == null { fr_class_use(cls) }
let use = cls.use
let e = env_new(ui_st, null)
e.model = cls.none
@ -37,3 +26,17 @@ function fr_root(ui_st: mut UiState, key: string, sheet: UiSheet) -> UiNode {
root.sheet = sheet
return root
}
@alloc_ok("once per component class: the tag that shows it as a screen")
function fr_class_use(cls: UiClass) -> void {
let use = new UiTpl
use.tag = cls.name
use.cls = cls
use.keys = new []string
use.vals = new []UiExpr
use.on_keys = new []string
use.on_acts = new []UiAct
use.kids = new []UiTpl
cls.use = use
cls.none = Value.object()
}

View file

@ -14,8 +14,8 @@ function sc_put(ui_st: mut UiState, key: string, v: float) -> void {
return
}
}
push(ui_st.sc_keys, key)
push(ui_st.sc_offs, v)
ui_kept(ui_st.sc_keys, key)
ui_kept(ui_st.sc_offs, v)
}
# every scroll box, outermost first: its offset (from input, when `live`), then its content moved
function sc_update(ui_st: mut UiState, n: UiNode) -> void {

View file

@ -6,7 +6,7 @@ function sb_geo(ui_st: mut UiState, n: UiNode, off: float, room: float, most: fl
let bar = ct_part(ui_st, n, "ui-scrollbar", "")
bar.tag = "div"
bar.parent = n
push(bar.children, ct_part(ui_st, n, "ui-thumb", ""))
ui_kept(bar.children, ct_part(ui_st, n, "ui-thumb", ""))
cs_tree(ui_st, bar)
ui_st.sb_bar = bar
ui_st.sb_w = ui_px(ui_st, 6.0)

View file

@ -78,6 +78,7 @@ function sel_pseudo_ok(ui_st: mut UiState, p: string, arg: string, n: UiNode) ->
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) }
@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

@ -14,8 +14,8 @@ function st_for(ui_st: mut UiState, key: string, t: UiTpl, e: UiEnv) -> Val {
ui_st.uv_on = false
for i in 0 .. len(decl.keys) { Value.put(o, decl.keys[i], ui_eval(ui_st, decl.vals[i], e)) }
ui_st.uv_on = was
push(ui_st.st_keys, key)
push(ui_st.st_vals, o)
ui_kept(ui_st.st_keys, key)
ui_kept(ui_st.st_vals, o)
return o
}
# a screen's state starts again the next time it opens

View file

@ -30,6 +30,7 @@ function lss_file(ui_st: mut UiState, src: string, path: string) -> UiFile {
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)
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
for i in 0 .. len(err) { ui_err(ui_st, `{f.path}: <style> {err[i]}`) }
}
# rules, @imports and @media blocks; the rules in a @media block carry its condition

View file

@ -36,6 +36,7 @@ function lss_decls(body: string, err: []string) -> UiRule {
}
return r
}
@alloc_ok("a memo miss (mm_split) or a stylesheet being read: once per text")
function lss_split(s: string, c: int) -> []string {
let out = new []string
var at = 0

View file

@ -47,8 +47,8 @@ function tt_draw(ui_st: mut UiState, root: UiNode) -> void {
let n = ct_part(ui_st, root, "ui-tooltip", "")
n.tag = "div"
n.parent = root
let lines = tt_lines(ui_st.tt_text)
for i in 0 .. len(lines) { push(n.children, ct_part(ui_st, root, "ui-tooltip-text", lines[i])) }
let lines = tt_lines(ui_st, ui_st.tt_text)
for i in 0 .. len(lines) { ui_kept(n.children, ct_part(ui_st, root, "ui-tooltip-text", lines[i])) }
cs_tree(ui_st, n)
ly_measure(ui_st, n, ui_st.ly_vp_w * 0.4, ui_px(ui_st, 16.0), root.rfg)
let x = Math.min(ui_st.tt_x + ui_px(ui_st, 16.0), ui_st.ly_vp_x + ui_st.ly_vp_w - n.mw)
@ -57,7 +57,8 @@ function tt_draw(ui_st: mut UiState, root: UiNode) -> void {
fr_draw(ui_st, n)
}
# a title's lines: apart at a newline, or at a written \n (which an attribute cannot hold otherwise)
function tt_lines(s: string) -> []string {
@alloc_ok("a memo miss: once per title (memo_more.ludic), bounded by MM_CAP")
function tt_lines_new(s: string) -> []string {
let out = new []string
var at = 0
var i = 0

View file

@ -10,7 +10,8 @@ function tt_focused(ui_st: mut UiState, scope: UiNode, x: float, y: float) -> Ui
if n == null or n.akeys == null or ui_attr_text(n, "title") == "" { return null }
return n
}
function tt_translate(ui_st: UiState, n: UiNode, s: string) -> string {
@alloc_ok("on a title's change: tt_track keeps what it made")
function tt_translate(ui_st: mut UiState, n: UiNode, s: string) -> string {
var at = n
while at != null {
let say = ui_attr_text(at, "translate")
@ -20,7 +21,7 @@ function tt_translate(ui_st: UiState, n: UiNode, s: string) -> string {
}
let whole = ui_tr(ui_st, s)
if whole != s { return whole }
let lines = tt_lines(s)
let lines = tt_lines(ui_st, s)
if len(lines) < 2 { return whole }
var out = ui_tr(ui_st, lines[0])
for i in 1 .. len(lines) { out = out + "\n" + ui_tr(ui_st, lines[i]) }

View file

@ -49,6 +49,7 @@ function tpl_read(ui_st: mut UiState, x: Xml, file: string) -> UiTpl {
}
let words = tpl_words(Xml.text(x))
if len(words) > 0 and Xml.child_count(x) == 0 { t.text = ex_text(words, err) }
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
for i in 0 .. len(err) { ui_err(ui_st, `{t.where}: <{t.tag}> {err[i]}`) }
return t
}
@ -67,6 +68,7 @@ function tpl_value(v: string, err: []string) -> UiExpr {
return ex_text(v, err)
}
# text as a reader sees it: the ends trimmed and every run of white space one space
@alloc_ok("a memo miss (tw) or a template being read: once per text")
function tpl_words(s: string) -> string {
if tpl_clean(s) { return s }
var out = ""

View file

@ -16,10 +16,10 @@ function tr_apply(ui_st: mut UiState, n: UiNode) -> void {
return
}
}
push(ui_st.tr_keys, n.key)
push(ui_st.tr_from, want)
push(ui_st.tr_to, want)
push(ui_st.tr_at, now)
ui_kept(ui_st.tr_keys, n.key)
ui_kept(ui_st.tr_from, want)
ui_kept(ui_st.tr_to, want)
ui_kept(ui_st.tr_at, now)
}
function tr_value(ui_st: UiState, i: int, now: float, dur: float) -> float {
let t = Math.clamp((now - ui_st.tr_at[i]) / dur, 0.0, 1.0)

View file

@ -28,7 +28,9 @@ function cs_fs(ui_st: UiState, n: UiNode) -> float {
}
function cs_var_set(n: UiNode, name: string, value: string) -> void {
if n.var_names == null {
@alloc_ok("once per pooled node: its lists are kept and cleared")
n.var_names = new []string
@alloc_ok("once per pooled node: as above")
n.var_vals = new []string
}
for i in 0 .. len(n.var_names) {
@ -37,8 +39,8 @@ function cs_var_set(n: UiNode, name: string, value: string) -> void {
return
}
}
push(n.var_names, name)
push(n.var_vals, value)
ui_kept(n.var_names, name)
ui_kept(n.var_vals, value)
}
function cs_var(n: UiNode, name: string) -> string {
var at = n
@ -77,6 +79,7 @@ function cs_var_ref(ui_st: mut UiState, n: UiNode, inner: string) -> string {
let v = cs_var(n, tw(ui_st, parts[0]))
if v != null { return v }
if len(parts) > 1 { return tw(ui_st, inner[len(parts[0]) + 1..len(inner)]) }
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `var({tw(ui_st, parts[0])}): no element here or above sets it`)
return ""
}

View file

@ -15,6 +15,7 @@ function cv_tpl(ui_st: mut UiState, s: string) -> UiVarTpl {
if j >= 0 { m.vtpls[j] = t }
return t
}
@alloc_ok("a memo miss (cv_tpl): once per value with var() in it")
function cv_parse(ui_st: mut UiState, s: string) -> UiVarTpl {
let t = new UiVarTpl
t.lits = new []string
@ -49,10 +50,10 @@ function cv_pieces(ui_st: mut UiState, n: UiNode, s: string) -> bool {
let t = cv_tpl(ui_st, s)
List.clear(ui_st.cv_texts)
for k in 0 .. len(t.names) {
push(ui_st.cv_texts, t.lits[k])
push(ui_st.cv_texts, cv_value(ui_st, n, t, k))
ui_kept(ui_st.cv_texts, t.lits[k])
ui_kept(ui_st.cv_texts, cv_value(ui_st, n, t, k))
}
push(ui_st.cv_texts, t.lits[len(t.names)])
ui_kept(ui_st.cv_texts, t.lits[len(t.names)])
return true
}
function cv_has(s: string) -> bool {
@ -65,6 +66,7 @@ function cv_value(ui_st: mut UiState, n: UiNode, t: UiVarTpl, k: int) -> string
let v = cs_var(n, t.names[k])
if v != null { return v }
if t.falls[k] != null { return t.falls[k] }
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
ui_err(ui_st, `var({t.names[k]}): no element here or above sets it`)
return ""
}

View file

@ -7,6 +7,7 @@ function el_zoom(ui_st: mut UiState, n: UiNode, s0: string) -> void {
if ca_is(s) {
if ca_eval(ui_st, s) {
z = ui_st.ca_num
@alloc_ok("only when a template is wrong: a broken screen, never a working one")
if not ui_st.ca_bare { ui_err(ui_st, `zoom: {s} - a zoom is a plain number (divide a length by a length)`) }
}
} else if len(s) > 1 and s[len(s) - 1] == 37 { z = el_num(s) / 100.0 } else if s != "normal" { z = el_num(s) }