-- Run: lua test/ui_layout.lua -- Stubs the firmware bindings and asserts the rects ui.lua computes. package.path = "sdcard/lib/?.lua;" .. package.path local FONT_HEIGHT, CHAR_WIDTH = 8, 6 local now = 0 local painted = {} gui = { color = function(r, g, b) return r * 65536 + g * 256 + b end, width = function() return 320 end, height = function() return 480 end, clear = function() end, fillRect = function() end, drawText = function(label, x, y) painted[#painted + 1] = {label = label, x = x, y = y} end, fillRoundRect = function() end, drawRoundRect = function() end, fontHeight = function() return FONT_HEIGHT end, textWidth = function(text) return #text * CHAR_WIDTH end, } sys = {millis = function() return now end} 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)) -- 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") now = now + 100 app:draw() assert(not button.pressed, "pressed look must clear after the hold") print("ok")