2cdb7b3702
The launcher clock read UTC and sat next to the title because the toolkit could only stack children from the start of an axis. justify adds the CSS main-axis modes that had a caller -- start, end, center, between -- so a header keeps its title left and its clock right without any app doing arithmetic. Timezones are stored as POSIX TZ rules rather than offsets, so newlib applies DST changeovers and os.date() in Lua reports local time with no binding of its own. /lib/timezones.lua is only the picker list: a zone missing from it still works if its rule is written into settings, the same split themes already use. settings_calibration.lua now finds rows by label. Adding the timezone row shifted every hardcoded y coordinate in it, which is the failure that had been predicted and would have silently retargeted taps at the wrong control.
97 lines
4.0 KiB
Lua
97 lines
4.0 KiB
Lua
-- Run: lua test/ui_layout.lua
|
|
-- Asserts the rects ui.lua computes, against the shared fake device.
|
|
package.path = "sdcard/lib/?.lua;test/?.lua;" .. package.path
|
|
|
|
local device = require("fake_device").install()
|
|
local ui = require("ui")
|
|
|
|
local function rect(node)
|
|
return string.format("%d,%d %dx%d", node.rect.x, node.rect.y, node.rect.w, node.rect.h)
|
|
end
|
|
|
|
-- Vertical flow: children fill the content width, stack by their own height plus gap.
|
|
local title = ui.text("settings")
|
|
local first = ui.button{label = "one", on_press = function() end}
|
|
local second = ui.button{label = "two", on_press = function() end}
|
|
local screen = ui.screen(ui.box{pad = 12, gap = 8, title, first, second})
|
|
|
|
assert(rect(title) == "12,12 296x8", rect(title))
|
|
-- Button height is text plus its own padding: 8 + 8 + 8 = 24.
|
|
assert(rect(first) == "12,28 296x24", rect(first))
|
|
assert(rect(second) == "12,60 296x24", rect(second))
|
|
|
|
-- Fractions resolve against the parent content box, absolutes stay absolute.
|
|
local half = ui.box{w = 0.5, h = 40}
|
|
local fixed = ui.box{w = 100, h = 40}
|
|
ui.screen(ui.box{pad = 10, half, fixed})
|
|
assert(rect(half) == "10,10 150x40", rect(half))
|
|
assert(rect(fixed) == "10,50 100x40", rect(fixed))
|
|
|
|
-- Horizontal flow with centre alignment on the cross axis of a fixed-height row.
|
|
local tall = ui.box{w = 40, h = 40}
|
|
local short = ui.box{w = 40, h = 10}
|
|
local row = ui.box{row = true, gap = 6, align = "center", h = 40, tall, short}
|
|
ui.screen(ui.box{row})
|
|
assert(rect(tall) == "0,0 40x40", rect(tall))
|
|
assert(rect(short) == "46,15 40x10", rect(short))
|
|
|
|
-- justify distributes the main axis: "between" pushes the last child to the far edge,
|
|
-- which is how a header keeps a title left and a clock right without arithmetic.
|
|
local left = ui.box{w = 40, h = 10}
|
|
local right = ui.box{w = 60, h = 10}
|
|
ui.screen(ui.box{pad = 10, ui.box{row = true, justify = "between", left, right}})
|
|
assert(rect(left) == "10,10 40x10", rect(left))
|
|
assert(rect(right) == "250,10 60x10", rect(right))
|
|
|
|
-- The other modes shift the whole run rather than spreading it.
|
|
local a = ui.box{w = 40, h = 10}
|
|
local b = ui.box{w = 60, h = 10}
|
|
ui.screen(ui.box{ui.box{row = true, gap = 10, justify = "end", a, b}})
|
|
assert(rect(a) == "210,0 40x10", rect(a))
|
|
assert(rect(b) == "260,0 60x10", rect(b))
|
|
|
|
-- One child cannot be spread against anything, so "between" degrades to "start".
|
|
local only = ui.box{w = 40, h = 10}
|
|
ui.screen(ui.box{ui.box{row = true, justify = "between", only}})
|
|
assert(rect(only) == "0,0 40x10", rect(only))
|
|
|
|
-- The root always fills the screen, so alignment there spans the whole panel.
|
|
local lone = ui.box{w = 40, h = 40}
|
|
ui.screen(ui.box{align = "center", lone})
|
|
assert(rect(lone) == "140,0 40x40", rect(lone))
|
|
|
|
-- A fraction inside an auto-sized parent is a build-time error, not a silent zero.
|
|
local ok = pcall(function()
|
|
ui.screen(ui.box{ui.box{h = "auto", ui.box{w = 20, h = 0.5}}})
|
|
end)
|
|
assert(not ok, "fraction inside an auto parent must fail loudly")
|
|
|
|
-- Hit testing: deepest interactive component wins over its tappable ancestor.
|
|
local inner = ui.button{label = "inner", on_press = function() end}
|
|
local outer = ui.box{pad = 20, on_press = function() end, inner}
|
|
local nested = ui.screen(outer)
|
|
assert(outer:hit(160, 30) == inner, "inner button should win inside its rect")
|
|
assert(outer:hit(2, 2) == outer, "the container claims its own padding")
|
|
assert(outer:hit(-50, -50) == nil, "outside the tree is a miss")
|
|
|
|
-- Capture: press then release outside cancels; release inside fires once.
|
|
local fired = 0
|
|
local button = ui.button{label = "go", on_press = function() fired = fired + 1 end}
|
|
local app = ui.screen(ui.box{pad = 10, button})
|
|
app:down(100, 20)
|
|
assert(button.pressed, "press must show immediately")
|
|
app:up(300, 470)
|
|
assert(fired == 0, "release outside must cancel")
|
|
|
|
app:down(100, 20)
|
|
app:up(100, 20)
|
|
assert(fired == 1, "release inside must fire once")
|
|
|
|
-- The pressed look is held briefly, then cleared on a later draw.
|
|
assert(button.pressed, "pressed look must outlast the release")
|
|
device.now = device.now + 100
|
|
app:draw()
|
|
assert(not button.pressed, "pressed look must clear after the hold")
|
|
|
|
print("ok")
|