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