feat(ui): one mounted tree instead of a screen object per build

Chrome and the app now share a tree, so a screen is no longer something an
app constructs and holds: ui.mount() takes the function that builds the
whole thing and ui.rebuild() runs it again. Building a node after layout
is refused rather than silently resetting the arena under the panel.
This commit is contained in:
2026-08-04 20:53:15 -04:00
parent e3153f57c5
commit 3f26c4ff10
3 changed files with 96 additions and 82 deletions
+44 -30
View File
@@ -186,27 +186,30 @@ local function handler(name)
end
end
local first = ui.button {
label = "one",
on_enter = handler "enter",
on_exit = handler "exit",
on_click = handler "click",
}
local second = ui.button {
label = "two",
on_enter = handler "enter",
on_exit = handler "exit",
on_click = handler "click",
}
local screen = ui.screen(ui.box { row = true, first, second })
local first, second
ui.mount(function()
first = ui.button {
label = "one",
on_enter = handler "enter",
on_exit = handler "exit",
on_click = handler "click",
}
second = ui.button {
label = "two",
on_enter = handler "enter",
on_exit = handler "exit",
on_click = handler "click",
}
return ui.box { row = true, first, second }
end)
assert(screen:down(10, 10))
assert(ui.down(10, 10))
assert(node.isPressed(first))
assert(screen:move(95, 10))
assert(ui.move(95, 10))
assert(not node.isPressed(first))
assert(screen:move(10, 10))
assert(ui.move(10, 10))
assert(node.isPressed(first))
assert(screen:up(10, 10))
assert(ui.up(10, 10))
assert(not node.isPressed(first))
local expectedTouch = { "enter", "exit", "enter", "exit", "click" }
@@ -218,16 +221,16 @@ for index, name in ipairs(expectedTouch) do
end
events = {}
assert(screen:button("right", true))
assert(screen:button("right", false))
assert(ui.buttonPress("right", true))
assert(ui.buttonPress("right", false))
assert(node.getFocus() == first)
assert(screen:button("right", true))
assert(ui.buttonPress("right", true))
assert(node.getFocus() == second)
assert(screen:button("confirm", true))
assert(ui.buttonPress("confirm", true))
assert(node.isPressed(second))
assert(screen:button("confirm", false))
assert(ui.buttonPress("confirm", false))
assert(not node.isPressed(second))
assert(screen:button("back", true) == false)
assert(ui.buttonPress("back", true) == false)
local expectedButtons = {
{ "enter", first },
@@ -246,7 +249,7 @@ assert(ui.setTheme "mono" == true)
assert(ui.getTheme() == "mono" and #invalidated == before + 1)
assert(cleared == ui.theme.background)
screen:draw()
ui.draw()
-- 320x480 portrait: two columns, and the reserve comes off the height budget.
local side, columns = ui.cardSide(5, 12, 8)
@@ -265,21 +268,32 @@ local pressedCalls = {}
node.setPressed = function(_, on)
pressedCalls[#pressedCalls + 1] = on
end
local own = ui.custom { h = 20, press_style = false, on_click = function() end }
local styled = ui.button { h = 20, label = "ok", on_click = function() end }
local board = ui.screen(ui.box { own, styled })
local own, styled
ui.mount(function()
own = ui.custom { h = 20, press_style = false, on_click = function() end }
styled = ui.button { h = 20, label = "ok", on_click = function() end }
return ui.box { own, styled }
end)
local target
node.hit = function()
return target
end
target = own
board:down(0, 0)
board:up(0, 0)
ui.down(0, 0)
ui.up(0, 0)
assert(#pressedCalls == 0, "a self-painting widget is not styled on press")
target = styled
board:down(0, 0)
ui.down(0, 0)
assert(pressedCalls[1] == true, "an ordinary widget still gets its pressed style")
-- Building outside a rebuild would reset the arena under the screen on the panel.
local built = pcall(ui.button, { label = "stray" })
assert(not built, "a node built after layout is refused")
local rebuilt = false
ui.rebuild()
rebuilt = true
assert(rebuilt and ui.down(0, 0), "a rebuild replaces the screen and keeps dispatch live")
print "ok"