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:
parent
637d07400e
commit
20d011f8e9
40 changed files with 62224 additions and 49996 deletions
|
|
@ -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)
|
||||
|
|
|
|||
45
packages/ludic.ui/build_class.ludic
Normal file
45
packages/ludic.ui/build_class.ludic
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
|
|||
72
packages/ludic.ui/class.ludic
Normal file
72
packages/ludic.ui/class.ludic
Normal 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 }
|
||||
}
|
||||
23
packages/ludic.ui/consts.ludic
Normal file
23
packages/ludic.ui/consts.ludic
Normal 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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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) }
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
35
packages/ludic.ui/instance.ludic
Normal file
35
packages/ludic.ui/instance.ludic
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
44
packages/ludic.ui/pointer.ludic
Normal file
44
packages/ludic.ui/pointer.ludic
Normal 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) }
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
36
packages/ludic.ui/screen_class.ludic
Normal file
36
packages/ludic.ui/screen_class.ludic
Normal 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
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
19
packages/ludic.ui/tpl_lines.ludic
Normal file
19
packages/ludic.ui/tpl_lines.ludic
Normal 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
|
||||
}
|
||||
|
||||
8
packages/ludic.ui/view.ludic
Normal file
8
packages/ludic.ui/view.ludic
Normal 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
|
||||
}
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue