Files
slate32/test/keyboard.lua
T
evan f7c5cc09ba feat(ui)!: move the widget tree into C++
A node was a Lua table of ~625 bytes, of which 21 keys pushed it over a
power-of-two hash boundary and eight were style copies inheritance had
splattered down from its parent. A 400 node screen cost ~250 KB and could not
coexist with wifi's buffers.

The tree now lives in src/ui/layout.h as a 16 byte struct in a flat arena, and
splits by lifetime: Node holds what hit testing and repainting need forever,
Spec holds what only measure/place read and is dropped when layout ends. Style
is sparse and resolved by walking parents, so a node naming no colours costs
nothing. Re-layout rebuilds from Lua rather than retaining the inputs.

    401 nodes:  8218 B steady, 21050 B peak
           Lua: ~250000 B steady

sdcard/lib/ui.lua stays the toolkit and keeps every constructor signature, but
returns integer handles: 627 lines to 374. Composition, the palette and custom
painters are still Lua on the SD card; only primitives now need a reflash.

BREAKING CHANGE: ui constructors return handles, not tables. Use
ui.setText(id, text) and keep per-node app data in a table keyed by id.
2026-08-02 18:48:48 -04:00

91 lines
3.9 KiB
Lua

-- Run: lua test/keyboard.lua
-- The keyboard's geometry: which key covers a point, and where each key is drawn. This
-- is the part that decides what a tap enters, and it is pure, so it is asserted here.
-- Painting, press highlighting and the 80 ms hold need the panel and the node tree, so
-- they are emulator behaviour.
package.path = "sdcard/lib/?.lua;test/?.lua;" .. package.path
require("fake_device").install()
local keyboard = require("keyboard")
-- The board as ui.screen would place it: full width inside 8px of padding.
local RECT = {x = 8, y = 8, w = 304, h = 4 * 30 + 3 * 5}
local function keys(page)
local found = {}
keyboard.eachKey(page, RECT, function(id, label, x, y, width)
found[#found + 1] = {id = id, label = label, x = x, y = y, w = width}
end)
return found
end
local function centreOf(page, label)
for _, key in ipairs(keys(page)) do
if key.label == label then return key.x + key.w // 2, key.y + 15 end
end
error("no key labelled " .. label)
end
local function entered(page, label)
local x, y = centreOf(page, label)
local id, action, char = keyboard.keyAt(page, RECT, x, y)
return id, action, char
end
-- Rows are staggered and centred like a physical keyboard: ten keys, then nine, then
-- seven between two wide keys.
local lower = keys("lower")
assert(lower[1].label == "q" and lower[10].label == "p", "top row is qwertyuiop")
assert(lower[1].x == RECT.x + 4, "top row was not centred, x=" .. lower[1].x)
assert(lower[11].label == "a" and lower[11].x == RECT.x + 19,
"home row was not centred, x=" .. lower[11].x)
-- Every key that gets painted must be findable by a tap at its centre, or a key exists
-- that cannot be pressed.
for _, page in ipairs({"lower", "upper", "numbers", "symbols"}) do
for _, key in ipairs(keys(page)) do
local id = keyboard.keyAt(page, RECT, key.x + key.w // 2, key.y + 15)
assert(id == key.id, string.format("%s key %q at %d,%d hit %s", page, key.label, key.x,
key.y, tostring(id)))
end
end
-- Gaps between keys are dead, so a tap that lands between two of them enters neither.
assert(keyboard.keyAt("lower", RECT, lower[1].x + lower[1].w + 1, lower[1].y + 15) == nil,
"the gap between q and w entered a key")
assert(keyboard.keyAt("lower", RECT, lower[1].x, RECT.y + 30 + 2) == nil,
"the gap between rows entered a key")
assert(keyboard.keyAt("lower", RECT, lower[1].x, RECT.y - 1) == nil, "above the board hit a key")
-- Characters report themselves; the rest report what they do.
local _, action, char = entered("lower", "q")
assert(action == "char" and char == "q", "q did not enter itself")
local _, upperAction, upperChar = entered("upper", "Q")
assert(upperAction == "char" and upperChar == "Q", "the upper page did not enter Q")
assert(select(2, entered("lower", "shift")) == "shift", "shift key")
assert(select(2, entered("lower", "<-")) == "backspace", "backspace key")
assert(select(2, entered("lower", "123")) == "mode", "mode key")
assert(select(2, entered("lower", "space")) == "space", "space key")
assert(select(2, entered("lower", "OK")) == "submit", "submit key")
-- The number page swaps shift for the symbols toggle, which is the only key whose
-- meaning changes with the page it is on.
assert(select(2, entered("numbers", "#+=")) == "symbols", "numbers page toggles symbols")
assert(select(2, entered("symbols", "123")) == "symbols", "symbols page toggles back")
assert(select(2, entered("numbers", "ABC")) == "mode", "numbers page returns to letters")
-- Rows shift and shrink between pages, but a key id keeps its place, so the highlight
-- drawn on press lands on the key that was pressed even after the labels change.
local function positions(page)
local at = {}
for _, key in ipairs(keys(page)) do at[key.id] = key.x end
return at
end
local lowerAt, upperAt = positions("lower"), positions("upper")
for id, x in pairs(lowerAt) do
assert(upperAt[id] == x, "key " .. id .. " moved when the page changed case")
end
print("ok")