Merge branch 'lang/foundations' into lang/nav

This commit is contained in:
Orkun ÇAKILKAYA 2026-09-28 00:29:53 +03:00
commit 24c16ebee4
30 changed files with 45219 additions and 44778 deletions

View file

@ -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 {

View file

@ -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
@ -21,17 +21,25 @@ function el_url(s: string) -> string {
}
# border-image: url(chip.png) 14 / 12px - the source's corner size, then how wide they are drawn
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 = mm_split(ui_st, s[Math.min(close + 1, len(s))..len(s)], 47)
n.bi_slice = el_num(tpl_words(rest[0]))
let m = mm_of(ui_st)
var at = mm_find(m, MM_BIMG, s)
if at < 0 {
var close = 0
while close < len(s) and s[close] != 41 { close += 1 }
at = mm_add(m, MM_BIMG, s)
if at < 0 { return }
m.texts[at] = el_url(s[0..Math.min(close + 1, len(s))])
m.lists[at] = lss_split(s[Math.min(close + 1, len(s))..len(s)], 47)
}
n.bi_src = m.texts[at]
let rest = m.lists[at]
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
}

View file

@ -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 = ""

View file

@ -39,6 +39,13 @@ 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
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
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 +97,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 +118,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

View file

@ -14,7 +14,8 @@ function bd_class(ui_st: mut UiState, k: UiTpl, e: UiEnv, key: string, out: []Ui
}
cls.props(it.ptr, props)
let ce = env_new(ui_st, null)
ce.model = cls.model(it.ptr)
if it.model == null { it.model = Value.object() }
ce.model = cls.model(it.ptr, it.model)
ce.inst = it
ce.ctx = e.ctx
ce.slot = k

View file

@ -9,12 +9,14 @@ export property UiClass {
lss: string = ""
make: fn() -> pointer = null
props: fn(pointer, Val) -> void = null
model: fn(pointer) -> Val = null
model: fn(pointer, Val) -> Val = null # fills the object it is given (the instance keeps one)
call: fn(pointer, string, Val) -> Val = null
root: UiTpl = null # the template, once read
sheet: UiSheet = null
seen_xml: string = "" # the files as a developer's build last found them on disk
seen_lss: string = ""
use: UiTpl = null # screen_class.ludic: the element that shows it as a screen, made once
none: Val = null # and the empty model it is shown with
}
export function ui_register(ui_st: mut UiState, k: UiClass) -> int {
push(ui_st.uc_list, k)

View file

@ -1,8 +1,24 @@
# color.ludic - CSS colours: #rgb, #rgba, #rrggbb, #rrggbbaa, rgb(r, g, b), rgba(r, g, b, a), the
# 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 "
# a colour is worked out once per text and kept with its alpha
function ui_color(ui_st: mut UiState, name0: string) -> int {
let name = tpl_words(name0)
let m = mm_of(ui_st)
let i = mm_find(m, MM_COLOR, name0)
if i >= 0 {
ui_st.cl_alpha = m.n2[i]
return int(m.n1[i])
}
let c = ui_color_new(ui_st, name0)
let j = mm_add(m, MM_COLOR, name0)
if j >= 0 {
m.n1[j] = float(c)
m.n2[j] = ui_st.cl_alpha
}
return c
}
function ui_color_new(ui_st: mut UiState, name0: string) -> int {
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 +54,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

View file

@ -56,7 +56,7 @@ function fr_image(ui_st: mut UiState, n: UiNode) -> void {
nat_w = ui_st.ui_be.image_w(n.src)
nat_h = ui_st.ui_be.image_h(n.src)
}
let r = ui_object_fit(n, nat_w, nat_h)
let r = ui_object_fit_into(ui_st.fit_out, n, nat_w, nat_h)
let clipped = ui_st.ui_be.clip != null and (r[2] > n.cw - ly_hin(n) or r[3] > n.ch - ly_vin(n))
if clipped { fr_clip_push(ui_st, n.x + n.pl + n.bl, n.y + n.pt + n.bt, n.cw - ly_hin(n), n.ch - ly_vin(n)) }
ui_st.ui_be.image(n.src, r[0], r[1], r[2], r[3], c, a)

View file

@ -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)

View file

@ -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))

View file

@ -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 }

View file

@ -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"

View file

@ -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
}

View file

@ -6,6 +6,7 @@ property UiInst {
ptr: pointer = null
seen: int = 0
props: Val = null # its props, filled again each frame into the same object
model: Val = null # and its model, filled in place by the class's model()
}
function in_get(ui_st: mut UiState, key: string, k: UiClass) -> UiInst {
for i in 0 .. len(ui_st.in_list) {
@ -25,10 +26,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) }
}

View file

@ -12,7 +12,8 @@ function ly_ratio(n: UiNode, fixed_w: float, avail: float) -> void {
} else if fixed_w < 0.0 and n.w != UI_FILL and n.h >= 0.0 { n.mw = n.h * r }
}
# x, y, w, h: a `nat_w` x `nat_h` picture in the node's content box by its object-fit
export function ui_object_fit(n: UiNode, nat_w: float, nat_h: float) -> []float {
export function ui_object_fit(n: UiNode, nat_w: float, nat_h: float) -> []float { return ui_object_fit_into(new []float, n, nat_w, nat_h) }
function ui_object_fit_into(out: []float, n: UiNode, nat_w: float, nat_h: float) -> []float {
let bx = n.x + n.pl + n.bl
let by = n.y + n.pt + n.bt
let bw = n.cw - ly_hin(n)
@ -29,7 +30,7 @@ export function ui_object_fit(n: UiNode, nat_w: float, nat_h: float) -> []float
w = nat_w * s
h = nat_h * s
}
let out = new []float
List.clear(out)
push(out, bx + (bw - w) / 2.0)
push(out, by + (bh - h) / 2.0)
push(out, w)

View file

@ -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)
}

