diff --git a/LANGUAGE.md b/LANGUAGE.md
index dbdc76dc..764a9dd0 100644
--- a/LANGUAGE.md
+++ b/LANGUAGE.md
@@ -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:
+ 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 `` 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 ``s
+and ``s (loaded with `ui_load`) one model and one call, and the rest of this section
+applies to both:
```xml
diff --git a/changes/defaults-views-templates.md b/changes/defaults-views-templates.md
index 00ff92d4..078aac6f 100644
--- a/changes/defaults-views-templates.md
+++ b/changes/defaults-views-templates.md
@@ -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_() -> 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;
diff --git a/examples/library/ui_component.ludic b/examples/library/ui_component.ludic
new file mode 100644
index 00000000..b79a3074
--- /dev/null
+++ b/examples/library/ui_component.ludic
@@ -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)
+ }
+}
diff --git a/examples/library/ui_component_parts/App.lss b/examples/library/ui_component_parts/App.lss
new file mode 100644
index 00000000..29c72305
--- /dev/null
+++ b/examples/library/ui_component_parts/App.lss
@@ -0,0 +1 @@
+.wide { margin: 9px }
diff --git a/examples/library/ui_component_parts/App.ludic b/examples/library/ui_component_parts/App.ludic
new file mode 100644
index 00000000..788d54e7
--- /dev/null
+++ b/examples/library/ui_component_parts/App.ludic
@@ -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"
+}
diff --git a/examples/library/ui_component_parts/App.xml b/examples/library/ui_component_parts/App.xml
new file mode 100644
index 00000000..afd44365
--- /dev/null
+++ b/examples/library/ui_component_parts/App.xml
@@ -0,0 +1,6 @@
+
+
{title}
+
+
+
outside
+
diff --git a/examples/library/ui_component_parts/Counter.lss b/examples/library/ui_component_parts/Counter.lss
new file mode 100644
index 00000000..15174c36
--- /dev/null
+++ b/examples/library/ui_component_parts/Counter.lss
@@ -0,0 +1,3 @@
+@import "theme.lss";
+.counter { padding: 4px }
+p { color: #ff0000 }
diff --git a/examples/library/ui_component_parts/Counter.ludic b/examples/library/ui_component_parts/Counter.ludic
new file mode 100644
index 00000000..02cc48f8
--- /dev/null
+++ b/examples/library/ui_component_parts/Counter.ludic
@@ -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 }
+}
diff --git a/examples/library/ui_component_parts/Counter.xml b/examples/library/ui_component_parts/Counter.xml
new file mode 100644
index 00000000..617de5aa
--- /dev/null
+++ b/examples/library/ui_component_parts/Counter.xml
@@ -0,0 +1,5 @@
+
+
{label}: {count} ({doubled}){big() ? ' big' : ''}
+
+
+
diff --git a/examples/library/ui_component_parts/theme.lss b/examples/library/ui_component_parts/theme.lss
new file mode 100644
index 00000000..c7297243
--- /dev/null
+++ b/examples/library/ui_component_parts/theme.lss
@@ -0,0 +1,2 @@
+/* theme.lss - shared by whoever imports it */
+button { height: 30px }
diff --git a/examples/rejected/component_no_template.ludic b/examples/rejected/component_no_template.ludic
new file mode 100644
index 00000000..0108b740
--- /dev/null
+++ b/examples/rejected/component_no_template.ludic
@@ -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") }
+}
diff --git a/packages/ludic.ui/build.ludic b/packages/ludic.ui/build.ludic
index 37dde5c8..fd848054 100644
--- a/packages/ludic.ui/build.ludic
+++ b/packages/ludic.ui/build.ludic
@@ -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)
diff --git a/packages/ludic.ui/build_class.ludic b/packages/ludic.ui/build_class.ludic
new file mode 100644
index 00000000..7011212b
--- /dev/null
+++ b/packages/ludic.ui/build_class.ludic
@@ -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) }
+}
+# 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
+}
diff --git a/packages/ludic.ui/cascade.ludic b/packages/ludic.ui/cascade.ludic
index 5ea03cc5..16ae67ee 100644
--- a/packages/ludic.ui/cascade.ludic
+++ b/packages/ludic.ui/cascade.ludic
@@ -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
diff --git a/packages/ludic.ui/class.ludic b/packages/ludic.ui/class.ludic
new file mode 100644
index 00000000..39ddd39b
--- /dev/null
+++ b/packages/ludic.ui/class.ludic
@@ -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 }
+}
diff --git a/packages/ludic.ui/consts.ludic b/packages/ludic.ui/consts.ludic
new file mode 100644
index 00000000..5b3cc0f7
--- /dev/null
+++ b/packages/ludic.ui/consts.ludic
@@ -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 # : 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
+
diff --git a/packages/ludic.ui/env.ludic b/packages/ludic.ui/env.ludic
index 2ddaa1a5..ea35bee9 100644
--- a/packages/ludic.ui/env.ludic
+++ b/packages/ludic.ui/env.ludic
@@ -13,6 +13,7 @@ export property UiEnv {
on_acts: []UiAct = null
on_envs: []UiEnv = null
ctx: UiEnv = null # what a 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 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
diff --git a/packages/ludic.ui/eval.ludic b/packages/ludic.ui/eval.ludic
index 612378ff..2c757675 100644
--- a/packages/ludic.ui/eval.ludic
+++ b/packages/ludic.ui/eval.ludic
@@ -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)
diff --git a/packages/ludic.ui/frame.ludic b/packages/ludic.ui/frame.ludic
index d67a273a..bdb555e3 100644
--- a/packages/ludic.ui/frame.ludic
+++ b/packages/ludic.ui/frame.ludic
@@ -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) }
diff --git a/packages/ludic.ui/index.ludic b/packages/ludic.ui/index.ludic
index e4ee012f..655e534f 100644
--- a/packages/ludic.ui/index.ludic
+++ b/packages/ludic.ui/index.ludic
@@ -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"
diff --git a/packages/ludic.ui/instance.ludic b/packages/ludic.ui/instance.ludic
new file mode 100644
index 00000000..dc9b7acf
--- /dev/null
+++ b/packages/ludic.ui/instance.ludic
@@ -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
+}
diff --git a/packages/ludic.ui/lib_util.ludic b/packages/ludic.ui/lib_util.ludic
index 9037c9a1..2bcbd92f 100644
--- a/packages/ludic.ui/lib_util.ludic
+++ b/packages/ludic.ui/lib_util.ludic
@@ -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
}
diff --git a/packages/ludic.ui/link.ludic b/packages/ludic.ui/link.ludic
index 489c1ce5..ca839ef2 100644
--- a/packages/ludic.ui/link.ludic
+++ b/packages/ludic.ui/link.ludic
@@ -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 }
diff --git a/packages/ludic.ui/node.ludic b/packages/ludic.ui/node.ludic
index 0be762ac..98b75ddc 100644
--- a/packages/ludic.ui/node.ludic
+++ b/packages/ludic.ui/node.ludic
@@ -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 # : 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 's picture
diff --git a/packages/ludic.ui/pointer.ludic b/packages/ludic.ui/pointer.ludic
new file mode 100644
index 00000000..27977402
--- /dev/null
+++ b/packages/ludic.ui/pointer.ludic
@@ -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) }
diff --git a/packages/ludic.ui/reload.ludic b/packages/ludic.ui/reload.ludic
index 33f94943..6c089507 100644
--- a/packages/ludic.ui/reload.ludic
+++ b/packages/ludic.ui/reload.ludic
@@ -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
}
diff --git a/packages/ludic.ui/screen_class.ludic b/packages/ludic.ui/screen_class.ludic
new file mode 100644
index 00000000..40b53a08
--- /dev/null
+++ b/packages/ludic.ui/screen_class.ludic
@@ -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
+}
diff --git a/packages/ludic.ui/tpl.ludic b/packages/ludic.ui/tpl.ludic
index 61ed7353..5144aee0 100644
--- a/packages/ludic.ui/tpl.ludic
+++ b/packages/ludic.ui/tpl.ludic
@@ -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