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:
+51
-51
@@ -54,7 +54,9 @@ local enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||
local pressStyles = {}
|
||||
local laidOut = false
|
||||
local themeName
|
||||
local activeScreen
|
||||
-- One tree per state, built by the function ui.mount() was given. Rebuilding is
|
||||
-- cheap enough that nothing is retained between screens.
|
||||
local builder, root, captured, insideCaptured, confirming
|
||||
local applyPalette
|
||||
|
||||
local function mix(a, b, amount)
|
||||
@@ -117,10 +119,10 @@ function ui.setTheme(name)
|
||||
return nil, err
|
||||
end
|
||||
loadTheme(name)
|
||||
if activeScreen then
|
||||
applyPalette(activeScreen.root)
|
||||
if root then
|
||||
applyPalette(root)
|
||||
gui.clear(ui.theme.background)
|
||||
node.invalidate(activeScreen.root)
|
||||
node.invalidate(root)
|
||||
end
|
||||
return true
|
||||
end
|
||||
@@ -169,8 +171,10 @@ end
|
||||
|
||||
local function build(spec, kind)
|
||||
spec = spec or {}
|
||||
-- Nodes outside a build would reset the arena under the screen already on the
|
||||
-- panel, chrome included. Rebuilding is the only way to change one.
|
||||
if laidOut then
|
||||
ui.reset()
|
||||
error("build nodes from the function ui.mount() was given, then ui.rebuild()", 3)
|
||||
end
|
||||
|
||||
local children = {}
|
||||
@@ -326,13 +330,9 @@ function ui.reset()
|
||||
node.reset()
|
||||
clearState()
|
||||
laidOut = false
|
||||
activeScreen = nil
|
||||
root, captured, insideCaptured, confirming = nil, nil, nil, nil
|
||||
end
|
||||
|
||||
---@class UiScreen
|
||||
local Screen = {}
|
||||
Screen.__index = Screen
|
||||
|
||||
applyPalette = function(root)
|
||||
-- The root is the panel background, not a card: no border, so it takes the fast fillRect
|
||||
-- path rather than the per-pixel roundRect one. Radius stays so cards inherit it.
|
||||
@@ -348,33 +348,34 @@ applyPalette = function(root)
|
||||
})
|
||||
end
|
||||
|
||||
---@param root NodeId
|
||||
---@param style? NodeStyle
|
||||
---@return UiScreen
|
||||
function ui.screen(root, style)
|
||||
node.setSize(root, "fill", "fill")
|
||||
applyPalette(root)
|
||||
if style then
|
||||
node.setStyle(root, style)
|
||||
end
|
||||
local screen = setmetatable({ root = root }, Screen)
|
||||
activeScreen = screen
|
||||
screen:relayout()
|
||||
return screen
|
||||
---Registers the function that builds the whole tree and shows what it returns.
|
||||
---@param fn fun(): NodeId
|
||||
function ui.mount(fn)
|
||||
builder = fn
|
||||
ui.rebuild()
|
||||
end
|
||||
|
||||
function Screen:relayout()
|
||||
local ok, err = node.layout(self.root, 0, 0, gui.getWidth(), gui.getHeight())
|
||||
---Rebuilds the tree from scratch and repaints. Screens are not retained, so this
|
||||
---is how a screen changes, a rotation is answered and a dialog opens.
|
||||
function ui.rebuild()
|
||||
ui.reset()
|
||||
root = builder()
|
||||
node.setSize(root, "fill", "fill")
|
||||
applyPalette(root)
|
||||
local ok, err = node.layout(root, 0, 0, gui.getWidth(), gui.getHeight())
|
||||
if not ok then
|
||||
error(err, 2)
|
||||
end
|
||||
node.dropScratch()
|
||||
laidOut = true
|
||||
gui.clear(ui.theme.background)
|
||||
node.draw(root)
|
||||
end
|
||||
|
||||
function Screen:draw()
|
||||
node.draw(self.root)
|
||||
function ui.draw()
|
||||
if root then
|
||||
node.draw(root)
|
||||
end
|
||||
end
|
||||
|
||||
local function inside(id, x, y)
|
||||
@@ -405,7 +406,7 @@ end
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:down(x, y)
|
||||
function ui.down(x, y)
|
||||
local focused = node.getFocus()
|
||||
if focused then
|
||||
node.setFocus(nil)
|
||||
@@ -415,11 +416,11 @@ function Screen:down(x, y)
|
||||
end
|
||||
end
|
||||
|
||||
local target = node.hit(self.root, x, y)
|
||||
local target = root and node.hit(root, x, y)
|
||||
if not target then
|
||||
return false
|
||||
end
|
||||
self.captured, self.inside = target, true
|
||||
captured, insideCaptured = target, true
|
||||
enter(target, x, y)
|
||||
return true
|
||||
end
|
||||
@@ -427,18 +428,17 @@ end
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:move(x, y)
|
||||
local target = self.captured
|
||||
if not target then
|
||||
function ui.move(x, y)
|
||||
if not captured then
|
||||
return false
|
||||
end
|
||||
local isInside = inside(target, x, y)
|
||||
if isInside ~= self.inside then
|
||||
self.inside = isInside
|
||||
local isInside = inside(captured, x, y)
|
||||
if isInside ~= insideCaptured then
|
||||
insideCaptured = isInside
|
||||
if isInside then
|
||||
enter(target, x, y)
|
||||
enter(captured, x, y)
|
||||
else
|
||||
exit(target, x, y)
|
||||
exit(captured, x, y)
|
||||
end
|
||||
end
|
||||
return true
|
||||
@@ -447,15 +447,15 @@ end
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:up(x, y)
|
||||
local target = self.captured
|
||||
function ui.up(x, y)
|
||||
local target = captured
|
||||
if not target then
|
||||
return false
|
||||
end
|
||||
local wasActive = self.inside
|
||||
local wasActive = insideCaptured
|
||||
local releasedInside = inside(target, x, y)
|
||||
local handler = releasedInside and clickHandlers[target] or nil
|
||||
self.captured, self.inside = nil, nil
|
||||
captured, insideCaptured = nil, nil
|
||||
if wasActive then
|
||||
exit(target, x, y)
|
||||
end
|
||||
@@ -467,8 +467,8 @@ end
|
||||
|
||||
local DIRECTIONS = { up = true, down = true, left = true, right = true }
|
||||
|
||||
local function focusFirst(screen)
|
||||
local focused = node.focusFirst(screen.root)
|
||||
local function focusFirst()
|
||||
local focused = node.focusFirst(root)
|
||||
if focused then
|
||||
local handler = enterHandlers[focused]
|
||||
if handler then
|
||||
@@ -481,7 +481,7 @@ end
|
||||
---@param name string Button name; directions and confirm are handled.
|
||||
---@param pressed boolean
|
||||
---@return boolean handled
|
||||
function Screen:button(name, pressed)
|
||||
function ui.buttonPress(name, pressed)
|
||||
if type(pressed) ~= "boolean" then
|
||||
error("button state must be boolean", 2)
|
||||
end
|
||||
@@ -492,10 +492,10 @@ function Screen:button(name, pressed)
|
||||
end
|
||||
local previous = node.getFocus()
|
||||
if not previous then
|
||||
focusFirst(self)
|
||||
focusFirst()
|
||||
return true
|
||||
end
|
||||
local focused = node.moveFocus(self.root, name)
|
||||
local focused = node.moveFocus(root, name)
|
||||
if focused ~= previous then
|
||||
local leave = exitHandlers[previous]
|
||||
if leave then
|
||||
@@ -512,16 +512,16 @@ function Screen:button(name, pressed)
|
||||
if name ~= "confirm" then
|
||||
return false
|
||||
end
|
||||
local focused = node.getFocus() or focusFirst(self)
|
||||
local focused = node.getFocus() or focusFirst()
|
||||
if not focused then
|
||||
return false
|
||||
end
|
||||
if pressed then
|
||||
node.setPressed(focused, true)
|
||||
self.confirming = focused
|
||||
confirming = focused
|
||||
else
|
||||
local target = self.confirming
|
||||
self.confirming = nil
|
||||
local target = confirming
|
||||
confirming = nil
|
||||
if target then
|
||||
node.setPressed(target, false)
|
||||
local handler = clickHandlers[target]
|
||||
|
||||
+44
-30
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user