diff --git a/packages/ludic.ui/anim.ludic b/packages/ludic.ui/anim.ludic
index 22932f41..1b5ed653 100644
--- a/packages/ludic.ui/anim.ludic
+++ b/packages/ludic.ui/anim.ludic
@@ -55,7 +55,7 @@ function an_apply(ui_st: mut UiState, n: UiNode) -> void {
ui_err(ui_st, `animation {n.anim}: there is no @keyframes of that name`)
return
}
- var t = (an_now(ui_st) - an_began(ui_st, `{n.key}@{n.anim}`)) / n.anim_dur
+ var t = (an_now(ui_st) - an_began(ui_st, mm_join(ui_st, n.key, "@", n.anim))) / n.anim_dur
if n.anim_loop {
let lap = int(t)
t = t - float(lap)
diff --git a/packages/ludic.ui/attr.ludic b/packages/ludic.ui/attr.ludic
index fcd19c22..a5e13b1d 100644
--- a/packages/ludic.ui/attr.ludic
+++ b/packages/ludic.ui/attr.ludic
@@ -16,7 +16,7 @@ function el_apply(ui_st: mut UiState, n: UiNode, key: string, v0: Val) -> bool {
return true
}
let v = cs_vars_in(ui_st, n, v0)
- return el_size(ui_st, n, k, v) or el_box(ui_st, n, k, v) or el_flex(n, k, v) or el_look(ui_st, n, k, v) or el_place_prop(ui_st, n, k, v) or el_edge(ui_st, n, k, v) or el_more(ui_st, n, k, v)
+ return el_size(ui_st, n, k, v) or el_box(ui_st, n, k, v) or el_flex(ui_st, n, k, v) or el_look(ui_st, n, k, v) or el_place_prop(ui_st, n, k, v) or el_edge(ui_st, n, k, v) or el_more(ui_st, n, k, v)
}
function at_alias(key: string) -> string {
# the CSS name to the short one; literals, so asking allocates nothing (asked per rule per element)
@@ -47,12 +47,12 @@ function el_size(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
if key == "w" or key == "h" { el_dim(ui_st, n, key == "w", v) } else if key == "min-w" { n.min_w = el_len(ui_st, v) } else if key == "max-w" { n.max_w = el_len(ui_st, v) } else if key == "min-h" { n.min_h = el_len(ui_st, v) } else if key == "max-h" { n.max_h = el_len(ui_st, v) } else if key == "gap" { n.gap = el_len(ui_st, v) } else { return false }
return true
}
-function el_flex(n: UiNode, key: string, v: Val) -> bool {
+function el_flex(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
if key == "dir" {
n.dir = UI_COL
if s == "row" { n.dir = UI_ROW }
- } else if key == "grow" { n.grow = el_num(s) } else if key == "flex" { el_flex_short(n, s) } else if key == "wrap" { n.wrap = s == "wrap" or (vl_truthy(v) and s != "false" and s != "nowrap") } else if key == "align" { n.align = el_place(v) } else if key == "self" { n.self = el_place(v) } else if key == "justify" { n.justify = el_place(v) } else if key == "talign" { n.talign = el_place(v) } else if key == "display" { n.hidden = s == "none" } else { return false }
+ } else if key == "grow" { n.grow = el_num(s) } else if key == "flex" { el_flex_short(ui_st, n, s) } else if key == "wrap" { n.wrap = s == "wrap" or (vl_truthy(v) and s != "false" and s != "nowrap") } else if key == "align" { n.align = el_place(v) } else if key == "self" { n.self = el_place(v) } else if key == "justify" { n.justify = el_place(v) } else if key == "talign" { n.talign = el_place(v) } else if key == "display" { n.hidden = s == "none" } else { return false }
return true
}
function el_look(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
@@ -76,7 +76,7 @@ function el_look(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
}
# border: 1 #ffcc00, or 1px solid accent
function el_border(ui_st: mut UiState, n: UiNode, s: string) -> void {
- let parts = lss_split(s, 32)
+ let parts = mm_split(ui_st, s, 32)
for i in 0 .. len(parts) {
let p = parts[i]
if len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { el_sides_border(n, el_unit(ui_st, p)) } else if p == "none" { el_sides_border(n, 0.0) } else if len(p) > 0 and p != "solid" {
diff --git a/packages/ludic.ui/attr_box.ludic b/packages/ludic.ui/attr_box.ludic
index ef47a222..edc37eb1 100644
--- a/packages/ludic.ui/attr_box.ludic
+++ b/packages/ludic.ui/attr_box.ludic
@@ -18,20 +18,27 @@ function el_box(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
}
# top right bottom left out of 1 to 4 lengths
function el_sides(ui_st: mut UiState, v: Val) -> void {
- let parts = new []float
- if vl_num(v) { push(parts, el_px(ui_st, Value.as_float(v))) } else {
- let words = el_words(vl_text(v))
+ var n = 0
+ if vl_num(v) {
+ ui_st.el_s0 = el_px(ui_st, Value.as_float(v))
+ n = 1
+ } else {
+ let words = mm_words(ui_st, vl_text(v))
for i in 0 .. len(words) {
- if len(words[i]) > 0 { push(parts, el_word_len(ui_st, words[i])) }
+ if len(words[i]) > 0 and n < 4 {
+ el_side_set(ui_st, n, el_word_len(ui_st, words[i]))
+ n += 1
+ }
}
}
- while len(parts) < 4 {
- if len(parts) == 0 { push(parts, 0.0) } else if len(parts) == 1 { push(parts, parts[0]) } else if len(parts) == 2 { push(parts, parts[0]) } else { push(parts, parts[1]) }
- }
- ui_st.el_s0 = parts[0]
- ui_st.el_s1 = parts[1]
- ui_st.el_s2 = parts[2]
- ui_st.el_s3 = parts[3]
+ if n == 0 { ui_st.el_s0 = 0.0 }
+ if n < 2 { ui_st.el_s1 = ui_st.el_s0 }
+ if n < 3 { ui_st.el_s2 = ui_st.el_s0 }
+ if n < 4 { ui_st.el_s3 = ui_st.el_s1 }
+}
+# CSS's one to four sides, top right bottom left, without a list made for them
+function el_side_set(ui_st: mut UiState, i: int, v: float) -> void {
+ if i == 0 { ui_st.el_s0 = v } else if i == 1 { ui_st.el_s1 = v } else if i == 2 { ui_st.el_s2 = v } else { ui_st.el_s3 = v }
}
# 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 {
diff --git a/packages/ludic.ui/attr_edge.ludic b/packages/ludic.ui/attr_edge.ludic
index 1912fee7..51e4e09b 100644
--- a/packages/ludic.ui/attr_edge.ludic
+++ b/packages/ludic.ui/attr_edge.ludic
@@ -5,10 +5,10 @@ function el_edge(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool {
n.outline_c = ui_color(ui_st, s)
n.outline_a = ui_st.cl_alpha
} else if key == "scrollbar-color" {
- let parts = lss_split(s, 32)
+ let parts = mm_split(ui_st, s, 32)
if len(parts) > 0 { n.bar_c = ui_color(ui_st, parts[0]) }
if len(parts) > 1 { n.bar_track = ui_color(ui_st, parts[1]) }
- } else if key == "background-image" and gr_is(tpl_words(s)) { nx(n).grad = gr_parse(ui_st, s) } else if key == "background-image" { n.bg_img = el_url(s) } else if key == "border-image" { el_border_image(ui_st, n, s) } else if key == "box-shadow" { el_shadow(ui_st, n, s) } else if key == "animation" { el_animation(n, s) } else if key == "transition" { n.tr_dur = el_secs(s) } else { return false }
+ } else if key == "background-image" and gr_is(tpl_words(s)) { nx(n).grad = gr_parse(ui_st, s) } else if key == "background-image" { n.bg_img = el_url(s) } else if key == "border-image" { el_border_image(ui_st, n, s) } else if key == "box-shadow" { el_shadow(ui_st, n, s) } else if key == "animation" { el_animation(ui_st, n, s) } else if key == "transition" { n.tr_dur = el_secs(s) } else { return false }
return true
}
# url(path) or url("path") -> path
@@ -20,11 +20,11 @@ function el_url(s: string) -> string {
return t
}
# border-image: url(chip.png) 14 / 12px - the source's corner size, then how wide they are drawn
-function el_border_image(ui_st: UiState, n: UiNode, s: string) -> void {
+function el_border_image(ui_st: mut UiState, n: UiNode, s: string) -> void {
var close = 0
while close < len(s) and s[close] != 41 { close += 1 }
n.bi_src = el_url(s[0..Math.min(close + 1, len(s))])
- let rest = lss_split(s[Math.min(close + 1, len(s))..len(s)], 47)
+ let rest = mm_split(ui_st, s[Math.min(close + 1, len(s))..len(s)], 47)
n.bi_slice = el_num(tpl_words(rest[0]))
n.bi_width = el_px(ui_st, n.bi_slice)
if len(rest) > 1 { n.bi_width = el_unit(ui_st, tpl_words(rest[1])) }
@@ -35,7 +35,7 @@ function el_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
n.sh_c = -2
return
}
- let parts = lss_split(s, 32)
+ let parts = mm_split(ui_st, s, 32)
var nums = 0
var col = ""
for i in 0 .. len(parts) {
@@ -53,7 +53,7 @@ function el_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
}
# outline: 2px solid #ffcc00, or none
function el_outline(ui_st: mut UiState, n: UiNode, s: string) -> void {
- let parts = lss_split(s, 32)
+ let parts = mm_split(ui_st, s, 32)
n.outline_w = 0.0
for i in 0 .. len(parts) {
let p = parts[i]
@@ -64,8 +64,8 @@ function el_outline(ui_st: mut UiState, n: UiNode, s: string) -> void {
}
}
# animation: fade 0.25s infinite alternate
-function el_animation(n: UiNode, s: string) -> void {
- let parts = lss_split(s, 32)
+function el_animation(ui_st: mut UiState, n: UiNode, s: string) -> void {
+ let parts = mm_split(ui_st, s, 32)
n.anim = ""
for i in 0 .. len(parts) {
let p = parts[i]
diff --git a/packages/ludic.ui/attr_fit.ludic b/packages/ludic.ui/attr_fit.ludic
index fa083941..b54c1834 100644
--- a/packages/ludic.ui/attr_fit.ludic
+++ b/packages/ludic.ui/attr_fit.ludic
@@ -2,7 +2,7 @@
# for pictures and natives, zoom (zoom.ludic), and a list split at its top-level commas
function el_fit_props(ui_st: mut UiState, n: UiNode, key: string, s: string) -> bool {
if key == "aspect-ratio" {
- let parts = lss_split(s, 47)
+ let parts = mm_split(ui_st, s, 47)
var r = el_num(tpl_words(parts[0]))
if len(parts) > 1 and el_num(tpl_words(parts[1])) > 0.0 { r = r / el_num(tpl_words(parts[1])) }
if tpl_words(s) == "auto" { r = 0.0 }
diff --git a/packages/ludic.ui/attr_more.ludic b/packages/ludic.ui/attr_more.ludic
index 426b78b0..df55e8bb 100644
--- a/packages/ludic.ui/attr_more.ludic
+++ b/packages/ludic.ui/attr_more.ludic
@@ -40,8 +40,8 @@ function el_calc_len(ui_st: mut UiState, s: string) -> float {
return ui_st.ca_px
}
# flex: 1 | flex: 1 0 | flex: 2 1 auto | flex: none - the grow, then the shrink
-function el_flex_short(n: UiNode, s: string) -> void {
- let parts = el_words(s)
+function el_flex_short(ui_st: mut UiState, n: UiNode, s: string) -> void {
+ let parts = mm_words(ui_st, s)
if s == "none" {
n.grow = 0.0
nx(n).shrink = 0.0
@@ -72,7 +72,7 @@ function el_text_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void {
x.tsh_set = true
x.tsh_c = -2
if tpl_words(s) == "none" { return }
- let parts = el_words(s)
+ let parts = mm_words(ui_st, s)
var nums = 0
var col = ""
for i in 0 .. len(parts) {
diff --git a/packages/ludic.ui/backend.ludic b/packages/ludic.ui/backend.ludic
index dbbba037..74fe587d 100644
--- a/packages/ludic.ui/backend.ludic
+++ b/packages/ludic.ui/backend.ludic
@@ -34,6 +34,8 @@ function ui_default_backend() -> UiBackend {
export state UiState {
pl_pools: []UiPool = new []UiPool # pool.ludic: each screen's nodes and envs, used again
pl_cur: UiPool = null
+ mm: UiMemo = null # memo.ludic: splits, gradients and keys by their text
+ mm_ints: []string = null
an_frames: []UiKeyframes = new []UiKeyframes
an_keys: []string = new []string
an_start: []float = new []float
diff --git a/packages/ludic.ui/build.ludic b/packages/ludic.ui/build.ludic
index 0e76d38b..7ff8d45c 100644
--- a/packages/ludic.ui/build.ludic
+++ b/packages/ludic.ui/build.ludic
@@ -8,7 +8,7 @@ function bd_kids(ui_st: mut UiState, kids: []UiTpl, e0: UiEnv, key: string, out:
for i in 0 .. len(kids) {
let k = kids[i]
ui_st.ui_at = k.where
- let ck = `{key}/{i}`
+ let ck = mm_join(ui_st, key, "/", mm_int(ui_st, i))
if k.tag == "state" or k.tag == "style" { continue }
if k.tag == "let" {
e = bd_let(ui_st, k, e)
@@ -50,9 +50,9 @@ function bd_each(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []UiN
let le = env_new(ui_st, e)
env_bind(le, as_name, Value.at(list, j))
env_bind(le, at_name, Value.int(j))
- var id = string(j)
+ var id = mm_int(ui_st, j)
if by != null { id = ui_eval_text(ui_st, by, le) }
- bd_kids(ui_st, k.kids, le, `{key}#{id}`, out)
+ bd_kids(ui_st, k.kids, le, mm_join(ui_st, key, "#", id), out)
}
}
# is what the component's user put inside it; their
diff --git a/packages/ludic.ui/build_class.ludic b/packages/ludic.ui/build_class.ludic
index d2b2ed76..641720a5 100644
--- a/packages/ludic.ui/build_class.ludic
+++ b/packages/ludic.ui/build_class.ludic
@@ -32,7 +32,7 @@ function bd_class(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []Ui
function bd_pass_down(ui_st: mut UiState, n: UiNode, k: UiTpl, e: UiEnv) -> void {
let c = tpl_attr(k, "class")
if c != null {
- let parts = lss_split(ui_eval_text(ui_st, c, e), 32)
+ 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]) }
}
diff --git a/packages/ludic.ui/color.ludic b/packages/ludic.ui/color.ludic
index 7d98456b..d3a462a2 100644
--- a/packages/ludic.ui/color.ludic
+++ b/packages/ludic.ui/color.ludic
@@ -36,7 +36,7 @@ function cl_hex_at(ui_st: mut UiState, s: string, at: int, n: int) -> int {
function cl_rgb(ui_st: mut UiState, s: string) -> int {
var open = 0
while open < len(s) and s[open] != 40 { open += 1 }
- let parts = lss_split(s[open + 1..len(s) - 1], 44)
+ let parts = mm_split(ui_st, s[open + 1..len(s) - 1], 44)
var c = 0
for i in 0 .. Math.min(3, len(parts)) { c = c * 256 + Math.clamp(int(el_num(tpl_words(parts[i]))), 0, 255) }
if len(parts) > 3 { ui_st.cl_alpha = el_num(tpl_words(parts[3])) }
diff --git a/packages/ludic.ui/controls_build.ludic b/packages/ludic.ui/controls_build.ludic
index 631a61d0..a690cedd 100644
--- a/packages/ludic.ui/controls_build.ludic
+++ b/packages/ludic.ui/controls_build.ludic
@@ -6,7 +6,7 @@
# with a note="...", the label is .ui-labels > .ui-label + .ui-note
function ct_part(ui_st: mut UiState, n: UiNode, cls: string, text: string) -> UiNode {
let p = nd_take(ui_st)
- p.key = `{n.key}/{cls}`
+ p.key = mm_join(ui_st, n.key, "/", cls)
p.tag = "span"
p.env = n.env
p.sheet = n.sheet
diff --git a/packages/ludic.ui/draw_gradient.ludic b/packages/ludic.ui/draw_gradient.ludic
index 907e613d..2bee4fff 100644
--- a/packages/ludic.ui/draw_gradient.ludic
+++ b/packages/ludic.ui/draw_gradient.ludic
@@ -8,7 +8,17 @@ export property UiGradient {
at: []float = null # each stop's place, 0 to 1
}
function gr_is(s: string) -> bool { return len(s) > 16 and sat(s, 0, "linear-gradient(") }
+# a gradient is parsed once per text and shared by every element that wears it - never changed after
function gr_parse(ui_st: mut UiState, s0: string) -> UiGradient {
+ let m = mm_of(ui_st)
+ let i = mm_find(m, MM_GRAD, s0)
+ if i >= 0 { return m.grads[i] }
+ let g = gr_parse_new(ui_st, s0)
+ let j = mm_add(m, MM_GRAD, s0)
+ if j >= 0 { m.grads[j] = g }
+ return g
+}
+function gr_parse_new(ui_st: mut UiState, s0: string) -> UiGradient {
let s = tpl_words(s0)
let parts = el_commas(s[16..len(s) - 1])
let g = new UiGradient
@@ -18,7 +28,7 @@ function gr_parse(ui_st: mut UiState, s0: string) -> UiGradient {
var from = 0
if len(parts) > 0 and gr_dir(g, tpl_words(parts[0])) { from = 1 }
for i in from .. len(parts) {
- let words = el_words(tpl_words(parts[i]))
+ let words = mm_words(ui_st, tpl_words(parts[i]))
var col = tpl_words(parts[i])
var at = -1.0
let last = words[len(words) - 1]
diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic
index 342e6f03..788175b1 100644
--- a/packages/ludic.ui/index.ludic
+++ b/packages/ludic.ui/index.ludic
@@ -10,6 +10,8 @@ import "backend.ludic"
import "pool.ludic"
import "pool_reset.ludic"
import "pool_reset_more.ludic"
+import "memo.ludic"
+import "memo_join.ludic"
import "expr.ludic"
import "expr_parse.ludic"
import "expr_prim.ludic"
diff --git a/packages/ludic.ui/layout_text.ludic b/packages/ludic.ui/layout_text.ludic
index f69f2552..012a11ac 100644
--- a/packages/ludic.ui/layout_text.ludic
+++ b/packages/ludic.ui/layout_text.ludic
@@ -1,6 +1,6 @@
# layout_text.ludic - a text's size: one line, or as many as it takes to fit its room (white-space
# normal), breaking between words; nowrap keeps one line, and text-overflow: ellipsis cuts it
-function ly_measure_text(ui_st: UiState, n: UiNode, avail: float) -> void {
+function ly_measure_text(ui_st: mut UiState, n: UiNode, avail: float) -> void {
n.lines = null
let lh = ly_line_h(n)
var one = ly_text_w(ui_st, n.text, n.rsize)
@@ -19,8 +19,8 @@ function ly_measure_text(ui_st: 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: UiState, s: string, size: float, room: float) -> []string {
- let words = lss_split(s, 32)
+function ly_wrap(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 = ""
for i in 0 .. len(words) {
diff --git a/packages/ludic.ui/memo.ludic b/packages/ludic.ui/memo.ludic
new file mode 100644
index 00000000..0c00b1da
--- /dev/null
+++ b/packages/ludic.ui/memo.ludic
@@ -0,0 +1,68 @@
+# memo.ludic - what a frame would work out again from the same text is kept by that text: a value
+# split into its words, a gradient, a key joined from its parent's. Ludic frees nothing, so parsing
+# "2px 6px" per element per frame was a leak; the table is bounded, and past it a result is made
+# (and lost) as before rather than the table growing without end.
+const MM_CAP: int = 8192 # slots; at most three quarters are used
+export property UiMemo {
+ slots: words = null # index + 1 into keys, 0 empty
+ keys: []string = null # the text a result was made from (or the joined text itself)
+ tags: []int = null # what kind of result: a separator, or a kind below
+ lists: [][]string = null
+ grads: []UiGradient = null
+}
+const MM_WORDS: int = -1 # el_words: spaces, a bracket's own kept
+const MM_GRAD: int = -2
+const MM_JOIN: int = -3 # keys[i] is the joined text itself
+function mm_new() -> UiMemo {
+ let m = new UiMemo
+ m.slots = words(MM_CAP)
+ m.keys = new []string
+ m.tags = new []int
+ m.lists = new [][]string
+ m.grads = new []UiGradient
+ return m
+}
+function mm_of(ui_st: mut UiState) -> UiMemo {
+ if ui_st.mm == null { ui_st.mm = mm_new() }
+ return ui_st.mm
+}
+# FNV-1a over the text, the tag mixed in first
+function mm_hash(tag: int, s: string) -> int {
+ var h = 216613626 ^ (tag & 255)
+ for i in 0 .. len(s) { h = ((h ^ s[i]) * 16777619) & 2147483647 }
+ return h
+}
+# the entry for (tag, s), or -1
+function mm_find(m: UiMemo, tag: int, s: string) -> 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.keys[i] == s { return i }
+ at = (at + 1) & (MM_CAP - 1)
+ }
+ return -1
+}
+# a new entry for (tag, s) with its results empty, or -1 when the table is full
+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)
+ while m.slots[at] != 0 { at = (at + 1) & (MM_CAP - 1) }
+ push(m.keys, s)
+ push(m.tags, tag)
+ push(m.lists, null)
+ push(m.grads, null)
+ m.slots[at] = len(m.keys)
+ return len(m.keys) - 1
+}
+# lss_split's parts of `s` at `c`, made once per text; the list is shared - read it, never change it
+function mm_split(ui_st: mut UiState, s: string, c: int) -> []string {
+ let m = mm_of(ui_st)
+ let i = mm_find(m, c, s)
+ if i >= 0 { return m.lists[i] }
+ var parts: []string = null
+ if c == MM_WORDS { parts = el_words(s) } else { parts = lss_split(s, c) }
+ let j = mm_add(m, c, s)
+ if j >= 0 { m.lists[j] = parts }
+ return parts
+}
+function mm_words(ui_st: mut UiState, s: string) -> []string { return mm_split(ui_st, s, MM_WORDS) }
diff --git a/packages/ludic.ui/memo_join.ludic b/packages/ludic.ui/memo_join.ludic
new file mode 100644
index 00000000..c429cff4
--- /dev/null
+++ b/packages/ludic.ui/memo_join.ludic
@@ -0,0 +1,42 @@
+# memo_join.ludic - a key joined from its parent's key, a separator and a part ("App/3",
+# "App/3#7", "App/3/ui-thumb"), found by comparing in place, so an element's key is made once
+# the element first appears and never again
+function mm_hash_join(a: string, sep: string, b: string) -> int {
+ var h = 216613626 ^ (MM_JOIN & 255)
+ for i in 0 .. len(a) { h = ((h ^ a[i]) * 16777619) & 2147483647 }
+ h = ((h ^ sep[0]) * 16777619) & 2147483647
+ for i in 0 .. len(b) { h = ((h ^ b[i]) * 16777619) & 2147483647 }
+ return h
+}
+function mm_is_join(k: string, a: string, sep: string, b: string) -> bool {
+ let na = len(a)
+ if len(k) != na + 1 + len(b) or k[na] != sep[0] { return false }
+ return sat(k, 0, a) and sat(k, na + 1, b)
+}
+# a + sep + b, the same string every frame
+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)
+ while m.slots[at] != 0 {
+ let i = m.slots[at] - 1
+ if m.tags[i] == MM_JOIN and mm_is_join(m.keys[i], a, sep, b) { return m.keys[i] }
+ at = (at + 1) & (MM_CAP - 1)
+ }
+ let k = a + sep + b
+ if len(m.keys) * 4 >= MM_CAP * 3 { return k }
+ push(m.keys, k)
+ push(m.tags, MM_JOIN)
+ push(m.lists, null)
+ push(m.grads, null)
+ m.slots[at] = len(m.keys)
+ return k
+}
+# a small whole number's text, made once (a key's index, a loop's count)
+function mm_int(ui_st: mut UiState, i: int) -> string {
+ if i < 0 or i >= 1024 { return string(i) }
+ if ui_st.mm_ints == null {
+ ui_st.mm_ints = new []string
+ for k in 0 .. 1024 { push(ui_st.mm_ints, string(k)) }
+ }
+ return ui_st.mm_ints[i]
+}
diff --git a/packages/ludic.ui/text_fit.ludic b/packages/ludic.ui/text_fit.ludic
index 4deac948..10408770 100644
--- a/packages/ludic.ui/text_fit.ludic
+++ b/packages/ludic.ui/text_fit.ludic
@@ -1,8 +1,8 @@
# text_fit.ludic - text-fit: shrink 12px draws a line that does not fit its box smaller, down to that
# size, and cuts what still does not fit with an ellipsis; line-height (a multiple of the font size,
# px or em) spaces a text's lines. Both are inherited, as font properties are.
-function el_text_fit(ui_st: UiState, n: UiNode, s: string) -> void {
- let parts = lss_split(tpl_words(s), 32)
+function el_text_fit(ui_st: mut UiState, n: UiNode, s: string) -> void {
+ let parts = mm_split(ui_st, tpl_words(s), 32)
nx(n).fit_min = 0.0
if len(parts) >= 2 and parts[0] == "shrink" { nx(n).fit_min = Math.max(el_unit(ui_st, parts[1]), 1.0) }
}
diff --git a/packages/ludic.ui/units.ludic b/packages/ludic.ui/units.ludic
index 8b564098..1d9fbb7f 100644
--- a/packages/ludic.ui/units.ludic
+++ b/packages/ludic.ui/units.ludic
@@ -73,7 +73,7 @@ function cs_vars_in(ui_st: mut UiState, n: UiNode, v: Val) -> Val {
return Value.str(tpl_words(out + s[at..len(s)]))
}
function cs_var_ref(ui_st: mut UiState, n: UiNode, inner: string) -> string {
- let parts = lss_split(inner, 44)
+ let parts = mm_split(ui_st, inner, 44)
let v = cs_var(n, tpl_words(parts[0]))
if v != null { return v }
if len(parts) > 1 { return tpl_words(inner[len(parts[0]) + 1..len(inner)]) }