feat: scrolling

This commit is contained in:
2026-08-06 09:05:02 -04:00
parent 26f3fdb6e6
commit e5ed980294
9 changed files with 527 additions and 37 deletions
+5
View File
@@ -162,6 +162,11 @@ struct Gui : GuiProvider {
return (r << 16) | (g << 8) | b;
}
void clear(int32_t) override { trace += "clear;"; }
void setClip(int32_t x, int32_t y, int32_t w, int32_t h) override {
trace += "clip(" + std::to_string(x) + "," + std::to_string(y) + "," +
std::to_string(w) + "," + std::to_string(h) + ");";
}
void clearClip() override { trace += "unclip;"; }
void fillRect(int32_t, int32_t, int32_t, int32_t, int32_t) override {
trace += "fillRect;";
}
+50
View File
@@ -198,6 +198,56 @@ int main() {
"tree.draw(root)\n"
"assert(painted == 320)");
// A scrolling box: children measure past its edges, panning moves them and is
// clamped to the content, and what is scrolled out of the box is neither hit
// nor painted outside it.
bench.gui.trace.clear();
run(state,
"tree.reset()\n"
"local list = tree.create(nil, {type = 'box', w = 'fill', h = 'fill',\n"
" scrollX = true, scrollY = true})\n"
"local rows = {}\n"
"for i = 1, 5 do rows[i] = tree.create(list, {type = 'box', w = 400, h = "
"100, interactive = true}) end\n"
"tree.setStyle(list, {background = 0xFFFFFF, fill = 0xFFFFFF, border = "
"0x333333})\n"
// The box is the panel; the content is deliberately larger on both axes.
"assert(tree.layout(list, 0, 0, 320, 240))\n"
"local maxX, maxY = tree.getScrollRange(list)\n"
"assert(maxX == 400 - 320, 'content is wider than the box')\n"
"assert(maxY == 5 * 100 - 240, 'content is taller than the box')\n"
// Panning shifts the children, and getRect keeps answering screen space.
"tree.setScroll(list, 30, 150)\n"
"local x, y = tree.getRect(rows[1])\n"
"assert(x == -30 and y == -150, 'row moved by the scroll')\n"
"local sx, sy = tree.getScroll(list)\n"
"assert(sx == 30 and sy == 150)\n"
// The box itself does not move, only what it holds.
"local bx, by = tree.getRect(list)\n"
"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"
// 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"
"assert(cx == maxX and cy == maxY, 'clamped to the content')\n"
"tree.setScroll(list, -50, -50)\n"
"local zx, zy = tree.getScroll(list)\n"
"assert(zx == 0 and zy == 0, 'clamped at the origin')\n"
"tree.draw(list)\n"
"tree.dropScratch()");
// The subtree is scissored to the box, so a row hanging past it cannot paint
// over whatever sits outside.
assert(bench.gui.trace.find("clip(0,0,320,240);") != std::string::npos);
assert(bench.gui.trace.find("unclip;") != std::string::npos);
// A scrolled axis has no size to hand down, so the box needs one of its own.
expectError(state, "tree.reset()\n"
"local l = tree.create(nil, {type = 'box', scrollY = "
"true})\n"
"tree.create(l, {type = 'box', w = 10, h = 10})\n"
"assert(tree.layout(l, 0, 0, 320, 240))");
// A timer firing inside draw is still one batch.
run(state, "function draw() timer.after(1, function() end) end\n"
"nested = timer.after(1, function() draw() end)");