feat(lang): UI components as files - component Name { ... } beside Name.xml and Name.lss

The compiler turns a component declaration into:
- a record of its props and state;
- a constructor, a props setter, a model and a call;
- a class, registered with ludic.ui at start.

Its template and styles are read from beside it and compiled in, with @import inlined, and a
missing template fails the build.

In the runtime:
- each mounted instance keeps its own props and state, and `set` in a template writes the state;
- styles are scoped to the component;
- class, style and id on a component's tag land on its root, styled by the parent's sheet too;
- ui_reload re-reads a component's files from disk and keeps instances.

The package's own module is now ludic_ui, so a program may call a directory of its own ui.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-24 16:50:12 +03:00
parent 637d07400e
commit 20d011f8e9
40 changed files with 62224 additions and 49996 deletions

View file

@ -299,27 +299,42 @@ A default is an expression written with the function and evaluated for each call
out. A call that leaves out a parameter with no default, names one the function does not have, or
puts a positional argument after a named one is refused with that said.
### Views and templates (`ludic.ui`)
### Components and templates (`ludic.ui`)
A screen is not code. It is a template - markup in a file the program loads at run time - and it
sees only what the program's `view` lets it: its fields to read, its functions to ask, and its
`on` events to send. Nothing else crosses in either direction.
A UI is components, and a component is three files side by side:
- `Name.ludic` declares what it takes, keeps and does;
- `Name.xml` is its HTML-shaped template;
- `Name.lss` holds its CSS-shaped styles, which apply to its own elements only.
```ludic
# doc-check: skip — a fragment of a program that imports ludic.ui
view Shop {
purse = purse # a global's own type
stock = stock # a list of records is a list of objects
owned: int = len(bought) # any expression, with its type said
function afford(i: int) -> bool { return purse >= stock[i].cost }
on buy(i: int) { purse -= stock[i].cost }
component Counter {
prop label: string # its parent passes it: <Counter label="a" step="{5}"/>
prop step: int = 1 # with a default
state count: int = 0 # the instance's own, kept while it is on screen
doubled: int = count * 2 # worked out every frame
function big() -> bool { return count > 3 } # callable from the template
on bump() { count += step } # an event: on-click="bump()"
}
```
`view Shop` writes `view_shop() -> UiView`. Its model is a `Value` object of every field, with a
record as an object of the fields a template can read and a list or registry as a list. Its `call`
runs a function or an event by name with arguments from the template: an int, a float, a bool or a
string.
- **Props and state** are plain names in the component's code; each mounted instance is a record
of them. A prop or state is an `int`, a `float`, a `bool`, a `string` or a `Val`. A field is
anything a template can read, and records and lists become objects and lists.
- **The template** has one root (use `<fragment>` for several). A component is used by its name as
a tag. `set count = 0` in an action sets its state, and `emit close` runs what its parent passed
as `on-close`. `class`, `style` and `id` on a component's tag land on its root element, styled by
the parent's sheet as well as its own.
- **Compiled in.** The compiler reads the template and the styles from beside the declaration and
inlines each `@import` (a path starting with `/` is from the project's root), so a missing
template fails the build and nothing has to ship beside the program. `ui_reload()` reads the
files again when they change on disk and keeps every instance's state.
- **A screen is a component** with no props: `ui_show("Counter", null, x, y, w, h)`, or
`ui_nodes("Counter", null)` for a test.
An older, lighter bridge remains: `view Name { ... }` gives a whole template file of `<screen>`s
and `<component>`s (loaded with `ui_load`) one model and one call, and the rest of this section
applies to both:
```xml
<ui>

View file

@ -1,6 +1,6 @@
bump: minor
type: feature
**Default parameters, views and templates.** A parameter can have a default (`pad: float = 8.0`).
**Default parameters, components, views and templates.** A parameter can have a default (`pad: float = 8.0`).
A call leaves out what it does not change, and may pass its first arguments by position and the
rest by name.
@ -8,6 +8,11 @@ A `view` declaration is the one bridge between a program and its UI. It names th
template may read, the functions it may ask and the `on` events it may send, and it writes
`view_<name>() -> UiView`.
A `component Name { prop, state, fields, functions, on events }` declaration beside `Name.xml` and
`Name.lss` is a UI component. Its template and styles are compiled in (with `@import` inlined), each
mounted instance keeps its own props and state, styles are scoped to it, and a parent's `class`,
`style` and `id` land on its root.
`ludic.ui` is now a template runtime. Screens and components are XML files loaded at run time,
with:
- `{expression}` bindings;

View file

@ -0,0 +1,37 @@
# ui_component.ludic — components as files: Counter.ludic declares props, state, a field, a function
# and an event; Counter.xml is its template; Counter.lss its scoped styles (which @import a theme).
# Two counters keep their own state, a parent's class reaches the child's root, and the child's
# `p { color }` does not leak into the parent.
import "ludic.ui"
import "ui_component_parts/Counter.ludic"
import "ui_component_parts/App.ludic"
program UiComponent {
numbers float
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 {
let root: UiNode = ui_nodes("App", null)
ui_place(root, 0.0, 0.0, 400.0, 400.0)
return root
}
entry {
var root = frame()
let b = root.children[0].children[2]
ui_press(b.children[1])
ui_press(b.children[1])
ui_press(root.children[0].children[1].children[1])
root = frame()
let out = new []string
texts(root, out)
var joined = ""
for i in 0 .. len(out) { joined = joined + out[i] + "|" }
print(joined)
let b2 = root.children[0].children[2]
print(`b margin {int(b2.mt)}; b text {b2.children[0].fg}; outside {root.children[0].children[3].fg}; button {int(b2.children[1].ch)}`)
ui_press(b2.children[2])
root = frame()
print(root.children[0].children[2].children[0].text)
}
}

View file

@ -0,0 +1 @@
.wide { margin: 9px }

View file

@ -0,0 +1,4 @@
# App.ludic — a screen is a component too: this one has no props, only what it shows
component App {
title: string = "counters"
}

View file

@ -0,0 +1,6 @@
<div>
<h1>{title}</h1>
<Counter label="a"/>
<Counter label="b" step="{5}" class="wide"/>
<p>outside</p>
</div>

View file

@ -0,0 +1,3 @@
@import "theme.lss";
.counter { padding: 4px }
p { color: #ff0000 }

View file

@ -0,0 +1,9 @@
# Counter.ludic — a component: what it takes (props), keeps (state), works out and does
component Counter {
prop label: string
prop step: int = 1
state count: int = 0
doubled: int = count * 2
function big() -> bool { return count > 3 }
on bump() { count += step }
}

View file

@ -0,0 +1,5 @@
<div class="counter">
<p>{label}: {count} ({doubled}){big() ? ' big' : ''}</p>
<button on-click="bump()">+</button>
<button on-click="set count = 0">reset</button>
</div>

View file

@ -0,0 +1,2 @@
/* theme.lss - shared by whoever imports it */
button { height: 30px }

View file

@ -0,0 +1,8 @@
# a component's template is NAME.xml beside it; this one has none
import "ludic.ui"
program ComponentNoTemplate {
component Lonely {
prop name: string
}
entry { print("never") }
}

View file

@ -71,6 +71,10 @@ function bd_slot(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
}
}
function bd_comp(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> bool {
if k.cls != null {
bd_class(k, e, key, out)
return true
}
if k.comp == null { return false }
let c = k.comp
let ce = env_new(null)

View file

@ -0,0 +1,45 @@
# build_class.ludic - a Ludic component's instance built: the parent's attributes are its props (id,
# class and style go on its root element instead, styled by the parent's sheet as well as its own),
# its model is its props, state and fields, and its template reads nothing else
function bd_class(k: UiTpl, e: UiEnv, key: string, out: []UiNode) -> void {
let cls = k.cls
uc_load(cls)
let it = in_get(key, cls)
let props = Value.object()
for i in 0 .. len(k.keys) {
let a = k.keys[i]
if a != "class" and a != "style" and a != "id" { Value.put(props, a, ui_eval(k.vals[i], e)) }
}
cls.props(it.ptr, props)
let ce = env_new(null)
ce.model = cls.model(it.ptr)
ce.inst = it
ce.ctx = e.ctx
ce.slot = k
ce.slot_env = e
ce.on_names = k.on_keys
ce.on_acts = k.on_acts
ce.on_envs = new []UiEnv
for i in 0 .. len(k.on_keys) { push(ce.on_envs, e) }
lc_note(key, k.on_keys, k.on_acts, e)
let at = len(out)
let root = new []UiTpl
push(root, cls.root)
bd_kids(root, ce, key, out)
if len(out) > at { bd_pass_down(out[at], k, e) }
}
# <Card class="wide" style="margin: 4px" id="c"> puts those on the card's own root element
function bd_pass_down(n: UiNode, k: UiTpl, e: UiEnv) -> void {
let c = tpl_attr(k, "class")
if c != null {
let parts = lss_split(ui_eval_text(c, e), 32)
for i in 0 .. len(parts) {
if len(parts[i]) > 0 { push(n.classes, parts[i]) }
}
}
let id = tpl_attr(k, "id")
if id != null { n.id = ui_eval_text(id, e) }
if k.style != null { n.style2 = k.style }
n.sheet2 = k.sheet
n.env2 = e
}

View file

@ -18,7 +18,9 @@ function cs_tree(n: UiNode) -> void {
function cs_node(n: UiNode) -> void {
cs_sheet(n, ua_sheet())
if n.sheet != null { cs_sheet(n, n.sheet) }
if n.sheet2 != null and n.sheet2 != n.sheet { cs_sheet(n, n.sheet2) }
if n.style != null { cs_rule(n, n.style) }
if n.style2 != null { cs_rule_in(n, n.style2, n.env2) }
for i in 0 .. len(n.akeys) { el_apply(n, n.akeys[i], n.vals[i]) }
}
function cs_sheet(n: UiNode, sh: UiSheet) -> void {
@ -27,13 +29,14 @@ function cs_sheet(n: UiNode, sh: UiSheet) -> void {
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 { cs_rule_in(n, r, n.env) }
# a rule's custom properties first, so its other declarations can read them; values read in `e`
function cs_rule_in(n: UiNode, r: UiRule, e: UiEnv) -> void {
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)) }
if len(r.keys[j]) > 2 and r.keys[j][0..2] == "--" { el_attr(n, r.keys[j], ui_eval(r.vals[j], e)) }
}
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)) }
if not (len(r.keys[j]) > 2 and r.keys[j][0..2] == "--") { el_attr(n, r.keys[j], ui_eval(r.vals[j], e)) }
}
}
# what every element looks like before anyone styles it, as a browser's own sheet does

View file

@ -0,0 +1,72 @@
# class.ludic - components declared in Ludic (`component Name { ... }` beside Name.xml and
# Name.lss): the compiler registers each at start with its template, its styles and its functions,
# and a tag of its name anywhere is an instance of it
export property UiClass {
name: string = ""
xml_path: string = "" # where the template is, for a developer's build to read again
xml: string = ""
lss_path: string = ""
lss: string = ""
make: fn() -> pointer = null
props: fn(pointer, Val) -> void = null
model: fn(pointer) -> Val = null
call: fn(pointer, string, Val) -> Val = null
root: UiTpl = null # the template, once read
sheet: UiSheet = null
seen_xml: string = "" # the files as a developer's build last found them on disk
seen_lss: string = ""
}
var uc_list: []UiClass = null # filled by the compiler's registrations, before the program's globals
export function ui_register(k: UiClass) -> int {
if uc_list == null { uc_list = new []UiClass }
push(uc_list, k)
return len(uc_list)
}
function uc_find(name: string) -> UiClass {
if uc_list == null { return null }
for i in 0 .. len(uc_list) {
if uc_list[i].name == name { return uc_list[i] }
}
return null
}
# the class's template and styles, read the first time it is shown
function uc_load(k: UiClass) -> void {
if k.root != null { return }
let f = lib_blank(k.xml_path)
f.src = k.xml
if k.lss != "" { lss_text(f, k.lss) }
f.sheet = sheet_of(f)
tp_lines(k.xml)
let root = tpl_read(Xml.parse(k.xml), k.xml_path)
k.sheet = f.sheet
lib_link(f, root)
k.root = root
}
# a developer's build reads a class's files again when they change on disk: the first look is what
# they are, and a later one that differs is taken (an @import in the styles is read from disk too)
function uc_reload() -> bool {
if uc_list == null { return false }
var changed = false
for i in 0 .. len(uc_list) {
let k = uc_list[i]
if k.xml_path != "" and Fs.exists(k.xml_path) {
let x = lib_text(k.xml_path)
var l = ""
if k.lss_path != "" and Fs.exists(k.lss_path) { l = lib_text(k.lss_path) }
if k.seen_xml != "" and (x != k.seen_xml or l != k.seen_lss) {
k.xml = x
k.lss = l
k.root = null
changed = true
}
k.seen_xml = x
k.seen_lss = l
}
}
return changed
}
# every class reads its template and styles again, as when a stylesheet they import has changed
function uc_invalidate() -> void {
if uc_list == null { return }
for i in 0 .. len(uc_list) { uc_list[i].root = null }
}

View file

@ -0,0 +1,23 @@
# consts.ludic - the kinds of node, directions and the words that place things
export const UI_BOX: int = 0
export const UI_TEXT: int = 1
export const UI_SPACER: int = 2
export const UI_BUTTON: int = 3
export const UI_SCROLL: int = 4
export const UI_IMAGE: int = 5
export const UI_RULE: int = 6 # <hr>: a line across
export const UI_NATIVE: int = 7 # an element the host draws itself (ui_native)
export const UI_AUTO: float = -1000000.0 # an inset that is not set
export const UI_COL: int = 0
export const UI_ROW: int = 1
export const UI_FILL: float = -1.0
export const UI_START: int = 0
export const UI_CENTER: int = 1
export const UI_END: int = 2
export const UI_STRETCH: int = 3 # align: the whole cross axis
export const UI_BETWEEN: int = 3 # justify: the spare room between, around or evenly
export const UI_AROUND: int = 4
export const UI_EVENLY: int = 5
export const UI_INHERIT: int = -1
export const UI_NONE: int = -2

View file

@ -13,6 +13,7 @@ export property UiEnv {
on_acts: []UiAct = null
on_envs: []UiEnv = null
ctx: UiEnv = null # what a <provide> above gave, seen through components too
inst: UiInst = null # the Ludic component whose template this is
}
var ui_view: UiView = null
var ui_errs: []string = new []string
@ -54,10 +55,15 @@ function env_get(e: UiEnv, name: string) -> Val {
ui_err(`no {name} here: not a loop's, a prop, a state or in the view`)
return Value.null()
}
# `set name = value`: the nearest state that holds the name
# `set name = value`: the nearest component's state of that name, or <state> that holds it
function env_set(e: UiEnv, name: string, v: Val) -> void {
var at = e
while at != null {
if at.inst != null {
let args = Value.list()
Value.add(args, v)
if Value.kind(at.inst.cls.call(at.inst.ptr, `set:{name}`, args)) != 99 { return }
}
if at.state != null and Value.has(at.state, name) != 0 {
Value.put(at.state, name, v)
return

View file

@ -42,8 +42,16 @@ function ev_call(x: UiExpr, e: UiEnv) -> Val {
for i in 0 .. Value.as_int(Value.at(args, 0)) { Value.add(out, Value.int(i)) }
return out
}
var at = e
while at != null {
if at.inst != null {
let r = at.inst.cls.call(at.inst.ptr, x.s, args)
if Value.kind(r) != 99 { return r }
}
at = at.parent
}
if ui_view == null or ui_view.call == null {
ui_err(`{x.s}(): no view is showing to ask`)
ui_err(`{x.s}(): there is no function or event of that name here`)
return Value.null()
}
return ui_view.call(x.s, args)

View file

@ -17,6 +17,8 @@ export function ui_show(screen: string, view: UiView, x: float, y: float, w: flo
# the screen's nodes for this frame, not yet laid out - for a test, or a host that places them
export function ui_nodes(screen: string, view: UiView) -> UiNode {
let si = lib_find(lib_screen_names, screen)
let cls = uc_find(screen)
if si < 0 and cls != null { return fr_class_nodes(cls) }
if si < 0 {
ui_err(`there is no screen {screen} - ui_load the file that has it`)
return null
@ -65,46 +67,3 @@ export function ui_press(n: UiNode) -> void {
}
# any event, as if the element had fired it - for a test
export function ui_event(n: UiNode, name: string, value: Val) -> void { nt_run(n, name, value) }
# the pointer: what it is over is :hover next frame (the element and all around it, as in CSS),
# and without a renderer's own button a press on the deepest pressable thing is a press
var fr_hover: []string = new []string
function fr_was_hovered(key: string) -> bool { return sel_has(fr_hover, key) }
function fr_pointer(root: UiNode) -> void {
if ui_be == null or ui_be.pointer_x == null {
fr_hover = new []string
return
}
let px = ui_be.pointer_x()
let py = ui_be.pointer_y()
let keys = new []string
fr_under(root, px, py, keys)
fr_hover = keys
if ui_be.pointer_down == null { return }
let down = ui_be.pointer_down()
fr_down_now = down
if ui_be.button != null { return }
let hit = fr_hit(root, px, py)
if hit != null and down and not fr_was_down and hit.enabled {
if hit.kind == UI_NATIVE { fm_press(hit) } else { ui_fire(hit, "press", Value.null()) }
}
fr_was_down = down
}
function fr_under(n: UiNode, px: float, py: float, keys: []string) -> void {
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return }
if n.key != "" { push(keys, n.key) }
for i in 0 .. len(n.children) { fr_under(n.children[i], px, py, keys) }
}
function fr_hit(n: UiNode, px: float, py: float) -> UiNode {
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return null }
if n.children != null {
var i = len(n.children) - 1
while i >= 0 {
let h = fr_hit(n.children[i], px, py)
if h != null { return h }
i -= 1
}
}
if n.act != null or (n.kind == UI_NATIVE and n.on_names != null and len(n.on_names) > 0) { return n }
return null
}
export function ui_hit(root: UiNode, px: float, py: float) -> UiNode { return fr_hit(root, px, py) }

View file

@ -1,8 +1,10 @@
# ludic.ui - templates: screens and components written as markup in files the program loads at
# run time, reading only what a `view` declaration lets them, and answering with its events
module ui
module ludic_ui
numbers float
import "consts.ludic"
import "node.ludic"
import "view.ludic"
import "backend.ludic"
import "expr.ludic"
import "expr_parse.ludic"
@ -15,13 +17,17 @@ import "native.ludic"
import "forms.ludic"
import "life.ludic"
import "tpl.ludic"
import "tpl_lines.ludic"
import "lib.ludic"
import "lib_util.ludic"
import "class.ludic"
import "instance.ludic"
import "reload.ludic"
import "link.ludic"
import "state.ludic"
import "build.ludic"
import "build_more.ludic"
import "build_class.ludic"
import "elem.ludic"
import "attr.ludic"
import "attr_box.ludic"
@ -39,5 +45,7 @@ import "layout_text.ludic"
import "layout_run.ludic"
import "layout_wrap.ludic"
import "frame.ludic"
import "pointer.ludic"
import "screen_class.ludic"
import "draw.ludic"
import "dump.ludic"

View file

@ -0,0 +1,35 @@
# instance.ludic - a mounted component's record (its props and state), found by where it sits in the
# tree; one that a build of its screen did not reach is gone, and its state with it
property UiInst {
key: string = ""
cls: UiClass = null
ptr: pointer = null
seen: int = 0
}
var in_list: []UiInst = new []UiInst
var in_build: int = 0
function in_get(key: string, k: UiClass) -> UiInst {
for i in 0 .. len(in_list) {
let it = in_list[i]
if it.key == key and it.cls.name == k.name {
it.seen = in_build
return it
}
}
let it = new UiInst
it.key = key
it.cls = k
it.ptr = k.make()
it.seen = in_build
push(in_list, it)
return it
}
# after a build of `screen`: its instances the build did not reach are unmounted
function in_sweep(screen: string) -> void {
let keep = new []UiInst
for i in 0 .. len(in_list) {
let it = in_list[i]
if it.seen == in_build or not lc_mine(it.key, screen) { push(keep, it) }
}
in_list = keep
}

View file

@ -1,4 +1,5 @@
# lib_util.ludic - a file made, found, read, and a path beside another
# lib_util.ludic - a file made, found, read, and a path beside another (or from the project's
# root, when it starts with /)
function lib_find(names: []string, name: string) -> int {
for i in 0 .. len(names) {
if names[i] == name { return i }
@ -6,6 +7,12 @@ function lib_find(names: []string, name: string) -> int {
return -1
}
function lib_new(path: string) -> UiFile {
let f = lib_blank(path)
push(lib_files, f)
return f
}
# a file that is not one of the loaded ones (a component's own)
function lib_blank(path: string) -> UiFile {
let f = new UiFile
f.path = path
f.names = new []string
@ -14,7 +21,6 @@ function lib_new(path: string) -> UiFile {
f.imp_as = new []string
f.imp = new []UiFile
f.own = new []UiRule
push(lib_files, f)
return f
}
function lib_text(path: string) -> string {
@ -29,6 +35,7 @@ function lib_beside(path: string, src: string) -> string {
for i in 0 .. len(path) {
if path[i] == 47 { cut = i }
}
if cut < 0 or (len(src) > 0 and src[0] == 47) { return src }
if len(src) > 0 and src[0] == 47 { return src[1..len(src)] }
if cut < 0 { return src }
return path[0..cut + 1] + src
}

View file

@ -5,8 +5,16 @@ function lib_link(f: UiFile, t: UiTpl) -> void {
t.sheet = f.sheet
if not lk_element(t.tag) {
t.comp = lk_find(f, t.tag, false)
if t.comp == null { t.cls = uc_find(t.tag) }
if t.cls != null {
lk_kids(f, t)
return
}
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`) }
}
lk_kids(f, t)
}
function lk_kids(f: UiFile, t: UiTpl) -> void {
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) or nt_find(tag) != null }

View file

@ -1,33 +1,4 @@
# node.ludic - what a template becomes each frame, the view it reads, and the renderer seam
export const UI_BOX: int = 0
export const UI_TEXT: int = 1
export const UI_SPACER: int = 2
export const UI_BUTTON: int = 3
export const UI_SCROLL: int = 4
export const UI_IMAGE: int = 5
export const UI_RULE: int = 6 # <hr>: a line across
export const UI_NATIVE: int = 7 # an element the host draws itself (ui_native)
export const UI_AUTO: float = -1000000.0 # an inset that is not set
export const UI_COL: int = 0
export const UI_ROW: int = 1
export const UI_FILL: float = -1.0
export const UI_START: int = 0
export const UI_CENTER: int = 1
export const UI_END: int = 2
export const UI_STRETCH: int = 3 # align: the whole cross axis
export const UI_BETWEEN: int = 3 # justify: the spare room between, around or evenly
export const UI_AROUND: int = 4
export const UI_EVENLY: int = 5
export const UI_INHERIT: int = -1
export const UI_NONE: int = -2
# what a program lets its templates see: `view Name { ... }` writes view_name() -> UiView
export property UiView {
name: string = ""
model: fn() -> Val = null
call: fn(string, Val) -> Val = null
}
export property UiNode {
kind: int = 0
dir: int = 0
@ -41,6 +12,9 @@ export property UiNode {
vals: []Val = null # the same, as values, applied after every rule
style: UiRule = null # its style="..." declarations
sheet: UiSheet = null # the stylesheets of the file it was written in
sheet2: UiSheet = null # a component's root: its user's sheet too, and their style="..."
style2: UiRule = null
env2: UiEnv = null
parent: UiNode = null
index: int = 0 # its place among its parent's children, for :first-child and the rest
src: string = "" # an <img>'s picture

View file

@ -0,0 +1,44 @@
# pointer.ludic - where the pointer is, what it presses
# the pointer: what it is over is :hover next frame (the element and all around it, as in CSS),
# and without a renderer's own button a press on the deepest pressable thing is a press
var fr_hover: []string = new []string
function fr_was_hovered(key: string) -> bool { return sel_has(fr_hover, key) }
function fr_pointer(root: UiNode) -> void {
if ui_be == null or ui_be.pointer_x == null {
fr_hover = new []string
return
}
let px = ui_be.pointer_x()
let py = ui_be.pointer_y()
let keys = new []string
fr_under(root, px, py, keys)
fr_hover = keys
if ui_be.pointer_down == null { return }
let down = ui_be.pointer_down()
fr_down_now = down
if ui_be.button != null { return }
let hit = fr_hit(root, px, py)
if hit != null and down and not fr_was_down and hit.enabled {
if hit.kind == UI_NATIVE { fm_press(hit) } else { ui_fire(hit, "press", Value.null()) }
}
fr_was_down = down
}
function fr_under(n: UiNode, px: float, py: float, keys: []string) -> void {
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return }
if n.key != "" { push(keys, n.key) }
for i in 0 .. len(n.children) { fr_under(n.children[i], px, py, keys) }
}
function fr_hit(n: UiNode, px: float, py: float) -> UiNode {
if px < n.x or py < n.y or px >= n.x + n.cw or py >= n.y + n.ch { return null }
if n.children != null {
var i = len(n.children) - 1
while i >= 0 {
let h = fr_hit(n.children[i], px, py)
if h != null { return h }
i -= 1
}
}
if n.act != null or (n.kind == UI_NATIVE and n.on_names != null and len(n.on_names) > 0) { return n }
return null
}
export function ui_hit(root: UiNode, px: float, py: float) -> UiNode { return fr_hit(root, px, py) }

View file

@ -2,12 +2,17 @@
# 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
var changed = uc_reload()
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 }
ui_errs = new []string
if len(lib_files) == 0 {
print(`ui: templates reloaded`)
return true
}
let old = lib_files
lib_files = new []UiFile
lib_screen_names = new []string
@ -22,6 +27,7 @@ export function ui_reload() -> bool {
if have { continue }
if f.disk { lib_file(f.path) } else { ui_load_text(f.src, f.path) }
}
uc_invalidate()
print(`ui: templates reloaded`)
return true
}

View file

@ -0,0 +1,36 @@
# screen_class.ludic - a Ludic component shown as a whole screen
# a Ludic component shown as a screen: an instance at the top, keyed by its name
function fr_class_nodes(cls: UiClass) -> UiNode {
lc_begin()
in_build += 1
ui_view = null
uc_load(cls)
let root = fr_root(cls.name, cls.sheet)
let use = new UiTpl
use.tag = cls.name
use.cls = cls
use.keys = new []string
use.vals = new []UiExpr
use.on_keys = new []string
use.on_acts = new []UiAct
use.kids = new []UiTpl
let e = env_new(null)
e.model = Value.object()
root.env = e
bd_class(use, e, cls.name, root.children)
in_sweep(cls.name)
cs_tree(root)
return root
}
function fr_root(key: string, sheet: UiSheet) -> UiNode {
let root = new UiNode
root.key = key
root.tag = "screen"
root.sheet = sheet
root.children = new []UiNode
root.classes = new []string
root.akeys = new []string
root.avals = new []string
root.vals = new []Val
return root
}

View file

@ -10,29 +10,12 @@ export property UiTpl {
text: UiExpr = null
kids: []UiTpl = null
comp: UiTpl = null # the component this tag names, found when its file loads
cls: UiClass = null # or the Ludic-declared component it names
sheet: UiSheet = null # the styles of the file it was written in
style: UiRule = null # its style="..." attribute, read as declarations
raw: string = "" # a <style>'s text, which is not a template's
where: string = "" # file:line, for an error
}
# the text being read, and where its lines start, so an element can say which line it is on
var tp_nl: []int = new []int
function tp_lines(src: string) -> void {
tp_nl = new []int
for i in 0 .. len(src) {
if src[i] == 10 { push(tp_nl, i) }
}
}
function tp_line(at: int) -> int {
var lo = 0
var hi = len(tp_nl)
while lo < hi {
let mid = (lo + hi) / 2
if tp_nl[mid] < at { lo = mid + 1 } else { hi = mid }
}
return lo + 1
}
function tpl_read(x: Xml, file: string) -> UiTpl {
let t = new UiTpl
t.tag = Xml.tag(x)

View file

@ -0,0 +1,19 @@
# tpl_lines.ludic - which line of its file an element is on
# 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
}

View file

@ -0,0 +1,8 @@
# view.ludic - a `view` declaration's bridge, for a screen written as a template file of its own
# what a program lets its templates see: `view Name { ... }` writes view_name() -> UiView
export property UiView {
name: string = ""
model: fn() -> Val = null
call: fn(string, Val) -> Val = null
}

View file

@ -0,0 +1,116 @@
# component.ludic — L11: a UI component is three files side by side: NAME.ludic declares it, NAME.xml
# is its template and NAME.lss its styles (optional). The declaration says what it takes, keeps and
# does, in Ludic:
#
# component ShopRow {
# prop item: Val # given by the parent's template, as an attribute
# prop picked: bool = false
# state taps: int = 0 # the instance's own, kept while it is mounted
# price: int = cost_of(item) # worked out each frame
# function label() -> string { return `{taps}` }
# on buy() { taps += 1 } # props and state are plain names here
# }
#
# Each mounted instance is a record of its props and state. The compiler reads the template and the
# styles into the program, so a missing template fails the build and nothing more has to ship, and
# registers the component with ludic.ui when the program starts.
var g_cm_name: []pointer = new []pointer
var g_cm_file: []pointer = new []pointer
var g_cm_line: []int = new []int
var g_cm_vis: []int = new []int
var g_cm_keep: [][]pointer = new [][]pointer # props and state, which are fields of the record
var g_cm_keep_ty: [][]pointer = new [][]pointer
var g_cm_prop: [][]bool = new [][]bool
var g_cm_defs: [][]Node = new [][]Node # a getter per default, or null
var g_cm_fields: [][]Node = new [][]Node
var g_cm_fnames: [][]pointer = new [][]pointer
var g_cm_calls: [][]Node = new [][]Node
var g_cm_cnames: [][]pointer = new [][]pointer
var g_cm_head: []Node = new []Node
function cm_prefix(name: pointer) -> pointer { return `cmp_{reg_lower(name)}_` }
# component NAME { members }
function parse_ui_component() -> void {
pi += 1
let name = eat_id()
let px = cm_prefix(name)
g_uses_value = true
let c = len(g_cm_name)
push(g_cm_name, name)
push(g_cm_file, g_parse_file)
push(g_cm_line, toks[pi].line)
push(g_cm_keep, new []pointer)
push(g_cm_keep_ty, new []pointer)
push(g_cm_prop, new []bool)
push(g_cm_defs, new []Node)
push(g_cm_fields, new []Node)
push(g_cm_fnames, new []pointer)
push(g_cm_calls, new []Node)
push(g_cm_cnames, new []pointer)
skipnl()
eat_op("{")
while true {
skipnl()
if is_op("}") { break }
cm_member(c, px)
}
eat_op("}")
let head = node(N_VAR)
head.s = px + "reg"
head.ty = "int"
push(g_cm_head, head)
push(prog, head)
}
function cm_member(c: int, px: pointer) -> void {
if is_id("prop") or is_id("state") {
let is_prop = is_id("prop")
pi += 1
let fname = eat_id()
eat_op(":")
let ty = ptype()
push(g_cm_keep[c], fname)
push(g_cm_keep_ty[c], ty)
push(g_cm_prop[c], is_prop)
var def: Node = null
if is_op("=") {
pi += 1
def = cm_getter(px + "def_" + fname, ty, expr())
}
push(g_cm_defs[c], def)
return
}
if is_id("function") or is_id("on") {
let ev = is_id("on")
if ev { toks[pi].text = "function" }
let f = parse_fn()
push(g_cm_cnames[c], f.s)
if ev { f.s = px + "on_" + f.s } else { f.s = px + f.s }
push(g_cm_calls[c], f)
push(prog, f)
return
}
let fname = eat_id()
var ty: pointer = null
if is_op(":") {
pi += 1
ty = ptype()
}
if not is_op("=") { perr(`component field {fname}: say what it is - {fname} = some_global`) }
pi += 1
push(g_cm_fnames[c], fname)
push(g_cm_fields[c], cm_getter(px + "get_" + fname, ty, expr()))
}
# a function of the instance that answers one expression (a field, or a default)
function cm_getter(name: pointer, ty: pointer, e: Node) -> Node {
let g = node(N_FN)
g.s = name
g.ty = ty
let r = node(S_RETURN)
r.a = e
let body = node(N_BLOCK)
push(body.kids, r)
g.a = body
push(prog, g)
return g
}

View file

@ -0,0 +1,78 @@
# component_class.ludic — L11: a component's class: its name, its template and styles as read from
# beside it (and where they are, for a developer's build to read again), and its functions; the
# registration is a global, so every component is known before the program's first frame
function cm_class(c: int, px: pointer, head: Node) -> void {
let name = g_cm_name[c]
let dir = dir_of(g_cm_file[c])
let xml_path = join_path(dir, name + ".xml")
let lss_path = join_path(dir, name + ".lss")
let xml = read_file(xml_path)
if xml == null { cm_err(c, `there is no {name}.xml beside it - a component's template is NAME.xml in its directory`) }
var lss = read_file(lss_path)
var lss_at = lss_path
if lss == null {
lss = ""
lss_at = ""
} else { lss = cm_lss_inline(c, lss, dir, 0) }
var src = `function {px}class() -> UiClass {{\n let k = new UiClass\n k.name = "{name}"\n`
src = src + ` k.xml_path = "{xml_path}"\n k.xml = {px}xml_text()\n k.lss_path = "{lss_at}"\n k.lss = {px}lss_text()\n`
src = src + ` k.make = fn {px}new\n k.props = fn {px}props\n k.model = fn {px}model\n k.call = fn {px}call\n return k\n}}\n`
vw_parse(src, g_cm_file[c], g_cm_line[c], head.vis)
cm_text_fn(c, px + "xml_text", xml, head)
cm_text_fn(c, px + "lss_text", lss, head)
let reg = node(E_CALL)
let callee = node(E_ID)
callee.s = "ui_register"
reg.a = callee
let arg = node(E_CALL)
let make = node(E_ID)
make.s = px + "class"
arg.a = make
arg.kids = new []Node
reg.kids = new []Node
push(reg.kids, arg)
head.a = reg
}
# a function answering a file's text, carried in the program as it was read
function cm_text_fn(c: int, name: pointer, text: pointer, head: Node) -> void {
let at = len(prog)
vw_parse(`function {name}() -> string {{\n return ""\n}}\n`, g_cm_file[c], g_cm_line[c], head.vis)
prog[at].a.kids[0].a.s = text
}
# a stylesheet with each `@import "x.lss";` replaced by that file (beside it, or from the project's
# root when it starts with /), so the program carries its whole look
function cm_lss_inline(c: int, text: pointer, dir: pointer, depth: int) -> pointer {
if depth > 8 { cm_err(c, "its stylesheets @import each other in a circle") }
var out = ""
var i = 0
let n = len(text)
while i < n {
var j = i
while j < n and text[j] != '\n' { j += 1 }
let line: pointer = text[i..j]
let path = cm_import_of(line)
if path == null { out = out + line + "\n" } else {
var at = join_path(dir, path)
if path[0] == '/' { at = path[1..len(path)] }
let inner = read_file(at)
if inner == null { cm_err(c, `its stylesheet imports {path}, which is not there`) }
out = out + cm_lss_inline(c, inner, dir_of(at), depth + 1) + "\n"
}
i = j + 1
}
return out
}
# the path in `@import "x";` (or with single quotes), or null for any other line
function cm_import_of(line: pointer) -> pointer {
var i = 0
while i < len(line) and (line[i] == ' ' or line[i] == '\t') { i += 1 }
if i + 7 > len(line) { return null }
if not (line[i..i + 7] == "@import") { return null }
var a = i + 7
while a < len(line) and line[a] != '"' and line[a] != '\'' { a += 1 }
if a >= len(line) { return null }
let q = line[a]
var b = a + 1
while b < len(line) and line[b] != q { b += 1 }
return line[a + 1..b]
}

View file

@ -0,0 +1,91 @@
# component_gen.ludic — L11: when the program is read, each component gets its record, its
# constructor, a props setter, a model, a call and its class, registered with ludic.ui at start
function components_finish() -> void {
var c = 0
while c < len(g_cm_name) {
cm_finish(c)
c += 1
}
}
function cm_err(c: int, msg: pointer) -> void {
g_err_file = g_cm_file[c]
g_err_line = g_cm_line[c]
g_parsing = false
perr(`component {g_cm_name[c]}: {msg}`)
}
function cm_finish(c: int) -> void {
if find_comp("UiClass") == null { cm_err(c, `a component is shown by ludic.ui - import "ludic.ui"`) }
let name = g_cm_name[c]
let px = cm_prefix(name)
let rec = "Cmp" + name
let head = g_cm_head[c]
let file = g_cm_file[c]
let line = g_cm_line[c]
var src = `property {rec} {{\n`
var i = 0
while i < len(g_cm_keep[c]) {
src = src + ` {g_cm_keep[c][i]}: {g_cm_keep_ty[c][i]}\n`
i += 1
}
vw_parse(src + "}\n", file, line, head.vis)
cm_bodies(c, rec)
vw_parse(cm_new_src(c, px, rec), file, line, head.vis)
vw_parse(cm_props_src(c, px, rec), file, line, head.vis)
vw_parse(cm_model_src(c, px, rec, head), file, line, head.vis)
vw_parse(cm_call_src(c, px, rec, head), file, line, head.vis)
cm_class(c, px, head)
}
# every function of the component (getters, defaults, functions, events) takes the instance
function cm_bodies(c: int, rec: pointer) -> void {
var i = 0
while i < len(g_cm_defs[c]) {
if g_cm_defs[c][i] != null { cm_rewrite(c, g_cm_defs[c][i], rec) }
i += 1
}
i = 0
while i < len(g_cm_fields[c]) {
let g = g_cm_fields[c][i]
if g.ty == null { g.ty = cm_infer(c, g, g_cm_fnames[c][i]) }
cm_rewrite(c, g, rec)
i += 1
}
i = 0
while i < len(g_cm_calls[c]) {
cm_rewrite(c, g_cm_calls[c][i], rec)
i += 1
}
}
function cm_infer(c: int, g: Node, fname: pointer) -> pointer {
let e = g.a.kids[0].a
if e.kind == E_ID and cm_is_keep(c, e.s) {
var i = 0
while i < len(g_cm_keep[c]) {
if (g_cm_keep[c][i] == e.s) { return g_cm_keep_ty[c][i] }
i += 1
}
}
if e.kind == E_ID {
let d = find_global(e.s)
if d != null and d.ty != null { return d.ty }
}
if e.kind == E_INT { return "int" }
if e.kind == E_FLOAT { return "float" }
if e.kind == E_BOOL { return "bool" }
if e.kind == E_STR { return "string" }
if e.kind == E_CALL and e.a != null and e.a.kind == E_ID {
let f = find_fn(e.a.s)
if f != null and f.ty != null and not (f.ty == "void") { return f.ty }
}
g_cm_line[c] = g.line
cm_err(c, `say what type {fname} is - {fname}: int = ...`)
return "int"
}
function cm_new_src(c: int, px: pointer, rec: pointer) -> pointer {
var src = `function {px}new() -> pointer {{\n let s = new {rec}\n`
var i = 0
while i < len(g_cm_keep[c]) {
if g_cm_defs[c][i] != null { src = src + ` s.{g_cm_keep[c][i]} = {px}def_{g_cm_keep[c][i]}(s)\n` }
i += 1
}
return src + " return s\n}\n"
}

View file

@ -0,0 +1,90 @@
# component_gen2.ludic — L11: a component's props from the parent's attributes, its model for the
# template (props, state and fields), and its call (functions, events, and `set` of a state)
function cm_props_src(c: int, px: pointer, rec: pointer) -> pointer {
var src = `function {px}props(p: pointer, v: Val) -> void {{\n let s: {rec} = p\n`
var i = 0
while i < len(g_cm_keep[c]) {
if g_cm_prop[c][i] {
let k = g_cm_keep[c][i]
let got = cm_from(c, g_cm_keep_ty[c][i], "value_get(v, \"" + k + "\")")
src = src + ` if value_has(v, "{k}") != 0 {{ s.{k} = {got} }}\n`
}
i += 1
}
return src + "}\n"
}
# a template's value as a prop or state of the component's own type
function cm_from(c: int, ty: pointer, x: pointer) -> pointer {
if (ty == "Val") { return x }
if (ty == "int") { return `value_as_int({x})` }
if (ty == "float") { return `value_as_float({x})` }
if (ty == "bool") { return `value_as_int({x}) != 0` }
if (ty == "string") { return `value_as_str({x})` }
cm_err(c, `a prop or state is an int, a float, a bool, a string or a Val, not a {ty}`)
return x
}
function cm_model_src(c: int, px: pointer, rec: pointer, head: Node) -> pointer {
var src = `function {px}model(p: pointer) -> Val {{\n let s: {rec} = p\n let o = value_object()\n`
var i = 0
while i < len(g_cm_keep[c]) {
let k = g_cm_keep[c][i]
let put = cm_to(c, g_cm_keep_ty[c][i], "s." + k, head)
src = src + ` value_put(o, "{k}", {put})\n`
i += 1
}
i = 0
while i < len(g_cm_fields[c]) {
let g = g_cm_fields[c][i]
let put = cm_to(c, g.ty, g.s + "(s)", head)
src = src + ` value_put(o, "{g_cm_fnames[c][i]}", {put})\n`
i += 1
}
return src + " return o\n}\n"
}
function cm_to(c: int, ty: pointer, x: pointer, head: Node) -> pointer {
if (ty == "Val") { return x }
let conv = vw_conv(0, ty, x, head)
if conv == null { cm_err(c, `a {ty} is something a template cannot read`) }
return conv
}
# call(name, args): a function or an event by name, or "set:NAME" for a state the template sets.
# A name it does not know answers a Value of kind 99, which the runtime reports.
function cm_call_src(c: int, px: pointer, rec: pointer, head: Node) -> pointer {
var src = `function {px}call(p: pointer, name: string, args: Val) -> Val {{\n let s: {rec} = p\n`
let calls = g_cm_calls[c]
var i = 0
while i < len(calls) {
let f = calls[i]
var args = "s"
var k = 1
while k < len(f.kids) {
args = args + ", " + cm_arg(c, f.kids[k], k - 1)
k += 1
}
src = src + ` if name == "{g_cm_cnames[c][i]}" {{\n`
let call = f.s + "(" + args + ")"
if f.ty == "void" { src = src + ` {call}\n return value_null()\n` } else {
let back = cm_to(c, f.ty, call, head)
src = src + ` return {back}\n`
}
src = src + " }\n"
i += 1
}
i = 0
while i < len(g_cm_keep[c]) {
if not g_cm_prop[c][i] {
let k = g_cm_keep[c][i]
let got = cm_from(c, g_cm_keep_ty[c][i], "value_at(args, 0)")
src = src + ` if name == "set:{k}" {{\n s.{k} = {got}\n return value_null()\n }}\n`
}
i += 1
}
return src + " return value_new(99)\n}\n"
}
# a parameter out of the template's argument list
function cm_arg(c: int, p: Node, i: int) -> pointer {
let at = `value_at(args, {i})`
if (p.ty == "int") or (p.ty == "float") or (p.ty == "bool") or (p.ty == "string") or (p.ty == "Val") { return cm_from(c, p.ty, at) }
cm_err(c, `{p.s} is a {p.ty}; a template can pass an int, a float, a bool, a string or a Val`)
return ""
}

View file

@ -0,0 +1,99 @@
# component_rw.ludic — L11: a component's code names its props, state, fields and functions plainly;
# each becomes the instance's: `taps += 1` is `self.taps += 1`, `label()` is cmp_x_label(self), a
# field is its getter called on self. Every function of the component takes `self` first.
function cm_rewrite(c: int, f: Node, self_ty: pointer) -> void {
var i = 0
while i < len(f.kids) {
let p = f.kids[i]
if cm_is_keep(c, p.s) or cm_is_field(c, p.s) {
g_err_file = f.file
g_err_line = f.line
g_parsing = false
perr(`component {g_cm_name[c]}: the parameter {p.s} has the name of its prop, state or field; call it something else`)
}
i += 1
}
let me = node(N_PARAM)
me.s = "self"
me.ty = self_ty
let kids = new []Node
push(kids, me)
i = 0
while i < len(f.kids) {
push(kids, f.kids[i])
i += 1
}
f.kids = kids
cm_rw(c, f.a)
}
function cm_is_keep(c: int, s: pointer) -> bool {
var i = 0
while i < len(g_cm_keep[c]) {
if (g_cm_keep[c][i] == s) { return true }
i += 1
}
return false
}
function cm_is_field(c: int, s: pointer) -> bool {
var i = 0
while i < len(g_cm_fnames[c]) {
if (g_cm_fnames[c][i] == s) { return true }
i += 1
}
return false
}
function cm_is_call(c: int, s: pointer) -> bool {
var i = 0
while i < len(g_cm_cnames[c]) {
if (g_cm_cnames[c][i] == s) { return true }
i += 1
}
return false
}
function cm_self() -> Node {
let e = node(E_ID)
e.s = "self"
return e
}
function cm_rw(c: int, n: Node) -> void {
if n == null { return }
if (n.kind == S_LET or n.kind == S_FOR) and n.s != null and (cm_is_keep(c, n.s) or cm_is_field(c, n.s)) {
g_err_file = n.file
g_err_line = n.line
g_parsing = false
perr(`component {g_cm_name[c]}: {n.s} is its own prop, state or field; name the local something else`)
}
if n.kind == E_ID and cm_is_keep(c, n.s) {
n.kind = E_MEMBER
n.a = cm_self()
return
}
if n.kind == E_ID and cm_is_field(c, n.s) {
n.kind = E_CALL
let callee = node(E_ID)
callee.s = cm_prefix(g_cm_name[c]) + "get_" + n.s
n.a = callee
n.kids = new []Node
push(n.kids, cm_self())
return
}
if n.kind == E_CALL and n.a != null and n.a.kind == E_ID and cm_is_call(c, n.a.s) and not cm_is_keep(c, n.a.s) {
n.a.s = cm_prefix(g_cm_name[c]) + n.a.s
let args = new []Node
push(args, cm_self())
var i = 0
while i < len(n.kids) {
push(args, n.kids[i])
i += 1
}
n.kids = args
}
cm_rw(c, n.a)
cm_rw(c, n.b)
cm_rw(c, n.c)
var i = 0
while i < len(n.kids) {
cm_rw(c, n.kids[i])
i += 1
}
}

View file

@ -1017,6 +1017,7 @@ function parse_one_decl() -> void {
if is_id("registry") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "of") { parse_registry(); return } # L8
if is_id("def") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].kind == TK_ID) { parse_def(); return } # L8
if is_id("view") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_view(); return } # L11
if is_id("component") and (toks[pi + 1].kind == TK_ID) and (toks[pi + 2].text == "{") { parse_ui_component(); return } # L11
if is_id("var") { push(prog, parse_var()); return }
if is_id("const") { push(prog, parse_const()); return }
var is_unsafe = false
@ -1514,4 +1515,5 @@ function parse_program() -> void {
}
registries_finish() # L8: each registry gets its defs
views_finish() # L11: each view gets its model and call
components_finish() # L11: each component gets its class
}

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -27,6 +27,11 @@ function selfhost_frags() -> []pointer {
push(f, "selfhost/frontend/view.ludic")
push(f, "selfhost/frontend/view_conv.ludic")
push(f, "selfhost/frontend/view_finish.ludic")
push(f, "selfhost/frontend/component.ludic")
push(f, "selfhost/frontend/component_rw.ludic")
push(f, "selfhost/frontend/component_gen.ludic")
push(f, "selfhost/frontend/component_gen2.ludic")
push(f, "selfhost/frontend/component_class.ludic")
push(f, "selfhost/backend/emit_core.ludic")
push(f, "selfhost/backend/emit_head.ludic")
push(f, "selfhost/backend/emit_addr.ludic")

View file

@ -755,6 +755,8 @@ function cmd_dev_test() -> int {
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)")
controller_case("library/ui_component", "", "counters|a: 1 (2)|+|reset|b: 10 (20) big|+|reset|outside| b margin 9; b text 16711680; outside -1; button 30 b: 0 (0)", "ui_component.ludic (L11: components as NAME.ludic + .xml + .lss - props, state, fields, events, scoped styles, @import, class passed down)")
reject_case("rejected/component_no_template", "there is no Lonely.xml beside it", "a component needs its template beside it")
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")