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
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()}`)
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue