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

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

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

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

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

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

View file

@ -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) }