runtime, ludic.ui: a scalar Value is one allocation, not three (lists only for a list or an object); layout and cascade lists kept a depth; small numbers' text made once - benchmark 63 -> 36 KB a frame

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 23:43:25 +03:00
parent 1d40f6d95a
commit 46888b6716
14 changed files with 84 additions and 33 deletions

View file

@ -12,7 +12,7 @@ function el_apply(ui_st: mut UiState, n: UiNode, key: string, v0: Val) -> bool {
ui_st.el_cur = n
ui_st.el_key = k
if len(k) > 2 and sat(k, 0, "--") {
cs_var_set(n, k, vl_text(v0))
cs_var_set(n, k, vl_text_u(ui_st, v0))
return true
}
let v = cs_vars_in(ui_st, n, v0)
@ -48,7 +48,7 @@ function el_size(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
return true
}
function el_flex(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
let s = vl_text_u(ui_st, v)
if key == "dir" {
n.dir = UI_COL
if s == "row" { n.dir = UI_ROW }
@ -56,7 +56,7 @@ function el_flex(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
return true
}
function el_look(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
let s = vl_text_u(ui_st, v)
if key == "size" { n.size = el_len(ui_st, v) } else if key == "color" {
n.fg = ui_color(ui_st, s)
n.fg_a = ui_st.cl_alpha

View file

@ -23,7 +23,7 @@ function el_sides(ui_st: mut UiState, v: Val) -> void {
ui_st.el_s0 = el_px(ui_st, Value.as_float(v))
n = 1
} else {
let words = mm_words(ui_st, vl_text(v))
let words = mm_words(ui_st, vl_text_u(ui_st, v))
for i in 0 .. len(words) {
if len(words[i]) > 0 and n < 4 {
el_side_set(ui_st, n, el_word_len(ui_st, words[i]))
@ -43,7 +43,7 @@ function el_side_set(ui_st: mut UiState, i: int, v: float) -> void {
# a length: a number, 10px, 1.5em, 2rem, 50vw, calc(...), fill, fit or auto (a percentage is el_pct's)
function el_len(ui_st: mut UiState, v: Val) -> float {
if vl_num(v) { return el_px(ui_st, Value.as_float(v)) }
let s = vl_text(v)
let s = vl_text_u(ui_st, v)
if s == "fill" { return UI_FILL }
if ca_is(s) { return el_calc_len(ui_st, s) }
if s == "fit" or s == "auto" or (len(s) > 0 and s[len(s) - 1] == 37) { return 0.0 }

View file

@ -1,6 +1,6 @@
# attr_edge.ludic - border-radius, outline (and its width, colour and offset), scrollbar-color
function el_edge(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
let s = vl_text_u(ui_st, v)
if key == "border-radius" { n.radius = el_len(ui_st, v) } else if key == "outline" { el_outline(ui_st, n, s) } else if key == "outline-width" { n.outline_w = el_len(ui_st, v) } else if key == "outline-offset" { n.outline_off = el_len(ui_st, v) } else if key == "outline-color" {
n.outline_c = ui_color(ui_st, s)
n.outline_a = ui_st.cl_alpha

View file

@ -2,7 +2,7 @@
# percentage is kept apart and taken of the containing block's width (left, right) or height (top,
# bottom) when the element is placed
function el_inset(ui_st: mut UiState, n: UiNode, side: int, v: Val) -> float {
let s = vl_text(v)
let s = vl_text_u(ui_st, v)
var pct = 0.0
var px = UI_AUTO
if s != "auto" {

View file

@ -1,13 +1,13 @@
# attr_more.ludic - width and height (where 0 is 0 and calc() may hold a percentage), the flex
# shorthand, flex-shrink, order, text-shadow, pointer-events, text-fit and line-height (text_fit.ludic)
function el_more(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
let s = vl_text_u(ui_st, v)
if key == "shrink" { nx(n).shrink = Math.max(el_num(s), 0.0) } else if key == "order" { nx(n).order = int(el_num(s)) } else if key == "text-shadow" { el_text_shadow(ui_st, n, s) } else if key == "pointer-events" { nx(n).no_pointer = tpl_words(s) == "none" } else if key == "text-fit" { el_text_fit(ui_st, n, s) } else if key == "line-height" { el_line_h(ui_st, n, s) } else { return el_fit_props(ui_st, n, key, s) }
return true
}
# width or height: a length (0 included), fill, a percentage, calc(), or auto/fit (none)
function el_dim(ui_st: mut UiState, n: UiNode, wide: bool, v: Val) -> void {
let s = vl_text(v)
let s = vl_text_u(ui_st, v)
var fixed = UI_AUTO
var pct = 0.0
var off = 0.0

View file

@ -1,6 +1,6 @@
# attr_place.ludic - position and its insets, z-index, overflow, white-space and text-overflow
function el_place_prop(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
let s = vl_text_u(ui_st, v)
if key == "position" {
n.pos = 0
if s == "relative" { n.pos = 1 }

View file

@ -36,6 +36,12 @@ export state UiState {
pl_cur: UiPool = null
mm: UiMemo = null # memo.ludic: splits, gradients and keys by their text
mm_ints: []string = null
ly_depth: int = 0 # layout_run.ludic: the lists a line lays out with, one a depth
ly_sizes: [][]float = new [][]float
ly_frozen: [][]bool = new [][]bool
cs_rules: []UiRule = new []UiRule # cascade_merge.ludic: a component root's rules, gathered
cs_envs: []UiEnv = new []UiEnv
cs_seen: []UiSheet = new []UiSheet
an_frames: []UiKeyframes = new []UiKeyframes
an_keys: []string = new []string
an_start: []float = new []float

View file

@ -3,9 +3,12 @@
# specificity as one sheet's are. On a tie the user's rule wins (the outermost user's last), as a
# later rule does. Then `order`, which rearranges a flex box's children without touching the tree.
function cs_merged(ui_st: mut UiState, n: UiNode) -> void {
let rules = new []UiRule
let envs = new []UiEnv
let seen = new []UiSheet
let rules = ui_st.cs_rules # kept and emptied: a merge runs once an element, never inside another
let envs = ui_st.cs_envs
let seen = ui_st.cs_seen
List.clear(rules)
List.clear(envs)
List.clear(seen)
if n.sheet != null { cs_gather(ui_st, n, n.sheet, n.env, rules, envs, seen) }
for i in 0 .. len(n.ups) {
if n.ups[i].sheet != null { cs_gather(ui_st, n, n.ups[i].sheet, n.ups[i].env, rules, envs, seen) }

View file

@ -11,7 +11,7 @@ function bd_elem(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string) -> UiNode
n.sheet = k.sheet
n.style = k.style
el_kind(ui_st, n, k.tag)
for i in 0 .. len(k.keys) { el_keep(n, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
for i in 0 .. len(k.keys) { el_keep(ui_st, n, k.keys[i], ui_eval(ui_st, k.vals[i], e)) }
n.hovered = fr_was_hovered(ui_st, key)
n.on_names = k.on_keys
n.on_acts = k.on_acts
@ -52,8 +52,8 @@ function el_text(ui_st: mut UiState, n: UiNode, s: string) -> void {
push(n.children, t)
}
# id, class, hidden, disabled and src say what the element is; the rest are applied after the rules
function el_keep(n: UiNode, key: string, v: Val) -> void {
let s = vl_text(v)
function el_keep(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> void {
let s = vl_text_u(ui_st, v)
if key == "id" { n.id = s } else if key == "class" {
let parts = lss_split(s, 32)
for i in 0 .. len(parts) {

View file

@ -51,7 +51,7 @@ export function ui_nodes(ui_st: mut UiState, screen: string, view: UiView) -> Ui
root.sheet = t.sheet
root.tag = "screen"
for i in 0 .. len(t.keys) {
if t.keys[i] != "name" { el_keep(root, t.keys[i], ui_eval(ui_st, t.vals[i], e)) }
if t.keys[i] != "name" { el_keep(ui_st, root, t.keys[i], ui_eval(ui_st, t.vals[i], e)) }
}
bd_kids(ui_st, t.kids, e, screen, root.children)
cs_tree(ui_st, root)

View file

@ -41,7 +41,9 @@ function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: f
var used = 0.0
var grows = 0.0
var count = 0
let sizes = new []float
let depth = ui_st.ly_depth
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))
if n.children[i].pos < 2 {
@ -52,7 +54,7 @@ function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: f
}
used = used + n.gap * float(Math.max(count - 1, 0))
let free = main_room - used
if free < 0.0 { ly_shrink_line(n, from, sizes, 0.0 - free) }
if free < 0.0 { ly_shrink_line(ui_st, depth, n, from, sizes, 0.0 - free) }
ly_justify(ui_st, n, free, count, grows)
let extra = ui_st.ly_extra
var at = ui_st.ly_lead
@ -65,6 +67,17 @@ function ly_place_line(ui_st: mut UiState, n: UiNode, from: int, to: int, top: f
if c.pos == 1 { ly_nudge(n, c) }
at = at + m + n.gap + extra
}
ui_st.ly_depth = depth
}
# a line's sizes, and its shrink's frozen flags, in lists kept one a depth (a line's children lay out
# 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 {
push(ui_st.ly_sizes, new []float)
push(ui_st.ly_frozen, new []bool)
}
List.clear(ui_st.ly_sizes[depth])
return ui_st.ly_sizes[depth]
}
# one child: `m` along the run and its own size (or the whole room, stretched) across, less margins
function ly_place_child(ui_st: mut UiState, n: UiNode, c: UiNode, at: float, top: float, m: float, cross_room: float) -> void {

View file

@ -23,9 +23,10 @@ function ly_min_h(c: UiNode) -> float {
return c.mh
}
# `sizes` (the children from..to) shrunk by `over` in all
function ly_shrink_line(n: UiNode, from: int, sizes: []float, over0: float) -> void {
function ly_shrink_line(ui_st: mut UiState, depth: int, n: UiNode, from: int, sizes: []float, over0: float) -> void {
var over = over0
let frozen = new []bool
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 pass in 0 .. 8 {
var wsum = 0.0

View file

@ -71,3 +71,14 @@ function vl_binary(op: string, a: Val, b: Val) -> Val {
# a value as text, whatever kind it is: a component's string prop given a number (label="{3}") reads
# "3", not nothing
export function ui_val_text(v: Val) -> string { return vl_text(v) }
# vl_text for what is asked per element per frame: a small whole number's text comes from the table
# made once (mm_int) rather than being written out again
function vl_text_u(ui_st: mut UiState, v: Val) -> string {
let k = Value.kind(v)
if k == 1 { return mm_int(ui_st, Value.as_int(v)) }
if k == 2 or k == 7 {
let f = Value.as_float(v)
if f == float(int(f)) { return mm_int(ui_st, int(f)) }
}
return vl_text(v)
}

View file

@ -24,12 +24,22 @@ property Val {
kids: []Val
}
# a number, a string or a null is one record; only a list or an object carries lists (made when first
# filled) - three allocations for every scalar was most of what a UI frame made and never gave back
function value_new(tag: int) -> Val {
let v = new Val
v.tag = tag
if tag == 5 or tag == 6 { value_lists(v) }
return v
}
function value_lists(v: Val) -> void {
if v.kids != null { return }
v.keys = new []pointer
v.kids = new []Val
return v
}
function value_n(v: Val) -> int {
if v.kids == null { return 0 }
return len(v.kids)
}
# --- constructors -----------------------------------------------------------
@ -43,8 +53,13 @@ function value_float(x: float) -> Val { let v = value_new(7); v.num = float_bits
function value_object() -> Val { return value_new(6) }
# --- builders (return the container, so calls chain) ------------------------
function value_add(list: Val, item: Val) -> Val { push(list.kids, item); return list }
function value_add(list: Val, item: Val) -> Val {
value_lists(list)
push(list.kids, item)
return list
}
function value_put(obj: Val, key: pointer, item: Val) -> Val {
value_lists(obj)
var i = 0
while i < len(obj.keys) { if obj.keys[i] == key { obj.kids[i] = item; return obj }; i += 1 }
push(obj.keys, key); push(obj.kids, item)
@ -53,7 +68,7 @@ function value_put(obj: Val, key: pointer, item: Val) -> Val {
# --- accessors --------------------------------------------------------------
function value_kind(v: Val) -> int { return v.tag }
function value_count(v: Val) -> int { return len(v.kids) }
function value_count(v: Val) -> int { return value_n(v) }
function value_as_int(v: Val) -> int {
if v.tag == 7 { return int(float_from_bits(v.num)) }
return v.num
@ -66,21 +81,21 @@ function value_as_float(v: Val) -> float {
}
function value_as_str(v: Val) -> string { if v.txt == null { return "" }; return v.txt }
function value_at(list: Val, i: int) -> Val {
if i < 0 or i >= len(list.kids) { return value_null() }
if i < 0 or i >= value_n(list) { return value_null() }
return list.kids[i]
}
function value_key_at(obj: Val, i: int) -> string {
if i < 0 or i >= len(obj.keys) { return "" }
if i < 0 or i >= value_n(obj) { return "" }
return obj.keys[i]
}
function value_get(obj: Val, key: pointer) -> Val {
var i = 0
while i < len(obj.keys) { if obj.keys[i] == key { return obj.kids[i] }; i += 1 }
while i < value_n(obj) { if obj.keys[i] == key { return obj.kids[i] }; i += 1 }
return value_null()
}
function value_has(obj: Val, key: pointer) -> int {
var i = 0
while i < len(obj.keys) { if obj.keys[i] == key { return 1 }; i += 1 }
while i < value_n(obj) { if obj.keys[i] == key { return 1 }; i += 1 }
return 0
}
@ -132,7 +147,7 @@ function json_encode(v: Val) -> string {
if v.tag == 5 {
var out = "["
var i = 0
while i < len(v.kids) {
while i < value_n(v) {
if i > 0 { out += "," }
out += json_encode(v.kids[i])
i += 1
@ -141,7 +156,7 @@ function json_encode(v: Val) -> string {
}
var out = "{"
var i = 0
while i < len(v.keys) {
while i < value_n(v) {
if i > 0 { out += "," }
out = out + json_quote(v.keys[i]) + ":" + json_encode(v.kids[i])
i += 1
@ -176,9 +191,11 @@ function json_parse(s: pointer) -> Val {
# a node or hold a literal, is not for this (Ludic has no collector; plan 23 of maroon-lake).
function value_free(v: Val) -> void {
if v == null { return }
for i in 0 .. len(v.kids) { value_free(v.kids[i]) }
free(v.kids)
free(v.keys)
if v.kids != null {
for i in 0 .. len(v.kids) { value_free(v.kids[i]) }
free(v.kids)
free(v.keys)
}
free(v)
}
function json_free(v: Val) -> void { value_free(v) }