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
+5 -6
View File
@@ -32,7 +32,6 @@ enum Type : uint8_t { BOX, TEXT, BUTTON, CUSTOM };
enum Flag : uint8_t {
ROW = 1 << 0, // main axis is horizontal
CAPTURE = 1 << 1, // swallows the taps its children missed
INTERACTIVE = 1 << 2, // has an on_press
DIRTY = 1 << 3,
PRESSED = 1 << 4,
@@ -204,8 +203,10 @@ public:
return error == nullptr;
}
// Deepest interactive node wins, so a tappable child beats its tappable
// parent. The list is singly linked, so "last match walking forward" stands
// Deepest node by geometry wins, so a child beats its parent and a later sibling
// beats an earlier one it overlaps. Whether that node responds to a press or a pan is
// dispatch's business, which bubbles up from here -- hit-testing is not fused with who
// handles the gesture. The list is singly linked, so "last match walking forward" stands
// in for "first match walking backward"; they name the same node.
uint16_t hit(uint16_t id, int px, int py) const {
const Node& n = nodes[id];
@@ -222,9 +223,7 @@ public:
}
if (found != NONE)
return found;
if (n.flags & CAPTURE)
return id;
return (n.flags & INTERACTIVE) ? id : NONE;
return id;
}
bool scrolls(uint16_t id) const {