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]
|
||||
|
||||
Reference in New Issue
Block a user