feat(ui): tree-owned scroll gestures and geometry hit-testing

Drag, flick and tap-vs-scroll now live on the scrollX/scrollY flag in
ui.lua, so any scroll box pans with no app code. tree.hit returns the
deepest node by geometry and dispatch bubbles to the nearest handler,
dropping the now-unused CAPTURE flag. keyboard moves in as ui.keyboard,
and embed compiles nested lib dirs to dotted module names.
This commit is contained in:
2026-08-06 17:07:56 -04:00
parent e5ed980294
commit 2046938d32
9 changed files with 479 additions and 45 deletions
+10 -1
View File
@@ -173,7 +173,8 @@ int main() {
"assert(x == 4 and y == 4 and w == 312 and h == 16)\n"
"assert(tree.getLabel(label) == 'Hello')\n"
"assert(tree.hit(root, 10, 40) == button)\n"
"assert(tree.hit(root, 10, 200) == nil)\n"
// Hit is geometry: a point inside root but over no child answers root, not nil.
"assert(tree.hit(root, 10, 200) == root)\n"
"assert(tree.focusFirst(root) == button)\n"
"assert(tree.getFocus() == button)\n"
"assert(tree.moveFocus(root, 'up') == button)\n"
@@ -227,6 +228,14 @@ int main() {
"assert(bx == 0 and by == 0)\n"
// Row 1 is scrolled above the box, so a tap at the top hits row 2.
"assert(tree.hit(list, 10, 10) == rows[2], 'scrolled-out row is not hit')\n"
// Hit is pure geometry: the deepest node covering the point, else the container. A
// scroll pan finds its box by bubbling up from here, not by a flag on hit.
"local bare = tree.create(nil, {type = 'box', w = 'fill', h = 'fill', scrollY = true})\n"
"local child = tree.create(bare, {type = 'box', w = 100, h = 900})\n"
"assert(tree.layout(bare, 0, 0, 320, 240))\n"
"assert(tree.hit(bare, 10, 10) == child, 'deepest node by geometry wins')\n"
"assert(tree.hit(bare, 200, 10) == bare, 'the container answers where no child covers it')\n"
"tree.dropScratch()\n"
// Clamped, never past the content's far edge or before its start.
"tree.setScroll(list, 9999, 9999)\n"
"local cx, cy = tree.getScroll(list)\n"