feat(ui): ludic.ui grows toward HTML, CSS and React (Phase 0.U1-U4)

- Natives: ui_native(tag, measure, draw) makes an element the program draws itself; ui_fire
  runs its on-<event> with event.value. input, select and textarea have simple defaults.
- React:
  - keyed <each>, <let>, <provide> context through components, <fragment>;
  - named slots, default props on <component>;
  - on-mount / on-unmount;
  - inline text inside text elements.
- CSS:
  - custom properties and var();
  - position relative/absolute/fixed with insets and z-index;
  - em/rem/vw/vh and @media;
  - wrapping text and ellipsis, overflow;
  - + and ~ combinators, :nth-child(an+b), :checked, :active.
- Developing: errors with file:line, ui_errors(), ui_reload() keeping state, and an
  inspector-style ui_dump.
- view fields infer the type of a literal or a named function's result.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 16:17:25 +03:00
parent 66a2bc2214
commit 637d07400e
39 changed files with 42012 additions and 40795 deletions

View file

@ -388,6 +388,37 @@ string.
- A file's cascade is its imports' rules, then its own. `<import src="look.lss"/>` brings a
stylesheet in, and one stylesheet can `@import` another, so a look is a file others can use.
A library's components keep the styles of the file they were written in.
- **More CSS.**
- Custom properties: `--accent: #fc0` on any element or `:root`, read with `var(--accent)` or
`var(--accent, gold)`, and inherited by what is inside.
- `position: relative|absolute|fixed` with `top`, `right`, `bottom`, `left` and `inset`, and
`z-index`. An absolute element sits in its parent's box, a fixed one in the screen's, and
neither takes room in the flow.
- Units: `em`, `rem`, `vw` and `vh`.
- `@media (min-width: ..) and (max-height: ..) { ... }`.
- Text wraps between words to fit its box; `white-space: nowrap` keeps one line and
`text-overflow: ellipsis` cuts it.
- `overflow: auto|scroll|hidden` makes a box scroll.
- Selectors also take `+` and `~`, `:nth-child(2n+1)`, `:checked` and `:active`.
- **More React.**
- `<each ... key="{item.id}">` keeps an item's state when the list is reordered.
- `<let name="{value}"/>` names a value for the siblings after it.
- `<provide name="{value}">` gives a value to everything inside, components included.
- `<fragment>` groups without a box.
- `<slot name="title"/>` takes the user's `<template slot="title">`.
- `on-mount` and `on-unmount` run when an element or component appears and goes.
- Words inside a text element read as one line: `<p>Hi <b>there</b></p>`.
- **Natives and forms.** `ui_native("dial", measure, draw)` makes `<dial>` an element the program
draws itself, laid out and styled like any other. It reports with `ui_fire(n, "change",
value)`, which runs the element's `on-change` with `event.value`, and it reads its attributes
with `ui_attr`. `<input type="checkbox|radio|text|range">`, `<select>` with `<option
value>`, and `<textarea>` are natives with simple defaults, which a renderer replaces with its
own.
- **Developing.**
- Every error names the template's file and line, and `ui_errors()` lists them for a host to show.
- `ui_reload()` reads changed templates and stylesheets again and keeps each screen's state.
- `ui_dump` prints the tree the way an inspector would (`div#id.class`, its box and its
computed styles).
- **Bindings.** Any attribute and any text can hold `{expressions}`. An attribute that is one
`{expression}` and nothing else keeps its type. Expressions read loop names, props, state and the
model, and support `.field`, `[index]`, arithmetic, comparisons, `and` / `or` / `not`,

View file

@ -27,6 +27,15 @@ with:
`:hover`, `:disabled`, `:first-child`, `:last-child`, `:nth-child`, `:not` and more, weighed by
specificity.
- more CSS: custom properties and `var()`, `position` with insets and `z-index`, `em`/`rem`/`vw`/`vh`,
`@media`, wrapping text and ellipsis, `overflow`, `+`/`~`, `:nth-child(an+b)`, `:checked`, `:active`;
- more React: keyed lists, `<let>`, `<provide>` context, `<fragment>`, named slots, `on-mount` and
`on-unmount`;
- native elements a program draws itself (`ui_native`, `ui_fire`), and form controls;
- errors with file and line, hot reload (`ui_reload`), and an inspector-style dump.
A `view` field set to a literal or a named function's result needs no type.
Screens are drawn through a registered renderer. `Value` gains a float kind.
Also:

View file

@ -0,0 +1,26 @@
# ui_css.ludic — ludic.ui's CSS without a renderer: custom properties and var(), absolute, fixed and
# relative positioning, em, rem and vw, @media, text that wraps, sibling combinators, :nth-child
# formulas, :checked, and overflow making a box scroll.
import "ludic.ui"
program UiCss {
numbers float
view Css {
wide = true
}
const LOOK: string = "screen { --gap: 6px; --accent: #ff8800 }\n.panel { position: relative; width: 300px; height: 200px; padding: 10px }\n.badge { position: absolute; right: 5px; bottom: 5px; width: 40px; height: 20px; background: var(--accent) }\n.corner { position: fixed; left: 0; top: 0; width: 10vw; height: 2rem }\n.nudge { position: relative; left: 7px; top: 3px }\n.big { font-size: 2em }\n.missing { color: var(--nope, #00ff00) }\n@media (max-width: 500px) { .wide-only { display: none } }\n@media (min-width: 500px) { .narrow-only { display: none } }\nli + li { margin-top: var(--gap) }\nh3 ~ p { color: var(--accent) }\nli:nth-child(2n+1) { color: #0000ff }\ninput:checked { opacity: 0.25 }\n.list { overflow: auto; height: 50px }\n.para { width: 120px }\n"
const PAGE: string = "<ui>\n<import src='look.lss'/>\n<screen name='css'>\n <div class='panel'><span class='badge'>!</span><p class='nudge'>moved</p><p class='big'>big</p></div>\n <div class='corner'/>\n <p class='wide-only'>wide</p><p class='narrow-only'>narrow</p>\n <ul><li>a</li><li>b</li><li>c</li></ul>\n <h3>h</h3><p>after</p><p class='missing'>fallback</p>\n <input type='checkbox' checked='true'/>\n <div class='list'><p>1</p><p>2</p><p>3</p><p>4</p></div>\n <p class='para'>a sentence long enough that it has to wrap onto several lines</p>\n</screen>\n</ui>\n"
entry {
ui_load_text(LOOK, "ui/look.lss")
ui_load_text(PAGE, "ui/css.xml")
ui_viewport(400.0, 300.0)
let root: UiNode = ui_nodes("css", view_css())
ui_place(root, 0.0, 0.0, 400.0, 300.0)
let panel = root.children[0]
let badge = panel.children[0]
let ul = root.children[3]
let para = root.children[len(root.children) - 1]
print(`badge at {int(badge.x)},{int(badge.y)} bg {badge.bg}; nudged {int(panel.children[1].x)},{int(panel.children[1].y)}; big {int(panel.children[2].rsize)}; corner {int(root.children[1].x)},{int(root.children[1].y)} {int(root.children[1].cw)}x{int(root.children[1].ch)}`)
print(`media keeps {root.children[2].text}; li gaps {int(ul.children[1].mt)} {int(ul.children[0].mt)}; li colours {ul.children[0].fg} {ul.children[1].fg} {ul.children[2].fg}; after {root.children[5].fg}; fallback {root.children[6].fg}`)
print(`checked opacity {root.children[7].alpha}; list scrolls {root.children[8].kind == UI_SCROLL} {int(root.children[8].ch)} of {int(root.children[8].content_h)}; para lines {len(para.lines)}`)
}
}

View file

@ -0,0 +1,59 @@
# ui_react.ludic — ludic.ui's React side without a renderer: keyed lists keep each item's state when
# the list is reordered, <let> names a value, context reaches through a component, named slots,
# on-mount and on-unmount, and a native element of the program's own firing on-change with a value.
import "ludic.ui"
program UiReact {
numbers float
var names: []string = new []string
var volume: int = 3
var mounted: int = 0
var unmounted: int = 0
var show_extra: bool = true
view Mixer {
names = names
volume = volume
extra = show_extra
on set_volume(v: int) { volume = v }
on mount() { mounted += 1 }
on unmount() { unmounted += 1 }
}
# a dial the program draws itself: it measures 40 square and, pressed, turns up by one
function dial_measure(n: UiNode, avail: float) -> void {
n.mw = 40.0
n.mh = 40.0
}
function dial_draw(n: UiNode) -> void { }
const PAGE: string = "<ui>\n<component name='Card'>\n <div class='card'><h3><slot name='title'/></h3><slot/><p>{theme}</p></div>\n</component>\n<component name='Item'>\n <state taps='{0}'/>\n <button on-click='set taps = taps + 1'>{label} {taps}</button>\n</component>\n<screen name='mix'>\n <let loud='{volume > 5}'/>\n <provide theme='dark'>\n <Card>\n <template slot='title'>Mixer</template>\n <fragment><p>{loud ? 'loud' : 'quiet'}</p></fragment>\n </Card>\n </provide>\n <each in='{names}' as='n' key='{n}'><Item label='{n}'/></each>\n <dial value='{volume}' on-change='set_volume(event.value)'/>\n <input type='checkbox' checked='{extra}' on-change='set on = event.value'/>\n <if test='{extra}'><p on-mount='mount()' on-unmount='unmount()'>extra</p></if>\n <state on='{false}'/>\n <p>checkbox {on}</p>\n</screen>\n</ui>\n"
function texts(n: UiNode, out: []string) -> void {
if n.text != "" { push(out, n.text) }
for i in 0 .. len(n.children) { texts(n.children[i], out) }
}
function frame() -> UiNode {
ui_show("mix", view_mixer(), 0.0, 0.0, 400.0, 400.0)
let root: UiNode = ui_nodes("mix", view_mixer())
ui_place(root, 0.0, 0.0, 400.0, 400.0)
return root
}
entry {
ui_native("dial", fn dial_measure, fn dial_draw)
push(names, "bass")
push(names, "drums")
ui_load_text(PAGE, "ui/mix.xml")
var root = frame()
ui_press(root.children[2])
names[0] = "drums"
names[1] = "bass"
root = frame()
let dial = root.children[3]
ui_event(dial, "change", Value.int(volume + 4))
ui_event(root.children[4], "change", Value.bool(1))
show_extra = false
root = frame()
let out = new []string
texts(root, out)
var joined = ""
for i in 0 .. len(out) { joined = joined + out[i] + "|" }
print(joined)
print(`volume {volume}; dial {int(dial.cw)}x{int(dial.ch)}; mounted {mounted}, unmounted {unmounted}`)
}
}

View file

@ -0,0 +1,27 @@
# ui_reload.ludic — a template read from disk, changed there, and read again by ui_reload: the new
# text shows at once, and the screen's state (a counter pressed before the change) is kept.
import "ludic.ui"
program UiReload {
numbers float
view Note {
title = "note"
}
const PATH: string = "build/ui_reload_example.xml"
const V1: string = "<ui><screen name='note'><state n='{0}'/><button on-click='set n = n + 1'>{title} {n}</button></screen></ui>"
const V2: string = "<ui><screen name='note'><state n='{0}'/><h1>{title} again {n}</h1></screen></ui>"
function shown() -> string {
let root: UiNode = ui_nodes("note", view_note())
return root.children[0].text
}
entry {
Fs.write_text(PATH, V1)
ui_load(PATH)
let root: UiNode = ui_nodes("note", view_note())
ui_press(root.children[0])
let before = shown()
let same = ui_reload()
Fs.write_text(PATH, V2)
let changed = ui_reload()
print(`{before} | reloaded {same} then {changed} | {shown()}`)
}
}

View file

@ -1,16 +1,23 @@
# 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 "
const AT_UNDRAWN: string = " border-radius font-weight font-family font-style box-shadow text-shadow cursor transition animation line-height letter-spacing text-decoration "
# 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 {
function el_apply(n: UiNode, key: string, v0: 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)
el_cur = n
el_key = k
if len(k) > 2 and k[0..2] == "--" {
cs_var_set(n, k, vl_text(v0))
return true
}
let v = cs_vars_in(n, v0)
return el_size(n, k, v) or el_box(n, k, v) or el_flex(n, k, v) or el_look(n, k, v) or el_place_prop(n, k, v)
}
function at_alias(key: string) -> string {
let pat = ` {key}=`
@ -52,7 +59,7 @@ 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 len(p) > 0 and ((p[0] >= 48 and p[0] <= 57) or p[0] == 46) { n.border = el_unit(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 }
}

View file

@ -26,7 +26,7 @@ function el_sides(v: Val) -> void {
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]))) }
if len(words[i]) > 0 { push(parts, el_unit(words[i])) }
}
}
while len(parts) < 4 {
@ -37,13 +37,13 @@ function el_sides(v: Val) -> void {
el_s2 = parts[2]
el_s3 = parts[3]
}
# a length: a number, 10px, fill, fit or auto (a percentage is el_pct's)
# a length: a number, 10px, 1.5em, 2rem, 50vw, 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))
return el_unit(s)
}
function el_pct(v: Val) -> float {
let s = vl_text(v)

View file

@ -0,0 +1,23 @@
# attr_place.ludic - position and its insets, z-index, overflow, white-space and text-overflow
function el_place_prop(n: UiNode, key: string, v: Val) -> bool {
let s = vl_text(v)
if key == "position" {
n.pos = 0
if s == "relative" { n.pos = 1 }
if s == "absolute" { n.pos = 2 }
if s == "fixed" { n.pos = 3 }
} else if key == "top" { n.top = el_inset(v) } else if key == "right" { n.right = el_inset(v) } else if key == "bottom" { n.bottom = el_inset(v) } else if key == "left" { n.left = el_inset(v) } else if key == "inset" {
el_sides(v)
n.top = el_s0
n.right = el_s1
n.bottom = el_s2
n.left = el_s3
} else if key == "z-index" { n.z = int(el_num(s)) } else if key == "overflow" or key == "overflow-y" {
if (s == "auto" or s == "scroll" or s == "hidden") and n.kind == UI_BOX { n.kind = UI_SCROLL }
} else if key == "white-space" { n.nowrap = s == "nowrap" or s == "pre" } else if key == "text-overflow" { n.ellipsis = s == "ellipsis" } else { return false }
return true
}
function el_inset(v: Val) -> float {
if vl_text(v) == "auto" { return UI_AUTO }
return el_len(v)
}

View file

@ -1,19 +1,26 @@
# 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 {
# given its attributes as props (over the defaults its own tag gives: <component name="Card"
# sub="">), its on-* as what `emit` runs, and its content as <slot/>.
function bd_kids(kids: []UiTpl, e0: UiEnv, key: string, out: []UiNode) -> void {
var last_if = true
var e = e0
for i in 0 .. len(kids) {
let k = kids[i]
ui_at = k.where
let ck = `{key}/{i}`
if k.tag == "state" { continue }
if k.tag == "if" {
if k.tag == "state" or k.tag == "style" { continue }
if k.tag == "let" {
e = bd_let(k, e)
continue
}
if k.tag == "fragment" or (k.tag == "template" and tpl_attr(k, "slot") == null) { bd_group(k, e, ck, out) } else if k.tag == "provide" { bd_provide(k, e, ck, out) } else if k.tag == "template" { continue } else 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)) }
} else if k.tag == "each" { bd_each(k, e, ck, out) } else if k.tag == "slot" { bd_slot(k, 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 {
@ -38,23 +45,40 @@ function bd_each(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
let list = ui_eval(src, e)
let as_name = bd_name(k, "as", "item")
let at_name = bd_name(k, "index", "index")
let by = tpl_attr(k, "key")
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)
var id = string(j)
if by != null { id = ui_eval_text(by, le) }
bd_kids(k.kids, le, `{key}#{id}`, out)
}
}
function bd_slot(e: UiEnv, key: string, out: []UiNode) -> void {
# <slot/> is what the component's user put inside it; <slot name="x"/> their <template slot="x">
function bd_slot(k: UiTpl, 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) }
if at == null { return }
let name = bd_name(k, "name", "")
if name == "" {
bd_kids(at.slot.kids, at.slot_env, key, out)
return
}
for i in 0 .. len(at.slot.kids) {
let t = at.slot.kids[i]
if t.tag == "template" and bd_name(t, "slot", "") == name { bd_group(t, 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
ce.ctx = e.ctx
for i in 0 .. len(c.keys) {
if c.keys[i] != "name" and c.keys[i] != "export" { env_bind(ce, c.keys[i], ui_eval(c.vals[i], ce)) }
}
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
@ -62,7 +86,9 @@ function bd_comp(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> bool {
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
ce.slot = k
ce.slot_env = e
ce.ctx = e.ctx
ce.state = st_for(key, c, ce)
lc_note(key, k.on_keys, k.on_acts, e)
bd_kids(c.kids, ce, key, out)
return true
}

View file

@ -0,0 +1,41 @@
# build_more.ludic - <let name="{value}"/> names a value for the siblings after it, and
# <provide name="{value}"> gives it to everything inside, components included (React's context)
function bd_let(k: UiTpl, e: UiEnv) -> UiEnv {
let le = env_new(e)
for i in 0 .. len(k.keys) { env_bind(le, k.keys[i], ui_eval(k.vals[i], e)) }
return le
}
function bd_provide(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
let cx = env_new(e.ctx)
for i in 0 .. len(k.keys) { env_bind(cx, k.keys[i], ui_eval(k.vals[i], e)) }
let pe = env_new(e)
pe.ctx = cx
bd_kids(k.kids, pe, key, out)
}
# a fragment's (or a template's) content: its elements, or its words as a text of their own
function bd_group(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
if k.text == null {
bd_kids(k.kids, e, key, out)
return
}
let t = new UiNode
t.kind = UI_TEXT
t.key = key
t.text = ui_eval_text(k.text, e)
t.env = e
t.children = new []UiNode
push(out, t)
}
# text inside text - <p>Hi <b>there</b></p>, <h3><slot/></h3> - reads as one line of it
function el_inline(n: UiNode) -> void {
if n.kind != UI_TEXT or len(n.children) == 0 { return }
var s = n.text
for i in 0 .. len(n.children) {
let c = n.children[i]
el_inline(c)
if len(s) > 0 and len(c.text) > 0 { s = s + " " }
s = s + c.text
}
n.text = s
n.children = new []UiNode
}

View file

@ -23,11 +23,18 @@ function cs_node(n: UiNode) -> void {
}
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]) }
let r = sh.rules[i]
if md_ok(r) and sel_match(r.sel, n) { cs_rule(n, r) }
}
}
# a rule's custom properties first, so its other declarations can read them
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)) }
for j in 0 .. len(r.keys) {
if len(r.keys[j]) > 2 and r.keys[j][0..2] == "--" { el_attr(n, r.keys[j], ui_eval(r.vals[j], n.env)) }
}
for j in 0 .. len(r.keys) {
if not (len(r.keys[j]) > 2 and r.keys[j][0..2] == "--") { 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 }"

View file

@ -4,28 +4,63 @@ function fr_draw(n: UiNode) -> void {
fr_button(n)
return
}
if n.kind == UI_NATIVE {
n.native.draw(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_TEXT { fr_text(n) }
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 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.content_h) }
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.children != null { fr_children(n) }
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 {
# a text's lines (one, or the wrapped ones), each placed by text-align, the last cut by an ellipsis
function fr_text(n: UiNode) -> void {
let y = n.y + n.pt + n.border
if n.lines == null {
var s = n.text
if n.ellipsis { s = ly_ellipsis(s, n.rsize, n.cw - ly_hin(n)) }
ui_be.text(fr_line_x(n, s), y, s, n.rsize, n.rfg)
return
}
for i in 0 .. len(n.lines) { ui_be.text(fr_line_x(n, n.lines[i]), y + float(i) * n.rsize * 1.25, n.lines[i], n.rsize, n.rfg) }
}
function fr_text_x(n: UiNode) -> float { return fr_line_x(n, n.text) }
# where a line starts: text-align within the room the box gives it
function fr_line_x(n: UiNode, s: string) -> 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)
let spare = n.cw - ly_hin(n) - ly_text_w(s, n.rsize)
if n.talign == UI_CENTER { return x + spare / 2.0 }
return x + spare
}
# the children in painting order: those in the flow first, then positioned ones by z-index
function fr_children(n: UiNode) -> void {
var positioned = 0
for i in 0 .. len(n.children) {
if n.children[i].pos == 0 { fr_draw(n.children[i]) } else { positioned += 1 }
}
if positioned == 0 { return }
var lo = 1000000
var hi = -1000000
for i in 0 .. len(n.children) {
if n.children[i].pos != 0 {
lo = Math.min(lo, n.children[i].z)
hi = Math.max(hi, n.children[i].z)
}
}
for z in lo .. hi + 1 {
for i in 0 .. len(n.children) {
if n.children[i].pos != 0 and n.children[i].z == z { fr_draw(n.children[i]) }
}
}
}
function fr_border(n: UiNode) -> void {
if n.border <= 0.0 or ui_be.rect == null { return }
var c = n.border_c
@ -39,7 +74,7 @@ function fr_border(n: UiNode) -> void {
# 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) }
if ui_be.button(n.x, n.y, n.cw, n.ch, n.text, n.enabled) and n.enabled { ui_fire(n, "press", Value.null()) }
return
}
var bg = n.bg
@ -54,13 +89,3 @@ function fr_shift(n: UiNode, off: float) -> void {
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
}

View file

@ -0,0 +1,36 @@
# dump.ludic - a laid-out tree, for a test or a look
# 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}{dp_name(n)} {int(n.x)},{int(n.y)} {int(n.cw)}x{int(n.ch)}{dp_style(n)} {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
}
# tag#id.class.class, or the kind of a node the template did not name
function dp_name(n: UiNode) -> string {
var s = n.tag
if s == "" { s = `({n.kind})` }
if n.id != "" { s = s + "#" + n.id }
if n.classes != null {
for i in 0 .. len(n.classes) { s = s + "." + n.classes[i] }
}
return s
}
# the computed styles that differ from an element's defaults
function dp_style(n: UiNode) -> string {
var s = ""
if n.size > 0.0 { s = s + ` font {int(n.size)}` }
if n.fg >= 0 { s = s + ` color {n.fg}` }
if n.bg >= 0 { s = s + ` bg {n.bg}` }
if n.alpha < 1.0 { s = s + ` opacity {n.alpha}` }
if n.pt + n.pr + n.pb + n.pl > 0.0 { s = s + ` pad {int(n.pt)} {int(n.pr)} {int(n.pb)} {int(n.pl)}` }
if n.mt + n.mr + n.mb + n.ml > 0.0 { s = s + ` margin {int(n.mt)} {int(n.mr)} {int(n.mb)} {int(n.ml)}` }
if n.grow > 0.0 { s = s + ` grow {n.grow}` }
if n.pos > 0 { s = s + ` pos {n.pos} z {n.z}` }
if not n.enabled { s = s + " disabled" }
if s == "" { return "" }
return " [" + s[1..len(s)] + "]"
}

View file

@ -1,8 +1,9 @@
# 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 "
const EL_TEXTS: string = " text span p label h1 h2 h3 h4 h5 h6 strong em small b i a option "
function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode {
ui_at = k.where
let n = new UiNode
n.key = key
n.tag = k.tag
@ -17,18 +18,23 @@ function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode {
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)
n.on_names = k.on_keys
n.on_acts = k.on_acts
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`) }
if k.on_keys[i] == "press" { n.act = k.on_acts[i] }
}
lc_note(key, k.on_keys, k.on_acts, e)
bd_kids(k.kids, e, key, n.children)
if k.text != null { el_text(n, ui_eval_text(k.text, e)) }
el_inline(n)
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`) }
let nv = nt_find(tag)
if nv != null {
n.kind = UI_NATIVE
n.native = nv
} else 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 {

View file

@ -12,6 +12,7 @@ export property UiEnv {
on_names: []string = null # a component's on-* attributes, which its `emit` runs
on_acts: []UiAct = null
on_envs: []UiEnv = null
ctx: UiEnv = null # what a <provide> above gave, seen through components too
}
var ui_view: UiView = null
var ui_errs: []string = new []string
@ -21,7 +22,10 @@ function env_new(parent: UiEnv) -> UiEnv {
e.names = new []string
e.vals = new []Val
e.parent = parent
if parent != null { e.model = parent.model }
if parent != null {
e.model = parent.model
e.ctx = parent.ctx
}
return e
}
function env_bind(e: UiEnv, name: string, v: Val) -> void {
@ -39,6 +43,13 @@ function env_get(e: UiEnv, name: string) -> Val {
if at.state != null and Value.has(at.state, name) != 0 { return Value.get(at.state, name) }
at = at.parent
}
var cx = e.ctx
while cx != null {
for i in 0 .. len(cx.names) {
if cx.names[i] == name { return cx.vals[i] }
}
cx = cx.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()
@ -55,11 +66,24 @@ function env_set(e: UiEnv, name: string, v: Val) -> void {
}
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 {
# an error is said once, however many frames make it, and names the template line being built
var ui_at: string = ""
# said by the runtime, or by a host about something a template asked for
export function ui_err(msg0: string) -> void {
var msg = msg0
if ui_at != "" and not ui_has(msg, ".xml:") { msg = `{ui_at}: {msg}` }
for i in 0 .. len(ui_errs) {
if ui_errs[i] == msg { return }
}
push(ui_errs, msg)
print(`ui: {msg}`)
}
function ui_has(s: string, part: string) -> bool {
let n = len(part)
for i in 0 .. len(s) - n + 1 {
if s[i..i + n] == part { return true }
}
return false
}
# every error said so far - for a host to show them where a developer will see them
export function ui_errors() -> []string { return ui_errs }

View file

@ -0,0 +1,49 @@
# forms.ludic - <input>, <select> and <textarea> for a renderer that has none of its own: a value
# drawn in a box, a checkbox or radio pressed to change it, a select stepping through its options.
# A game registers its own with ui_native under the same tags and these give way.
var nt_ready: bool = false
function nt_defaults() -> void {
if nt_ready { return }
nt_ready = true
ui_native("input", fn fm_measure, fn fm_draw)
ui_native("select", fn fm_measure, fn fm_draw)
ui_native("textarea", fn fm_measure, fn fm_draw)
}
# what the control shows: a checkbox its tick, a select its chosen option's text, else its value
function fm_shown(n: UiNode) -> string {
let t = ui_attr_text(n, "type")
if t == "checkbox" or t == "radio" {
if ui_attr_on(n, "checked") { return "[x]" }
return "[ ]"
}
if n.tag == "select" {
let v = ui_attr_text(n, "value")
for i in 0 .. len(n.children) {
if ui_attr_text(n.children[i], "value") == v { return n.children[i].text }
}
}
return ui_attr_text(n, "value")
}
function fm_measure(n: UiNode, avail: float) -> void {
n.mw = Math.max(ly_text_w(fm_shown(n), n.rsize), n.rsize * 2.0)
n.mh = n.rsize * 1.25
}
function fm_draw(n: UiNode) -> void {
if ui_be.rect != null { ui_be.rect(n.x, n.y, n.cw, n.ch, 2105376, n.alpha) }
fr_border(n)
ui_be.text(n.x + n.pl + n.border, n.y + n.pt + n.border, fm_shown(n), n.rsize, n.rfg)
}
# the default pointer presses a control: a checkbox flips, a select moves to its next option
function fm_press(n: UiNode) -> void {
let t = ui_attr_text(n, "type")
if t == "checkbox" { ui_fire(n, "change", vl_bool(not ui_attr_on(n, "checked"))) }
if t == "radio" { ui_fire(n, "change", ui_attr(n, "value")) }
if n.tag == "select" and len(n.children) > 0 {
let v = ui_attr_text(n, "value")
var at = 0
for i in 0 .. len(n.children) {
if ui_attr_text(n.children[i], "value") == v { at = (i + 1) % len(n.children) }
}
ui_fire(n, "change", ui_attr(n.children[at], "value"))
}
}

View file

@ -1,17 +1,18 @@
# 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
var fr_down_now: bool = false
export function ui_show(screen: string, view: UiView, x: float, y: float, w: float, h: float) -> void {
ui_viewport(w, h)
let root = ui_nodes(screen, view)
if root == null { return }
ui_place(root, x, y, w, h)
fr_pressed = new []UiNode
nt_fired_clear()
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]) }
lc_run(screen)
nt_fired_run()
}
# 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 {
@ -21,6 +22,7 @@ export function ui_nodes(screen: string, view: UiView) -> UiNode {
return null
}
let t = lib_screens[si]
lc_begin()
ui_view = view
let e = env_new(null)
if view != null and view.model != null { e.model = view.model() } else { e.model = Value.object() }
@ -34,15 +36,22 @@ export function ui_nodes(screen: string, view: UiView) -> UiNode {
root.akeys = new []string
root.avals = new []string
root.vals = new []Val
root.tag = "screen"
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
}
# the room vw, vh and @media measure against; ui_show sets it to the screen's rectangle
export function ui_viewport(w: float, h: float) -> void {
ly_vp_w = w
ly_vp_h = h
}
export function ui_place(root: UiNode, x: float, y: float, w: float, h: float) -> void {
ly_vp_x = x
ly_vp_y = y
root.w = UI_FILL
root.h = UI_FILL
var fg = 15658734
@ -54,6 +63,8 @@ export function ui_place(root: UiNode, x: float, y: float, w: float, h: float) -
export function ui_press(n: UiNode) -> void {
if n.act != null and n.enabled { act_run(n.act, n.env) }
}
# any event, as if the element had fired it - for a test
export function ui_event(n: UiNode, name: string, value: Val) -> void { nt_run(n, name, value) }
# 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
@ -68,10 +79,14 @@ function fr_pointer(root: UiNode) -> void {
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)
if ui_be.pointer_down == null { return }
let down = ui_be.pointer_down()
if hit != null and down and not fr_was_down { push(fr_pressed, hit) }
fr_down_now = down
if ui_be.button != null { return }
let hit = fr_hit(root, px, py)
if hit != null and down and not fr_was_down and hit.enabled {
if hit.kind == UI_NATIVE { fm_press(hit) } else { ui_fire(hit, "press", Value.null()) }
}
fr_was_down = down
}
function fr_under(n: UiNode, px: float, py: float, keys: []string) -> void {
@ -89,7 +104,7 @@ function fr_hit(n: UiNode, px: float, py: float) -> UiNode {
i -= 1
}
}
if n.act != null { return n }
if n.act != null or (n.kind == UI_NATIVE and n.on_names != null and len(n.on_names) > 0) { return n }
return null
}
export function ui_hit(root: UiNode, px: float, py: float) -> UiNode { return fr_hit(root, px, py) }

View file

@ -11,23 +11,33 @@ import "values.ludic"
import "env.ludic"
import "eval.ludic"
import "act.ludic"
import "native.ludic"
import "forms.ludic"
import "life.ludic"
import "tpl.ludic"
import "lib.ludic"
import "lib_util.ludic"
import "reload.ludic"
import "link.ludic"
import "state.ludic"
import "build.ludic"
import "build_more.ludic"
import "elem.ludic"
import "attr.ludic"
import "attr_box.ludic"
import "attr_place.ludic"
import "units.ludic"
import "style.ludic"
import "style_rule.ludic"
import "media.ludic"
import "select.ludic"
import "select_parts.ludic"
import "select_match.ludic"
import "cascade.ludic"
import "layout.ludic"
import "layout_text.ludic"
import "layout_run.ludic"
import "layout_wrap.ludic"
import "frame.ludic"
import "draw.ludic"
import "dump.ludic"

View file

@ -8,10 +8,20 @@ function ly_measure(n: UiNode, avail: float, size: float, fg: int) -> void {
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 {
if 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_TEXT { ly_measure_text(n, avail) }
if n.kind == UI_NATIVE and n.native.measure != null {
let base_w = n.mw
let base_h = n.mh
n.mw = 0.0
n.mh = 0.0
n.native.measure(n, avail)
n.mw = n.mw + base_w
n.mh = n.mh + base_h
}
if n.kind == UI_IMAGE {
n.mw = n.mw + n.rsize
n.mh = n.mh + n.rsize
@ -24,6 +34,7 @@ function ly_measure(n: UiNode, avail: float, size: float, fg: int) -> void {
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) }
}
n.content_h = n.mh
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)
@ -40,12 +51,16 @@ function ly_clamp(v: float, lo: float, hi: float) -> float {
function ly_measure_box(n: UiNode, inner: float) -> void {
var main = 0.0
var cross = 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)
main = main + ly_main(c, n.dir)
if i > 0 { main = main + n.gap }
cross = Math.max(cross, ly_cross(c, n.dir))
if c.pos < 2 {
main = main + ly_main(c, n.dir)
if count > 0 { main = main + n.gap }
cross = Math.max(cross, ly_cross(c, n.dir))
count += 1
}
}
if n.dir == UI_ROW {
n.mw = n.mw + main
@ -65,6 +80,7 @@ function ly_measure_wrap(n: UiNode, inner: float) -> void {
for i in 0 .. len(n.children) {
let c = n.children[i]
ly_measure(c, inner, n.rsize, n.rfg)
if c.pos >= 2 { continue }
let cw = ly_main(c, UI_ROW)
if count > 0 and line_w + n.gap + cw > inner {
widest = Math.max(widest, line_w)
@ -81,16 +97,3 @@ function ly_measure_wrap(n: UiNode, inner: float) -> void {
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
}

View file

@ -10,13 +10,12 @@ function ly_place(n: UiNode, x: float, y: float, w: float, h: float) -> void {
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) }
if n.kind == UI_SCROLL { inner_h = Math.max(n.content_h, 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 and n.wrap { ly_place_wrap(n, top, inner_w) } else 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) }
for i in 0 .. len(n.children) {
if n.children[i].pos >= 2 { ly_place_abs(n, n.children[i]) }
}
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 }
@ -43,20 +42,26 @@ function ly_stretches(n: UiNode, c: UiNode) -> bool {
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
var count = 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)
if n.children[i].pos < 2 {
used = used + ly_basis(n.children[i], n.dir, main_room)
grows = grows + ly_grow(n.children[i], n.dir)
count += 1
}
}
used = used + n.gap * float(Math.max(to - from - 1, 0))
used = used + n.gap * float(Math.max(count - 1, 0))
let free = main_room - used
ly_justify(n, free, to - from, grows)
ly_justify(n, free, count, grows)
let extra = ly_extra
var at = ly_lead
for i in from .. to {
let c = n.children[i]
if c.pos >= 2 { continue }
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)
if c.pos == 1 { ly_offset(c, ly_or0(c.left) - ly_or0(c.right), ly_or0(c.top) - ly_or0(c.bottom)) }
at = at + m + n.gap + extra
}
}
@ -80,20 +85,3 @@ function ly_place_child(n: UiNode, c: UiNode, at: float, top: float, m: float, c
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
}
}

View file

@ -0,0 +1,55 @@
# layout_text.ludic - a text's size: one line, or as many as it takes to fit its room (white-space
# normal), breaking between words; nowrap keeps one line, and text-overflow: ellipsis cuts it
function ly_measure_text(n: UiNode, avail: float) -> void {
n.lines = null
let lh = n.rsize * 1.25
let one = ly_text_w(n.text, n.rsize)
var room = avail - n.ml - n.mr - ly_hin(n)
if n.w > 0.0 { room = n.w - ly_hin(n) }
if n.nowrap or room <= 0.0 or one <= room {
n.mw = n.mw + one
n.mh = n.mh + lh
return
}
n.lines = ly_wrap(n.text, n.rsize, room)
var widest = 0.0
for i in 0 .. len(n.lines) { widest = Math.max(widest, ly_text_w(n.lines[i], n.rsize)) }
n.mw = n.mw + widest
n.mh = n.mh + lh * float(len(n.lines))
}
function ly_wrap(s: string, size: float, room: float) -> []string {
let words = lss_split(s, 32)
let out = new []string
var line = ""
for i in 0 .. len(words) {
if len(words[i]) == 0 { continue }
var next = words[i]
if len(line) > 0 { next = line + " " + words[i] }
if len(line) > 0 and ly_text_w(next, size) > room {
push(out, line)
line = words[i]
} else { line = next }
}
if len(line) > 0 { push(out, line) }
return out
}
# a line cut to `room` with an ellipsis, when it does not fit
function ly_ellipsis(s: string, size: float, room: float) -> string {
if ly_text_w(s, size) <= room { return s }
var n = len(s)
while n > 0 and ly_text_w(s[0..n] + "...", size) > room { n -= 1 }
return s[0..n] + "..."
}
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
}

View file

@ -6,7 +6,11 @@ function ly_place_wrap(n: UiNode, top0: float, inner_w: float) -> void {
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) {
while to < len(n.children) and (to == from or n.children[to].pos >= 2 or line_w + n.gap + ly_main(n.children[to], UI_ROW) <= inner_w) {
if n.children[to].pos >= 2 {
to += 1
continue
}
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))
@ -17,3 +21,54 @@ function ly_place_wrap(n: UiNode, top0: float, inner_w: float) -> void {
from = to
}
}
# position: absolute (in the parent's padding box) or fixed (in the screen): the insets say where,
# and two opposite ones say how big; relative is placed as usual and moved by its insets
function ly_place_abs(n: UiNode, c: UiNode) -> void {
var bx = n.x + n.border
var by = n.y + n.border
var bw = n.cw - n.border * 2.0
var bh = n.ch - n.border * 2.0
if c.pos == 3 {
bx = ly_vp_x
by = ly_vp_y
bw = ly_vp_w
bh = ly_vp_h
}
var w = c.mw
var h = c.mh
if c.w_pct > 0.0 { w = bw * c.w_pct / 100.0 }
if c.h_pct > 0.0 { h = bh * c.h_pct / 100.0 }
if c.w == UI_FILL or (c.left != UI_AUTO and c.right != UI_AUTO and c.w <= 0.0) { w = bw - ly_or0(c.left) - ly_or0(c.right) - c.ml - c.mr }
if c.h == UI_FILL or (c.top != UI_AUTO and c.bottom != UI_AUTO and c.h <= 0.0) { h = bh - ly_or0(c.top) - ly_or0(c.bottom) - c.mt - c.mb }
var x = bx + n.pl + c.ml
if c.left != UI_AUTO { x = bx + c.left + c.ml } else if c.right != UI_AUTO { x = bx + bw - c.right - c.mr - w }
var y = by + n.pt + c.mt
if c.top != UI_AUTO { y = by + c.top + c.mt } else if c.bottom != UI_AUTO { y = by + bh - c.bottom - c.mb - h }
ly_place(c, x, y, w, h)
}
function ly_or0(v: float) -> float {
if v == UI_AUTO { return 0.0 }
return v
}
function ly_offset(n: UiNode, dx: float, dy: float) -> void {
n.x = n.x + dx
n.y = n.y + dy
for i in 0 .. len(n.children) { ly_offset(n.children[i], dx, dy) }
}
# 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
}
}

View file

@ -11,6 +11,8 @@ export property UiFile {
imp: []UiFile = null
own: []UiRule = null
sheet: UiSheet = null
src: string = "" # the text it was read from
disk: bool = false # read from a file (ui_reload looks again), or handed over as text
}
var lib_files: []UiFile = new []UiFile
var lib_screen_names: []string = new []string
@ -26,8 +28,10 @@ function lib_file(path: string) -> UiFile {
ui_err(`{path}: cannot read it`)
return null
}
if lss_is(path) { return lss_file(src, path) }
return lib_read(src, path)
var f: UiFile = null
if lss_is(path) { f = lss_file(src, path) } else { f = lib_read(src, path) }
f.disk = true
return f
}
# 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 {
@ -35,6 +39,8 @@ export function ui_load_text(src: string, path: string) -> void {
}
function lib_read(src: string, path: string) -> UiFile {
let f = lib_new(path)
f.src = src
tp_lines(src)
let root = tpl_read(Xml.parse(src), path)
var tops = root.kids
if root.tag != "ui" { tops = [root] }

View file

@ -0,0 +1,64 @@
# life.ludic - on-mount and on-unmount: an element or component that appears in a frame it was not
# in last frame is mounted, and one that is gone is unmounted, each once, after the frame is drawn
var lc_keys: []string = new []string
var lc_mounts: []UiAct = new []UiAct
var lc_unmounts: []UiAct = new []UiAct
var lc_envs: []UiEnv = new []UiEnv
var lc_prev_keys: []string = new []string
var lc_prev_unmounts: []UiAct = new []UiAct
var lc_prev_envs: []UiEnv = new []UiEnv
# a build starts its own records: a screen built twice keeps only the last build's
function lc_begin() -> void {
lc_keys = new []string
lc_mounts = new []UiAct
lc_unmounts = new []UiAct
lc_envs = new []UiEnv
}
function lc_note(key: string, names: []string, acts: []UiAct, e: UiEnv) -> void {
var m: UiAct = null
var u: UiAct = null
for i in 0 .. len(names) {
if names[i] == "mount" { m = acts[i] }
if names[i] == "unmount" { u = acts[i] }
}
if m == null and u == null { return }
push(lc_keys, key)
push(lc_mounts, m)
push(lc_unmounts, u)
push(lc_envs, e)
}
function lc_mine(key: string, screen: string) -> bool {
let n = len(screen)
return key == screen or (len(key) > n and key[0..n] == screen and key[n] == 47)
}
# the screen's mounts and unmounts since its last frame; other screens' records are kept
function lc_run(screen: string) -> void {
for i in 0 .. len(lc_keys) {
if lc_mounts[i] != null and not sel_has(lc_prev_keys, lc_keys[i]) { act_run(lc_mounts[i], lc_envs[i]) }
}
let keys = new []string
let unmounts = new []UiAct
let envs = new []UiEnv
for i in 0 .. len(lc_prev_keys) {
let k = lc_prev_keys[i]
if lc_mine(k, screen) {
if lc_prev_unmounts[i] != null and not sel_has(lc_keys, k) { act_run(lc_prev_unmounts[i], lc_prev_envs[i]) }
} else {
push(keys, k)
push(unmounts, lc_prev_unmounts[i])
push(envs, lc_prev_envs[i])
}
}
for i in 0 .. len(lc_keys) {
push(keys, lc_keys[i])
push(unmounts, lc_unmounts[i])
push(envs, lc_envs[i])
}
lc_prev_keys = keys
lc_prev_unmounts = unmounts
lc_prev_envs = envs
lc_keys = new []string
lc_mounts = new []UiAct
lc_unmounts = new []UiAct
lc_envs = new []UiEnv
}

View file

@ -1,15 +1,15 @@
# 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 "
const LK_OWN: string = " row button spacer scroll img hr if else each slot state style component screen import let fragment template provide "
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`) }
if t.comp == null and lk_find(f, t.tag, true) != null { ui_err(`{t.where}: <{t.tag}>: that component is not exported - export="true" on it lets other files use it`) } else if t.comp == null { ui_err(`{t.where}: <{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) }
function lk_element(tag: string) -> bool { return lk_in(LK_OWN, tag) or lk_in(EL_BOXES, tag) or lk_in(EL_TEXTS, tag) or nt_find(tag) != null }
# a word in a list of words written " a b c "
function lk_in(words: string, w: string) -> bool {
let pat = ` {w} `

View file

@ -0,0 +1,36 @@
# media.ludic - @media (min-width: 1280px) and (max-height: 720px) { ... }: rules that hold only
# while the screen's area is that size
export property UiMedia {
min_w: float = 0.0
max_w: float = 0.0
min_h: float = 0.0
max_h: float = 0.0
}
function md_parse(cond: string) -> UiMedia {
let m = new UiMedia
let parts = lss_split(cond, 40)
for i in 0 .. len(parts) {
let p = parts[i]
var colon = -1
for k in 0 .. len(p) {
if colon < 0 and p[k] == 58 { colon = k }
}
if colon < 0 { continue }
let key = tpl_words(p[0..colon])
let v = el_num(tpl_words(p[colon + 1..len(p)]))
if key == "min-width" { m.min_w = v }
if key == "max-width" { m.max_w = v }
if key == "min-height" { m.min_h = v }
if key == "max-height" { m.max_h = v }
}
return m
}
function md_ok(r: UiRule) -> bool {
let m = r.md
if m == null { return true }
if m.min_w > 0.0 and ly_vp_w < m.min_w { return false }
if m.max_w > 0.0 and ly_vp_w > m.max_w { return false }
if m.min_h > 0.0 and ly_vp_h < m.min_h { return false }
if m.max_h > 0.0 and ly_vp_h > m.max_h { return false }
return true
}

View file

@ -0,0 +1,77 @@
# native.ludic - elements the host draws itself: ui_native("keybind", measure, draw) makes
# <keybind> a tag like any other, laid out and styled by the template, drawn by the host. A native
# says what happened with ui_fire(n, "change", value): the element's on-change runs once the frame
# is drawn, with `event.value` (and `event.name`) to read.
export property UiNative {
tag: string = ""
measure: fn(UiNode, float) -> void = null # the content's size into n.mw, n.mh; avail is the room
draw: fn(UiNode) -> void = null
}
var nt_list: []UiNative = new []UiNative
export function ui_native(tag: string, measure: fn(UiNode, float) -> void, draw: fn(UiNode) -> void) -> void {
let nv = new UiNative
nv.tag = tag
nv.measure = measure
nv.draw = draw
for i in 0 .. len(nt_list) {
if nt_list[i].tag == tag {
nt_list[i] = nv
return
}
}
push(nt_list, nv)
}
function nt_find(tag: string) -> UiNative {
nt_defaults()
for i in 0 .. len(nt_list) {
if nt_list[i].tag == tag { return nt_list[i] }
}
return null
}
var nt_q_nodes: []UiNode = new []UiNode
var nt_q_names: []string = new []string
var nt_q_vals: []Val = new []Val
function nt_fired_clear() -> void {
nt_q_nodes = new []UiNode
nt_q_names = new []string
nt_q_vals = new []Val
}
export function ui_fire(n: UiNode, name: string, value: Val) -> void {
push(nt_q_nodes, n)
push(nt_q_names, name)
push(nt_q_vals, value)
}
function nt_fired_run() -> void {
let nodes = nt_q_nodes
let names = nt_q_names
let vals = nt_q_vals
nt_fired_clear()
for i in 0 .. len(nodes) { nt_run(nodes[i], names[i], vals[i]) }
}
# the element's on-<name>, with `event` bound to what happened
function nt_run(n: UiNode, name: string, value: Val) -> void {
if n.on_names == null or not n.enabled { return }
for i in 0 .. len(n.on_names) {
if n.on_names[i] == name {
let ev = Value.object()
Value.put(ev, "name", Value.str(name))
Value.put(ev, "value", value)
let e = env_new(n.env)
env_bind(e, "event", ev)
act_run(n.on_acts[i], e)
}
}
}
# an attribute of an element, as the template gave it (null when it has none) - for a native
export function ui_attr(n: UiNode, key: string) -> Val {
if n.akeys == null { return Value.null() }
for i in 0 .. len(n.akeys) {
if n.akeys[i] == key { return n.vals[i] }
}
return Value.null()
}
export function ui_attr_text(n: UiNode, key: string) -> string { return vl_text(ui_attr(n, key)) }
export function ui_attr_on(n: UiNode, key: string) -> bool {
let v = ui_attr(n, key)
return vl_truthy(v) and vl_text(v) != "false"
}

View file

@ -6,6 +6,8 @@ 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_NATIVE: int = 7 # an element the host draws itself (ui_native)
export const UI_AUTO: float = -1000000.0 # an inset that is not set
export const UI_COL: int = 0
export const UI_ROW: int = 1
export const UI_FILL: float = -1.0
@ -69,15 +71,30 @@ export property UiNode {
min_h: float = 0.0
max_h: float = 0.0
hidden: bool = false # display: none, or hidden
pos: int = 0 # position: 0 static, 1 relative, 2 absolute, 3 fixed
top: float = -1000000.0 # the insets a positioned element is placed by (UI_AUTO: unset)
right: float = -1000000.0
bottom: float = -1000000.0
left: float = -1000000.0
z: int = 0
nowrap: bool = false # white-space: nowrap
ellipsis: bool = false # text-overflow: ellipsis
lines: []string = null # a wrapped text's lines
var_names: []string = null # its custom properties (--name), which its descendants inherit
var_vals: []string = null
fg: int = -1
bg: int = -2
alpha: float = 1.0
enabled: bool = true
act: UiAct = null
act: UiAct = null # on-press, which the pointer and the renderer's button fire
on_names: []string = null # every on-* it answers
on_acts: []UiAct = null
native: UiNative = null
env: UiEnv = null
children: []UiNode = null
mw: float = 0.0
mh: float = 0.0
content_h: float = 0.0 # what a scroll box holds, however tall the box itself is
x: float = 0.0
y: float = 0.0
cw: float = 0.0

View file

@ -0,0 +1,27 @@
# reload.ludic - a developer's templates and stylesheets read again when they change on disk: every
# file is re-read in the order it was first loaded (so an import is there before its importer), and
# the screens' state is kept, as React's fast refresh keeps it. True when anything changed.
export function ui_reload() -> bool {
var changed = false
for i in 0 .. len(lib_files) {
let f = lib_files[i]
if f.disk and lib_text(f.path) != f.src { changed = true }
}
if not changed { return false }
let old = lib_files
lib_files = new []UiFile
lib_screen_names = new []string
lib_screens = new []UiTpl
ui_errs = new []string
for i in 0 .. len(old) {
let f = old[i]
var have = false
for k in 0 .. len(lib_files) {
if lib_files[k].path == f.path { have = true }
}
if have { continue }
if f.disk { lib_file(f.path) } else { ui_load_text(f.src, f.path) }
}
print(`ui: templates reloaded`)
return true
}

View file

@ -11,7 +11,7 @@ export property UiSel {
pargs: []string = null
nots: []UiSel = null
prev: UiSel = null
comb: int = 0 # 0: prev is an ancestor, 1: prev is the parent
comb: int = 0 # prev is: 0 an ancestor, 1 the parent, 2 the sibling before, 3 any sibling before
}
function sel_new() -> UiSel {
let s = new UiSel
@ -36,6 +36,14 @@ function sel_parse(src: string, err: []string) -> UiSel {
comb = 1
continue
}
if rd_op(r, "+") {
comb = 2
continue
}
if rd_op(r, "~") {
comb = 3
continue
}
let c = sel_compound(r)
c.prev = cur
c.comb = comb

View file

@ -15,6 +15,14 @@ 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) }
if s.comb == 2 { return n.parent != null and n.index > 0 and sel_match(s.prev, n.parent.children[n.index - 1]) }
if s.comb == 3 {
if n.parent == null { return false }
for i in 0 .. n.index {
if sel_match(s.prev, n.parent.children[i]) { return true }
}
return false
}
var a = n.parent
while a != null {
if sel_match(s.prev, a) { return true }
@ -62,13 +70,30 @@ function sel_pseudo_ok(p: string, arg: string, n: UiNode) -> bool {
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
}
if p == "root" { return n.parent == null }
if p == "checked" { return ui_attr_on(n, "checked") }
if p == "active" { return n.hovered and fr_down_now }
if p == "nth-child" { return sel_nth(arg, n.index + 1) }
ui_err(`:{p}: there is no such pseudo-class here`)
return false
}
# :nth-child(odd), (even), (3), (2n+1), (n+2), (-n+3): is the k-th child one of them
function sel_nth(arg: string, k: int) -> bool {
if arg == "odd" { return k % 2 == 1 }
if arg == "even" { return k % 2 == 0 }
var npos = -1
for i in 0 .. len(arg) {
if arg[i] == 110 { npos = i }
}
if npos < 0 { return string(k) == arg }
var a = 1
let head: string = arg[0..npos]
if head == "-" { a = -1 } else if len(head) > 0 { a = int(el_num(head)) }
var b = 0
var tail = tpl_words(arg[npos + 1..len(arg)])
if len(tail) > 0 and tail[0] == 43 { tail = tpl_words(tail[1..len(tail)]) }
if len(tail) > 0 { b = int(el_num(tail)) }
if a == 0 { return k == b }
let d = k - b
return d % a == 0 and d / a >= 0
}

View file

@ -5,6 +5,7 @@
export property UiRule {
sel: UiSel = null
spec: int = 0
md: UiMedia = null # the @media it sits in, if any
keys: []string = null
vals: []UiExpr = null
}
@ -21,14 +22,20 @@ function lss_is(path: string) -> bool { return len(path) > 4 and path[len(path)
# 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)
f.src = src
lss_text(f, src)
f.sheet = sheet_of(f)
return f
}
function lss_text(f: UiFile, src: string) -> void {
let err = new []string
lss_block(f, lss_strip(src), null, err)
for i in 0 .. len(err) { ui_err(`{f.path}: <style> {err[i]}`) }
}
# rules, @imports and @media blocks; the rules in a @media block carry its condition
function lss_block(f: UiFile, src: string, md: UiMedia, err: []string) -> void {
let r = new UiRd
r.s = lss_strip(src)
r.s = src
while r.err == "" {
rd_ws(r)
if r.i >= len(r.s) { break }
@ -36,11 +43,18 @@ function lss_text(f: UiFile, src: string) -> void {
lss_import(f, lss_upto(r, 59))
continue
}
if rd_op(r, "@media") {
let cond = md_parse(lss_upto(r, 123))
lss_block(f, lss_upto(r, 125), cond, err)
continue
}
let sels = lss_upto(r, 123)
let body = lss_upto(r, 125)
let at = len(f.own)
lss_rule(f, sels, body, err)
for i in at .. len(f.own) { f.own[i].md = md }
}
for i in 0 .. len(err) { ui_err(`{f.path}: <style> {err[i]}`) }
if r.err != "" { push(err, r.err) }
}
# @import "base.lss"; - its rules come before this file's own
function lss_import(f: UiFile, what: string) -> void {

View file

@ -13,6 +13,24 @@ export property UiTpl {
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
where: string = "" # file:line, for an error
}
# the text being read, and where its lines start, so an element can say which line it is on
var tp_nl: []int = new []int
function tp_lines(src: string) -> void {
tp_nl = new []int
for i in 0 .. len(src) {
if src[i] == 10 { push(tp_nl, i) }
}
}
function tp_line(at: int) -> int {
var lo = 0
var hi = len(tp_nl)
while lo < hi {
let mid = (lo + hi) / 2
if tp_nl[mid] < at { lo = mid + 1 } else { hi = mid }
}
return lo + 1
}
function tpl_read(x: Xml, file: string) -> UiTpl {
@ -23,6 +41,7 @@ function tpl_read(x: Xml, file: string) -> UiTpl {
t.on_keys = new []string
t.on_acts = new []UiAct
t.kids = new []UiTpl
t.where = `{file}:{tp_line(x.at)}`
let err = new []string
for i in 0 .. len(x.akeys) {
let k: string = x.akeys[i]
@ -45,7 +64,7 @@ function tpl_read(x: Xml, file: string) -> UiTpl {
}
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]}`) }
for i in 0 .. len(err) { ui_err(`{t.where}: <{t.tag}> {err[i]}`) }
return t
}
# on-click is on-press, as a browser would have it

View file

@ -0,0 +1,88 @@
# units.ludic - lengths in px (the renderer's scale), em (the font size here), rem (the root's),
# vw and vh (the screen's), and custom properties: --name set on an element, read by var(--name)
# in it and everything inside it
var el_cur: UiNode = null
var el_key: string = ""
var ly_vp_x: float = 0.0
var ly_vp_y: float = 0.0
var ly_vp_w: float = 0.0
var ly_vp_h: float = 0.0
function el_unit(s: string) -> float {
let f = el_num(s)
let n = len(s)
if n > 3 and s[n - 3..n] == "rem" { return f * ui_px(16.0) }
if n > 2 and s[n - 2..n] == "em" {
if el_cur == null { return f * ui_px(16.0) }
if el_key == "size" and el_cur.parent != null { return f * cs_fs(el_cur.parent) }
return f * cs_fs(el_cur)
}
if n > 2 and s[n - 2..n] == "vw" { return f * ly_vp_w / 100.0 }
if n > 2 and s[n - 2..n] == "vh" { return f * ly_vp_h / 100.0 }
return ui_px(f)
}
# the font size an element has so far in its cascade (its own, else its parent's)
function cs_fs(n: UiNode) -> float {
if n.size > 0.0 { return n.size }
if n.parent != null { return cs_fs(n.parent) }
return ui_px(16.0)
}
function cs_var_set(n: UiNode, name: string, value: string) -> void {
if n.var_names == null {
n.var_names = new []string
n.var_vals = new []string
}
for i in 0 .. len(n.var_names) {
if n.var_names[i] == name {
n.var_vals[i] = value
return
}
}
push(n.var_names, name)
push(n.var_vals, value)
}
function cs_var(n: UiNode, name: string) -> string {
var at = n
while at != null {
if at.var_names != null {
for i in 0 .. len(at.var_names) {
if at.var_names[i] == name { return at.var_vals[i] }
}
}
at = at.parent
}
return null
}
# a value with var(--name) or var(--name, fallback) in it, with each replaced by what it names
function cs_vars_in(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 s[i..i + 4] == "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(n, s[i + 4..j - 1])
at = j
i = j
} else { i += 1 }
}
if not found { return v }
return Value.str(tpl_words(out + s[at..len(s)]))
}
function cs_var_ref(n: UiNode, inner: string) -> string {
let parts = lss_split(inner, 44)
let v = cs_var(n, tpl_words(parts[0]))
if v != null { return v }
if len(parts) > 1 { return tpl_words(inner[len(parts[0]) + 1..len(inner)]) }
ui_err(`var({tpl_words(parts[0])}): no element here or above sets it`)
return ""
}

View file

@ -21,6 +21,7 @@ property Xml {
akeys: []pointer # attribute names
avals: []pointer # attribute values (entity-decoded)
kids: []Xml # child elements, in document order
at: int = 0 # where its '<' is in the text, for an error that names a line
}
function xml_new(tag: pointer) -> Xml {
@ -243,9 +244,11 @@ function xp_attrs(p: XP, node: Xml) -> void {
# parse one element (cursor on its opening '<'). Recurses for children.
function xp_element(p: XP) -> Xml {
let at = p.i
p.i += 1 # skip '<'
let name = xp_name(p)
let node = xml_new(name)
node.at = at
xp_attrs(p, node)
# self-closing '/>'
if p.i < p.n and p.s[p.i] == '/' { # '/'

View file

@ -35,13 +35,21 @@ function vw_finish(v: int) -> void {
vw_parse(model, head.file, head.line, head.vis)
vw_parse(vw_call_src(v, px, head), head.file, head.line, head.vis)
}
# a field written `name = some_global` has that global's declared type
# a field's type when it is not said: a global's, a literal's, or a named function's result
function vw_infer(v: int, g: Node, fname: pointer) -> pointer {
let e = g.a.kids[0].a
if e.kind == E_ID {
let d = find_global(e.s)
if d != null and d.ty != null { return d.ty }
}
if e.kind == E_INT { return "int" }
if e.kind == E_FLOAT { return "float" }
if e.kind == E_BOOL { return "bool" }
if e.kind == E_STR { return "string" }
if e.kind == E_CALL and e.a != null and e.a.kind == E_ID {
let f = find_fn(e.a.s)
if f != null and f.ty != null and not (f.ty == "void") { return f.ty }
}
g_vw_node[v].line = g.line
vw_err(v, `say what type {fname} is - {fname}: int = ...`)
return "int"

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -749,9 +749,12 @@ function cmd_dev_test() -> int {
reject_case("rejected/default_missing", "this call to f leaves out a, which has no default", "a call leaves out only what has a default")
reject_case("rejected/default_unknown", "f has no parameter c", "a named argument names a parameter the function has")
reject_case("rejected/positional_after_named", "a positional argument after a named one", "positional arguments come first")
controller_case("library/ui_templates", "", "ui: ui/peek.xml: <kit:Price>: that component is not exported - export=\"true\" on it lets other files use it purse 70, bought 1; the lantern is enabled: 0", "ui_templates.ludic (L11: a template reads a view, a press sends its event, component state and emit)")
controller_case("library/ui_templates", "", "ui: ui/peek.xml:1: <kit:Price>: that component is not exported - export=\"true\" on it lets other files use it purse 70, bought 1; the lantern is enabled: 0", "ui_templates.ludic (L11: a template reads a view, a press sends its event, component state and emit)")
controller_case("library/ui_flex", "", "grow 97:282; between at 197; wrap lines at 51, 67; red 16711680; stretched 40; own size 12", "ui_flex.ludic (L11: flex layout - grow, justify, wrap, stretch - and stylesheets with imports and specificity)")
controller_case("library/ui_html", "", "id beats class: 40; card 200 wide at 3,53 pad 4 8 12 16 border 2 inside 65280, outside -1; warn bg 16776960; items 3 of 30: 30; colours 16711680 255 -1; hr 1; children 7 clicks 1; the button now disabled 1 at opacity 0.5; text-align 1", "ui_html.ludic (L11: HTML elements, ids, classes, attributes, style=, CSS selectors and properties, the box model)")
controller_case("library/ui_react", "", "Mixer|loud|dark|drums 1|bass 0|checkbox true| volume 7; dial 40x40; mounted 1, unmounted 1", "ui_react.ludic (L11: keyed lists, let, context, named slots, mount and unmount, native elements and form controls)")
controller_case("library/ui_css", "", "badge at 255,175 bg 16746496; nudged 17,13; big 32; corner 0,0 40x32 media keeps narrow; li gaps 6 0; li colours 255 -1 255; after 16746496; fallback 65280 checked opacity 0.25; list scrolls 1 50 of 80; para lines 5", "ui_css.ludic (L11: var(), positioning, em/rem/vw, @media, wrapping, sibling combinators, :nth-child formulas, :checked, overflow)")
controller_case("library/ui_reload", "", "ui: templates reloaded note 1 | reloaded 0 then 1 | note again 1", "ui_reload.ludic (L11: a template changed on disk is read again, and its state kept)")
reject_case("rejected/view_untyped", "view Sums: say what type total is", "a view field that is not a bare global says its type")
reject_case("rejected/view_without_ui", "a view is shown by ludic.ui", "a view needs ludic.ui to show it")