From 08ac2f1125e9f0521b8347a56040223f3b2c0942 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Mon, 28 Sep 2026 00:10:56 +0300 Subject: [PATCH] ludic.ui: var() values found by their pieces, tpl_words/url/border-image by their text, nine-slices, picking, lifecycle, focus, hover, the event queue and input without a list or record made a frame Co-Authored-By: Claude Opus 5.5 --- packages/ludic.ui/attr.ludic | 2 +- packages/ludic.ui/attr_edge.ludic | 10 +-- packages/ludic.ui/attr_more.ludic | 4 +- packages/ludic.ui/backend.ludic | 15 +++++ packages/ludic.ui/color.ludic | 6 +- packages/ludic.ui/draw_gradient.ludic | 10 +-- packages/ludic.ui/draw_nine.ludic | 6 +- packages/ludic.ui/focus.ludic | 2 +- packages/ludic.ui/index.ludic | 1 + packages/ludic.ui/input.ludic | 32 ++++++++- packages/ludic.ui/instance.ludic | 9 ++- packages/ludic.ui/life.ludic | 28 ++++---- packages/ludic.ui/memo.ludic | 34 ++++++++++ packages/ludic.ui/memo_join.ludic | 3 + packages/ludic.ui/native.ludic | 14 ++-- packages/ludic.ui/pointer.ludic | 4 +- packages/ludic.ui/pointer_pick.ludic | 44 ++++++------- packages/ludic.ui/render3d_nine.ludic | 14 ++-- packages/ludic.ui/text_fit.ludic | 6 +- packages/ludic.ui/units.ludic | 42 ++++++------ packages/ludic.ui/units_pieces.ludic | 93 +++++++++++++++++++++++++++ 21 files changed, 281 insertions(+), 98 deletions(-) create mode 100644 packages/ludic.ui/units_pieces.ludic diff --git a/packages/ludic.ui/attr.ludic b/packages/ludic.ui/attr.ludic index 7abacd78..162fb264 100644 --- a/packages/ludic.ui/attr.ludic +++ b/packages/ludic.ui/attr.ludic @@ -61,7 +61,7 @@ function el_look(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool { n.fg = ui_color(ui_st, s) n.fg_a = ui_st.cl_alpha } else if key == "bg" { - if gr_is(tpl_words(s)) { nx(n).grad = gr_parse(ui_st, s) } else if len(s) > 4 and sat(s, 0, "url(") { n.bg_img = el_url(s) } else if tpl_words(s) == "transparent" { + if gr_is(tw(ui_st, s)) { nx(n).grad = gr_parse(ui_st, s) } else if len(s) > 4 and sat(s, 0, "url(") { n.bg_img = el_url_u(ui_st, s) } else if tw(ui_st, s) == "transparent" { n.bg = 0 n.bg_a = 0.0 } else { diff --git a/packages/ludic.ui/attr_edge.ludic b/packages/ludic.ui/attr_edge.ludic index 62df9758..416b80ac 100644 --- a/packages/ludic.ui/attr_edge.ludic +++ b/packages/ludic.ui/attr_edge.ludic @@ -8,7 +8,7 @@ function el_edge(ui_st: mut UiState, n: UiNode, key: string, v: Val) -> bool { 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(ui_st, n, s) } else if key == "transition" { n.tr_dur = el_secs(s) } else { return false } + } else if key == "background-image" and gr_is(tw(ui_st, s)) { nx(n).grad = gr_parse(ui_st, s) } else if key == "background-image" { n.bg_img = el_url_u(ui_st, 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 @@ -23,15 +23,15 @@ function el_url(s: string) -> string { 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))]) + n.bi_src = el_url_u(ui_st, s[0..Math.min(close + 1, len(s))]) 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_slice = el_num(tw(ui_st, 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])) } + if len(rest) > 1 { n.bi_width = el_unit(ui_st, tw(ui_st, rest[1])) } } # box-shadow: 6px 10px 0 rgba(0, 0, 0, 0.45) - x, y, a blur this renderer draws sharp, the colour function el_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void { - if tpl_words(s) == "none" { + if tw(ui_st, s) == "none" { n.sh_c = -2 return } diff --git a/packages/ludic.ui/attr_more.ludic b/packages/ludic.ui/attr_more.ludic index bf00a328..efe5ade8 100644 --- a/packages/ludic.ui/attr_more.ludic +++ b/packages/ludic.ui/attr_more.ludic @@ -2,7 +2,7 @@ # 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_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) } + 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 = tw(ui_st, 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) @@ -71,7 +71,7 @@ function el_text_shadow(ui_st: mut UiState, n: UiNode, s: string) -> void { let x = nx(n) x.tsh_set = true x.tsh_c = -2 - if tpl_words(s) == "none" { return } + if tw(ui_st, s) == "none" { return } let parts = mm_words(ui_st, s) var nums = 0 var col = "" diff --git a/packages/ludic.ui/backend.ludic b/packages/ludic.ui/backend.ludic index 309256c3..879abe77 100644 --- a/packages/ludic.ui/backend.ludic +++ b/packages/ludic.ui/backend.ludic @@ -39,6 +39,12 @@ export state UiState { mm_ivals: []Val = null ev_depth: int = 0 # eval.ludic: a call's arguments, in lists kept one a depth ev_args: []Val = new []Val + cv_texts: []string = new []string # units_pieces.ludic: a value's var() pieces, kept + r3n_cut: []float = new []float # render3d_nine.ludic: a nine-slice's edges, kept + r3n_xs: []float = new []float + r3n_ys: []float = new []float + r3n_us: []float = new []float + r3n_vs: []float = new []float 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 @@ -90,6 +96,8 @@ export state UiState { in_now: UiInput = new UiInput in_was_down: bool = false in_last: UiInput = new UiInput + in_rec_a: UiInput = new UiInput + in_rec_b: UiInput = new UiInput in_given: bool = false rp_next: []float = [-1.0, -1.0, -1.0, -1.0] rp_a: bool = false @@ -109,14 +117,21 @@ export state UiState { lc_mounts: []UiAct = new []UiAct lc_unmounts: []UiAct = new []UiAct lc_envs: []UiEnv = new []UiEnv + lc_sp_keys: []string = new []string # life.ludic: the spare set lc_run fills and swaps in + lc_sp_unmounts: []UiAct = new []UiAct + lc_sp_envs: []UiEnv = new []UiEnv lc_prev_keys: []string = new []string lc_prev_unmounts: []UiAct = new []UiAct lc_prev_envs: []UiEnv = new []UiEnv nt_list: []UiNative = new []UiNative + nt_sp_nodes: []UiNode = new []UiNode # native.ludic: the queue's spare, swapped while it runs + nt_sp_names: []string = new []string + nt_sp_vals: []Val = new []Val nt_q_nodes: []UiNode = new []UiNode nt_q_names: []string = new []string nt_q_vals: []Val = new []Val fr_hover: []string = new []string + fr_hover_sp: []string = new []string fr_press_key: string = "" fr_down_now: bool = false fr_key_press: string = "" # the control Enter or the pad's A is holding down diff --git a/packages/ludic.ui/color.ludic b/packages/ludic.ui/color.ludic index d3a462a2..a7b0a49e 100644 --- a/packages/ludic.ui/color.ludic +++ b/packages/ludic.ui/color.ludic @@ -2,7 +2,7 @@ # basic names, transparent and none. A colour is 0xRRGGBB; its alpha is left in cl_alpha. const CL_NAMES: string = " black=000000 white=ffffff red=ff0000 green=008000 lime=00ff00 blue=0000ff yellow=ffff00 cyan=00ffff aqua=00ffff magenta=ff00ff fuchsia=ff00ff gray=808080 grey=808080 silver=c0c0c0 maroon=800000 olive=808000 navy=000080 purple=800080 teal=008080 orange=ffa500 " function ui_color(ui_st: mut UiState, name0: string) -> int { - let name = tpl_words(name0) + let name = tw(ui_st, name0) ui_st.cl_alpha = 1.0 if name == "none" or name == "" or name == "transparent" { return UI_NONE } if name[0] == 35 { return cl_hex_at(ui_st, name, 1, len(name) - 1) } @@ -38,8 +38,8 @@ function cl_rgb(ui_st: mut UiState, s: string) -> int { while open < len(s) and s[open] != 40 { open += 1 } 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])) } + for i in 0 .. Math.min(3, len(parts)) { c = c * 256 + Math.clamp(int(el_num(tw(ui_st, parts[i]))), 0, 255) } + if len(parts) > 3 { ui_st.cl_alpha = el_num(tw(ui_st, parts[3])) } return c } # a colour as #rrggbb, for a dump or a test diff --git a/packages/ludic.ui/draw_gradient.ludic b/packages/ludic.ui/draw_gradient.ludic index 2bee4fff..c56f8bec 100644 --- a/packages/ludic.ui/draw_gradient.ludic +++ b/packages/ludic.ui/draw_gradient.ludic @@ -19,22 +19,22 @@ function gr_parse(ui_st: mut UiState, s0: string) -> UiGradient { return g } function gr_parse_new(ui_st: mut UiState, s0: string) -> UiGradient { - let s = tpl_words(s0) + let s = tw(ui_st, s0) let parts = el_commas(s[16..len(s) - 1]) let g = new UiGradient g.cols = new []int g.alphas = new []float g.at = new []float var from = 0 - if len(parts) > 0 and gr_dir(g, tpl_words(parts[0])) { from = 1 } + if len(parts) > 0 and gr_dir(g, tw(ui_st, parts[0])) { from = 1 } for i in from .. len(parts) { - let words = mm_words(ui_st, tpl_words(parts[i])) - var col = tpl_words(parts[i]) + let words = mm_words(ui_st, tw(ui_st, parts[i])) + var col = tw(ui_st, parts[i]) var at = -1.0 let last = words[len(words) - 1] if len(words) > 1 and last[len(last) - 1] == 37 { at = el_num(last) / 100.0 - col = tpl_words(col[0..len(col) - len(last)]) + col = tw(ui_st, col[0..len(col) - len(last)]) } push(g.cols, ui_color(ui_st, col)) push(g.alphas, ui_st.cl_alpha) diff --git a/packages/ludic.ui/draw_nine.ludic b/packages/ludic.ui/draw_nine.ludic index 88b6425b..213702f6 100644 --- a/packages/ludic.ui/draw_nine.ludic +++ b/packages/ludic.ui/draw_nine.ludic @@ -2,10 +2,12 @@ # more than half the box across or half of it down (each direction on its own, so a small key cap # keeps its round ends and has no seam), on whole pixels so the nine pieces meet exactly # x0 x1 x2 x3 then y0 y1 y2 y3: the columns' and the rows' edges -export function ui_nine_cuts(x: float, y: float, w: float, h: float, dst: float) -> []float { +export function ui_nine_cuts(x: float, y: float, w: float, h: float, dst: float) -> []float { return ui_nine_cuts_into(new []float, x, y, w, h, dst) } +# the same into a list the caller keeps, emptied first +function ui_nine_cuts_into(out: []float, x: float, y: float, w: float, h: float, dst: float) -> []float { let dx = Math.max(Math.min(dst, w / 2.0), 0.0) let dy = Math.max(Math.min(dst, h / 2.0), 0.0) - let out = new []float + List.clear(out) push(out, nn_px(x)) push(out, nn_px(x + dx)) push(out, nn_px(x + w - dx)) diff --git a/packages/ludic.ui/focus.ludic b/packages/ludic.ui/focus.ludic index c73e6c25..e81b8eeb 100644 --- a/packages/ludic.ui/focus.ludic +++ b/packages/ludic.ui/focus.ludic @@ -23,7 +23,7 @@ function fc_move(ui_st: mut UiState, step: int) -> void { } # the keys, after the tree is laid out: moving, then acting on what has the focus function fc_keys(ui_st: mut UiState, root: UiNode) -> void { - ui_st.fc_list = new []UiNode + List.clear(ui_st.fc_list) let top = pp_top(root) if top != null { fc_collect(ui_st, top) } else { fc_collect(ui_st, root) } if ct_key_capture(ui_st, ui_st.fc_list) { return } diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic index 788175b1..07401b9c 100644 --- a/packages/ludic.ui/index.ludic +++ b/packages/ludic.ui/index.ludic @@ -12,6 +12,7 @@ import "pool_reset.ludic" import "pool_reset_more.ludic" import "memo.ludic" import "memo_join.ludic" +import "units_pieces.ludic" import "expr.ludic" import "expr_parse.ludic" import "expr_prim.ludic" diff --git a/packages/ludic.ui/input.ludic b/packages/ludic.ui/input.ludic index 5ab70f58..99cde3c0 100644 --- a/packages/ludic.ui/input.ludic +++ b/packages/ludic.ui/input.ludic @@ -34,8 +34,10 @@ function in_read(ui_st: mut UiState) -> void { return } ui_st.in_was_down = ui_st.in_now.down + var i = ui_st.in_rec_a # the package's own two records, in turn (never a host's) + if ui_st.in_now == ui_st.in_rec_a { i = ui_st.in_rec_b } ui_st.in_last = ui_st.in_now - let i = new UiInput + in_reset(i) if ui_st.ui_be != null and ui_st.ui_be.input { let ps = in_pointer_scale(ui_st) i.x = float(Input.mouse_x()) * ps @@ -81,3 +83,31 @@ export function ui_input(ui_st: mut UiState, i: UiInput) -> void { ui_st.in_now = i ui_st.in_given = true } +# every field back to its default (generated from UiInput) +function in_reset(i: UiInput) -> void { + i.x = -1.0 + i.y = -1.0 + i.down = false + i.wheel = 0.0 + i.tab = false + i.shift = false + i.up = false + i.down_key = false + i.left = false + i.right = false + i.enter = false + i.space = false + i.escape = false + i.backspace = false + i.typed = "" + i.key = 0 + i.held_up = false + i.held_down = false + i.held_left = false + i.held_right = false + i.pad_a = false + i.pad_b = false + i.held_enter = false + i.down_right = false + i.down_middle = false +} diff --git a/packages/ludic.ui/instance.ludic b/packages/ludic.ui/instance.ludic index 30bec83e..7ff46e8c 100644 --- a/packages/ludic.ui/instance.ludic +++ b/packages/ludic.ui/instance.ludic @@ -25,10 +25,13 @@ function in_get(ui_st: mut UiState, key: string, k: UiClass) -> UiInst { } # after a build of `screen`: its instances the build did not reach are unmounted function in_sweep(ui_st: mut UiState, screen: string) -> void { - let keep = new []UiInst + var kept = 0 for i in 0 .. len(ui_st.in_list) { let it = ui_st.in_list[i] - if it.seen == ui_st.in_build or not lc_mine(it.key, screen) { push(keep, it) } + if it.seen == ui_st.in_build or not lc_mine(it.key, screen) { + ui_st.in_list[kept] = it + kept += 1 + } } - ui_st.in_list = keep + while len(ui_st.in_list) > kept { List.pop(ui_st.in_list) } } diff --git a/packages/ludic.ui/life.ludic b/packages/ludic.ui/life.ludic index 611b920a..f1ac2651 100644 --- a/packages/ludic.ui/life.ludic +++ b/packages/ludic.ui/life.ludic @@ -2,10 +2,10 @@ # in last frame is mounted, and one that is gone is unmounted, each once, after the frame is drawn # a build starts its own records: a screen built twice keeps only the last build's function lc_begin(ui_st: mut UiState) -> void { - ui_st.lc_keys = new []string - ui_st.lc_mounts = new []UiAct - ui_st.lc_unmounts = new []UiAct - ui_st.lc_envs = new []UiEnv + List.clear(ui_st.lc_keys) + List.clear(ui_st.lc_mounts) + List.clear(ui_st.lc_unmounts) + List.clear(ui_st.lc_envs) } function lc_note(ui_st: mut UiState, key: string, names: []string, acts: []UiAct, e: UiEnv) -> void { var m: UiAct = null @@ -29,9 +29,12 @@ function lc_run(ui_st: mut UiState, screen: string) -> void { for i in 0 .. len(ui_st.lc_keys) { if ui_st.lc_mounts[i] != null and not sel_has(ui_st.lc_prev_keys, ui_st.lc_keys[i]) { act_run(ui_st, ui_st.lc_mounts[i], ui_st.lc_envs[i]) } } - let keys = new []string - let unmounts = new []UiAct - let envs = new []UiEnv + let keys = ui_st.lc_sp_keys # the spare set, emptied: the records carried over + let unmounts = ui_st.lc_sp_unmounts + let envs = ui_st.lc_sp_envs + List.clear(keys) + List.clear(unmounts) + List.clear(envs) for i in 0 .. len(ui_st.lc_prev_keys) { let k = ui_st.lc_prev_keys[i] if lc_mine(k, screen) { @@ -47,11 +50,14 @@ function lc_run(ui_st: mut UiState, screen: string) -> void { push(unmounts, ui_st.lc_unmounts[i]) push(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 + ui_st.lc_sp_envs = ui_st.lc_prev_envs ui_st.lc_prev_keys = keys ui_st.lc_prev_unmounts = unmounts ui_st.lc_prev_envs = envs - ui_st.lc_keys = new []string - ui_st.lc_mounts = new []UiAct - ui_st.lc_unmounts = new []UiAct - ui_st.lc_envs = new []UiEnv + List.clear(ui_st.lc_keys) + List.clear(ui_st.lc_mounts) + List.clear(ui_st.lc_unmounts) + List.clear(ui_st.lc_envs) } diff --git a/packages/ludic.ui/memo.ludic b/packages/ludic.ui/memo.ludic index 0c00b1da..27f8a9d1 100644 --- a/packages/ludic.ui/memo.ludic +++ b/packages/ludic.ui/memo.ludic @@ -9,10 +9,17 @@ export property UiMemo { tags: []int = null # what kind of result: a separator, or a kind below lists: [][]string = null grads: []UiGradient = null + texts: []string = null # a text worked out from the key: tw, el_url + vals: []Val = null # a Value made from it: cs_vars_in's + vtpls: []UiVarTpl = 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 +const MM_TW: int = -4 # tpl_words +const MM_URL: int = -5 # el_url +const MM_VARS: int = -6 # cs_vars_in: keys[i] is the text with every var() put in +const MM_VTPL: int = -7 # a value's var()s parsed (units_pieces.ludic) function mm_new() -> UiMemo { let m = new UiMemo m.slots = words(MM_CAP) @@ -20,6 +27,9 @@ function mm_new() -> UiMemo { m.tags = new []int m.lists = new [][]string m.grads = new []UiGradient + m.texts = new []string + m.vals = new []Val + m.vtpls = new []UiVarTpl return m } function mm_of(ui_st: mut UiState) -> UiMemo { @@ -51,6 +61,9 @@ function mm_add(m: UiMemo, tag: int, s: string) -> int { push(m.tags, tag) push(m.lists, null) push(m.grads, null) + push(m.texts, null) + push(m.vals, null) + push(m.vtpls, null) m.slots[at] = len(m.keys) return len(m.keys) - 1 } @@ -66,3 +79,24 @@ function mm_split(ui_st: mut UiState, s: string, c: int) -> []string { return parts } function mm_words(ui_st: mut UiState, s: string) -> []string { return mm_split(ui_st, s, MM_WORDS) } +# tpl_words, made once per text (a clean one is its own answer, and nothing is kept for it) +function tw(ui_st: mut UiState, s: string) -> string { + if tpl_clean(s) { return s } + let m = mm_of(ui_st) + let i = mm_find(m, MM_TW, s) + if i >= 0 { return m.texts[i] } + let t = tpl_words(s) + let j = mm_add(m, MM_TW, s) + if j >= 0 { m.texts[j] = t } + return t +} +# el_url, made once per text +function el_url_u(ui_st: mut UiState, s: string) -> string { + let m = mm_of(ui_st) + let i = mm_find(m, MM_URL, s) + if i >= 0 { return m.texts[i] } + let t = el_url(s) + let j = mm_add(m, MM_URL, s) + if j >= 0 { m.texts[j] = t } + return t +} diff --git a/packages/ludic.ui/memo_join.ludic b/packages/ludic.ui/memo_join.ludic index 6031be13..0ae8f27b 100644 --- a/packages/ludic.ui/memo_join.ludic +++ b/packages/ludic.ui/memo_join.ludic @@ -28,6 +28,9 @@ function mm_join(ui_st: mut UiState, a: string, sep: string, b: string) -> strin push(m.tags, MM_JOIN) push(m.lists, null) push(m.grads, null) + push(m.texts, null) + push(m.vals, null) + push(m.vtpls, null) m.slots[at] = len(m.keys) return k } diff --git a/packages/ludic.ui/native.ludic b/packages/ludic.ui/native.ludic index 374878ab..fede399b 100644 --- a/packages/ludic.ui/native.ludic +++ b/packages/ludic.ui/native.ludic @@ -39,9 +39,9 @@ function nt_find(ui_st: UiState, tag: string) -> UiNative { return null } function nt_fired_clear(ui_st: mut UiState) -> void { - ui_st.nt_q_nodes = new []UiNode - ui_st.nt_q_names = new []string - ui_st.nt_q_vals = new []Val + List.clear(ui_st.nt_q_nodes) + List.clear(ui_st.nt_q_names) + 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) @@ -49,11 +49,17 @@ export function ui_fire(ui_st: mut UiState, n: UiNode, name: string, value: Val) push(ui_st.nt_q_vals, value) } function nt_fired_run(ui_st: mut UiState) -> void { - let nodes = ui_st.nt_q_nodes + let nodes = ui_st.nt_q_nodes # run from these while what they fire goes to the spares let names = ui_st.nt_q_names let vals = ui_st.nt_q_vals + ui_st.nt_q_nodes = ui_st.nt_sp_nodes + ui_st.nt_q_names = ui_st.nt_sp_names + ui_st.nt_q_vals = ui_st.nt_sp_vals nt_fired_clear(ui_st) for i in 0 .. len(nodes) { nt_run(ui_st, nodes[i], names[i], vals[i]) } + ui_st.nt_sp_nodes = nodes + ui_st.nt_sp_names = names + ui_st.nt_sp_vals = vals } # the element's on-, with `event` bound to what happened function nt_run(ui_st: mut UiState, n: UiNode, name: string, value: Val) -> void { diff --git a/packages/ludic.ui/pointer.ludic b/packages/ludic.ui/pointer.ludic index 92b36bcc..8e0a25b6 100644 --- a/packages/ludic.ui/pointer.ludic +++ b/packages/ludic.ui/pointer.ludic @@ -5,11 +5,13 @@ function fr_was_hovered(ui_st: UiState, key: string) -> bool { return sel_has(ui function fr_pointer(ui_st: mut UiState, root: UiNode) -> void { let i = ui_st.in_now ui_st.fr_down_now = i.down - let keys = new []string + let keys = ui_st.fr_hover_sp # the spare list, emptied; last frame's goes spare + List.clear(keys) let top = pp_top(root) var scope = root if top != null { scope = top } fr_under(scope, i.x, i.y, keys) + ui_st.fr_hover_sp = ui_st.fr_hover ui_st.fr_hover = keys if top != null and pp_pointer(ui_st, top) { return } if ui_st.sc_drag != "" or ct_key_mouse(ui_st, root) { return } diff --git a/packages/ludic.ui/pointer_pick.ludic b/packages/ludic.ui/pointer_pick.ludic index 54d48014..4692ba67 100644 --- a/packages/ludic.ui/pointer_pick.ludic +++ b/packages/ludic.ui/pointer_pick.ludic @@ -8,29 +8,11 @@ function fr_in(n: UiNode, px: float, py: float) -> bool { if n.ex != null and n.ex.no_pointer { return false } return px >= n.x and py >= n.y and px < n.x + n.cw and py < n.y + n.ch } -# the nearest element that wants `kind`, out from the topmost one under the point +# the nearest element that wants `kind`, out from the topmost one under the point: the children are +# walked the other way from how they are painted (positioned ones by z-index, highest first, then +# the flow, last first; a popover is on the top layer, not here), with no list made for it function fr_pick(n: UiNode, px: float, py: float, kind: int) -> UiNode { - let kids = fr_paint_rev(n) - for i in 0 .. len(kids) { - if fr_in(kids[i], px, py) { - let r = fr_pick(kids[i], px, py, kind) - if r != null { return r } - return fr_wants(n, kind) - } - } - return fr_wants(n, kind) -} -function fr_wants(n: UiNode, kind: int) -> UiNode { - if kind == PK_CONTROL and (n.focusable or n.act != null) { return n } - if kind == PK_POINTER and pe_wants(n, false) { return n } - if kind == PK_WHEEL and pe_wants(n, true) { return n } - return null -} -# the children the other way from how they are painted: positioned ones by z-index, highest first, -# then the flow, last first; a popover is on the top layer, not here -function fr_paint_rev(n: UiNode) -> []UiNode { - let out = new []UiNode - if n.children == null { return out } + if n.children == null { return fr_wants(n, kind) } var lo = 1000000 var hi = -1000000 for i in 0 .. len(n.children) { @@ -45,15 +27,27 @@ function fr_paint_rev(n: UiNode) -> []UiNode { var i = len(n.children) - 1 while i >= 0 { let c = n.children[i] - if c.pos != 0 and not c.popover and c.z == z { push(out, c) } + if c.pos != 0 and not c.popover and c.z == z and fr_in(c, px, py) { return fr_pick_in(n, c, px, py, kind) } i -= 1 } z -= 1 } var k = len(n.children) - 1 while k >= 0 { - if n.children[k].pos == 0 and not n.children[k].popover { push(out, n.children[k]) } + let c = n.children[k] + if c.pos == 0 and not c.popover and fr_in(c, px, py) { return fr_pick_in(n, c, px, py, kind) } k -= 1 } - return out + return fr_wants(n, kind) +} +function fr_pick_in(n: UiNode, c: UiNode, px: float, py: float, kind: int) -> UiNode { + let r = fr_pick(c, px, py, kind) + if r != null { return r } + return fr_wants(n, kind) +} +function fr_wants(n: UiNode, kind: int) -> UiNode { + if kind == PK_CONTROL and (n.focusable or n.act != null) { return n } + if kind == PK_POINTER and pe_wants(n, false) { return n } + if kind == PK_WHEEL and pe_wants(n, true) { return n } + return null } diff --git a/packages/ludic.ui/render3d_nine.ludic b/packages/ludic.ui/render3d_nine.ludic index 641aced6..60ce4b1f 100644 --- a/packages/ludic.ui/render3d_nine.ludic +++ b/packages/ludic.ui/render3d_nine.ludic @@ -8,11 +8,11 @@ function r3u_nine(render3d_st: mut Render3dState, ui_st: mut UiState, src: strin if t == 0 { return } let su = slice / float(Math.max(tex_width(render3d_st, t), 1)) let sv = slice / float(Math.max(tex_height(render3d_st, t), 1)) - let cut = ui_nine_cuts(x, y, w, h, dst) - let xs = r3u_four(cut[0], cut[1], cut[2], cut[3]) - let ys = r3u_four(cut[4], cut[5], cut[6], cut[7]) - let us = r3u_four(0.0, su, 1.0 - su, 1.0) - let vs = r3u_four(0.0, sv, 1.0 - sv, 1.0) + let cut = ui_nine_cuts_into(ui_st.r3n_cut, x, y, w, h, dst) + let xs = r3u_four(ui_st.r3n_xs, cut[0], cut[1], cut[2], cut[3]) + let ys = r3u_four(ui_st.r3n_ys, cut[4], cut[5], cut[6], cut[7]) + let us = r3u_four(ui_st.r3n_us, 0.0, su, 1.0 - su, 1.0) + let vs = r3u_four(ui_st.r3n_vs, 0.0, sv, 1.0 - sv, 1.0) for j in 0 .. 3 { for i in 0 .. 3 { let x0 = int(xs[i]) @@ -21,8 +21,8 @@ function r3u_nine(render3d_st: mut Render3dState, ui_st: mut UiState, src: strin } } } -function r3u_four(a: float, b: float, c: float, d: float) -> []float { - let out = new []float +function r3u_four(out: []float, a: float, b: float, c: float, d: float) -> []float { + List.clear(out) push(out, a) push(out, b) push(out, c) diff --git a/packages/ludic.ui/text_fit.ludic b/packages/ludic.ui/text_fit.ludic index 10408770..cdd2f98f 100644 --- a/packages/ludic.ui/text_fit.ludic +++ b/packages/ludic.ui/text_fit.ludic @@ -2,12 +2,12 @@ # 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: mut UiState, n: UiNode, s: string) -> void { - let parts = mm_split(ui_st, tpl_words(s), 32) + let parts = mm_split(ui_st, tw(ui_st, 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) } } -function el_line_h(ui_st: UiState, n: UiNode, s0: string) -> void { - let s = tpl_words(s0) +function el_line_h(ui_st: mut UiState, n: UiNode, s0: string) -> void { + let s = tw(ui_st, s0) let x = nx(n) x.lh_kind = 0 if s == "normal" or s == "" { return } diff --git a/packages/ludic.ui/units.ludic b/packages/ludic.ui/units.ludic index 1d9fbb7f..82209b2f 100644 --- a/packages/ludic.ui/units.ludic +++ b/packages/ludic.ui/units.ludic @@ -46,37 +46,31 @@ function cs_var(n: UiNode, name: string) -> string { } return null } -# a value with var(--name) or var(--name, fallback) in it, with each replaced by what it names +# a value with var(--name) or var(--name, fallback) in it, with each replaced by what it names. What +# it becomes is found by its pieces - stretches of the value and each var's text - compared in place, +# so a result seen before (every frame's, nearly always) is its kept Value, and nothing is built function cs_vars_in(ui_st: mut UiState, n: UiNode, v: Val) -> Val { if Value.kind(v) != 4 { return v } let s = Value.as_str(v) - var at = 0 - var out = "" - var found = false - var i = 0 - while i + 4 <= len(s) { - if sat(s, i, "var(") { - found = true - var j = i + 4 - var depth = 1 - while j < len(s) and depth > 0 { - if s[j] == 40 { depth += 1 } - if s[j] == 41 { depth -= 1 } - j += 1 - } - out = out + s[at..i] + cs_var_ref(ui_st, n, s[i + 4..j - 1]) - at = j - i = j - } else { i += 1 } + if not cv_pieces(ui_st, n, s) { return v } + let m = mm_of(ui_st) + var at = cv_hash(ui_st) & (MM_CAP - 1) + while m.slots[at] != 0 { + let i = m.slots[at] - 1 + if m.tags[i] == MM_VARS and cv_same(ui_st, m.keys[i]) { return m.vals[i] } + at = (at + 1) & (MM_CAP - 1) } - if not found { return v } - return Value.str(tpl_words(out + s[at..len(s)])) + let raw = cv_join(ui_st) + let r = Value.str(tpl_words(raw)) + let j = mm_add(m, MM_VARS, raw) + if j >= 0 { m.vals[j] = r } + return r } function cs_var_ref(ui_st: mut UiState, n: UiNode, inner: string) -> string { let parts = mm_split(ui_st, inner, 44) - let v = cs_var(n, tpl_words(parts[0])) + let v = cs_var(n, tw(ui_st, parts[0])) if v != null { return v } - if len(parts) > 1 { return tpl_words(inner[len(parts[0]) + 1..len(inner)]) } - ui_err(ui_st, `var({tpl_words(parts[0])}): no element here or above sets it`) + if len(parts) > 1 { return tw(ui_st, inner[len(parts[0]) + 1..len(inner)]) } + ui_err(ui_st, `var({tw(ui_st, parts[0])}): no element here or above sets it`) return "" } diff --git a/packages/ludic.ui/units_pieces.ludic b/packages/ludic.ui/units_pieces.ludic new file mode 100644 index 00000000..d829735b --- /dev/null +++ b/packages/ludic.ui/units_pieces.ludic @@ -0,0 +1,93 @@ +# units_pieces.ludic - a value with var()s, parsed once by its text into its stretches, the names it +# asks for and their fallbacks; each frame its pieces are those stretches and what the names hold +# here, in a list kept on the state, hashed and compared against the memo without a copy +export property UiVarTpl { + lits: []string = null # the text around the var()s: one more than there are names + names: []string = null + falls: []string = null # a var's fallback, or null +} +function cv_tpl(ui_st: mut UiState, s: string) -> UiVarTpl { + let m = mm_of(ui_st) + let i = mm_find(m, MM_VTPL, s) + if i >= 0 { return m.vtpls[i] } + let t = cv_parse(ui_st, s) + let j = mm_add(m, MM_VTPL, s) + if j >= 0 { m.vtpls[j] = t } + return t +} +function cv_parse(ui_st: mut UiState, s: string) -> UiVarTpl { + let t = new UiVarTpl + t.lits = new []string + t.names = new []string + t.falls = new []string + var at = 0 + var i = 0 + while i + 4 <= len(s) { + if sat(s, i, "var(") { + var j = i + 4 + var depth = 1 + while j < len(s) and depth > 0 { + if s[j] == 40 { depth += 1 } + if s[j] == 41 { depth -= 1 } + j += 1 + } + push(t.lits, s[at..i]) + let inner = s[i + 4..j - 1] + let parts = lss_split(inner, 44) + push(t.names, tpl_words(parts[0])) + if len(parts) > 1 { push(t.falls, tpl_words(inner[len(parts[0]) + 1..len(inner)])) } else { push(t.falls, null) } + at = j + i = j + } else { i += 1 } + } + push(t.lits, s[at..len(s)]) + return t +} +# this element's pieces for `s` into cv_texts; false when it has no var() at all +function cv_pieces(ui_st: mut UiState, n: UiNode, s: string) -> bool { + if not cv_has(s) { return false } + 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)) + } + push(ui_st.cv_texts, t.lits[len(t.names)]) + return true +} +function cv_has(s: string) -> bool { + for i in 0 .. len(s) - 3 { + if s[i] == 118 and sat(s, i, "var(") { return true } + } + return false +} +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] } + ui_err(ui_st, `var({t.names[k]}): no element here or above sets it`) + return "" +} +function cv_hash(ui_st: UiState) -> int { + var h = 216613626 ^ (MM_VARS & 255) + for p in 0 .. len(ui_st.cv_texts) { + let t = ui_st.cv_texts[p] + for i in 0 .. len(t) { h = ((h ^ t[i]) * 16777619) & 2147483647 } + } + return h +} +# the pieces, one after another, are exactly `k` +function cv_same(ui_st: UiState, k: string) -> bool { + var at = 0 + for p in 0 .. len(ui_st.cv_texts) { + let t = ui_st.cv_texts[p] + if not sat(k, at, t) { return false } + at += len(t) + } + return at == len(k) +} +function cv_join(ui_st: UiState) -> string { + var out = "" + for p in 0 .. len(ui_st.cv_texts) { out = out + ui_st.cv_texts[p] } + return out +}