f7c5cc09ba
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.
91 lines
3.9 KiB
Lua
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")
|