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, } 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 320 end, getHeight = function() return 480 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(files["/.lua/theme"] == "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 = 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}) assert(screen:down(10, 10)) assert(node.isPressed(first)) assert(screen:move(95, 10)) assert(not node.isPressed(first)) assert(screen:move(10, 10)) assert(node.isPressed(first)) assert(screen: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(screen:button("right", true)) assert(screen:button("right", false)) assert(node.getFocus() == first) assert(screen:button("right", true)) assert(node.getFocus() == second) assert(screen:button("confirm", true)) assert(node.isPressed(second)) assert(screen:button("confirm", false)) assert(not node.isPressed(second)) assert(screen:button("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) screen:draw() -- 320x480 portrait: three columns, and the reserve comes off the height budget. local side, columns = ui.cardSide(5, 12, 8) assert(columns == 3 and side == 93, "width is the binding constraint in portrait") -- Five cards are two rows, so a reserve that eats the height budget shrinks the card. assert(select(1, ui.cardSide(5, 12, 8, 300)) == 74, "height binds once the reserve is large") print("ok")