View file

@ -9,10 +9,21 @@ 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
n1: []float = null # numbers worked out from the key: a colour and its alpha
n2: []float = 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)
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()
function mm_new() -> UiMemo {
let m = new UiMemo
m.slots = words(MM_CAP)
@ -20,6 +31,11 @@ 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
m.n1 = new []float
m.n2 = new []float
return m
}
function mm_of(ui_st: mut UiState) -> UiMemo {
@ -51,6 +67,11 @@ 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)
push(m.n1, 0.0)
push(m.n2, 0.0)
m.slots[at] = len(m.keys)
return len(m.keys) - 1
}
@ -66,3 +87,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
}

View file

@ -28,6 +28,11 @@ 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)
push(m.n1, 0.0)
push(m.n2, 0.0)
m.slots[at] = len(m.keys)
return k
}

View file

@ -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-<name>, with `event` bound to what happened
function nt_run(ui_st: mut UiState, n: UiNode, name: string, value: Val) -> void {

View file

@ -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 }

View file

@ -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
}

View file

@ -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)

View file

@ -7,16 +7,21 @@ 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)
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
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()
}
let use = cls.use
let e = env_new(ui_st, null)
e.model = Value.object()
e.model = cls.none
root.env = e
bd_class(ui_st, use, e, cls.name, root.children)
in_sweep(ui_st, cls.name)

View file

@ -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 }

View file

@ -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 ""
}

View file

@ -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
}

View file

@ -69,6 +69,42 @@ 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 value_n(v) }
# a scalar field of an object set in place: the Value under `key` is changed when it is already that
# kind, and made only the first time (a component's model, filled into the same object every frame)
function value_slot(o: Val, key: pointer, tag: int) -> Val {
value_lists(o)
var i = 0
while i < len(o.keys) {
if o.keys[i] == key {
if o.kids[i].tag == tag { return o.kids[i] }
let v = value_new(tag)
o.kids[i] = v
return v
}
i += 1
}
let v = value_new(tag)
push(o.keys, key)
push(o.kids, v)
return v
}
function value_set_int(o: Val, key: pointer, n: int) -> void {
let v = value_slot(o, key, 1)
v.num = n
}
function value_set_float(o: Val, key: pointer, x: float) -> void {
let v = value_slot(o, key, 7)
v.num = float_bits(x)
}
function value_set_str(o: Val, key: pointer, s: pointer) -> void {
let v = value_slot(o, key, 4)
v.txt = s
}
function value_set_bool(o: Val, key: pointer, b: bool) -> void {
let v = value_slot(o, key, 3)
v.num = 0
if b { v.num = 1 }
}
# a list or an object emptied in place, its room kept - for one filled again every frame
function value_clear(v: Val) -> void {
if v.kids == null { return }

View file

@ -23,24 +23,31 @@ function cm_from(c: int, ty: pointer, x: pointer) -> pointer {
cm_err(c, `a prop or state is an int, a float, a bool, a string or a Val, not a {ty}`)
return x
}
# the model is filled into the object the instance keeps (ludic.ui's): a scalar field updates its own
# Value in place, so showing a component allocates nothing for it frame after frame
function cm_model_src(c: int, px: pointer, rec: pointer, head: Node) -> pointer {
var src = `function {px}model(p: pointer) -> Val {{\n let s: {rec} = p\n let o = value_object()\n`
var src = `function {px}model(p: pointer, o: Val) -> Val {{\n let s: {rec} = p\n`
var i = 0
while i < len(g_cm_keep[c]) {
let k = g_cm_keep[c][i]
let put = cm_to(c, g_cm_keep_ty[c][i], "s." + k, head)
src = src + ` value_put(o, "{k}", {put})\n`
src = src + cm_set(c, g_cm_keep_ty[c][i], k, "s." + k, head)
i += 1
}
i = 0
while i < len(g_cm_fields[c]) {
let g = g_cm_fields[c][i]
let put = cm_to(c, g.ty, g.s + "(s)", head)
src = src + ` value_put(o, "{g_cm_fnames[c][i]}", {put})\n`
src = src + cm_set(c, g.ty, g_cm_fnames[c][i], g.s + "(s)", head)
i += 1
}
return src + " return o\n}\n"
}
function cm_set(c: int, ty: pointer, k: pointer, x: pointer, head: Node) -> pointer {
if (ty == "int") { return ` value_set_int(o, "{k}", {x})\n` }
if (ty == "float") { return ` value_set_float(o, "{k}", {x})\n` }
if (ty == "string") { return ` value_set_str(o, "{k}", {x})\n` }
if (ty == "bool") { return ` value_set_bool(o, "{k}", {x})\n` }
return ` value_put(o, "{k}", {cm_to(c, ty, x, head)})\n`
}
function cm_to(c: int, ty: pointer, x: pointer, head: Node) -> pointer {
if (ty == "Val") { return x }
let conv = vw_conv(0, ty, x, head)

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff