feat(lang): L11 views and templates - the UI is markup, not code
A `view Name { field = x; function q(..); on e(..) }` declaration is the one bridge between a
program and its UI: it writes view_<name>() -> UiView, whose model is a Value of every field and
whose call runs a query or an event by name.
ludic.ui is a template runtime:
- HTML-shaped XML screens and components, loaded at run time;
- {expression} bindings, if/else/each, props, slots, per-instance state;
- on-press / onclick actions (event, set, emit);
- component libraries (export="true", <import src as>).
Styling:
- stylesheets in <style> or importable .lss files (@import);
- CSS selectors (#id, .class, [attr=v], descendant and > combinators, :hover, :disabled,
:first-child, :last-child, :nth-child, :not) weighed by specificity;
- the box model and flex under CSS's property names.
Also:
- default parameters, and positional-then-named calls;
- Value gains a float kind;
- a function shadowing a runtime one is refused;
- an index is evaluated before the slice is read;
- runtime errors name the right file.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
334469ef61
commit
66a2bc2214
64 changed files with 74806 additions and 63975 deletions
62
packages/ludic.ui/act.ludic
Normal file
62
packages/ludic.ui/act.ludic
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
# act.ludic - what a press does: `buy(i)` asks the view, `set pick = i` changes a state, and
|
||||
# `emit picked` runs what the component's user said with on-picked. Several are `;` apart.
|
||||
const A_CALL: int = 0
|
||||
const A_SET: int = 1
|
||||
const A_EMIT: int = 2
|
||||
export property UiAct {
|
||||
kinds: []int = null
|
||||
names: []string = null
|
||||
exprs: []UiExpr = null
|
||||
}
|
||||
function act_parse(src: string, err: []string) -> UiAct {
|
||||
let a = new UiAct
|
||||
a.kinds = new []int
|
||||
a.names = new []string
|
||||
a.exprs = new []UiExpr
|
||||
let r = new UiRd
|
||||
r.s = src
|
||||
while r.err == "" {
|
||||
rd_ws(r)
|
||||
if r.i >= len(r.s) { break }
|
||||
if rd_word(r, "set") {
|
||||
push(a.kinds, A_SET)
|
||||
push(a.names, rd_name(r))
|
||||
if not rd_op(r, "=") { rd_fail(r, "'='") }
|
||||
push(a.exprs, px_cond(r))
|
||||
} else if rd_word(r, "emit") {
|
||||
push(a.kinds, A_EMIT)
|
||||
push(a.names, rd_name(r))
|
||||
push(a.exprs, null)
|
||||
} else {
|
||||
push(a.kinds, A_CALL)
|
||||
push(a.names, "")
|
||||
push(a.exprs, px_cond(r))
|
||||
}
|
||||
if not rd_op(r, ";") { break }
|
||||
}
|
||||
rd_ws(r)
|
||||
if r.err == "" and r.i < len(r.s) { rd_fail(r, "';' or the end") }
|
||||
if r.err != "" { push(err, r.err) }
|
||||
return a
|
||||
}
|
||||
function act_run(a: UiAct, e: UiEnv) -> void {
|
||||
for i in 0 .. len(a.kinds) {
|
||||
if a.kinds[i] == A_SET { env_set(e, a.names[i], ui_eval(a.exprs[i], e)) }
|
||||
if a.kinds[i] == A_CALL { ui_eval(a.exprs[i], e) }
|
||||
if a.kinds[i] == A_EMIT { act_emit(a.names[i], e) }
|
||||
}
|
||||
}
|
||||
function act_emit(name: string, e: UiEnv) -> void {
|
||||
var at = e
|
||||
while at != null {
|
||||
if at.on_names != null {
|
||||
for i in 0 .. len(at.on_names) {
|
||||
if at.on_names[i] == name {
|
||||
act_run(at.on_acts[i], at.on_envs[i])
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
at = at.parent
|
||||
}
|
||||
}
|
||||
58
packages/ludic.ui/attr.ludic
Normal file
58
packages/ludic.ui/attr.ludic
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
# attr.ludic - what a property sets, by its CSS name or the short one (width or w, background or
|
||||
# bg, font-size or size, ...). An attribute on an element is the same property, applied last.
|
||||
const AT_ALIAS: string = " width=w height=h min-width=min-w max-width=max-w min-height=min-h max-height=max-h padding=pad background=bg background-color=bg font-size=size opacity=alpha flex-grow=grow flex-direction=dir flex-wrap=wrap justify-content=justify align-items=align align-self=self row-gap=gap column-gap=gap text-align=talign "
|
||||
const AT_UNDRAWN: string = " border-radius font-weight font-family font-style box-shadow text-shadow cursor transition line-height letter-spacing text-decoration overflow z-index position "
|
||||
# a stylesheet's property: one that means nothing is said
|
||||
function el_attr(n: UiNode, key: string, v: Val) -> void {
|
||||
if el_apply(n, key, v) { return }
|
||||
if lk_in(AT_UNDRAWN, at_alias(key)) { ui_err(`{key}: this renderer does not draw it`) } else { ui_err(`{key}: there is no such property`) }
|
||||
}
|
||||
# an element's attribute: a property when it names one, otherwise only there for [selectors]
|
||||
function el_apply(n: UiNode, key: string, v: Val) -> bool {
|
||||
let k = at_alias(key)
|
||||
return el_size(n, k, v) or el_box(n, k, v) or el_flex(n, k, v) or el_look(n, k, v)
|
||||
}
|
||||
function at_alias(key: string) -> string {
|
||||
let pat = ` {key}=`
|
||||
let n = len(pat)
|
||||
for i in 0 .. len(AT_ALIAS) - n + 1 {
|
||||
if AT_ALIAS[i..i + n] == pat {
|
||||
var j = i + n
|
||||
while AT_ALIAS[j] != 32 { j += 1 }
|
||||
return AT_ALIAS[i + n..j]
|
||||
}
|
||||
}
|
||||
return key
|
||||
}
|
||||
function el_size(n: UiNode, key: string, v: Val) -> bool {
|
||||
if key == "w" {
|
||||
n.w_pct = el_pct(v)
|
||||
n.w = el_len(v)
|
||||
} else if key == "h" {
|
||||
n.h_pct = el_pct(v)
|
||||
n.h = el_len(v)
|
||||
} else if key == "min-w" { n.min_w = el_len(v) } else if key == "max-w" { n.max_w = el_len(v) } else if key == "min-h" { n.min_h = el_len(v) } else if key == "max-h" { n.max_h = el_len(v) } else if key == "gap" { n.gap = el_len(v) } else { return false }
|
||||
return true
|
||||
}
|
||||
function el_flex(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" or key == "flex" { n.grow = el_num(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(n: UiNode, key: string, v: Val) -> bool {
|
||||
let s = vl_text(v)
|
||||
if key == "size" { n.size = el_len(v) } else if key == "color" { n.fg = ui_color(s) } else if key == "bg" { n.bg = ui_color(s) } else if key == "alpha" { n.alpha = el_num(s) } else if key == "enabled" { n.enabled = vl_truthy(v) and s != "false" } else if key == "border-color" { n.border_c = ui_color(s) } else if key == "border-width" { n.border = el_len(v) } else if key == "border" { el_border(n, s) } else { return false }
|
||||
return true
|
||||
}
|
||||
# border: 1 #ffcc00, or 1px solid accent
|
||||
function el_border(n: UiNode, s: string) -> void {
|
||||
let parts = lss_split(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) { n.border = ui_px(el_num(p)) } else if p == "none" { n.border = 0.0 } else if len(p) > 0 and p != "solid" { n.border_c = ui_color(p) }
|
||||
}
|
||||
if n.border_c == UI_NONE { n.border_c = n.rfg }
|
||||
}
|
||||
81
packages/ludic.ui/attr_box.ludic
Normal file
81
packages/ludic.ui/attr_box.ludic
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# attr_box.ludic - padding and margin (one to four lengths, as CSS writes them, or one side by
|
||||
# name), lengths with px or %, and the words that place things
|
||||
function el_box(n: UiNode, key: string, v: Val) -> bool {
|
||||
if key == "pad" {
|
||||
el_sides(v)
|
||||
n.pt = el_s0
|
||||
n.pr = el_s1
|
||||
n.pb = el_s2
|
||||
n.pl = el_s3
|
||||
} else if key == "margin" {
|
||||
el_sides(v)
|
||||
n.mt = el_s0
|
||||
n.mr = el_s1
|
||||
n.mb = el_s2
|
||||
n.ml = el_s3
|
||||
} else if key == "padding-top" { n.pt = el_len(v) } else if key == "padding-right" { n.pr = el_len(v) } else if key == "padding-bottom" { n.pb = el_len(v) } else if key == "padding-left" { n.pl = el_len(v) } else if key == "margin-top" { n.mt = el_len(v) } else if key == "margin-right" { n.mr = el_len(v) } else if key == "margin-bottom" { n.mb = el_len(v) } else if key == "margin-left" { n.ml = el_len(v) } else { return false }
|
||||
return true
|
||||
}
|
||||
var el_s0: float = 0.0
|
||||
var el_s1: float = 0.0
|
||||
var el_s2: float = 0.0
|
||||
var el_s3: float = 0.0
|
||||
# top right bottom left out of 1 to 4 lengths
|
||||
function el_sides(v: Val) -> void {
|
||||
let parts = new []float
|
||||
if vl_num(v) { push(parts, ui_px(Value.as_float(v))) } else {
|
||||
let words = lss_split(vl_text(v), 32)
|
||||
for i in 0 .. len(words) {
|
||||
if len(words[i]) > 0 { push(parts, ui_px(el_num(words[i]))) }
|
||||
}
|
||||
}
|
||||
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]) }
|
||||
}
|
||||
el_s0 = parts[0]
|
||||
el_s1 = parts[1]
|
||||
el_s2 = parts[2]
|
||||
el_s3 = parts[3]
|
||||
}
|
||||
# a length: a number, 10px, fill, fit or auto (a percentage is el_pct's)
|
||||
function el_len(v: Val) -> float {
|
||||
if vl_num(v) { return ui_px(Value.as_float(v)) }
|
||||
let s = vl_text(v)
|
||||
if s == "fill" { return UI_FILL }
|
||||
if s == "fit" or s == "auto" or (len(s) > 0 and s[len(s) - 1] == 37) { return 0.0 }
|
||||
return ui_px(el_num(s))
|
||||
}
|
||||
function el_pct(v: Val) -> float {
|
||||
let s = vl_text(v)
|
||||
if len(s) > 1 and s[len(s) - 1] == 37 { return el_num(s) }
|
||||
return 0.0
|
||||
}
|
||||
# the number at the front of `12.5px`
|
||||
function el_num(s: string) -> float {
|
||||
var f = 0.0
|
||||
var unit = 0.0
|
||||
var neg = false
|
||||
for i in 0 .. len(s) {
|
||||
let c = s[i]
|
||||
if c == 45 and i == 0 { neg = true } else if c == 46 { unit = 0.1 } else if c >= 48 and c <= 57 {
|
||||
if unit > 0.0 {
|
||||
f = f + float(c - 48) * unit
|
||||
unit = unit * 0.1
|
||||
} else { f = f * 10.0 + float(c - 48) }
|
||||
} else { break }
|
||||
}
|
||||
if neg { return 0.0 - f }
|
||||
return f
|
||||
}
|
||||
# start, center, end, stretch; for justify also between, around, evenly - with CSS's spellings
|
||||
function el_place(v: Val) -> int {
|
||||
var s = vl_text(v)
|
||||
if len(s) > 6 and s[0..6] == "space-" { s = s[6..len(s)] }
|
||||
if len(s) > 5 and s[0..5] == "flex-" { s = s[5..len(s)] }
|
||||
if s == "center" { return UI_CENTER }
|
||||
if s == "end" or s == "right" { return UI_END }
|
||||
if s == "stretch" or s == "between" { return 3 }
|
||||
if s == "around" { return UI_AROUND }
|
||||
if s == "evenly" { return UI_EVENLY }
|
||||
return UI_START
|
||||
}
|
||||
26
packages/ludic.ui/backend.ludic
Normal file
26
packages/ludic.ui/backend.ludic
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
# backend.ludic - the renderer seam
|
||||
# the renderer. Only rect, text and measure are needed; the rest improve on a default:
|
||||
# button draws, focuses and answers a press itself; color names a colour (else #rrggbb);
|
||||
# image draws an <img src>; px scales a template's lengths; scroll_begin answers how far a region is scrolled; read
|
||||
# reads a template file (else Fs.read_text)
|
||||
export property UiBackend {
|
||||
rect: fn(float, float, float, float, int, float) -> void = null
|
||||
text: fn(float, float, string, float, int) -> void = null
|
||||
measure: fn(string, float) -> float = null
|
||||
pointer_x: fn() -> float = null
|
||||
pointer_y: fn() -> float = null
|
||||
pointer_down: fn() -> bool = null
|
||||
button: fn(float, float, float, float, string, bool) -> bool = null
|
||||
image: fn(string, float, float, float, float, int, float) -> void = null # src x y w h tint alpha
|
||||
color: fn(string) -> int = null
|
||||
px: fn(float) -> float = null
|
||||
scroll_begin: fn(string, float, float, float, float, float) -> float = null
|
||||
scroll_end: fn() -> void = null
|
||||
read: fn(string) -> string = null
|
||||
}
|
||||
var ui_be: UiBackend = null
|
||||
export function ui_backend(b: UiBackend) -> void { ui_be = b }
|
||||
function ui_px(v: float) -> float {
|
||||
if ui_be != null and ui_be.px != null { return ui_be.px(v) }
|
||||
return v
|
||||
}
|
||||
68
packages/ludic.ui/build.ludic
Normal file
68
packages/ludic.ui/build.ludic
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
# build.ludic - a template and the model, made into this frame's nodes. <if>/<else> and
|
||||
# <each in= as= index=> are the template's own; a tag its file linked to a component is that one,
|
||||
# given its attributes as props, its on-* as what `emit` runs, and its content as <slot/>.
|
||||
function bd_kids(kids: []UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
|
||||
var last_if = true
|
||||
for i in 0 .. len(kids) {
|
||||
let k = kids[i]
|
||||
let ck = `{key}/{i}`
|
||||
if k.tag == "state" { continue }
|
||||
if k.tag == "if" {
|
||||
last_if = bd_test(k, e)
|
||||
if last_if { bd_kids(k.kids, e, ck, out) }
|
||||
} else if k.tag == "else" {
|
||||
if not last_if { bd_kids(k.kids, e, ck, out) }
|
||||
last_if = true
|
||||
} else if k.tag == "each" { bd_each(k, e, ck, out) } else if k.tag == "slot" { bd_slot(e, ck, out) } else if not bd_comp(k, e, ck, out) { push(out, bd_elem(k, e, ck)) }
|
||||
}
|
||||
}
|
||||
function bd_test(k: UiTpl, e: UiEnv) -> bool {
|
||||
let t = tpl_attr(k, "test")
|
||||
if t == null {
|
||||
ui_err("<if> needs a test=\"{...}\"")
|
||||
return false
|
||||
}
|
||||
return ui_eval_bool(t, e)
|
||||
}
|
||||
function bd_name(k: UiTpl, attr: string, dflt: string) -> string {
|
||||
let x = tpl_attr(k, attr)
|
||||
if x == null or x.op != X_LIT { return dflt }
|
||||
return Value.as_str(x.v)
|
||||
}
|
||||
function bd_each(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
|
||||
let src = tpl_attr(k, "in")
|
||||
if src == null {
|
||||
ui_err("<each> needs an in=\"{...}\"")
|
||||
return
|
||||
}
|
||||
let list = ui_eval(src, e)
|
||||
let as_name = bd_name(k, "as", "item")
|
||||
let at_name = bd_name(k, "index", "index")
|
||||
for j in 0 .. Value.count(list) {
|
||||
let le = env_new(e)
|
||||
env_bind(le, as_name, Value.at(list, j))
|
||||
env_bind(le, at_name, Value.int(j))
|
||||
bd_kids(k.kids, le, `{key}#{j}`, out)
|
||||
}
|
||||
}
|
||||
function bd_slot(e: UiEnv, key: string, out: []UiNode) -> void {
|
||||
var at = e
|
||||
while at != null and at.slot == null { at = at.parent }
|
||||
if at != null { bd_kids(at.slot.kids, at.slot_env, key, out) }
|
||||
}
|
||||
function bd_comp(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> bool {
|
||||
if k.comp == null { return false }
|
||||
let c = k.comp
|
||||
let ce = env_new(null)
|
||||
ce.model = e.model
|
||||
for i in 0 .. len(k.keys) { env_bind(ce, k.keys[i], ui_eval(k.vals[i], e)) }
|
||||
ce.on_names = k.on_keys
|
||||
ce.on_acts = k.on_acts
|
||||
ce.on_envs = new []UiEnv
|
||||
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
|
||||
ce.slot = k
|
||||
ce.slot_env = e
|
||||
ce.state = st_for(key, c, ce)
|
||||
bd_kids(c.kids, ce, key, out)
|
||||
return true
|
||||
}
|
||||
44
packages/ludic.ui/cascade.ludic
Normal file
44
packages/ludic.ui/cascade.ludic
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
# cascade.ludic - once the frame's tree is built, every element is styled as a browser would:
|
||||
# the default sheet, then the file's rules from least to most specific, then its style="...", then
|
||||
# its own attributes. Then what is display: none leaves the tree.
|
||||
function cs_tree(n: UiNode) -> void {
|
||||
for i in 0 .. len(n.children) {
|
||||
let c = n.children[i]
|
||||
c.parent = n
|
||||
c.index = i
|
||||
}
|
||||
if n.tag != "" { cs_node(n) }
|
||||
for i in 0 .. len(n.children) { cs_tree(n.children[i]) }
|
||||
var shown = new []UiNode
|
||||
for i in 0 .. len(n.children) {
|
||||
if not n.children[i].hidden { push(shown, n.children[i]) }
|
||||
}
|
||||
n.children = shown
|
||||
}
|
||||
function cs_node(n: UiNode) -> void {
|
||||
cs_sheet(n, ua_sheet())
|
||||
if n.sheet != null { cs_sheet(n, n.sheet) }
|
||||
if n.style != null { cs_rule(n, n.style) }
|
||||
for i in 0 .. len(n.akeys) { el_apply(n, n.akeys[i], n.vals[i]) }
|
||||
}
|
||||
function cs_sheet(n: UiNode, sh: UiSheet) -> void {
|
||||
for i in 0 .. len(sh.rules) {
|
||||
if sel_match(sh.rules[i].sel, n) { cs_rule(n, sh.rules[i]) }
|
||||
}
|
||||
}
|
||||
function cs_rule(n: UiNode, r: UiRule) -> void {
|
||||
for j in 0 .. len(r.keys) { el_attr(n, r.keys[j], ui_eval(r.vals[j], n.env)) }
|
||||
}
|
||||
# what every element looks like before anyone styles it, as a browser's own sheet does
|
||||
const UA_LSS: string = "h1 { font-size: 32 } h2 { font-size: 26 } h3 { font-size: 22 } h4 { font-size: 19 } h5 { font-size: 16 } h6 { font-size: 14 } small { font-size: 13 } button { padding: 5 10 } hr { height: 1; width: fill; background: #808080; margin: 4 0 } scroll { width: fill; height: fill } img { width: 24; height: 24 }"
|
||||
var ua_sh: UiSheet = null
|
||||
function ua_sheet() -> UiSheet {
|
||||
if ua_sh != null { return ua_sh }
|
||||
let f = new UiFile
|
||||
f.path = "the default sheet"
|
||||
f.imp = new []UiFile
|
||||
f.own = new []UiRule
|
||||
lss_text(f, UA_LSS)
|
||||
ua_sh = sheet_of(f)
|
||||
return ua_sh
|
||||
}
|
||||
66
packages/ludic.ui/draw.ludic
Normal file
66
packages/ludic.ui/draw.ludic
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
# draw.ludic - the laid-out nodes through the renderer: background, border, then content
|
||||
function fr_draw(n: UiNode) -> void {
|
||||
if n.kind == UI_BUTTON {
|
||||
fr_button(n)
|
||||
return
|
||||
}
|
||||
if n.bg >= 0 and ui_be.rect != null { ui_be.rect(n.x, n.y, n.cw, n.ch, n.bg, n.alpha) }
|
||||
fr_border(n)
|
||||
if n.kind == UI_TEXT { ui_be.text(fr_text_x(n), n.y + n.pt + n.border, n.text, n.rsize, n.rfg) }
|
||||
if n.kind == UI_IMAGE and ui_be.image != null { ui_be.image(n.src, n.x + n.pl + n.border, n.y + n.pt + n.border, n.cw - ly_hin(n), n.ch - ly_vin(n), n.rfg, n.alpha) }
|
||||
var off = 0.0
|
||||
if n.kind == UI_SCROLL and ui_be.scroll_begin != null { off = ui_be.scroll_begin(n.key, n.x, n.y, n.cw, n.ch, n.mh) }
|
||||
if off != 0.0 {
|
||||
for i in 0 .. len(n.children) { fr_shift(n.children[i], off) }
|
||||
}
|
||||
if n.children != null {
|
||||
for i in 0 .. len(n.children) { fr_draw(n.children[i]) }
|
||||
}
|
||||
if n.kind == UI_SCROLL and ui_be.scroll_end != null { ui_be.scroll_end() }
|
||||
}
|
||||
# where a text starts: text-align within the room the box gives it
|
||||
function fr_text_x(n: UiNode) -> float {
|
||||
let x = n.x + n.pl + n.border
|
||||
if n.talign == UI_START { return x }
|
||||
let spare = n.cw - ly_hin(n) - ly_text_w(n.text, n.rsize)
|
||||
if n.talign == UI_CENTER { return x + spare / 2.0 }
|
||||
return x + spare
|
||||
}
|
||||
function fr_border(n: UiNode) -> void {
|
||||
if n.border <= 0.0 or ui_be.rect == null { return }
|
||||
var c = n.border_c
|
||||
if c < 0 { c = n.rfg }
|
||||
let b = n.border
|
||||
ui_be.rect(n.x, n.y, n.cw, b, c, n.alpha)
|
||||
ui_be.rect(n.x, n.y + n.ch - b, n.cw, b, c, n.alpha)
|
||||
ui_be.rect(n.x, n.y, b, n.ch, c, n.alpha)
|
||||
ui_be.rect(n.x + n.cw - b, n.y, b, n.ch, c, n.alpha)
|
||||
}
|
||||
# the renderer's own button when it has one: it draws, focuses and answers the press
|
||||
function fr_button(n: UiNode) -> void {
|
||||
if ui_be.button != null {
|
||||
if ui_be.button(n.x, n.y, n.cw, n.ch, n.text, n.enabled) and n.enabled { push(fr_pressed, n) }
|
||||
return
|
||||
}
|
||||
var bg = n.bg
|
||||
if bg < 0 { bg = 4210752 }
|
||||
if ui_be.rect != null { ui_be.rect(n.x, n.y, n.cw, n.ch, bg, n.alpha) }
|
||||
fr_border(n)
|
||||
ui_be.text(fr_text_x(n), n.y + n.pt + n.border, n.text, n.rsize, n.rfg)
|
||||
}
|
||||
function fr_shift(n: UiNode, off: float) -> void {
|
||||
n.y = n.y - off
|
||||
if n.children != null {
|
||||
for i in 0 .. len(n.children) { fr_shift(n.children[i], off) }
|
||||
}
|
||||
}
|
||||
# every node and where it went, one line each - for a test, or a look at a layout
|
||||
export function ui_dump(n: UiNode, depth: int) -> string {
|
||||
var pad = ""
|
||||
for d in 0 .. depth { pad = pad + " " }
|
||||
var out = `{pad}{n.kind} {int(n.x)},{int(n.y)} {int(n.cw)}x{int(n.ch)} {n.text}\n`
|
||||
if n.children != null {
|
||||
for i in 0 .. len(n.children) { out = out + ui_dump(n.children[i], depth + 1) }
|
||||
}
|
||||
return out
|
||||
}
|
||||
77
packages/ludic.ui/elem.ludic
Normal file
77
packages/ludic.ui/elem.ludic
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
# elem.ludic - an element made from its template: what kind of node its tag is, its id, classes
|
||||
# and attributes (kept for selectors, applied after every rule), its text, and its children
|
||||
const EL_BOXES: string = " col div section header footer nav main article aside ul ol li form box "
|
||||
const EL_TEXTS: string = " text span p label h1 h2 h3 h4 h5 h6 strong em small b i a "
|
||||
function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode {
|
||||
let n = new UiNode
|
||||
n.key = key
|
||||
n.tag = k.tag
|
||||
n.env = e
|
||||
n.sheet = k.sheet
|
||||
n.style = k.style
|
||||
n.children = new []UiNode
|
||||
n.classes = new []string
|
||||
n.akeys = new []string
|
||||
n.avals = new []string
|
||||
n.vals = new []Val
|
||||
el_kind(n, k.tag)
|
||||
for i in 0 .. len(k.keys) { el_keep(n, k.keys[i], ui_eval(k.vals[i], e)) }
|
||||
n.hovered = fr_was_hovered(key)
|
||||
for i in 0 .. len(k.on_keys) {
|
||||
if k.on_keys[i] == "press" {
|
||||
n.act = k.on_acts[i]
|
||||
n.env = e
|
||||
} else { ui_err(`on-{k.on_keys[i]}: an element answers on-press (or onclick); a component's other events are its user's`) }
|
||||
}
|
||||
bd_kids(k.kids, e, key, n.children)
|
||||
if k.text != null { el_text(n, ui_eval_text(k.text, e)) }
|
||||
return n
|
||||
}
|
||||
function el_kind(n: UiNode, tag: string) -> void {
|
||||
if lk_in(EL_TEXTS, tag) { n.kind = UI_TEXT } else if tag == "row" { n.dir = UI_ROW } else if tag == "button" { n.kind = UI_BUTTON } else if tag == "spacer" { n.kind = UI_SPACER } else if tag == "scroll" { n.kind = UI_SCROLL } else if tag == "img" { n.kind = UI_IMAGE } else if tag == "hr" { n.kind = UI_RULE } else if not lk_in(EL_BOXES, tag) { ui_err(`<{tag}>: there is no such element`) }
|
||||
}
|
||||
# a box with words in it holds them as a text of its own, as <div>words</div> does
|
||||
function el_text(n: UiNode, s: string) -> void {
|
||||
if n.kind != UI_BOX and n.kind != UI_SCROLL {
|
||||
n.text = s
|
||||
return
|
||||
}
|
||||
let t = new UiNode
|
||||
t.kind = UI_TEXT
|
||||
t.text = s
|
||||
t.env = n.env
|
||||
t.children = new []UiNode
|
||||
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)
|
||||
if key == "id" { n.id = s } else if key == "class" {
|
||||
let parts = lss_split(s, 32)
|
||||
for i in 0 .. len(parts) {
|
||||
if len(parts[i]) > 0 { push(n.classes, parts[i]) }
|
||||
}
|
||||
} else if key == "hidden" { n.hidden = vl_truthy(v) and s != "false" } else if key == "disabled" { n.enabled = not (vl_truthy(v) and s != "false") } else if key == "src" { n.src = s } else {
|
||||
if key == "enabled" { n.enabled = vl_truthy(v) and s != "false" }
|
||||
push(n.akeys, key)
|
||||
push(n.avals, s)
|
||||
push(n.vals, v)
|
||||
}
|
||||
}
|
||||
# a colour by the renderer's name for it, else #rrggbb (or #rgb)
|
||||
function ui_color(name: string) -> int {
|
||||
if name == "none" or name == "" or name == "transparent" { return UI_NONE }
|
||||
if name[0] != 35 {
|
||||
if ui_be != null and ui_be.color != null { return ui_be.color(name) }
|
||||
return 15658734
|
||||
}
|
||||
var c = 0
|
||||
for i in 1 .. len(name) {
|
||||
var d = name[i] - 48
|
||||
if name[i] >= 97 { d = name[i] - 87 }
|
||||
if name[i] >= 65 and name[i] < 97 { d = name[i] - 55 }
|
||||
c = c * 16 + d
|
||||
if len(name) == 4 { c = c * 16 + d }
|
||||
}
|
||||
return c
|
||||
}
|
||||
65
packages/ludic.ui/env.ludic
Normal file
65
packages/ludic.ui/env.ludic
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
# env.ludic - what a name means where an expression is read: the loop's own names, then the
|
||||
# component's props and state, then the view's model. A component sees its props, its state
|
||||
# and the model - never the names of whoever used it.
|
||||
export property UiEnv {
|
||||
names: []string = null
|
||||
vals: []Val = null
|
||||
state: Val = null
|
||||
parent: UiEnv = null
|
||||
model: Val = null
|
||||
slot: UiTpl = null
|
||||
slot_env: UiEnv = null
|
||||
on_names: []string = null # a component's on-* attributes, which its `emit` runs
|
||||
on_acts: []UiAct = null
|
||||
on_envs: []UiEnv = null
|
||||
}
|
||||
var ui_view: UiView = null
|
||||
var ui_errs: []string = new []string
|
||||
|
||||
function env_new(parent: UiEnv) -> UiEnv {
|
||||
let e = new UiEnv
|
||||
e.names = new []string
|
||||
e.vals = new []Val
|
||||
e.parent = parent
|
||||
if parent != null { e.model = parent.model }
|
||||
return e
|
||||
}
|
||||
function env_bind(e: UiEnv, name: string, v: Val) -> void {
|
||||
push(e.names, name)
|
||||
push(e.vals, v)
|
||||
}
|
||||
function env_get(e: UiEnv, name: string) -> Val {
|
||||
var at = e
|
||||
while at != null {
|
||||
var i = len(at.names) - 1
|
||||
while i >= 0 {
|
||||
if at.names[i] == name { return at.vals[i] }
|
||||
i -= 1
|
||||
}
|
||||
if at.state != null and Value.has(at.state, name) != 0 { return Value.get(at.state, name) }
|
||||
at = at.parent
|
||||
}
|
||||
if e.model != null and Value.has(e.model, name) != 0 { return Value.get(e.model, name) }
|
||||
ui_err(`no {name} here: not a loop's, a prop, a state or in the view`)
|
||||
return Value.null()
|
||||
}
|
||||
# `set name = value`: the nearest state that holds the name
|
||||
function env_set(e: UiEnv, name: string, v: Val) -> void {
|
||||
var at = e
|
||||
while at != null {
|
||||
if at.state != null and Value.has(at.state, name) != 0 {
|
||||
Value.put(at.state, name, v)
|
||||
return
|
||||
}
|
||||
at = at.parent
|
||||
}
|
||||
ui_err(`set {name}: no state here holds it - declare it with <state {name}="..."/>`)
|
||||
}
|
||||
# an error is said once, however many frames make it
|
||||
function ui_err(msg: string) -> void {
|
||||
for i in 0 .. len(ui_errs) {
|
||||
if ui_errs[i] == msg { return }
|
||||
}
|
||||
push(ui_errs, msg)
|
||||
print(`ui: {msg}`)
|
||||
}
|
||||
52
packages/ludic.ui/eval.ludic
Normal file
52
packages/ludic.ui/eval.ludic
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
# eval.ludic - an expression's value, here
|
||||
function ui_eval(x: UiExpr, e: UiEnv) -> Val {
|
||||
if x.op == X_LIT { return x.v }
|
||||
if x.op == X_NAME { return env_get(e, x.s) }
|
||||
if x.op == X_MEMBER {
|
||||
let o = ui_eval(x.a, e)
|
||||
if Value.kind(o) == 6 and Value.has(o, x.s) != 0 { return Value.get(o, x.s) }
|
||||
if Value.kind(o) == 5 and x.s == "length" { return Value.int(Value.count(o)) }
|
||||
ui_err(`.{x.s}: there is no such field`)
|
||||
return Value.null()
|
||||
}
|
||||
if x.op == X_INDEX { return Value.at(ui_eval(x.a, e), Value.as_int(ui_eval(x.b, e))) }
|
||||
if x.op == X_CALL { return ev_call(x, e) }
|
||||
if x.op == X_NOT { return vl_bool(not vl_truthy(ui_eval(x.a, e))) }
|
||||
if x.op == X_NEG { return vl_arith("-", Value.int(0), ui_eval(x.a, e)) }
|
||||
if x.op == X_AND {
|
||||
if not vl_truthy(ui_eval(x.a, e)) { return vl_bool(false) }
|
||||
return vl_bool(vl_truthy(ui_eval(x.b, e)))
|
||||
}
|
||||
if x.op == X_OR {
|
||||
if vl_truthy(ui_eval(x.a, e)) { return vl_bool(true) }
|
||||
return vl_bool(vl_truthy(ui_eval(x.b, e)))
|
||||
}
|
||||
if x.op == X_COND {
|
||||
if vl_truthy(ui_eval(x.a, e)) { return ui_eval(x.b, e) }
|
||||
return ui_eval(x.c, e)
|
||||
}
|
||||
if x.op == X_TEXT {
|
||||
var out = ""
|
||||
for i in 0 .. len(x.args) { out = out + vl_text(ui_eval(x.args[i], e)) }
|
||||
return Value.str(out)
|
||||
}
|
||||
return vl_binary(x.s, ui_eval(x.a, e), ui_eval(x.b, e))
|
||||
}
|
||||
# a call: len() and range() are the template's own, anything else is the view's
|
||||
function ev_call(x: UiExpr, e: UiEnv) -> Val {
|
||||
let args = Value.list()
|
||||
for i in 0 .. len(x.args) { Value.add(args, ui_eval(x.args[i], e)) }
|
||||
if x.s == "len" and len(x.args) == 1 { return Value.int(Value.count(Value.at(args, 0))) }
|
||||
if x.s == "range" and len(x.args) == 1 {
|
||||
let out = Value.list()
|
||||
for i in 0 .. Value.as_int(Value.at(args, 0)) { Value.add(out, Value.int(i)) }
|
||||
return out
|
||||
}
|
||||
if ui_view == null or ui_view.call == null {
|
||||
ui_err(`{x.s}(): no view is showing to ask`)
|
||||
return Value.null()
|
||||
}
|
||||
return ui_view.call(x.s, args)
|
||||
}
|
||||
function ui_eval_text(x: UiExpr, e: UiEnv) -> string { return vl_text(ui_eval(x, e)) }
|
||||
function ui_eval_bool(x: UiExpr, e: UiEnv) -> bool { return vl_truthy(ui_eval(x, e)) }
|
||||
82
packages/ludic.ui/expr.ludic
Normal file
82
packages/ludic.ui/expr.ludic
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
# expr.ludic - a template's expressions: `{pick == i}`, `{f.cost}`, `{placed(i) > 0 ? 'out' : ''}`.
|
||||
# Read once when the template loads, evaluated against the view's model every frame.
|
||||
const X_LIT: int = 0
|
||||
const X_NAME: int = 1
|
||||
const X_MEMBER: int = 2
|
||||
const X_INDEX: int = 3
|
||||
const X_CALL: int = 4
|
||||
const X_NOT: int = 5
|
||||
const X_NEG: int = 6
|
||||
const X_BIN: int = 7
|
||||
const X_AND: int = 8
|
||||
const X_OR: int = 9
|
||||
const X_COND: int = 10
|
||||
const X_TEXT: int = 11
|
||||
|
||||
export property UiExpr {
|
||||
op: int = 0
|
||||
s: string = ""
|
||||
v: Val = null
|
||||
a: UiExpr = null
|
||||
b: UiExpr = null
|
||||
c: UiExpr = null
|
||||
args: []UiExpr = null
|
||||
}
|
||||
# the text being read, and where the reading is
|
||||
property UiRd {
|
||||
s: string = ""
|
||||
i: int = 0
|
||||
err: string = ""
|
||||
}
|
||||
function ex_new(op: int) -> UiExpr {
|
||||
let e = new UiExpr
|
||||
e.op = op
|
||||
e.args = new []UiExpr
|
||||
return e
|
||||
}
|
||||
function ex_lit(v: Val) -> UiExpr {
|
||||
let e = ex_new(X_LIT)
|
||||
e.v = v
|
||||
return e
|
||||
}
|
||||
function rd_ws(r: UiRd) -> void {
|
||||
while r.i < len(r.s) and (r.s[r.i] == 32 or r.s[r.i] == 9 or r.s[r.i] == 10 or r.s[r.i] == 13) { r.i += 1 }
|
||||
}
|
||||
function rd_c(r: UiRd, k: int) -> int {
|
||||
if r.i + k < len(r.s) { return r.s[r.i + k] }
|
||||
return 0
|
||||
}
|
||||
# the operator `op` is next: take it
|
||||
function rd_op(r: UiRd, op: string) -> bool {
|
||||
rd_ws(r)
|
||||
let n = len(op)
|
||||
if r.i + n > len(r.s) { return false }
|
||||
if r.s[r.i..r.i + n] != op { return false }
|
||||
r.i += n
|
||||
return true
|
||||
}
|
||||
function rd_is_word(c: int) -> bool { return (c >= 97 and c <= 122) or (c >= 65 and c <= 90) or c == 95 or (c >= 48 and c <= 57) }
|
||||
# the word `w` is next, whole: take it
|
||||
function rd_word(r: UiRd, w: string) -> bool {
|
||||
rd_ws(r)
|
||||
let at = r.i
|
||||
if not rd_op(r, w) { return false }
|
||||
if rd_is_word(rd_c(r, 0)) {
|
||||
r.i = at
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
function rd_name(r: UiRd) -> string {
|
||||
rd_ws(r)
|
||||
let at = r.i
|
||||
while r.i < len(r.s) and rd_is_word(r.s[r.i]) { r.i += 1 }
|
||||
if r.i == at {
|
||||
rd_fail(r, "a name")
|
||||
return ""
|
||||
}
|
||||
return r.s[at..r.i]
|
||||
}
|
||||
function rd_fail(r: UiRd, want: string) -> void {
|
||||
if r.err == "" { r.err = `expected {want} at {r.i + 1} in "{r.s}"` }
|
||||
}
|
||||
96
packages/ludic.ui/expr_parse.ludic
Normal file
96
packages/ludic.ui/expr_parse.ludic
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# expr_parse.ludic - a condition, or/and/not, a comparison, sums and products, then a name
|
||||
# with its members, indexes and calls
|
||||
function px_cond(r: UiRd) -> UiExpr {
|
||||
let a = px_or(r)
|
||||
if not rd_op(r, "?") { return a }
|
||||
let e = ex_new(X_COND)
|
||||
e.a = a
|
||||
e.b = px_cond(r)
|
||||
if not rd_op(r, ":") { rd_fail(r, "':'") }
|
||||
e.c = px_cond(r)
|
||||
return e
|
||||
}
|
||||
function px_or(r: UiRd) -> UiExpr {
|
||||
var a = px_and(r)
|
||||
while rd_word(r, "or") or rd_op(r, "||") {
|
||||
let e = ex_new(X_OR)
|
||||
e.a = a
|
||||
e.b = px_and(r)
|
||||
a = e
|
||||
}
|
||||
return a
|
||||
}
|
||||
function px_and(r: UiRd) -> UiExpr {
|
||||
var a = px_not(r)
|
||||
while rd_word(r, "and") or rd_op(r, "&&") {
|
||||
let e = ex_new(X_AND)
|
||||
e.a = a
|
||||
e.b = px_not(r)
|
||||
a = e
|
||||
}
|
||||
return a
|
||||
}
|
||||
function px_not(r: UiRd) -> UiExpr {
|
||||
rd_ws(r)
|
||||
if rd_word(r, "not") or (rd_c(r, 0) == 33 and rd_c(r, 1) != 61 and rd_op(r, "!")) {
|
||||
let e = ex_new(X_NOT)
|
||||
e.a = px_not(r)
|
||||
return e
|
||||
}
|
||||
return px_cmp(r)
|
||||
}
|
||||
function px_bin(op: string, a: UiExpr, b: UiExpr) -> UiExpr {
|
||||
let e = ex_new(X_BIN)
|
||||
e.s = op
|
||||
e.a = a
|
||||
e.b = b
|
||||
return e
|
||||
}
|
||||
function px_cmp(r: UiRd) -> UiExpr {
|
||||
let a = px_sum(r)
|
||||
let ops = ["==", "!=", "<=", ">=", "<", ">"]
|
||||
for k in 0 .. len(ops) {
|
||||
if rd_op(r, ops[k]) { return px_bin(ops[k], a, px_sum(r)) }
|
||||
}
|
||||
return a
|
||||
}
|
||||
function px_sum(r: UiRd) -> UiExpr {
|
||||
var a = px_prod(r)
|
||||
while true {
|
||||
if rd_op(r, "+") { a = px_bin("+", a, px_prod(r)) } else if rd_op(r, "-") { a = px_bin("-", a, px_prod(r)) } else { return a }
|
||||
}
|
||||
return a
|
||||
}
|
||||
function px_prod(r: UiRd) -> UiExpr {
|
||||
var a = px_unary(r)
|
||||
while true {
|
||||
if rd_op(r, "*") { a = px_bin("*", a, px_unary(r)) } else if rd_op(r, "/") { a = px_bin("/", a, px_unary(r)) } else if rd_op(r, "%") { a = px_bin("%", a, px_unary(r)) } else { return a }
|
||||
}
|
||||
return a
|
||||
}
|
||||
function px_unary(r: UiRd) -> UiExpr {
|
||||
if rd_op(r, "-") {
|
||||
let e = ex_new(X_NEG)
|
||||
e.a = px_unary(r)
|
||||
return e
|
||||
}
|
||||
return px_post(r)
|
||||
}
|
||||
function px_post(r: UiRd) -> UiExpr {
|
||||
var a = px_primary(r)
|
||||
while r.err == "" {
|
||||
if rd_op(r, ".") {
|
||||
let e = ex_new(X_MEMBER)
|
||||
e.a = a
|
||||
e.s = rd_name(r)
|
||||
a = e
|
||||
} else if rd_op(r, "[") {
|
||||
let e = ex_new(X_INDEX)
|
||||
e.a = a
|
||||
e.b = px_cond(r)
|
||||
if not rd_op(r, "]") { rd_fail(r, "']'") }
|
||||
a = e
|
||||
} else { return a }
|
||||
}
|
||||
return a
|
||||
}
|
||||
88
packages/ludic.ui/expr_prim.ludic
Normal file
88
packages/ludic.ui/expr_prim.ludic
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
# expr_prim.ludic - a number, a quoted string, true/false/null, a name or a call, a bracket
|
||||
function px_primary(r: UiRd) -> UiExpr {
|
||||
rd_ws(r)
|
||||
let c = rd_c(r, 0)
|
||||
if c >= 48 and c <= 57 { return px_number(r) }
|
||||
if c == 39 or c == 34 { return px_string(r, c) }
|
||||
if rd_op(r, "(") {
|
||||
let e = px_cond(r)
|
||||
if not rd_op(r, ")") { rd_fail(r, "')'") }
|
||||
return e
|
||||
}
|
||||
if rd_word(r, "true") { return ex_lit(Value.bool(1)) }
|
||||
if rd_word(r, "false") { return ex_lit(Value.bool(0)) }
|
||||
if rd_word(r, "null") { return ex_lit(Value.null()) }
|
||||
let name = rd_name(r)
|
||||
if not rd_op(r, "(") {
|
||||
let e = ex_new(X_NAME)
|
||||
e.s = name
|
||||
return e
|
||||
}
|
||||
let e = ex_new(X_CALL)
|
||||
e.s = name
|
||||
if rd_op(r, ")") { return e }
|
||||
while r.err == "" {
|
||||
push(e.args, px_cond(r))
|
||||
if rd_op(r, ")") { return e }
|
||||
if not rd_op(r, ",") { rd_fail(r, "',' or ')'") }
|
||||
}
|
||||
return e
|
||||
}
|
||||
function px_number(r: UiRd) -> UiExpr {
|
||||
let at = r.i
|
||||
var whole = 0
|
||||
while r.i < len(r.s) and r.s[r.i] >= 48 and r.s[r.i] <= 57 {
|
||||
whole = whole * 10 + (r.s[r.i] - 48)
|
||||
r.i += 1
|
||||
}
|
||||
if rd_c(r, 0) != 46 or rd_c(r, 1) < 48 or rd_c(r, 1) > 57 { return ex_lit(Value.int(whole)) }
|
||||
r.i += 1
|
||||
var f = float(whole)
|
||||
var unit = 0.1
|
||||
while r.i < len(r.s) and r.s[r.i] >= 48 and r.s[r.i] <= 57 {
|
||||
f = f + float(r.s[r.i] - 48) * unit
|
||||
unit = unit * 0.1
|
||||
r.i += 1
|
||||
}
|
||||
return ex_lit(Value.float(f))
|
||||
}
|
||||
function px_string(r: UiRd, q: int) -> UiExpr {
|
||||
r.i += 1
|
||||
let at = r.i
|
||||
while r.i < len(r.s) and r.s[r.i] != q { r.i += 1 }
|
||||
let s: string = r.s[at..r.i]
|
||||
if r.i >= len(r.s) { rd_fail(r, "the string's closing quote") } else { r.i += 1 }
|
||||
return ex_lit(Value.str(s))
|
||||
}
|
||||
# one expression, the whole of `src`
|
||||
function ex_parse(src: string, err: []string) -> UiExpr {
|
||||
let r = new UiRd
|
||||
r.s = src
|
||||
let e = px_cond(r)
|
||||
rd_ws(r)
|
||||
if r.err == "" and r.i < len(r.s) { rd_fail(r, "the end") }
|
||||
if r.err != "" { push(err, r.err) }
|
||||
return e
|
||||
}
|
||||
# text with {holes}: a literal when it has none, the expression itself when it is one hole and
|
||||
# nothing else (so it keeps its type), otherwise the pieces joined as text
|
||||
function ex_text(src: string, err: []string) -> UiExpr {
|
||||
let e = ex_new(X_TEXT)
|
||||
var at = 0
|
||||
var i = 0
|
||||
while i < len(src) {
|
||||
if src[i] == 123 {
|
||||
var j = i + 1
|
||||
while j < len(src) and src[j] != 125 { j += 1 }
|
||||
if i > at { push(e.args, ex_lit(Value.str(src[at..i]))) }
|
||||
push(e.args, ex_parse(src[i + 1..j], err))
|
||||
at = j + 1
|
||||
i = j
|
||||
}
|
||||
i += 1
|
||||
}
|
||||
if at < len(src) { push(e.args, ex_lit(Value.str(src[at..len(src)]))) }
|
||||
if len(e.args) == 0 { return ex_lit(Value.str("")) }
|
||||
if len(e.args) == 1 { return e.args[0] }
|
||||
return e
|
||||
}
|
||||
95
packages/ludic.ui/frame.ludic
Normal file
95
packages/ludic.ui/frame.ludic
Normal file
|
|
@ -0,0 +1,95 @@
|
|||
# frame.ludic - a screen shown: built from the view's model, laid out, drawn, and its presses
|
||||
# run once the frame is drawn, so nothing the view does can change a frame half way through it
|
||||
var fr_was_down: bool = false
|
||||
var fr_pressed: []UiNode = new []UiNode
|
||||
|
||||
export function ui_show(screen: string, view: UiView, x: float, y: float, w: float, h: float) -> void {
|
||||
let root = ui_nodes(screen, view)
|
||||
if root == null { return }
|
||||
ui_place(root, x, y, w, h)
|
||||
fr_pressed = new []UiNode
|
||||
fr_pointer(root)
|
||||
if ui_be != null and ui_be.text != null { fr_draw(root) }
|
||||
let pressed = fr_pressed
|
||||
for i in 0 .. len(pressed) { ui_press(pressed[i]) }
|
||||
}
|
||||
# the screen's nodes for this frame, not yet laid out - for a test, or a host that places them
|
||||
export function ui_nodes(screen: string, view: UiView) -> UiNode {
|
||||
let si = lib_find(lib_screen_names, screen)
|
||||
if si < 0 {
|
||||
ui_err(`there is no screen {screen} - ui_load the file that has it`)
|
||||
return null
|
||||
}
|
||||
let t = lib_screens[si]
|
||||
ui_view = view
|
||||
let e = env_new(null)
|
||||
if view != null and view.model != null { e.model = view.model() } else { e.model = Value.object() }
|
||||
e.state = st_for(screen, t, e)
|
||||
let root = new UiNode
|
||||
root.key = screen
|
||||
root.children = new []UiNode
|
||||
root.env = e
|
||||
root.sheet = t.sheet
|
||||
root.classes = new []string
|
||||
root.akeys = new []string
|
||||
root.avals = new []string
|
||||
root.vals = new []Val
|
||||
for i in 0 .. len(t.keys) {
|
||||
if t.keys[i] != "name" { el_keep(root, t.keys[i], ui_eval(t.vals[i], e)) }
|
||||
}
|
||||
for i in 0 .. len(root.akeys) { el_apply(root, root.akeys[i], root.vals[i]) }
|
||||
bd_kids(t.kids, e, screen, root.children)
|
||||
cs_tree(root)
|
||||
return root
|
||||
}
|
||||
export function ui_place(root: UiNode, x: float, y: float, w: float, h: float) -> void {
|
||||
root.w = UI_FILL
|
||||
root.h = UI_FILL
|
||||
var fg = 15658734
|
||||
if ui_be != null and ui_be.color != null { fg = ui_be.color("text") }
|
||||
ly_measure(root, w, ui_px(16.0), fg)
|
||||
ly_place(root, x, y, w, h)
|
||||
}
|
||||
# a press, as if the pointer had made it
|
||||
export function ui_press(n: UiNode) -> void {
|
||||
if n.act != null and n.enabled { act_run(n.act, n.env) }
|
||||
}
|
||||
# the pointer: what it is over is :hover next frame (the element and all around it, as in CSS),
|
||||
# and without a renderer's own button a press on the deepest pressable thing is a press
|
||||
var fr_hover: []string = new []string
|
||||
function fr_was_hovered(key: string) -> bool { return sel_has(fr_hover, key) }
|
||||
function fr_pointer(root: UiNode) -> void {
|
||||
if ui_be == null or ui_be.pointer_x == null {
|
||||
fr_hover = new []string
|
||||
return
|
||||
}
|
||||
let px = ui_be.pointer_x()
|
||||
let py = ui_be.pointer_y()
|
||||
let keys = new []string
|
||||
fr_under(root, px, py, keys)
|
||||
fr_hover = keys
|
||||
if ui_be.button != null or ui_be.pointer_down == null { return }
|
||||
let hit = fr_hit(root, px, py)
|
||||
let down = ui_be.pointer_down()
|
||||
if hit != null and down and not fr_was_down { push(fr_pressed, hit) }
|
||||
fr_was_down = down
|
||||
}
|
||||
function fr_under(n: UiNode, px: float, py: float, keys: []string) -> void {
|
||||
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return }
|
||||
if n.key != "" { push(keys, n.key) }
|
||||
for i in 0 .. len(n.children) { fr_under(n.children[i], px, py, keys) }
|
||||
}
|
||||
function fr_hit(n: UiNode, px: float, py: float) -> UiNode {
|
||||
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return null }
|
||||
if n.children != null {
|
||||
var i = len(n.children) - 1
|
||||
while i >= 0 {
|
||||
let h = fr_hit(n.children[i], px, py)
|
||||
if h != null { return h }
|
||||
i -= 1
|
||||
}
|
||||
}
|
||||
if n.act != null { return n }
|
||||
return null
|
||||
}
|
||||
export function ui_hit(root: UiNode, px: float, py: float) -> UiNode { return fr_hit(root, px, py) }
|
||||
33
packages/ludic.ui/index.ludic
Normal file
33
packages/ludic.ui/index.ludic
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
# ludic.ui - templates: screens and components written as markup in files the program loads at
|
||||
# run time, reading only what a `view` declaration lets them, and answering with its events
|
||||
module ui
|
||||
numbers float
|
||||
import "node.ludic"
|
||||
import "backend.ludic"
|
||||
import "expr.ludic"
|
||||
import "expr_parse.ludic"
|
||||
import "expr_prim.ludic"
|
||||
import "values.ludic"
|
||||
import "env.ludic"
|
||||
import "eval.ludic"
|
||||
import "act.ludic"
|
||||
import "tpl.ludic"
|
||||
import "lib.ludic"
|
||||
import "lib_util.ludic"
|
||||
import "link.ludic"
|
||||
import "state.ludic"
|
||||
import "build.ludic"
|
||||
import "elem.ludic"
|
||||
import "attr.ludic"
|
||||
import "attr_box.ludic"
|
||||
import "style.ludic"
|
||||
import "style_rule.ludic"
|
||||
import "select.ludic"
|
||||
import "select_parts.ludic"
|
||||
import "select_match.ludic"
|
||||
import "cascade.ludic"
|
||||
import "layout.ludic"
|
||||
import "layout_run.ludic"
|
||||
import "layout_wrap.ludic"
|
||||
import "frame.ludic"
|
||||
import "draw.ludic"
|
||||
96
packages/ludic.ui/layout.ludic
Normal file
96
packages/ludic.ui/layout.ludic
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
# layout.ludic - flex layout on the CSS box model, top-down: a box is told how wide it may be,
|
||||
# measures its content at that width (so a wrapping row knows how tall it is), then places each
|
||||
# child. A size is the border box: padding and border inside it, margin outside it.
|
||||
function ly_measure(n: UiNode, avail: float, size: float, fg: int) -> void {
|
||||
n.rsize = size
|
||||
if n.size > 0.0 { n.rsize = n.size }
|
||||
n.rfg = fg
|
||||
if n.fg != UI_INHERIT { n.rfg = n.fg }
|
||||
n.mw = ly_hin(n)
|
||||
n.mh = ly_vin(n)
|
||||
if n.kind == UI_TEXT or n.kind == UI_BUTTON {
|
||||
n.mw = n.mw + ly_text_w(n.text, n.rsize)
|
||||
n.mh = n.mh + n.rsize * 1.25
|
||||
}
|
||||
if n.kind == UI_IMAGE {
|
||||
n.mw = n.mw + n.rsize
|
||||
n.mh = n.mh + n.rsize
|
||||
}
|
||||
var fixed_w = n.w
|
||||
if n.w_pct > 0.0 { fixed_w = avail * n.w_pct / 100.0 }
|
||||
if (n.kind == UI_BOX or n.kind == UI_SCROLL) and n.children != null {
|
||||
var inner = avail - n.ml - n.mr
|
||||
if fixed_w > 0.0 { inner = fixed_w }
|
||||
inner = ly_clamp(inner, n.min_w, n.max_w) - ly_hin(n)
|
||||
if n.dir == UI_ROW and n.wrap { ly_measure_wrap(n, inner) } else { ly_measure_box(n, inner) }
|
||||
}
|
||||
if fixed_w > 0.0 { n.mw = fixed_w }
|
||||
if n.h > 0.0 { n.mh = n.h }
|
||||
n.mw = ly_clamp(n.mw, n.min_w, n.max_w)
|
||||
n.mh = ly_clamp(n.mh, n.min_h, n.max_h)
|
||||
}
|
||||
function ly_hin(n: UiNode) -> float { return n.pl + n.pr + n.border * 2.0 }
|
||||
function ly_vin(n: UiNode) -> float { return n.pt + n.pb + n.border * 2.0 }
|
||||
function ly_clamp(v: float, lo: float, hi: float) -> float {
|
||||
var out = v
|
||||
if hi > 0.0 and out > hi { out = hi }
|
||||
if out < lo { out = lo }
|
||||
return out
|
||||
}
|
||||
function ly_measure_box(n: UiNode, inner: float) -> void {
|
||||
var main = 0.0
|
||||
var cross = 0.0
|
||||
for i in 0 .. len(n.children) {
|
||||
let c = n.children[i]
|
||||
ly_measure(c, inner, n.rsize, n.rfg)
|
||||
main = main + ly_main(c, n.dir)
|
||||
if i > 0 { main = main + n.gap }
|
||||
cross = Math.max(cross, ly_cross(c, n.dir))
|
||||
}
|
||||
if n.dir == UI_ROW {
|
||||
n.mw = n.mw + main
|
||||
n.mh = n.mh + cross
|
||||
} else {
|
||||
n.mw = n.mw + cross
|
||||
n.mh = n.mh + main
|
||||
}
|
||||
}
|
||||
# a wrapping row: children in lines no wider than `inner`, the lines stacked `gap` apart
|
||||
function ly_measure_wrap(n: UiNode, inner: float) -> void {
|
||||
var line_w = 0.0
|
||||
var line_h = 0.0
|
||||
var widest = 0.0
|
||||
var total = 0.0
|
||||
var count = 0
|
||||
for i in 0 .. len(n.children) {
|
||||
let c = n.children[i]
|
||||
ly_measure(c, inner, n.rsize, n.rfg)
|
||||
let cw = ly_main(c, UI_ROW)
|
||||
if count > 0 and line_w + n.gap + cw > inner {
|
||||
widest = Math.max(widest, line_w)
|
||||
total = total + line_h + n.gap
|
||||
line_w = 0.0
|
||||
line_h = 0.0
|
||||
count = 0
|
||||
}
|
||||
if count > 0 { line_w = line_w + n.gap }
|
||||
line_w = line_w + cw
|
||||
line_h = Math.max(line_h, ly_cross(c, UI_ROW))
|
||||
count += 1
|
||||
}
|
||||
n.mw = n.mw + Math.max(widest, line_w)
|
||||
n.mh = n.mh + total + line_h
|
||||
}
|
||||
function ly_text_w(s: string, size: float) -> float {
|
||||
if ui_be != null and ui_be.measure != null { return ui_be.measure(s, size) }
|
||||
return float(len(s)) * size * 0.5
|
||||
}
|
||||
# a child's size along and across a direction, margins included
|
||||
function ly_main(c: UiNode, dir: int) -> float {
|
||||
if dir == UI_ROW { return c.mw + c.ml + c.mr }
|
||||
return c.mh + c.mt + c.mb
|
||||
}
|
||||
function ly_cross(c: UiNode, dir: int) -> float {
|
||||
if dir == UI_ROW { return c.mh + c.mt + c.mb }
|
||||
return c.mw + c.ml + c.mr
|
||||
}
|
||||
99
packages/ludic.ui/layout_run.ludic
Normal file
99
packages/ludic.ui/layout_run.ludic
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
# layout_run.ludic - a box's children given their places: along its direction by size, grow and
|
||||
# justify, across it by align (or each child's own `self`), a wrapping row one line at a time
|
||||
var ly_lead: float = 0.0
|
||||
var ly_extra: float = 0.0
|
||||
function ly_place(n: UiNode, x: float, y: float, w: float, h: float) -> void {
|
||||
n.x = x
|
||||
n.y = y
|
||||
n.cw = w
|
||||
n.ch = h
|
||||
if (n.kind != UI_BOX and n.kind != UI_SCROLL) or n.children == null { return }
|
||||
let inner_w = w - ly_hin(n)
|
||||
var inner_h = h - ly_vin(n)
|
||||
if n.kind == UI_SCROLL { inner_h = Math.max(n.mh, h) - ly_vin(n) }
|
||||
let top = n.y + n.pt + n.border
|
||||
if n.dir == UI_ROW and n.wrap {
|
||||
ly_place_wrap(n, top, inner_w)
|
||||
return
|
||||
}
|
||||
if n.dir == UI_ROW { ly_place_line(n, 0, len(n.children), top, inner_w, inner_h) } else { ly_place_line(n, 0, len(n.children), top, inner_h, inner_w) }
|
||||
}
|
||||
function ly_fills(c: UiNode, dir: int) -> bool {
|
||||
if dir == UI_ROW { return c.w == UI_FILL }
|
||||
return c.h == UI_FILL
|
||||
}
|
||||
function ly_grow(c: UiNode, dir: int) -> float {
|
||||
if ly_fills(c, dir) { return Math.max(c.grow, 1.0) }
|
||||
return c.grow
|
||||
}
|
||||
# where a child starts along the run: `fill` from nothing, a percentage from the room it is in
|
||||
function ly_basis(c: UiNode, dir: int, room: float) -> float {
|
||||
if ly_fills(c, dir) { return 0.0 }
|
||||
if dir == UI_ROW and c.w_pct > 0.0 { return room * c.w_pct / 100.0 + c.ml + c.mr }
|
||||
if dir == UI_COL and c.h_pct > 0.0 { return room * c.h_pct / 100.0 + c.mt + c.mb }
|
||||
return ly_main(c, dir)
|
||||
}
|
||||
function ly_stretches(n: UiNode, c: UiNode) -> bool {
|
||||
if ly_fills(c, 1 - n.dir) { return true }
|
||||
var a = n.align
|
||||
if c.self >= 0 { a = c.self }
|
||||
return a == UI_STRETCH
|
||||
}
|
||||
# children from..to along one line starting at `top`: sizes, the spare room, then each place
|
||||
function ly_place_line(n: UiNode, from: int, to: int, top: float, main_room: float, cross_room: float) -> void {
|
||||
var used = 0.0
|
||||
var grows = 0.0
|
||||
for i in from .. to {
|
||||
used = used + ly_basis(n.children[i], n.dir, main_room)
|
||||
grows = grows + ly_grow(n.children[i], n.dir)
|
||||
}
|
||||
used = used + n.gap * float(Math.max(to - from - 1, 0))
|
||||
let free = main_room - used
|
||||
ly_justify(n, free, to - from, grows)
|
||||
let extra = ly_extra
|
||||
var at = ly_lead
|
||||
for i in from .. to {
|
||||
let c = n.children[i]
|
||||
var m = ly_basis(c, n.dir, main_room)
|
||||
if grows > 0.0 and free > 0.0 { m = m + free * ly_grow(c, n.dir) / grows }
|
||||
ly_place_child(n, c, at, top, m, cross_room)
|
||||
at = at + m + n.gap + extra
|
||||
}
|
||||
}
|
||||
# one child: `m` along the run and its own size (or the whole room, stretched) across, less margins
|
||||
function ly_place_child(n: UiNode, c: UiNode, at: float, top: float, m: float, cross_room: float) -> void {
|
||||
var cr = ly_cross(c, n.dir)
|
||||
if n.dir == UI_ROW and c.h_pct > 0.0 { cr = cross_room * c.h_pct / 100.0 + c.mt + c.mb }
|
||||
if n.dir == UI_COL and c.w_pct > 0.0 { cr = cross_room * c.w_pct / 100.0 + c.ml + c.mr }
|
||||
if ly_stretches(n, c) { cr = cross_room }
|
||||
var a = n.align
|
||||
if c.self >= 0 { a = c.self }
|
||||
var off = 0.0
|
||||
if a == UI_CENTER { off = (cross_room - cr) / 2.0 }
|
||||
if a == UI_END { off = cross_room - cr }
|
||||
let left = n.x + n.pl + n.border
|
||||
if n.dir == UI_ROW {
|
||||
let cw = ly_clamp(m - c.ml - c.mr, c.min_w, c.max_w)
|
||||
ly_place(c, left + at + c.ml, top + off + c.mt, cw, ly_clamp(cr - c.mt - c.mb, c.min_h, c.max_h))
|
||||
} else {
|
||||
let chh = ly_clamp(m - c.mt - c.mb, c.min_h, c.max_h)
|
||||
ly_place(c, left + off + c.ml, top + at + c.mt, ly_clamp(cr - c.ml - c.mr, c.min_w, c.max_w), chh)
|
||||
}
|
||||
}
|
||||
# where the first child starts and what goes between, when nothing grows into the spare room
|
||||
function ly_justify(n: UiNode, free: float, count: int, grows: float) -> void {
|
||||
ly_lead = 0.0
|
||||
ly_extra = 0.0
|
||||
if grows > 0.0 or free <= 0.0 or count == 0 { return }
|
||||
if n.justify == UI_CENTER { ly_lead = free / 2.0 }
|
||||
if n.justify == UI_END { ly_lead = free }
|
||||
if n.justify == UI_BETWEEN and count > 1 { ly_extra = free / float(count - 1) }
|
||||
if n.justify == UI_AROUND {
|
||||
ly_extra = free / float(count)
|
||||
ly_lead = ly_extra / 2.0
|
||||
}
|
||||
if n.justify == UI_EVENLY {
|
||||
ly_extra = free / float(count + 1)
|
||||
ly_lead = ly_extra
|
||||
}
|
||||
}
|
||||
19
packages/ludic.ui/layout_wrap.ludic
Normal file
19
packages/ludic.ui/layout_wrap.ludic
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# layout_wrap.ludic - a wrapping row, line by line: each line is as tall as its tallest child
|
||||
function ly_place_wrap(n: UiNode, top0: float, inner_w: float) -> void {
|
||||
var top = top0
|
||||
var from = 0
|
||||
while from < len(n.children) {
|
||||
var to = from
|
||||
var line_w = 0.0
|
||||
var line_h = 0.0
|
||||
while to < len(n.children) and (to == from or line_w + n.gap + ly_main(n.children[to], UI_ROW) <= inner_w) {
|
||||
if to > from { line_w = line_w + n.gap }
|
||||
line_w = line_w + ly_main(n.children[to], UI_ROW)
|
||||
line_h = Math.max(line_h, ly_cross(n.children[to], UI_ROW))
|
||||
to += 1
|
||||
}
|
||||
ly_place_line(n, from, to, top, inner_w, line_h)
|
||||
top = top + line_h + n.gap
|
||||
from = to
|
||||
}
|
||||
}
|
||||
78
packages/ludic.ui/lib.ludic
Normal file
78
packages/ludic.ui/lib.ludic
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
# lib.ludic - template files. A file's <component>s are its own unless marked export="true";
|
||||
# <import src="kit.xml"/> brings a file's exported components in by their names, and
|
||||
# <import src="kit.xml" as="kit"/> as <kit:Name>, so a component library is a file of them and two
|
||||
# libraries never collide. A file's <style>s, and its imports' before them, style what it writes. A <screen> is the program's, by name, from whichever file has it.
|
||||
export property UiFile {
|
||||
path: string = ""
|
||||
names: []string = null
|
||||
comps: []UiTpl = null
|
||||
exported: []bool = null
|
||||
imp_as: []string = null
|
||||
imp: []UiFile = null
|
||||
own: []UiRule = null
|
||||
sheet: UiSheet = null
|
||||
}
|
||||
var lib_files: []UiFile = new []UiFile
|
||||
var lib_screen_names: []string = new []string
|
||||
var lib_screens: []UiTpl = new []UiTpl
|
||||
# the file a path names, read once
|
||||
export function ui_load(path: string) -> bool { return lib_file(path) != null }
|
||||
function lib_file(path: string) -> UiFile {
|
||||
for i in 0 .. len(lib_files) {
|
||||
if lib_files[i].path == path { return lib_files[i] }
|
||||
}
|
||||
let src = lib_text(path)
|
||||
if len(src) == 0 {
|
||||
ui_err(`{path}: cannot read it`)
|
||||
return null
|
||||
}
|
||||
if lss_is(path) { return lss_file(src, path) }
|
||||
return lib_read(src, path)
|
||||
}
|
||||
# a file's text, as if read from `path` (its imports are found beside it)
|
||||
export function ui_load_text(src: string, path: string) -> void {
|
||||
if lss_is(path) { lss_file(src, path) } else { lib_read(src, path) }
|
||||
}
|
||||
function lib_read(src: string, path: string) -> UiFile {
|
||||
let f = lib_new(path)
|
||||
let root = tpl_read(Xml.parse(src), path)
|
||||
var tops = root.kids
|
||||
if root.tag != "ui" { tops = [root] }
|
||||
for i in 0 .. len(tops) { lib_top(f, tops[i]) }
|
||||
f.sheet = sheet_of(f)
|
||||
for i in 0 .. len(f.comps) { lib_link(f, f.comps[i]) }
|
||||
for i in 0 .. len(tops) {
|
||||
if tops[i].tag == "screen" { lib_link(f, tops[i]) }
|
||||
}
|
||||
return f
|
||||
}
|
||||
function lib_top(f: UiFile, t: UiTpl) -> void {
|
||||
if t.tag == "style" {
|
||||
sheet_read(f, t)
|
||||
return
|
||||
}
|
||||
if t.tag == "import" {
|
||||
let other = lib_file(lib_beside(f.path, bd_name(t, "src", "")))
|
||||
if other != null {
|
||||
push(f.imp, other)
|
||||
push(f.imp_as, bd_name(t, "as", ""))
|
||||
}
|
||||
return
|
||||
}
|
||||
let name = bd_name(t, "name", "")
|
||||
if name == "" or (t.tag != "component" and t.tag != "screen") {
|
||||
ui_err(`{f.path}: <{t.tag}> - a file holds <import>s, <style>s, <component name=...>s and <screen name=...>s`)
|
||||
return
|
||||
}
|
||||
if t.tag == "screen" {
|
||||
let at = lib_find(lib_screen_names, name)
|
||||
if at >= 0 { lib_screens[at] = t } else {
|
||||
push(lib_screen_names, name)
|
||||
push(lib_screens, t)
|
||||
}
|
||||
return
|
||||
}
|
||||
push(f.names, name)
|
||||
push(f.comps, t)
|
||||
push(f.exported, bd_name(t, "export", "") == "true")
|
||||
}
|
||||
34
packages/ludic.ui/lib_util.ludic
Normal file
34
packages/ludic.ui/lib_util.ludic
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# lib_util.ludic - a file made, found, read, and a path beside another
|
||||
function lib_find(names: []string, name: string) -> int {
|
||||
for i in 0 .. len(names) {
|
||||
if names[i] == name { return i }
|
||||
}
|
||||
return -1
|
||||
}
|
||||
function lib_new(path: string) -> UiFile {
|
||||
let f = new UiFile
|
||||
f.path = path
|
||||
f.names = new []string
|
||||
f.comps = new []UiTpl
|
||||
f.exported = new []bool
|
||||
f.imp_as = new []string
|
||||
f.imp = new []UiFile
|
||||
f.own = new []UiRule
|
||||
push(lib_files, f)
|
||||
return f
|
||||
}
|
||||
function lib_text(path: string) -> string {
|
||||
var src = ""
|
||||
if ui_be != null and ui_be.read != null { src = ui_be.read(path) } else { src = Fs.read_text(path) }
|
||||
if src == null { return "" }
|
||||
return src
|
||||
}
|
||||
# `src` read from beside the file that imports it
|
||||
function lib_beside(path: string, src: string) -> string {
|
||||
var cut = -1
|
||||
for i in 0 .. len(path) {
|
||||
if path[i] == 47 { cut = i }
|
||||
}
|
||||
if cut < 0 or (len(src) > 0 and src[0] == 47) { return src }
|
||||
return path[0..cut + 1] + src
|
||||
}
|
||||
42
packages/ludic.ui/link.ludic
Normal file
42
packages/ludic.ui/link.ludic
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
# link.ludic - each tag in a file that is not an element names a component: the file's own, an
|
||||
# unnamed import's exported one, or `as:Name` from the import called `as`
|
||||
const LK_OWN: string = " row button spacer scroll img hr if else each slot state style component screen import "
|
||||
function lib_link(f: UiFile, t: UiTpl) -> void {
|
||||
t.sheet = f.sheet
|
||||
if not lk_element(t.tag) {
|
||||
t.comp = lk_find(f, t.tag, false)
|
||||
if t.comp == null and lk_find(f, t.tag, true) != null { ui_err(`{f.path}: <{t.tag}>: that component is not exported - export="true" on it lets other files use it`) } else if t.comp == null { ui_err(`{f.path}: <{t.tag}>: there is no such element, and no component of that name here or exported by an import`) }
|
||||
}
|
||||
for i in 0 .. len(t.kids) { lib_link(f, t.kids[i]) }
|
||||
}
|
||||
function lk_element(tag: string) -> bool { return lk_in(LK_OWN, tag) or lk_in(EL_BOXES, tag) or lk_in(EL_TEXTS, tag) }
|
||||
# a word in a list of words written " a b c "
|
||||
function lk_in(words: string, w: string) -> bool {
|
||||
let pat = ` {w} `
|
||||
let n = len(pat)
|
||||
for i in 0 .. len(words) - n + 1 {
|
||||
if words[i..i + n] == pat { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
# `any` finds an unexported one too, to say why it cannot be used
|
||||
function lk_find(f: UiFile, tag: string, any: bool) -> UiTpl {
|
||||
var colon = -1
|
||||
for i in 0 .. len(tag) {
|
||||
if tag[i] == 58 { colon = i }
|
||||
}
|
||||
if colon < 0 {
|
||||
let own = lib_find(f.names, tag)
|
||||
if own >= 0 { return f.comps[own] }
|
||||
}
|
||||
for i in 0 .. len(f.imp) {
|
||||
var want = tag
|
||||
if colon >= 0 {
|
||||
if f.imp_as[i] != tag[0..colon] { continue }
|
||||
want = tag[colon + 1..len(tag)]
|
||||
} else if f.imp_as[i] != "" { continue }
|
||||
let at = lib_find(f.imp[i].names, want)
|
||||
if at >= 0 and (any or f.imp[i].exported[at]) { return f.imp[i].comps[at] }
|
||||
}
|
||||
return null
|
||||
}
|
||||
89
packages/ludic.ui/node.ludic
Normal file
89
packages/ludic.ui/node.ludic
Normal file
|
|
@ -0,0 +1,89 @@
|
|||
# node.ludic - what a template becomes each frame, the view it reads, and the renderer seam
|
||||
export const UI_BOX: int = 0
|
||||
export const UI_TEXT: int = 1
|
||||
export const UI_SPACER: int = 2
|
||||
export const UI_BUTTON: int = 3
|
||||
export const UI_SCROLL: int = 4
|
||||
export const UI_IMAGE: int = 5
|
||||
export const UI_RULE: int = 6 # <hr>: a line across
|
||||
export const UI_COL: int = 0
|
||||
export const UI_ROW: int = 1
|
||||
export const UI_FILL: float = -1.0
|
||||
export const UI_START: int = 0
|
||||
export const UI_CENTER: int = 1
|
||||
export const UI_END: int = 2
|
||||
export const UI_STRETCH: int = 3 # align: the whole cross axis
|
||||
export const UI_BETWEEN: int = 3 # justify: the spare room between, around or evenly
|
||||
export const UI_AROUND: int = 4
|
||||
export const UI_EVENLY: int = 5
|
||||
export const UI_INHERIT: int = -1
|
||||
export const UI_NONE: int = -2
|
||||
|
||||
# what a program lets its templates see: `view Name { ... }` writes view_name() -> UiView
|
||||
export property UiView {
|
||||
name: string = ""
|
||||
model: fn() -> Val = null
|
||||
call: fn(string, Val) -> Val = null
|
||||
}
|
||||
|
||||
export property UiNode {
|
||||
kind: int = 0
|
||||
dir: int = 0
|
||||
text: string = ""
|
||||
key: string = ""
|
||||
tag: string = "" # what it is called in the template, and what a selector names
|
||||
id: string = ""
|
||||
classes: []string = null
|
||||
akeys: []string = null # its attributes, as text, for [attr=value] selectors
|
||||
avals: []string = null
|
||||
vals: []Val = null # the same, as values, applied after every rule
|
||||
style: UiRule = null # its style="..." declarations
|
||||
sheet: UiSheet = null # the stylesheets of the file it was written in
|
||||
parent: UiNode = null
|
||||
index: int = 0 # its place among its parent's children, for :first-child and the rest
|
||||
src: string = "" # an <img>'s picture
|
||||
size: float = 0.0
|
||||
w: float = 0.0
|
||||
h: float = 0.0
|
||||
w_pct: float = 0.0 # a width or height as a percentage of the parent's room
|
||||
h_pct: float = 0.0
|
||||
pt: float = 0.0 # padding, top right bottom left
|
||||
pr: float = 0.0
|
||||
pb: float = 0.0
|
||||
pl: float = 0.0
|
||||
mt: float = 0.0 # margin
|
||||
mr: float = 0.0
|
||||
mb: float = 0.0
|
||||
ml: float = 0.0
|
||||
border: float = 0.0
|
||||
border_c: int = -2
|
||||
gap: float = 0.0
|
||||
align: int = 0
|
||||
justify: int = 0
|
||||
talign: int = 0 # text-align
|
||||
self: int = -1 # align-self; -1 takes the parent's align
|
||||
grow: float = 0.0 # a share of the spare room along the parent's direction
|
||||
wrap: bool = false # a row breaks into lines where it runs out of room
|
||||
min_w: float = 0.0
|
||||
max_w: float = 0.0 # 0 is no limit
|
||||
min_h: float = 0.0
|
||||
max_h: float = 0.0
|
||||
hidden: bool = false # display: none, or hidden
|
||||
fg: int = -1
|
||||
bg: int = -2
|
||||
alpha: float = 1.0
|
||||
enabled: bool = true
|
||||
act: UiAct = null
|
||||
env: UiEnv = null
|
||||
children: []UiNode = null
|
||||
mw: float = 0.0
|
||||
mh: float = 0.0
|
||||
x: float = 0.0
|
||||
y: float = 0.0
|
||||
cw: float = 0.0
|
||||
ch: float = 0.0
|
||||
rsize: float = 0.0
|
||||
rfg: int = 0
|
||||
hovered: bool = false
|
||||
}
|
||||
|
||||
5
packages/ludic.ui/package.ludic
Normal file
5
packages/ludic.ui/package.ludic
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
# ludic.ui — components: a UI declared as a tree of function calls, rebuilt from the program's
|
||||
# state every frame, laid out, and drawn through whatever renderer the program registers.
|
||||
package "ludic.ui"
|
||||
version "0.1.0"
|
||||
kind source
|
||||
17
packages/ludic.ui/screen.ludic
Normal file
17
packages/ludic.ui/screen.ludic
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# screen.ludic - the 2D screen as a renderer, for a program that draws with Screen.*
|
||||
function sc_rect(x: float, y: float, w: float, h: float, c: int, a: float) -> void { Screen.fill_rectangle(int(x), int(y), int(w), int(h), c) }
|
||||
function sc_text(x: float, y: float, s: string, size: float, c: int) -> void { Screen.draw_text(int(x), int(y), s, c, Math.max(1, int(size / 8.0))) }
|
||||
function sc_measure(s: string, size: float) -> float { return float(Screen.measure_text(s) * Math.max(1, int(size / 8.0))) }
|
||||
function sc_px() -> float { return float(Input.mouse_x()) }
|
||||
function sc_py() -> float { return float(Input.mouse_y()) }
|
||||
function sc_down() -> bool { return Input.mouse_down(0) }
|
||||
export function ui_screen_backend() -> UiBackend {
|
||||
let b = new UiBackend
|
||||
b.rect = fn sc_rect
|
||||
b.text = fn sc_text
|
||||
b.measure = fn sc_measure
|
||||
b.pointer_x = fn sc_px
|
||||
b.pointer_y = fn sc_py
|
||||
b.pointer_down = fn sc_down
|
||||
return b
|
||||
}
|
||||
70
packages/ludic.ui/select.ludic
Normal file
70
packages/ludic.ui/select.ludic
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# select.ludic - CSS selectors: a compound (`button.primary#buy[type=big]:hover:not(.off)`) joined
|
||||
# to the one on its left by a space (anywhere inside it) or `>` (straight inside it)
|
||||
export property UiSel {
|
||||
tag: string = ""
|
||||
id: string = ""
|
||||
classes: []string = null
|
||||
akeys: []string = null
|
||||
avals: []string = null # "" with aops 0 is [attr]; aops 1 is [attr=value]
|
||||
aops: []int = null
|
||||
pseudos: []string = null
|
||||
pargs: []string = null
|
||||
nots: []UiSel = null
|
||||
prev: UiSel = null
|
||||
comb: int = 0 # 0: prev is an ancestor, 1: prev is the parent
|
||||
}
|
||||
function sel_new() -> UiSel {
|
||||
let s = new UiSel
|
||||
s.classes = new []string
|
||||
s.akeys = new []string
|
||||
s.avals = new []string
|
||||
s.aops = new []int
|
||||
s.pseudos = new []string
|
||||
s.pargs = new []string
|
||||
s.nots = new []UiSel
|
||||
return s
|
||||
}
|
||||
function sel_parse(src: string, err: []string) -> UiSel {
|
||||
let r = new UiRd
|
||||
r.s = src
|
||||
var cur: UiSel = null
|
||||
var comb = 0
|
||||
while r.err == "" {
|
||||
rd_ws(r)
|
||||
if r.i >= len(r.s) { break }
|
||||
if rd_op(r, ">") {
|
||||
comb = 1
|
||||
continue
|
||||
}
|
||||
let c = sel_compound(r)
|
||||
c.prev = cur
|
||||
c.comb = comb
|
||||
cur = c
|
||||
comb = 0
|
||||
}
|
||||
if r.err != "" { push(err, r.err) }
|
||||
if cur == null { push(err, `"{src}" selects nothing`) }
|
||||
return cur
|
||||
}
|
||||
function sel_is_name(c: int) -> bool { return rd_is_word(c) or c == 45 }
|
||||
function sel_name(r: UiRd) -> string {
|
||||
let at = r.i
|
||||
while r.i < len(r.s) and sel_is_name(r.s[r.i]) { r.i += 1 }
|
||||
if r.i == at { rd_fail(r, "a name") }
|
||||
return r.s[at..r.i]
|
||||
}
|
||||
function sel_compound(r: UiRd) -> UiSel {
|
||||
let s = sel_new()
|
||||
if rd_c(r, 0) == 42 { r.i += 1 } else if sel_is_name(rd_c(r, 0)) { s.tag = sel_name(r) }
|
||||
while r.err == "" and r.i < len(r.s) {
|
||||
let c = rd_c(r, 0)
|
||||
if c == 35 {
|
||||
r.i += 1
|
||||
s.id = sel_name(r)
|
||||
} else if c == 46 {
|
||||
r.i += 1
|
||||
push(s.classes, sel_name(r))
|
||||
} else if c == 91 { sel_attr(r, s) } else if c == 58 { sel_pseudo(r, s) } else if c == 42 { r.i += 1 } else { return s }
|
||||
}
|
||||
return s
|
||||
}
|
||||
74
packages/ludic.ui/select_match.ludic
Normal file
74
packages/ludic.ui/select_match.ludic
Normal file
|
|
@ -0,0 +1,74 @@
|
|||
# select_match.ludic - whether a selector picks a node, and how specific it is (as CSS counts)
|
||||
function sel_spec(s: UiSel) -> int {
|
||||
var n = 0
|
||||
var at = s
|
||||
while at != null {
|
||||
if at.id != "" { n = n + 100 }
|
||||
n = n + (len(at.classes) + len(at.akeys) + len(at.pseudos)) * 10
|
||||
if at.tag != "" { n = n + 1 }
|
||||
for i in 0 .. len(at.nots) { n = n + sel_spec(at.nots[i]) }
|
||||
at = at.prev
|
||||
}
|
||||
return n
|
||||
}
|
||||
function sel_match(s: UiSel, n: UiNode) -> bool {
|
||||
if not sel_one(s, n) { return false }
|
||||
if s.prev == null { return true }
|
||||
if s.comb == 1 { return n.parent != null and sel_match(s.prev, n.parent) }
|
||||
var a = n.parent
|
||||
while a != null {
|
||||
if sel_match(s.prev, a) { return true }
|
||||
a = a.parent
|
||||
}
|
||||
return false
|
||||
}
|
||||
function sel_one(s: UiSel, n: UiNode) -> bool {
|
||||
if s.tag != "" and s.tag != n.tag { return false }
|
||||
if s.id != "" and s.id != n.id { return false }
|
||||
for i in 0 .. len(s.classes) {
|
||||
if not sel_has(n.classes, s.classes[i]) { return false }
|
||||
}
|
||||
for i in 0 .. len(s.akeys) {
|
||||
let v = sel_attr_of(n, s.akeys[i])
|
||||
if v == null or (s.aops[i] == 1 and v != s.avals[i]) { return false }
|
||||
}
|
||||
for i in 0 .. len(s.pseudos) {
|
||||
if not sel_pseudo_ok(s.pseudos[i], s.pargs[i], n) { return false }
|
||||
}
|
||||
for i in 0 .. len(s.nots) {
|
||||
if sel_one(s.nots[i], n) { return false }
|
||||
}
|
||||
return true
|
||||
}
|
||||
function sel_has(xs: []string, x: string) -> bool {
|
||||
if xs == null { return false }
|
||||
for i in 0 .. len(xs) {
|
||||
if xs[i] == x { return true }
|
||||
}
|
||||
return false
|
||||
}
|
||||
function sel_attr_of(n: UiNode, key: string) -> string {
|
||||
if n.akeys == null { return null }
|
||||
for i in 0 .. len(n.akeys) {
|
||||
if n.akeys[i] == key { return n.avals[i] }
|
||||
}
|
||||
return null
|
||||
}
|
||||
function sel_pseudo_ok(p: string, arg: string, n: UiNode) -> bool {
|
||||
if p == "hover" { return n.hovered }
|
||||
if p == "disabled" { return not n.enabled }
|
||||
if p == "enabled" { return n.enabled }
|
||||
if p == "first-child" { return n.index == 0 }
|
||||
if p == "last-child" { return n.parent != null and n.index == len(n.parent.children) - 1 }
|
||||
if p == "only-child" { return n.parent != null and len(n.parent.children) == 1 }
|
||||
if p == "empty" { return len(n.children) == 0 and n.text == "" }
|
||||
if p == "root" { return n.parent == null or n.parent.tag == "" }
|
||||
if p == "nth-child" {
|
||||
let k = n.index + 1
|
||||
if arg == "odd" { return k % 2 == 1 }
|
||||
if arg == "even" { return k % 2 == 0 }
|
||||
return string(k) == arg
|
||||
}
|
||||
ui_err(`:{p}: there is no such pseudo-class here`)
|
||||
return false
|
||||
}
|
||||
35
packages/ludic.ui/select_parts.ludic
Normal file
35
packages/ludic.ui/select_parts.ludic
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
# select_parts.ludic - a compound's [attribute] and :pseudo-class parts
|
||||
function sel_attr(r: UiRd, s: UiSel) -> void {
|
||||
r.i += 1
|
||||
push(s.akeys, sel_name(r))
|
||||
if not rd_op(r, "=") {
|
||||
push(s.aops, 0)
|
||||
push(s.avals, "")
|
||||
} else {
|
||||
push(s.aops, 1)
|
||||
rd_ws(r)
|
||||
let q = rd_c(r, 0)
|
||||
if q == 34 or q == 39 { push(s.avals, Value.as_str(px_string(r, q).v)) } else { push(s.avals, sel_name(r)) }
|
||||
}
|
||||
if not rd_op(r, "]") { rd_fail(r, "']'") }
|
||||
}
|
||||
function sel_pseudo(r: UiRd, s: UiSel) -> void {
|
||||
r.i += 1
|
||||
let name = sel_name(r)
|
||||
var arg = ""
|
||||
if rd_c(r, 0) == 40 {
|
||||
r.i += 1
|
||||
let at = r.i
|
||||
while r.i < len(r.s) and r.s[r.i] != 41 { r.i += 1 }
|
||||
arg = r.s[at..r.i]
|
||||
r.i += 1
|
||||
}
|
||||
if name == "not" {
|
||||
let inner = new UiRd
|
||||
inner.s = tpl_words(arg)
|
||||
push(s.nots, sel_compound(inner))
|
||||
return
|
||||
}
|
||||
push(s.pseudos, name)
|
||||
push(s.pargs, tpl_words(arg))
|
||||
}
|
||||
34
packages/ludic.ui/state.ludic
Normal file
34
packages/ludic.ui/state.ludic
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
# state.ludic - a component's or a screen's own values, <state name="{first}"/>, kept from frame
|
||||
# to frame by where the instance is in the tree, and set by `set name = value`
|
||||
var st_keys: []string = new []string
|
||||
var st_vals: []Val = new []Val
|
||||
function st_for(key: string, t: UiTpl, e: UiEnv) -> Val {
|
||||
var decl: UiTpl = null
|
||||
for i in 0 .. len(t.kids) {
|
||||
if t.kids[i].tag == "state" { decl = t.kids[i] }
|
||||
}
|
||||
if decl == null { return null }
|
||||
for i in 0 .. len(st_keys) {
|
||||
if st_keys[i] == key { return st_vals[i] }
|
||||
}
|
||||
let o = Value.object()
|
||||
for i in 0 .. len(decl.keys) { Value.put(o, decl.keys[i], ui_eval(decl.vals[i], e)) }
|
||||
push(st_keys, key)
|
||||
push(st_vals, o)
|
||||
return o
|
||||
}
|
||||
# a screen's state starts again the next time it opens
|
||||
export function ui_forget(screen: string) -> void {
|
||||
let keys = new []string
|
||||
let vals = new []Val
|
||||
for i in 0 .. len(st_keys) {
|
||||
let k = st_keys[i]
|
||||
let mine = len(k) >= len(screen) and k[0..len(screen)] == screen and (len(k) == len(screen) or k[len(screen)] == 47)
|
||||
if not mine {
|
||||
push(keys, k)
|
||||
push(vals, st_vals[i])
|
||||
}
|
||||
}
|
||||
st_keys = keys
|
||||
st_vals = vals
|
||||
}
|
||||
84
packages/ludic.ui/style.ludic
Normal file
84
packages/ludic.ui/style.ludic
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# style.ludic - stylesheets: `.chip { bg: bg2; pad: 6 } button { h: 40 }` in a <style>, or an .lss
|
||||
# file brought in by <import src="look.lss"/> (which may `@import "base.lss";` in turn). A selector is
|
||||
# a tag, a .class, both, or *; a property is any attribute, {holes} and all. The more specific rule
|
||||
# wins, then the later; an attribute beats them all.
|
||||
export property UiRule {
|
||||
sel: UiSel = null
|
||||
spec: int = 0
|
||||
keys: []string = null
|
||||
vals: []UiExpr = null
|
||||
}
|
||||
export property UiSheet {
|
||||
rules: []UiRule = null
|
||||
}
|
||||
function sheet_read(f: UiFile, t: UiTpl) -> void {
|
||||
var src = t.raw
|
||||
let from = bd_name(t, "src", "")
|
||||
if from != "" { src = lib_text(lib_beside(f.path, from)) }
|
||||
lss_text(f, src)
|
||||
}
|
||||
function lss_is(path: string) -> bool { return len(path) > 4 and path[len(path) - 4..len(path)] == ".lss" }
|
||||
# a stylesheet file: a file with rules and imports and nothing else
|
||||
function lss_file(src: string, path: string) -> UiFile {
|
||||
let f = lib_new(path)
|
||||
lss_text(f, src)
|
||||
f.sheet = sheet_of(f)
|
||||
return f
|
||||
}
|
||||
function lss_text(f: UiFile, src: string) -> void {
|
||||
let err = new []string
|
||||
let r = new UiRd
|
||||
r.s = lss_strip(src)
|
||||
while r.err == "" {
|
||||
rd_ws(r)
|
||||
if r.i >= len(r.s) { break }
|
||||
if rd_op(r, "@import") {
|
||||
lss_import(f, lss_upto(r, 59))
|
||||
continue
|
||||
}
|
||||
let sels = lss_upto(r, 123)
|
||||
let body = lss_upto(r, 125)
|
||||
lss_rule(f, sels, body, err)
|
||||
}
|
||||
for i in 0 .. len(err) { ui_err(`{f.path}: <style> {err[i]}`) }
|
||||
}
|
||||
# @import "base.lss"; - its rules come before this file's own
|
||||
function lss_import(f: UiFile, what: string) -> void {
|
||||
var p = tpl_words(what)
|
||||
if len(p) > 5 and p[0..4] == "url(" { p = p[4..len(p) - 1] }
|
||||
if len(p) >= 2 and (p[0] == 34 or p[0] == 39) { p = p[1..len(p) - 1] }
|
||||
let other = lib_file(lib_beside(f.path, p))
|
||||
if other != null {
|
||||
push(f.imp, other)
|
||||
push(f.imp_as, "")
|
||||
}
|
||||
}
|
||||
# the text up to the character `c`, which is taken too; a {hole} inside a body is skipped whole
|
||||
function lss_upto(r: UiRd, c: int) -> string {
|
||||
let at = r.i
|
||||
var depth = 0
|
||||
while r.i < len(r.s) and (r.s[r.i] != c or depth > 0) {
|
||||
if c == 125 and r.s[r.i] == 123 { depth += 1 }
|
||||
if c == 125 and r.s[r.i] == 125 { depth -= 1 }
|
||||
r.i += 1
|
||||
}
|
||||
let out: string = r.s[at..r.i]
|
||||
if r.i < len(r.s) { r.i += 1 } else { rd_fail(r, "a closing brace") }
|
||||
return out
|
||||
}
|
||||
function lss_strip(src: string) -> string {
|
||||
var out = ""
|
||||
var at = 0
|
||||
var i = 0
|
||||
while i + 1 < len(src) {
|
||||
if src[i] == 47 and src[i + 1] == 42 {
|
||||
out = out + src[at..i]
|
||||
var j = i + 2
|
||||
while j + 1 < len(src) and not (src[j] == 42 and src[j + 1] == 47) { j += 1 }
|
||||
i = j + 2
|
||||
at = i
|
||||
} else { i += 1 }
|
||||
}
|
||||
if at < len(src) { out = out + src[at..len(src)] }
|
||||
return out
|
||||
}
|
||||
71
packages/ludic.ui/style_rule.ludic
Normal file
71
packages/ludic.ui/style_rule.ludic
Normal file
|
|
@ -0,0 +1,71 @@
|
|||
# style_rule.ludic - a rule read, the cascade a file sees, and a node's rules applied to it
|
||||
function lss_rule(f: UiFile, sels: string, body: string, err: []string) -> void {
|
||||
let decls = lss_decls(body, err)
|
||||
let each = lss_split(sels, 44)
|
||||
for i in 0 .. len(each) {
|
||||
let sel = tpl_words(each[i])
|
||||
if len(sel) == 0 { continue }
|
||||
let r = new UiRule
|
||||
r.keys = decls.keys
|
||||
r.vals = decls.vals
|
||||
r.sel = sel_parse(sel, err)
|
||||
if r.sel != null {
|
||||
r.spec = sel_spec(r.sel)
|
||||
push(f.own, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
# `key: value; key: value`, a rule's body or a style="..." attribute
|
||||
function lss_decls(body: string, err: []string) -> UiRule {
|
||||
let r = new UiRule
|
||||
r.keys = new []string
|
||||
r.vals = new []UiExpr
|
||||
let decls = lss_split(body, 59)
|
||||
for i in 0 .. len(decls) {
|
||||
let d = decls[i]
|
||||
var colon = -1
|
||||
for k in 0 .. len(d) {
|
||||
if colon < 0 and d[k] == 58 { colon = k }
|
||||
}
|
||||
if colon < 0 {
|
||||
if len(tpl_words(d)) > 0 { push(err, `"{tpl_words(d)}" is not a property: value`) }
|
||||
continue
|
||||
}
|
||||
push(r.keys, tpl_words(d[0..colon]))
|
||||
push(r.vals, tpl_value(tpl_words(d[colon + 1..len(d)]), err))
|
||||
}
|
||||
return r
|
||||
}
|
||||
function lss_split(s: string, c: int) -> []string {
|
||||
let out = new []string
|
||||
var at = 0
|
||||
for i in 0 .. len(s) {
|
||||
if s[i] == c {
|
||||
push(out, s[at..i])
|
||||
at = i + 1
|
||||
}
|
||||
}
|
||||
push(out, s[at..len(s)])
|
||||
return out
|
||||
}
|
||||
# the imports' rules, then the file's own, ordered by specificity and otherwise as written
|
||||
function sheet_of(f: UiFile) -> UiSheet {
|
||||
let sh = new UiSheet
|
||||
sh.rules = new []UiRule
|
||||
for i in 0 .. len(f.imp) {
|
||||
if f.imp[i].sheet != null {
|
||||
for k in 0 .. len(f.imp[i].sheet.rules) { sheet_add(sh, f.imp[i].sheet.rules[k]) }
|
||||
}
|
||||
}
|
||||
for k in 0 .. len(f.own) { sheet_add(sh, f.own[k]) }
|
||||
return sh
|
||||
}
|
||||
function sheet_add(sh: UiSheet, r: UiRule) -> void {
|
||||
push(sh.rules, r)
|
||||
var i = len(sh.rules) - 1
|
||||
while i > 0 and sh.rules[i - 1].spec > r.spec {
|
||||
sh.rules[i] = sh.rules[i - 1]
|
||||
i -= 1
|
||||
}
|
||||
sh.rules[i] = r
|
||||
}
|
||||
84
packages/ludic.ui/tpl.ludic
Normal file
84
packages/ludic.ui/tpl.ludic
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
# tpl.ludic - a template file read into what the builder walks: every attribute and every
|
||||
# element's text an expression, every on-* attribute an action. A file holds <component>s and
|
||||
# <screen>s under one root; a component is used as a tag of its own name anywhere after.
|
||||
export property UiTpl {
|
||||
tag: string = ""
|
||||
keys: []string = null
|
||||
vals: []UiExpr = null
|
||||
on_keys: []string = null
|
||||
on_acts: []UiAct = null
|
||||
text: UiExpr = null
|
||||
kids: []UiTpl = null
|
||||
comp: UiTpl = null # the component this tag names, found when its file loads
|
||||
sheet: UiSheet = null # the styles of the file it was written in
|
||||
style: UiRule = null # its style="..." attribute, read as declarations
|
||||
raw: string = "" # a <style>'s text, which is not a template's
|
||||
}
|
||||
|
||||
function tpl_read(x: Xml, file: string) -> UiTpl {
|
||||
let t = new UiTpl
|
||||
t.tag = Xml.tag(x)
|
||||
t.keys = new []string
|
||||
t.vals = new []UiExpr
|
||||
t.on_keys = new []string
|
||||
t.on_acts = new []UiAct
|
||||
t.kids = new []UiTpl
|
||||
let err = new []string
|
||||
for i in 0 .. len(x.akeys) {
|
||||
let k: string = x.akeys[i]
|
||||
let v: string = x.avals[i]
|
||||
if len(k) > 3 and k[0..3] == "on-" {
|
||||
push(t.on_keys, tpl_event(k[3..len(k)]))
|
||||
push(t.on_acts, act_parse(v, err))
|
||||
} else if k == "onclick" {
|
||||
push(t.on_keys, "press")
|
||||
push(t.on_acts, act_parse(v, err))
|
||||
} else if k == "style" { t.style = lss_decls(v, err) } else {
|
||||
push(t.keys, k)
|
||||
push(t.vals, tpl_value(v, err))
|
||||
}
|
||||
}
|
||||
for i in 0 .. Xml.child_count(x) { push(t.kids, tpl_read(Xml.child(x, i), file)) }
|
||||
if t.tag == "style" {
|
||||
t.raw = Xml.text(x)
|
||||
return t
|
||||
}
|
||||
let words = tpl_words(Xml.text(x))
|
||||
if len(words) > 0 and len(t.kids) == 0 { t.text = ex_text(words, err) }
|
||||
for i in 0 .. len(err) { ui_err(`{file}: <{t.tag}> {err[i]}`) }
|
||||
return t
|
||||
}
|
||||
# on-click is on-press, as a browser would have it
|
||||
function tpl_event(name: string) -> string {
|
||||
if name == "click" { return "press" }
|
||||
return name
|
||||
}
|
||||
# an attribute: a plain number is a number, anything else text with {holes}
|
||||
function tpl_value(v: string, err: []string) -> UiExpr {
|
||||
var num = len(v) > 0
|
||||
for i in 0 .. len(v) {
|
||||
if not ((v[i] >= 48 and v[i] <= 57) or v[i] == 46 or (i == 0 and v[i] == 45)) { num = false }
|
||||
}
|
||||
if num { return ex_parse(v, err) }
|
||||
return ex_text(v, err)
|
||||
}
|
||||
# text as a reader sees it: the ends trimmed and every run of white space one space
|
||||
function tpl_words(s: string) -> string {
|
||||
var out = ""
|
||||
var gap = false
|
||||
for i in 0 .. len(s) {
|
||||
let c = s[i]
|
||||
if c == 32 or c == 9 or c == 10 or c == 13 { gap = len(out) > 0 } else {
|
||||
if gap { out = out + " " }
|
||||
gap = false
|
||||
out = out + s[i..i + 1]
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
function tpl_attr(t: UiTpl, key: string) -> UiExpr {
|
||||
for i in 0 .. len(t.keys) {
|
||||
if t.keys[i] == key { return t.vals[i] }
|
||||
}
|
||||
return null
|
||||
}
|
||||
70
packages/ludic.ui/values.ludic
Normal file
70
packages/ludic.ui/values.ludic
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
# values.ludic - arithmetic, comparison, truth and text over a template's values. A sum of two
|
||||
# whole numbers stays whole; anything with a fraction in it is a float; a string in a sum joins.
|
||||
function vl_bool(b: bool) -> Val {
|
||||
if b { return Value.bool(1) }
|
||||
return Value.bool(0)
|
||||
}
|
||||
function vl_num(v: Val) -> bool { return Value.kind(v) == 1 or Value.kind(v) == 2 or Value.kind(v) == 7 }
|
||||
function vl_truthy(v: Val) -> bool {
|
||||
let k = Value.kind(v)
|
||||
if k == 0 { return false }
|
||||
if k == 1 or k == 3 { return Value.as_int(v) != 0 }
|
||||
if k == 2 or k == 7 { return Value.as_float(v) != 0.0 }
|
||||
if k == 4 { return len(Value.as_str(v)) > 0 }
|
||||
return true
|
||||
}
|
||||
function vl_text(v: Val) -> string {
|
||||
let k = Value.kind(v)
|
||||
if k == 0 { return "" }
|
||||
if k == 1 { return string(Value.as_int(v)) }
|
||||
if k == 3 {
|
||||
if Value.as_int(v) != 0 { return "true" }
|
||||
return "false"
|
||||
}
|
||||
if k == 4 { return Value.as_str(v) }
|
||||
if k == 2 or k == 7 {
|
||||
let f = Value.as_float(v)
|
||||
if f == float(int(f)) { return string(int(f)) }
|
||||
return string(f)
|
||||
}
|
||||
return Json.encode(v)
|
||||
}
|
||||
function vl_arith(op: string, a: Val, b: Val) -> Val {
|
||||
if Value.kind(a) == 1 and Value.kind(b) == 1 {
|
||||
let x = Value.as_int(a)
|
||||
let y = Value.as_int(b)
|
||||
if op == "+" { return Value.int(x + y) }
|
||||
if op == "-" { return Value.int(x - y) }
|
||||
if op == "*" { return Value.int(x * y) }
|
||||
if y == 0 { return Value.int(0) }
|
||||
if op == "/" { return Value.int(x / y) }
|
||||
return Value.int(x % y)
|
||||
}
|
||||
let x = Value.as_float(a)
|
||||
let y = Value.as_float(b)
|
||||
if op == "+" { return Value.float(x + y) }
|
||||
if op == "-" { return Value.float(x - y) }
|
||||
if op == "*" { return Value.float(x * y) }
|
||||
if y == 0.0 { return Value.float(0.0) }
|
||||
if op == "/" { return Value.float(x / y) }
|
||||
return Value.float(x - y * float(int(x / y)))
|
||||
}
|
||||
function vl_equal(a: Val, b: Val) -> bool {
|
||||
if vl_num(a) and vl_num(b) { return Value.as_float(a) == Value.as_float(b) }
|
||||
if Value.kind(a) != Value.kind(b) { return false }
|
||||
if Value.kind(a) == 4 { return Value.as_str(a) == Value.as_str(b) }
|
||||
if Value.kind(a) == 3 { return Value.as_int(a) == Value.as_int(b) }
|
||||
return Value.kind(a) == 0
|
||||
}
|
||||
function vl_binary(op: string, a: Val, b: Val) -> Val {
|
||||
if op == "==" { return vl_bool(vl_equal(a, b)) }
|
||||
if op == "!=" { return vl_bool(not vl_equal(a, b)) }
|
||||
if op == "+" and (Value.kind(a) == 4 or Value.kind(b) == 4) { return Value.str(vl_text(a) + vl_text(b)) }
|
||||
if op == "+" or op == "-" or op == "*" or op == "/" or op == "%" { return vl_arith(op, a, b) }
|
||||
let x = Value.as_float(a)
|
||||
let y = Value.as_float(b)
|
||||
if op == "<" { return vl_bool(x < y) }
|
||||
if op == "<=" { return vl_bool(x <= y) }
|
||||
if op == ">" { return vl_bool(x > y) }
|
||||
return vl_bool(x >= y)
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue