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:
parent
66a2bc2214
commit
637d07400e
39 changed files with 42012 additions and 40795 deletions
31
LANGUAGE.md
31
LANGUAGE.md
|
|
@ -388,6 +388,37 @@ string.
|
||||||
- A file's cascade is its imports' rules, then its own. `<import src="look.lss"/>` brings a
|
- 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.
|
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.
|
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
|
- **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
|
`{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`,
|
model, and support `.field`, `[index]`, arithmetic, comparisons, `and` / `or` / `not`,
|
||||||
|
|
|
||||||
|
|
@ -27,6 +27,15 @@ with:
|
||||||
`:hover`, `:disabled`, `:first-child`, `:last-child`, `:nth-child`, `:not` and more, weighed by
|
`:hover`, `:disabled`, `:first-child`, `:last-child`, `:nth-child`, `:not` and more, weighed by
|
||||||
specificity.
|
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.
|
Screens are drawn through a registered renderer. `Value` gains a float kind.
|
||||||
|
|
||||||
Also:
|
Also:
|
||||||
|
|
|
||||||
26
examples/library/ui_css.ludic
Normal file
26
examples/library/ui_css.ludic
Normal 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)}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
59
examples/library/ui_react.ludic
Normal file
59
examples/library/ui_react.ludic
Normal 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}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
27
examples/library/ui_reload.ludic
Normal file
27
examples/library/ui_reload.ludic
Normal 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()}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,16 +1,23 @@
|
||||||
# attr.ludic - what a property sets, by its CSS name or the short one (width or w, background or
|
# 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.
|
# 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_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
|
# a stylesheet's property: one that means nothing is said
|
||||||
function el_attr(n: UiNode, key: string, v: Val) -> void {
|
function el_attr(n: UiNode, key: string, v: Val) -> void {
|
||||||
if el_apply(n, key, v) { return }
|
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`) }
|
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]
|
# 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)
|
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 {
|
function at_alias(key: string) -> string {
|
||||||
let pat = ` {key}=`
|
let pat = ` {key}=`
|
||||||
|
|
@ -52,7 +59,7 @@ function el_border(n: UiNode, s: string) -> void {
|
||||||
let parts = lss_split(s, 32)
|
let parts = lss_split(s, 32)
|
||||||
for i in 0 .. len(parts) {
|
for i in 0 .. len(parts) {
|
||||||
let p = parts[i]
|
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 }
|
if n.border_c == UI_NONE { n.border_c = n.rfg }
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ function el_sides(v: Val) -> void {
|
||||||
if vl_num(v) { push(parts, ui_px(Value.as_float(v))) } else {
|
if vl_num(v) { push(parts, ui_px(Value.as_float(v))) } else {
|
||||||
let words = lss_split(vl_text(v), 32)
|
let words = lss_split(vl_text(v), 32)
|
||||||
for i in 0 .. len(words) {
|
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 {
|
while len(parts) < 4 {
|
||||||
|
|
@ -37,13 +37,13 @@ function el_sides(v: Val) -> void {
|
||||||
el_s2 = parts[2]
|
el_s2 = parts[2]
|
||||||
el_s3 = parts[3]
|
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 {
|
function el_len(v: Val) -> float {
|
||||||
if vl_num(v) { return ui_px(Value.as_float(v)) }
|
if vl_num(v) { return ui_px(Value.as_float(v)) }
|
||||||
let s = vl_text(v)
|
let s = vl_text(v)
|
||||||
if s == "fill" { return UI_FILL }
|
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 }
|
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 {
|
function el_pct(v: Val) -> float {
|
||||||
let s = vl_text(v)
|
let s = vl_text(v)
|
||||||
|
|
|
||||||
23
packages/ludic.ui/attr_place.ludic
Normal file
23
packages/ludic.ui/attr_place.ludic
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -1,19 +1,26 @@
|
||||||
# build.ludic - a template and the model, made into this frame's nodes. <if>/<else> and
|
# 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,
|
# <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/>.
|
# given its attributes as props (over the defaults its own tag gives: <component name="Card"
|
||||||
function bd_kids(kids: []UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
|
# 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 last_if = true
|
||||||
|
var e = e0
|
||||||
for i in 0 .. len(kids) {
|
for i in 0 .. len(kids) {
|
||||||
let k = kids[i]
|
let k = kids[i]
|
||||||
|
ui_at = k.where
|
||||||
let ck = `{key}/{i}`
|
let ck = `{key}/{i}`
|
||||||
if k.tag == "state" { continue }
|
if k.tag == "state" or k.tag == "style" { continue }
|
||||||
if k.tag == "if" {
|
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)
|
last_if = bd_test(k, e)
|
||||||
if last_if { bd_kids(k.kids, e, ck, out) }
|
if last_if { bd_kids(k.kids, e, ck, out) }
|
||||||
} else if k.tag == "else" {
|
} else if k.tag == "else" {
|
||||||
if not last_if { bd_kids(k.kids, e, ck, out) }
|
if not last_if { bd_kids(k.kids, e, ck, out) }
|
||||||
last_if = true
|
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 {
|
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 list = ui_eval(src, e)
|
||||||
let as_name = bd_name(k, "as", "item")
|
let as_name = bd_name(k, "as", "item")
|
||||||
let at_name = bd_name(k, "index", "index")
|
let at_name = bd_name(k, "index", "index")
|
||||||
|
let by = tpl_attr(k, "key")
|
||||||
for j in 0 .. Value.count(list) {
|
for j in 0 .. Value.count(list) {
|
||||||
let le = env_new(e)
|
let le = env_new(e)
|
||||||
env_bind(le, as_name, Value.at(list, j))
|
env_bind(le, as_name, Value.at(list, j))
|
||||||
env_bind(le, at_name, Value.int(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
|
var at = e
|
||||||
while at != null and at.slot == null { at = at.parent }
|
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 {
|
function bd_comp(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> bool {
|
||||||
if k.comp == null { return false }
|
if k.comp == null { return false }
|
||||||
let c = k.comp
|
let c = k.comp
|
||||||
let ce = env_new(null)
|
let ce = env_new(null)
|
||||||
ce.model = e.model
|
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)) }
|
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_names = k.on_keys
|
||||||
ce.on_acts = k.on_acts
|
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) }
|
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
|
||||||
ce.slot = k
|
ce.slot = k
|
||||||
ce.slot_env = e
|
ce.slot_env = e
|
||||||
|
ce.ctx = e.ctx
|
||||||
ce.state = st_for(key, c, ce)
|
ce.state = st_for(key, c, ce)
|
||||||
|
lc_note(key, k.on_keys, k.on_acts, e)
|
||||||
bd_kids(c.kids, ce, key, out)
|
bd_kids(c.kids, ce, key, out)
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
|
||||||
41
packages/ludic.ui/build_more.ludic
Normal file
41
packages/ludic.ui/build_more.ludic
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -23,11 +23,18 @@ function cs_node(n: UiNode) -> void {
|
||||||
}
|
}
|
||||||
function cs_sheet(n: UiNode, sh: UiSheet) -> void {
|
function cs_sheet(n: UiNode, sh: UiSheet) -> void {
|
||||||
for i in 0 .. len(sh.rules) {
|
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 {
|
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
|
# 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 }"
|
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 }"
|
||||||
|
|
|
||||||
|
|
@ -4,28 +4,63 @@ function fr_draw(n: UiNode) -> void {
|
||||||
fr_button(n)
|
fr_button(n)
|
||||||
return
|
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) }
|
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)
|
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) }
|
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
|
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 {
|
if off != 0.0 {
|
||||||
for i in 0 .. len(n.children) { fr_shift(n.children[i], off) }
|
for i in 0 .. len(n.children) { fr_shift(n.children[i], off) }
|
||||||
}
|
}
|
||||||
if n.children != null {
|
if n.children != null { fr_children(n) }
|
||||||
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() }
|
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
|
# a text's lines (one, or the wrapped ones), each placed by text-align, the last cut by an ellipsis
|
||||||
function fr_text_x(n: UiNode) -> float {
|
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
|
let x = n.x + n.pl + n.border
|
||||||
if n.talign == UI_START { return x }
|
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 }
|
if n.talign == UI_CENTER { return x + spare / 2.0 }
|
||||||
return x + spare
|
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 {
|
function fr_border(n: UiNode) -> void {
|
||||||
if n.border <= 0.0 or ui_be.rect == null { return }
|
if n.border <= 0.0 or ui_be.rect == null { return }
|
||||||
var c = n.border_c
|
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
|
# the renderer's own button when it has one: it draws, focuses and answers the press
|
||||||
function fr_button(n: UiNode) -> void {
|
function fr_button(n: UiNode) -> void {
|
||||||
if ui_be.button != null {
|
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
|
return
|
||||||
}
|
}
|
||||||
var bg = n.bg
|
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) }
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
36
packages/ludic.ui/dump.ludic
Normal file
36
packages/ludic.ui/dump.ludic
Normal 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)] + "]"
|
||||||
|
}
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
# elem.ludic - an element made from its template: what kind of node its tag is, its id, classes
|
# 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
|
# 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_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 {
|
function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode {
|
||||||
|
ui_at = k.where
|
||||||
let n = new UiNode
|
let n = new UiNode
|
||||||
n.key = key
|
n.key = key
|
||||||
n.tag = k.tag
|
n.tag = k.tag
|
||||||
|
|
@ -17,18 +18,23 @@ function bd_elem(k: UiTpl, e: UiEnv, key: string) -> UiNode {
|
||||||
el_kind(n, k.tag)
|
el_kind(n, k.tag)
|
||||||
for i in 0 .. len(k.keys) { el_keep(n, k.keys[i], ui_eval(k.vals[i], e)) }
|
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.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) {
|
for i in 0 .. len(k.on_keys) {
|
||||||
if k.on_keys[i] == "press" {
|
if k.on_keys[i] == "press" { n.act = k.on_acts[i] }
|
||||||
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`) }
|
|
||||||
}
|
}
|
||||||
|
lc_note(key, k.on_keys, k.on_acts, e)
|
||||||
bd_kids(k.kids, e, key, n.children)
|
bd_kids(k.kids, e, key, n.children)
|
||||||
if k.text != null { el_text(n, ui_eval_text(k.text, e)) }
|
if k.text != null { el_text(n, ui_eval_text(k.text, e)) }
|
||||||
|
el_inline(n)
|
||||||
return n
|
return n
|
||||||
}
|
}
|
||||||
function el_kind(n: UiNode, tag: string) -> void {
|
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
|
# 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 {
|
function el_text(n: UiNode, s: string) -> void {
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ export property UiEnv {
|
||||||
on_names: []string = null # a component's on-* attributes, which its `emit` runs
|
on_names: []string = null # a component's on-* attributes, which its `emit` runs
|
||||||
on_acts: []UiAct = null
|
on_acts: []UiAct = null
|
||||||
on_envs: []UiEnv = null
|
on_envs: []UiEnv = null
|
||||||
|
ctx: UiEnv = null # what a <provide> above gave, seen through components too
|
||||||
}
|
}
|
||||||
var ui_view: UiView = null
|
var ui_view: UiView = null
|
||||||
var ui_errs: []string = new []string
|
var ui_errs: []string = new []string
|
||||||
|
|
@ -21,7 +22,10 @@ function env_new(parent: UiEnv) -> UiEnv {
|
||||||
e.names = new []string
|
e.names = new []string
|
||||||
e.vals = new []Val
|
e.vals = new []Val
|
||||||
e.parent = parent
|
e.parent = parent
|
||||||
if parent != null { e.model = parent.model }
|
if parent != null {
|
||||||
|
e.model = parent.model
|
||||||
|
e.ctx = parent.ctx
|
||||||
|
}
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
function env_bind(e: UiEnv, name: string, v: Val) -> void {
|
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) }
|
if at.state != null and Value.has(at.state, name) != 0 { return Value.get(at.state, name) }
|
||||||
at = at.parent
|
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) }
|
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`)
|
ui_err(`no {name} here: not a loop's, a prop, a state or in the view`)
|
||||||
return Value.null()
|
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}="..."/>`)
|
ui_err(`set {name}: no state here holds it - declare it with <state {name}="..."/>`)
|
||||||
}
|
}
|
||||||
# an error is said once, however many frames make it
|
# an error is said once, however many frames make it, and names the template line being built
|
||||||
function ui_err(msg: string) -> void {
|
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) {
|
for i in 0 .. len(ui_errs) {
|
||||||
if ui_errs[i] == msg { return }
|
if ui_errs[i] == msg { return }
|
||||||
}
|
}
|
||||||
push(ui_errs, msg)
|
push(ui_errs, msg)
|
||||||
print(`ui: {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 }
|
||||||
|
|
|
||||||
49
packages/ludic.ui/forms.ludic
Normal file
49
packages/ludic.ui/forms.ludic
Normal 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"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
# frame.ludic - a screen shown: built from the view's model, laid out, drawn, and its presses
|
# 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
|
# 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_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 {
|
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)
|
let root = ui_nodes(screen, view)
|
||||||
if root == null { return }
|
if root == null { return }
|
||||||
ui_place(root, x, y, w, h)
|
ui_place(root, x, y, w, h)
|
||||||
fr_pressed = new []UiNode
|
nt_fired_clear()
|
||||||
fr_pointer(root)
|
fr_pointer(root)
|
||||||
if ui_be != null and ui_be.text != null { fr_draw(root) }
|
if ui_be != null and ui_be.text != null { fr_draw(root) }
|
||||||
let pressed = fr_pressed
|
lc_run(screen)
|
||||||
for i in 0 .. len(pressed) { ui_press(pressed[i]) }
|
nt_fired_run()
|
||||||
}
|
}
|
||||||
# the screen's nodes for this frame, not yet laid out - for a test, or a host that places them
|
# 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 {
|
export function ui_nodes(screen: string, view: UiView) -> UiNode {
|
||||||
|
|
@ -21,6 +22,7 @@ export function ui_nodes(screen: string, view: UiView) -> UiNode {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
let t = lib_screens[si]
|
let t = lib_screens[si]
|
||||||
|
lc_begin()
|
||||||
ui_view = view
|
ui_view = view
|
||||||
let e = env_new(null)
|
let e = env_new(null)
|
||||||
if view != null and view.model != null { e.model = view.model() } else { e.model = Value.object() }
|
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.akeys = new []string
|
||||||
root.avals = new []string
|
root.avals = new []string
|
||||||
root.vals = new []Val
|
root.vals = new []Val
|
||||||
|
root.tag = "screen"
|
||||||
for i in 0 .. len(t.keys) {
|
for i in 0 .. len(t.keys) {
|
||||||
if t.keys[i] != "name" { el_keep(root, t.keys[i], ui_eval(t.vals[i], e)) }
|
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)
|
bd_kids(t.kids, e, screen, root.children)
|
||||||
cs_tree(root)
|
cs_tree(root)
|
||||||
return 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 {
|
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.w = UI_FILL
|
||||||
root.h = UI_FILL
|
root.h = UI_FILL
|
||||||
var fg = 15658734
|
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 {
|
export function ui_press(n: UiNode) -> void {
|
||||||
if n.act != null and n.enabled { act_run(n.act, n.env) }
|
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),
|
# 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
|
# and without a renderer's own button a press on the deepest pressable thing is a press
|
||||||
var fr_hover: []string = new []string
|
var fr_hover: []string = new []string
|
||||||
|
|
@ -68,10 +79,14 @@ function fr_pointer(root: UiNode) -> void {
|
||||||
let keys = new []string
|
let keys = new []string
|
||||||
fr_under(root, px, py, keys)
|
fr_under(root, px, py, keys)
|
||||||
fr_hover = keys
|
fr_hover = keys
|
||||||
if ui_be.button != null or ui_be.pointer_down == null { return }
|
if ui_be.pointer_down == null { return }
|
||||||
let hit = fr_hit(root, px, py)
|
|
||||||
let down = ui_be.pointer_down()
|
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
|
fr_was_down = down
|
||||||
}
|
}
|
||||||
function fr_under(n: UiNode, px: float, py: float, keys: []string) -> void {
|
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
|
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
|
return null
|
||||||
}
|
}
|
||||||
export function ui_hit(root: UiNode, px: float, py: float) -> UiNode { return fr_hit(root, px, py) }
|
export function ui_hit(root: UiNode, px: float, py: float) -> UiNode { return fr_hit(root, px, py) }
|
||||||
|
|
|
||||||
|
|
@ -11,23 +11,33 @@ import "values.ludic"
|
||||||
import "env.ludic"
|
import "env.ludic"
|
||||||
import "eval.ludic"
|
import "eval.ludic"
|
||||||
import "act.ludic"
|
import "act.ludic"
|
||||||
|
import "native.ludic"
|
||||||
|
import "forms.ludic"
|
||||||
|
import "life.ludic"
|
||||||
import "tpl.ludic"
|
import "tpl.ludic"
|
||||||
import "lib.ludic"
|
import "lib.ludic"
|
||||||
import "lib_util.ludic"
|
import "lib_util.ludic"
|
||||||
|
import "reload.ludic"
|
||||||
import "link.ludic"
|
import "link.ludic"
|
||||||
import "state.ludic"
|
import "state.ludic"
|
||||||
import "build.ludic"
|
import "build.ludic"
|
||||||
|
import "build_more.ludic"
|
||||||
import "elem.ludic"
|
import "elem.ludic"
|
||||||
import "attr.ludic"
|
import "attr.ludic"
|
||||||
import "attr_box.ludic"
|
import "attr_box.ludic"
|
||||||
|
import "attr_place.ludic"
|
||||||
|
import "units.ludic"
|
||||||
import "style.ludic"
|
import "style.ludic"
|
||||||
import "style_rule.ludic"
|
import "style_rule.ludic"
|
||||||
|
import "media.ludic"
|
||||||
import "select.ludic"
|
import "select.ludic"
|
||||||
import "select_parts.ludic"
|
import "select_parts.ludic"
|
||||||
import "select_match.ludic"
|
import "select_match.ludic"
|
||||||
import "cascade.ludic"
|
import "cascade.ludic"
|
||||||
import "layout.ludic"
|
import "layout.ludic"
|
||||||
|
import "layout_text.ludic"
|
||||||
import "layout_run.ludic"
|
import "layout_run.ludic"
|
||||||
import "layout_wrap.ludic"
|
import "layout_wrap.ludic"
|
||||||
import "frame.ludic"
|
import "frame.ludic"
|
||||||
import "draw.ludic"
|
import "draw.ludic"
|
||||||
|
import "dump.ludic"
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
if n.fg != UI_INHERIT { n.rfg = n.fg }
|
||||||
n.mw = ly_hin(n)
|
n.mw = ly_hin(n)
|
||||||
n.mh = ly_vin(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.mw = n.mw + ly_text_w(n.text, n.rsize)
|
||||||
n.mh = n.mh + n.rsize * 1.25
|
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 {
|
if n.kind == UI_IMAGE {
|
||||||
n.mw = n.mw + n.rsize
|
n.mw = n.mw + n.rsize
|
||||||
n.mh = n.mh + 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)
|
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 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 fixed_w > 0.0 { n.mw = fixed_w }
|
||||||
if n.h > 0.0 { n.mh = n.h }
|
if n.h > 0.0 { n.mh = n.h }
|
||||||
n.mw = ly_clamp(n.mw, n.min_w, n.max_w)
|
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 {
|
function ly_measure_box(n: UiNode, inner: float) -> void {
|
||||||
var main = 0.0
|
var main = 0.0
|
||||||
var cross = 0.0
|
var cross = 0.0
|
||||||
|
var count = 0
|
||||||
for i in 0 .. len(n.children) {
|
for i in 0 .. len(n.children) {
|
||||||
let c = n.children[i]
|
let c = n.children[i]
|
||||||
ly_measure(c, inner, n.rsize, n.rfg)
|
ly_measure(c, inner, n.rsize, n.rfg)
|
||||||
main = main + ly_main(c, n.dir)
|
if c.pos < 2 {
|
||||||
if i > 0 { main = main + n.gap }
|
main = main + ly_main(c, n.dir)
|
||||||
cross = Math.max(cross, ly_cross(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 {
|
if n.dir == UI_ROW {
|
||||||
n.mw = n.mw + main
|
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) {
|
for i in 0 .. len(n.children) {
|
||||||
let c = n.children[i]
|
let c = n.children[i]
|
||||||
ly_measure(c, inner, n.rsize, n.rfg)
|
ly_measure(c, inner, n.rsize, n.rfg)
|
||||||
|
if c.pos >= 2 { continue }
|
||||||
let cw = ly_main(c, UI_ROW)
|
let cw = ly_main(c, UI_ROW)
|
||||||
if count > 0 and line_w + n.gap + cw > inner {
|
if count > 0 and line_w + n.gap + cw > inner {
|
||||||
widest = Math.max(widest, line_w)
|
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.mw = n.mw + Math.max(widest, line_w)
|
||||||
n.mh = n.mh + total + line_h
|
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
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -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 }
|
if (n.kind != UI_BOX and n.kind != UI_SCROLL) or n.children == null { return }
|
||||||
let inner_w = w - ly_hin(n)
|
let inner_w = w - ly_hin(n)
|
||||||
var inner_h = h - ly_vin(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
|
let top = n.y + n.pt + n.border
|
||||||
if n.dir == UI_ROW and n.wrap {
|
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) }
|
||||||
ly_place_wrap(n, top, inner_w)
|
for i in 0 .. len(n.children) {
|
||||||
return
|
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 {
|
function ly_fills(c: UiNode, dir: int) -> bool {
|
||||||
if dir == UI_ROW { return c.w == UI_FILL }
|
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 {
|
function ly_place_line(n: UiNode, from: int, to: int, top: float, main_room: float, cross_room: float) -> void {
|
||||||
var used = 0.0
|
var used = 0.0
|
||||||
var grows = 0.0
|
var grows = 0.0
|
||||||
|
var count = 0
|
||||||
for i in from .. to {
|
for i in from .. to {
|
||||||
used = used + ly_basis(n.children[i], n.dir, main_room)
|
if n.children[i].pos < 2 {
|
||||||
grows = grows + ly_grow(n.children[i], n.dir)
|
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
|
let free = main_room - used
|
||||||
ly_justify(n, free, to - from, grows)
|
ly_justify(n, free, count, grows)
|
||||||
let extra = ly_extra
|
let extra = ly_extra
|
||||||
var at = ly_lead
|
var at = ly_lead
|
||||||
for i in from .. to {
|
for i in from .. to {
|
||||||
let c = n.children[i]
|
let c = n.children[i]
|
||||||
|
if c.pos >= 2 { continue }
|
||||||
var m = ly_basis(c, n.dir, main_room)
|
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 }
|
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)
|
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
|
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)
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
||||||
55
packages/ludic.ui/layout_text.ludic
Normal file
55
packages/ludic.ui/layout_text.ludic
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,11 @@ function ly_place_wrap(n: UiNode, top0: float, inner_w: float) -> void {
|
||||||
var to = from
|
var to = from
|
||||||
var line_w = 0.0
|
var line_w = 0.0
|
||||||
var line_h = 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 }
|
if to > from { line_w = line_w + n.gap }
|
||||||
line_w = line_w + ly_main(n.children[to], UI_ROW)
|
line_w = line_w + ly_main(n.children[to], UI_ROW)
|
||||||
line_h = Math.max(line_h, ly_cross(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
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ export property UiFile {
|
||||||
imp: []UiFile = null
|
imp: []UiFile = null
|
||||||
own: []UiRule = null
|
own: []UiRule = null
|
||||||
sheet: UiSheet = 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_files: []UiFile = new []UiFile
|
||||||
var lib_screen_names: []string = new []string
|
var lib_screen_names: []string = new []string
|
||||||
|
|
@ -26,8 +28,10 @@ function lib_file(path: string) -> UiFile {
|
||||||
ui_err(`{path}: cannot read it`)
|
ui_err(`{path}: cannot read it`)
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
if lss_is(path) { return lss_file(src, path) }
|
var f: UiFile = null
|
||||||
return lib_read(src, path)
|
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)
|
# 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 {
|
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 {
|
function lib_read(src: string, path: string) -> UiFile {
|
||||||
let f = lib_new(path)
|
let f = lib_new(path)
|
||||||
|
f.src = src
|
||||||
|
tp_lines(src)
|
||||||
let root = tpl_read(Xml.parse(src), path)
|
let root = tpl_read(Xml.parse(src), path)
|
||||||
var tops = root.kids
|
var tops = root.kids
|
||||||
if root.tag != "ui" { tops = [root] }
|
if root.tag != "ui" { tops = [root] }
|
||||||
|
|
|
||||||
64
packages/ludic.ui/life.ludic
Normal file
64
packages/ludic.ui/life.ludic
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -1,15 +1,15 @@
|
||||||
# link.ludic - each tag in a file that is not an element names a component: the file's own, an
|
# 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`
|
# 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 {
|
function lib_link(f: UiFile, t: UiTpl) -> void {
|
||||||
t.sheet = f.sheet
|
t.sheet = f.sheet
|
||||||
if not lk_element(t.tag) {
|
if not lk_element(t.tag) {
|
||||||
t.comp = lk_find(f, t.tag, false)
|
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]) }
|
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 "
|
# a word in a list of words written " a b c "
|
||||||
function lk_in(words: string, w: string) -> bool {
|
function lk_in(words: string, w: string) -> bool {
|
||||||
let pat = ` {w} `
|
let pat = ` {w} `
|
||||||
|
|
|
||||||
36
packages/ludic.ui/media.ludic
Normal file
36
packages/ludic.ui/media.ludic
Normal 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
|
||||||
|
}
|
||||||
77
packages/ludic.ui/native.ludic
Normal file
77
packages/ludic.ui/native.ludic
Normal 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"
|
||||||
|
}
|
||||||
|
|
@ -6,6 +6,8 @@ export const UI_BUTTON: int = 3
|
||||||
export const UI_SCROLL: int = 4
|
export const UI_SCROLL: int = 4
|
||||||
export const UI_IMAGE: int = 5
|
export const UI_IMAGE: int = 5
|
||||||
export const UI_RULE: int = 6 # <hr>: a line across
|
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_COL: int = 0
|
||||||
export const UI_ROW: int = 1
|
export const UI_ROW: int = 1
|
||||||
export const UI_FILL: float = -1.0
|
export const UI_FILL: float = -1.0
|
||||||
|
|
@ -69,15 +71,30 @@ export property UiNode {
|
||||||
min_h: float = 0.0
|
min_h: float = 0.0
|
||||||
max_h: float = 0.0
|
max_h: float = 0.0
|
||||||
hidden: bool = false # display: none, or hidden
|
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
|
fg: int = -1
|
||||||
bg: int = -2
|
bg: int = -2
|
||||||
alpha: float = 1.0
|
alpha: float = 1.0
|
||||||
enabled: bool = true
|
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
|
env: UiEnv = null
|
||||||
children: []UiNode = null
|
children: []UiNode = null
|
||||||
mw: float = 0.0
|
mw: float = 0.0
|
||||||
mh: 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
|
x: float = 0.0
|
||||||
y: float = 0.0
|
y: float = 0.0
|
||||||
cw: float = 0.0
|
cw: float = 0.0
|
||||||
|
|
|
||||||
27
packages/ludic.ui/reload.ludic
Normal file
27
packages/ludic.ui/reload.ludic
Normal 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
|
||||||
|
}
|
||||||
|
|
@ -11,7 +11,7 @@ export property UiSel {
|
||||||
pargs: []string = null
|
pargs: []string = null
|
||||||
nots: []UiSel = null
|
nots: []UiSel = null
|
||||||
prev: 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 {
|
function sel_new() -> UiSel {
|
||||||
let s = new UiSel
|
let s = new UiSel
|
||||||
|
|
@ -36,6 +36,14 @@ function sel_parse(src: string, err: []string) -> UiSel {
|
||||||
comb = 1
|
comb = 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if rd_op(r, "+") {
|
||||||
|
comb = 2
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if rd_op(r, "~") {
|
||||||
|
comb = 3
|
||||||
|
continue
|
||||||
|
}
|
||||||
let c = sel_compound(r)
|
let c = sel_compound(r)
|
||||||
c.prev = cur
|
c.prev = cur
|
||||||
c.comb = comb
|
c.comb = comb
|
||||||
|
|
|
||||||
|
|
@ -15,6 +15,14 @@ function sel_match(s: UiSel, n: UiNode) -> bool {
|
||||||
if not sel_one(s, n) { return false }
|
if not sel_one(s, n) { return false }
|
||||||
if s.prev == null { return true }
|
if s.prev == null { return true }
|
||||||
if s.comb == 1 { return n.parent != null and sel_match(s.prev, n.parent) }
|
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
|
var a = n.parent
|
||||||
while a != null {
|
while a != null {
|
||||||
if sel_match(s.prev, a) { return true }
|
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 == "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 == "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 == "empty" { return len(n.children) == 0 and n.text == "" }
|
||||||
if p == "root" { return n.parent == null or n.parent.tag == "" }
|
if p == "root" { return n.parent == null }
|
||||||
if p == "nth-child" {
|
if p == "checked" { return ui_attr_on(n, "checked") }
|
||||||
let k = n.index + 1
|
if p == "active" { return n.hovered and fr_down_now }
|
||||||
if arg == "odd" { return k % 2 == 1 }
|
if p == "nth-child" { return sel_nth(arg, n.index + 1) }
|
||||||
if arg == "even" { return k % 2 == 0 }
|
|
||||||
return string(k) == arg
|
|
||||||
}
|
|
||||||
ui_err(`:{p}: there is no such pseudo-class here`)
|
ui_err(`:{p}: there is no such pseudo-class here`)
|
||||||
return false
|
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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@
|
||||||
export property UiRule {
|
export property UiRule {
|
||||||
sel: UiSel = null
|
sel: UiSel = null
|
||||||
spec: int = 0
|
spec: int = 0
|
||||||
|
md: UiMedia = null # the @media it sits in, if any
|
||||||
keys: []string = null
|
keys: []string = null
|
||||||
vals: []UiExpr = 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
|
# a stylesheet file: a file with rules and imports and nothing else
|
||||||
function lss_file(src: string, path: string) -> UiFile {
|
function lss_file(src: string, path: string) -> UiFile {
|
||||||
let f = lib_new(path)
|
let f = lib_new(path)
|
||||||
|
f.src = src
|
||||||
lss_text(f, src)
|
lss_text(f, src)
|
||||||
f.sheet = sheet_of(f)
|
f.sheet = sheet_of(f)
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
function lss_text(f: UiFile, src: string) -> void {
|
function lss_text(f: UiFile, src: string) -> void {
|
||||||
let err = new []string
|
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
|
let r = new UiRd
|
||||||
r.s = lss_strip(src)
|
r.s = src
|
||||||
while r.err == "" {
|
while r.err == "" {
|
||||||
rd_ws(r)
|
rd_ws(r)
|
||||||
if r.i >= len(r.s) { break }
|
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))
|
lss_import(f, lss_upto(r, 59))
|
||||||
continue
|
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 sels = lss_upto(r, 123)
|
||||||
let body = lss_upto(r, 125)
|
let body = lss_upto(r, 125)
|
||||||
|
let at = len(f.own)
|
||||||
lss_rule(f, sels, body, err)
|
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
|
# @import "base.lss"; - its rules come before this file's own
|
||||||
function lss_import(f: UiFile, what: string) -> void {
|
function lss_import(f: UiFile, what: string) -> void {
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,24 @@ export property UiTpl {
|
||||||
sheet: UiSheet = null # the styles of the file it was written in
|
sheet: UiSheet = null # the styles of the file it was written in
|
||||||
style: UiRule = null # its style="..." attribute, read as declarations
|
style: UiRule = null # its style="..." attribute, read as declarations
|
||||||
raw: string = "" # a <style>'s text, which is not a template's
|
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 {
|
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_keys = new []string
|
||||||
t.on_acts = new []UiAct
|
t.on_acts = new []UiAct
|
||||||
t.kids = new []UiTpl
|
t.kids = new []UiTpl
|
||||||
|
t.where = `{file}:{tp_line(x.at)}`
|
||||||
let err = new []string
|
let err = new []string
|
||||||
for i in 0 .. len(x.akeys) {
|
for i in 0 .. len(x.akeys) {
|
||||||
let k: string = x.akeys[i]
|
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))
|
let words = tpl_words(Xml.text(x))
|
||||||
if len(words) > 0 and len(t.kids) == 0 { t.text = ex_text(words, err) }
|
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
|
return t
|
||||||
}
|
}
|
||||||
# on-click is on-press, as a browser would have it
|
# on-click is on-press, as a browser would have it
|
||||||
|
|
|
||||||
88
packages/ludic.ui/units.ludic
Normal file
88
packages/ludic.ui/units.ludic
Normal 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 ""
|
||||||
|
}
|
||||||
|
|
@ -21,6 +21,7 @@ property Xml {
|
||||||
akeys: []pointer # attribute names
|
akeys: []pointer # attribute names
|
||||||
avals: []pointer # attribute values (entity-decoded)
|
avals: []pointer # attribute values (entity-decoded)
|
||||||
kids: []Xml # child elements, in document order
|
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 {
|
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.
|
# parse one element (cursor on its opening '<'). Recurses for children.
|
||||||
function xp_element(p: XP) -> Xml {
|
function xp_element(p: XP) -> Xml {
|
||||||
|
let at = p.i
|
||||||
p.i += 1 # skip '<'
|
p.i += 1 # skip '<'
|
||||||
let name = xp_name(p)
|
let name = xp_name(p)
|
||||||
let node = xml_new(name)
|
let node = xml_new(name)
|
||||||
|
node.at = at
|
||||||
xp_attrs(p, node)
|
xp_attrs(p, node)
|
||||||
# self-closing '/>'
|
# self-closing '/>'
|
||||||
if p.i < p.n and p.s[p.i] == '/' { # '/'
|
if p.i < p.n and p.s[p.i] == '/' { # '/'
|
||||||
|
|
|
||||||
|
|
@ -35,13 +35,21 @@ function vw_finish(v: int) -> void {
|
||||||
vw_parse(model, head.file, head.line, head.vis)
|
vw_parse(model, head.file, head.line, head.vis)
|
||||||
vw_parse(vw_call_src(v, px, head), 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 {
|
function vw_infer(v: int, g: Node, fname: pointer) -> pointer {
|
||||||
let e = g.a.kids[0].a
|
let e = g.a.kids[0].a
|
||||||
if e.kind == E_ID {
|
if e.kind == E_ID {
|
||||||
let d = find_global(e.s)
|
let d = find_global(e.s)
|
||||||
if d != null and d.ty != null { return d.ty }
|
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
|
g_vw_node[v].line = g.line
|
||||||
vw_err(v, `say what type {fname} is - {fname}: int = ...`)
|
vw_err(v, `say what type {fname} is - {fname}: int = ...`)
|
||||||
return "int"
|
return "int"
|
||||||
|
|
|
||||||
40828
selfhost/ludicc.seed.ll
40828
selfhost/ludicc.seed.ll
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -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_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/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")
|
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_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_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_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")
|
reject_case("rejected/view_without_ui", "a view is shown by ludic.ui", "a view needs ludic.ui to show it")
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue