4b89b27947
Chrome takes the top of the panel before an app builds anything, and layout has not run yet when it does, so ui.frame() reports what the mount left it.
306 lines
7.6 KiB
Lua
306 lines
7.6 KiB
Lua
package.path = "./lua/lib/?.lua;" .. package.path
|
|
|
|
local files = {}
|
|
local cleared, invalidated = nil, {}
|
|
|
|
fs = {
|
|
readFile = function(path, maxBytes)
|
|
local value = files[path]
|
|
if value and #value > maxBytes then
|
|
return nil, "too large"
|
|
end
|
|
return value, value and nil or "missing"
|
|
end,
|
|
writeFile = function(path, value)
|
|
files[path] = value
|
|
return true
|
|
end,
|
|
}
|
|
|
|
local savedTheme = "light"
|
|
settings = {
|
|
getTheme = function()
|
|
return savedTheme
|
|
end,
|
|
setTheme = function(name)
|
|
savedTheme = name
|
|
return true
|
|
end,
|
|
}
|
|
|
|
local frameWidth, frameHeight = 320, 480
|
|
|
|
gui = {
|
|
FONT_SMALL = 0,
|
|
FONT_UI = 1,
|
|
FONT_BODY = 2,
|
|
FONT_LARGE = 3,
|
|
STYLE_NORMAL = 0,
|
|
STYLE_BOLD = 1,
|
|
color = function(r, g, b)
|
|
return r * 65536 + g * 256 + b
|
|
end,
|
|
getWidth = function()
|
|
return frameWidth
|
|
end,
|
|
getHeight = function()
|
|
return frameHeight
|
|
end,
|
|
getTextWidth = function(_, text)
|
|
return #text * 6
|
|
end,
|
|
getFontHeight = function()
|
|
return 8
|
|
end,
|
|
clear = function(color)
|
|
cleared = color
|
|
end,
|
|
}
|
|
|
|
local nodes, focus, painter = {}, nil, nil
|
|
local buttonCount = 0
|
|
|
|
local function interactiveNodes()
|
|
local result = {}
|
|
for id, entry in ipairs(nodes) do
|
|
if entry.interactive then
|
|
result[#result + 1] = id
|
|
end
|
|
end
|
|
return result
|
|
end
|
|
|
|
node = {
|
|
reset = function()
|
|
nodes, focus, buttonCount = {}, nil, 0
|
|
end,
|
|
create = function(parent, spec)
|
|
local id = #nodes + 1
|
|
local x = 0
|
|
if spec.type == "button" then
|
|
buttonCount = buttonCount + 1
|
|
x = (buttonCount - 1) * 100
|
|
end
|
|
nodes[id] = {
|
|
parent = parent,
|
|
type = spec.type,
|
|
label = spec.label,
|
|
interactive = spec.interactive,
|
|
rect = { x, 0, 90, 50 },
|
|
pressed = false,
|
|
style = {},
|
|
}
|
|
return id
|
|
end,
|
|
attach = function(parent, child)
|
|
nodes[child].parent = parent
|
|
end,
|
|
setSize = function() end,
|
|
setStyle = function(id, style)
|
|
for key, value in pairs(style) do
|
|
nodes[id].style[key] = value
|
|
end
|
|
end,
|
|
layout = function()
|
|
return true
|
|
end,
|
|
dropScratch = function() end,
|
|
hit = function(_, x, y)
|
|
for _, id in ipairs(interactiveNodes()) do
|
|
local rect = nodes[id].rect
|
|
if x >= rect[1] and x < rect[1] + rect[3] and y >= rect[2] and y < rect[2] + rect[4] then
|
|
return id
|
|
end
|
|
end
|
|
end,
|
|
getRect = function(id)
|
|
return table.unpack(nodes[id].rect)
|
|
end,
|
|
setLabel = function(id, text)
|
|
nodes[id].label = text
|
|
end,
|
|
getLabel = function(id)
|
|
return nodes[id].label
|
|
end,
|
|
invalidate = function(id)
|
|
invalidated[#invalidated + 1] = id
|
|
end,
|
|
setPressed = function(id, pressed)
|
|
nodes[id].pressed = pressed
|
|
end,
|
|
isPressed = function(id)
|
|
return nodes[id].pressed
|
|
end,
|
|
setPainter = function(callback)
|
|
painter = callback
|
|
end,
|
|
draw = function()
|
|
if painter then
|
|
for id, entry in ipairs(nodes) do
|
|
if entry.type == "custom" then
|
|
painter(id, table.unpack(entry.rect))
|
|
end
|
|
end
|
|
end
|
|
end,
|
|
focusFirst = function()
|
|
focus = interactiveNodes()[1]
|
|
return focus
|
|
end,
|
|
setFocus = function(id)
|
|
focus = id
|
|
end,
|
|
getFocus = function()
|
|
return focus
|
|
end,
|
|
moveFocus = function(_, direction)
|
|
local items, current = interactiveNodes(), nil
|
|
for index, id in ipairs(items) do
|
|
if id == focus then
|
|
current = index
|
|
break
|
|
end
|
|
end
|
|
local step = (direction == "right" or direction == "down") and 1 or -1
|
|
local nextIndex = current and current + step or 1
|
|
if items[nextIndex] then
|
|
focus = items[nextIndex]
|
|
end
|
|
return focus
|
|
end,
|
|
}
|
|
|
|
local ui = require "ui"
|
|
|
|
assert(ui.getTheme() == "light")
|
|
assert(table.concat(ui.themeNames(), ",") == "dark,light,mono")
|
|
local ok, err = ui.setTheme "missing"
|
|
assert(ok == nil and err == "Unknown theme")
|
|
assert(ui.setTheme "dark" == true)
|
|
assert(savedTheme == "dark" and ui.getTheme() == "dark")
|
|
|
|
local events = {}
|
|
local function handler(name)
|
|
return function(id, x, y)
|
|
events[#events + 1] = { name, id, x, y }
|
|
end
|
|
end
|
|
|
|
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(ui.down(10, 10))
|
|
assert(node.isPressed(first))
|
|
assert(ui.move(95, 10))
|
|
assert(not node.isPressed(first))
|
|
assert(ui.move(10, 10))
|
|
assert(node.isPressed(first))
|
|
assert(ui.up(10, 10))
|
|
assert(not node.isPressed(first))
|
|
|
|
local expectedTouch = { "enter", "exit", "enter", "exit", "click" }
|
|
for index, name in ipairs(expectedTouch) do
|
|
local event = events[index]
|
|
assert(event and event[1] == name and event[2] == first)
|
|
assert(event[3] == 10 or event[3] == 95)
|
|
assert(event[4] == 10)
|
|
end
|
|
|
|
events = {}
|
|
assert(ui.buttonPress("right", true))
|
|
assert(ui.buttonPress("right", false))
|
|
assert(node.getFocus() == first)
|
|
assert(ui.buttonPress("right", true))
|
|
assert(node.getFocus() == second)
|
|
assert(ui.buttonPress("confirm", true))
|
|
assert(node.isPressed(second))
|
|
assert(ui.buttonPress("confirm", false))
|
|
assert(not node.isPressed(second))
|
|
assert(ui.buttonPress("back", true) == false)
|
|
|
|
local expectedButtons = {
|
|
{ "enter", first },
|
|
{ "exit", first },
|
|
{ "enter", second },
|
|
{ "click", second },
|
|
}
|
|
for index, expected in ipairs(expectedButtons) do
|
|
local event = events[index]
|
|
assert(event and event[1] == expected[1] and event[2] == expected[2])
|
|
assert(event[3] == nil and event[4] == nil)
|
|
end
|
|
|
|
local before = #invalidated
|
|
assert(ui.setTheme "mono" == true)
|
|
assert(ui.getTheme() == "mono" and #invalidated == before + 1)
|
|
assert(cleared == ui.theme.background)
|
|
|
|
ui.draw()
|
|
|
|
-- The inset chrome took comes off the height budget before anything else.
|
|
ui.setInset(44)
|
|
assert(select(2, ui.frame()) == 436, "the frame is the panel minus the chrome")
|
|
assert(select(1, ui.cardSide(5, 12, 8)) == 132, "a grid fits the app's box, not the panel")
|
|
ui.setInset(0)
|
|
|
|
-- 320x480 portrait: two columns, and the reserve comes off the height budget.
|
|
local side, columns = ui.cardSide(5, 12, 8)
|
|
assert(columns == 2 and side == 144, "width is the binding constraint in portrait")
|
|
assert(select(1, ui.cardSide(5, 12, 8, 300)) == 46, "height binds once the reserve is large")
|
|
|
|
frameWidth, frameHeight = 480, 320
|
|
side, columns = ui.cardSide(5, 12, 8)
|
|
assert(columns == 3 and side == 144, "landscape uses three columns")
|
|
frameWidth, frameHeight = 320, 480
|
|
|
|
-- A widget painting its own feedback is never given a pressed style, while an ordinary one
|
|
-- still is. Which node was hit is geometry, so the test names the target directly.
|
|
ui.reset()
|
|
local pressedCalls = {}
|
|
node.setPressed = function(_, on)
|
|
pressedCalls[#pressedCalls + 1] = on
|
|
end
|
|
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
|
|
ui.down(0, 0)
|
|
ui.up(0, 0)
|
|
assert(#pressedCalls == 0, "a self-painting widget is not styled on press")
|
|
|
|
target = styled
|
|
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"
|