feat(runtime): embed platform modules as bytecode
ui.lua and hints.lua are compiled to LUA_32BITS bytecode (matching the firmware's Lua build) and linked into the binary. A new package.searchers entry checks them as the fallback after the SD card, so a local /.lua/lib/ui.lua still shadows the packaged one for debugging. Bytecode is ~40% smaller than source and loads without parsing. A fresh SD card with no make sdcard now has the platform available.
This commit is contained in:
@@ -10,10 +10,14 @@ LUA_OBJECTS := $(patsubst native/src/vendor/lua/%.c,_build/lua/%.o,$(LUA_C))
|
||||
LUA_LIBRARY := _build/liblua.a
|
||||
LUA_SMOKE := _build/lua-smoke
|
||||
RUNTIME_TEST := _build/runtime-test
|
||||
LUAC32 := _build/luac32
|
||||
RUNTIME_CPP := $(sort $(wildcard native/src/runtime/*.cpp native/src/bindings/core/*.cpp \
|
||||
native/src/bindings/features/*.cpp))
|
||||
native/src/bindings/features/*.cpp) native/src/embedded_modules.cpp)
|
||||
|
||||
.PHONY: api test
|
||||
.PHONY: api test embed
|
||||
|
||||
embed: $(LUAC32)
|
||||
@$(PYTHON) tools/embed.py $(LUAC32) lua/lib native/src/embedded_modules.cpp
|
||||
|
||||
api:
|
||||
@$(PYTHON) tools/gen_api.py
|
||||
@@ -21,6 +25,8 @@ api:
|
||||
test: $(LUA_SMOKE) $(RUNTIME_TEST)
|
||||
@$(PYTHON) tools/gen_api.py --check
|
||||
@printf '%-32s ' generated-api; echo ok
|
||||
@$(PYTHON) tools/embed.py $(LUAC32) lua/lib native/src/embedded_modules.cpp --check
|
||||
@printf '%-32s ' embedded-modules; echo ok
|
||||
@$(LUA) -e 'assert(_VERSION == "Lua 5.4", "tests require Lua 5.4")'
|
||||
@for file in $(CONTRACTS); do $(LUA) -e "assert(loadfile('$$file'))" || exit 1; done
|
||||
@printf '%-32s ' contracts; echo ok
|
||||
@@ -39,6 +45,9 @@ $(RUNTIME_TEST): $(RUNTIME_CPP) native/test/runtime_test.cpp $(LUA_LIBRARY)
|
||||
$(LUA_LIBRARY): $(LUA_OBJECTS)
|
||||
@$(AR) rcs $@ $^
|
||||
|
||||
$(LUAC32): tools/dump.c $(LUA_LIBRARY)
|
||||
@$(CC) -std=c99 -DLUA_32BITS -I native/src/vendor/lua $< $(LUA_LIBRARY) -lm -o $@
|
||||
|
||||
_build/lua/%.o: native/src/vendor/lua/%.c
|
||||
@mkdir -p $(@D)
|
||||
@$(CC) -std=c99 -DLUA_32BITS -I native/src/vendor/lua -c $< -o $@
|
||||
|
||||
+25
-25
@@ -4,42 +4,42 @@
|
||||
local hints = {}
|
||||
|
||||
-- Reading order across the strip, so a device showing three of them still reads left to right.
|
||||
local ORDER = {"back", "left", "up", "down", "right", "confirm"}
|
||||
local ORDER = { "back", "left", "up", "down", "right", "confirm" }
|
||||
|
||||
local function available()
|
||||
local roles = {}
|
||||
if input and input.getButtons then
|
||||
for _, role in ipairs(input.getButtons()) do roles[role] = true end
|
||||
end
|
||||
return roles
|
||||
local roles = {}
|
||||
if input and input.getButtons then
|
||||
for _, role in ipairs(input.getButtons()) do roles[role] = true end
|
||||
end
|
||||
return roles
|
||||
end
|
||||
|
||||
---Paints the labels this device can actually act on, and returns the height it used.
|
||||
---@param actions table<string, string> Label per Button role; roles the device lacks are skipped.
|
||||
---@param options table|nil `y`, `font`, `color`, and `background` overrides.
|
||||
function hints.draw(actions, options)
|
||||
options = options or {}
|
||||
local font = options.font or gui.FONT_SMALL
|
||||
local color = options.color or gui.color(0, 0, 0)
|
||||
local background = options.background or gui.color(255, 255, 255)
|
||||
local height = gui.getFontHeight(font) + 6
|
||||
local y = options.y or (gui.getHeight() - height)
|
||||
options = options or {}
|
||||
local font = options.font or gui.FONT_SMALL
|
||||
local color = options.color or gui.color(0, 0, 0)
|
||||
local background = options.background or gui.color(255, 255, 255)
|
||||
local height = gui.getFontHeight(font) + 6
|
||||
local y = options.y or (gui.getHeight() - height)
|
||||
|
||||
local roles = available()
|
||||
local labels = {}
|
||||
for _, role in ipairs(ORDER) do
|
||||
if roles[role] and actions[role] then labels[#labels + 1] = actions[role] end
|
||||
end
|
||||
local roles = available()
|
||||
local labels = {}
|
||||
for _, role in ipairs(ORDER) do
|
||||
if roles[role] and actions[role] then labels[#labels + 1] = actions[role] end
|
||||
end
|
||||
|
||||
gui.fillRect(0, y, gui.getWidth(), height, background)
|
||||
if #labels == 0 then return height end
|
||||
gui.fillRect(0, y, gui.getWidth(), height, background)
|
||||
if #labels == 0 then return height end
|
||||
|
||||
local slot = gui.getWidth() // #labels
|
||||
for index, label in ipairs(labels) do
|
||||
local left = slot * (index - 1) + (slot - gui.getTextWidth(font, label)) // 2
|
||||
gui.drawText(font, left, y + 3, label, color, gui.STYLE_NORMAL, background)
|
||||
end
|
||||
return height
|
||||
local slot = gui.getWidth() // #labels
|
||||
for index, label in ipairs(labels) do
|
||||
local left = slot * (index - 1) + (slot - gui.getTextWidth(font, label)) // 2
|
||||
gui.drawText(font, left, y + 3, label, color, gui.STYLE_NORMAL, background)
|
||||
end
|
||||
return height
|
||||
end
|
||||
|
||||
return hints
|
||||
|
||||
+232
-232
@@ -44,9 +44,9 @@ local ui = {}
|
||||
|
||||
local THEME_PATH = "/.lua/theme"
|
||||
local THEMES = {
|
||||
light = {background = {255, 255, 255}, color = {0, 0, 0}, accent = {0, 120, 255}, radius = 6},
|
||||
dark = {background = {18, 18, 20}, color = {235, 235, 235}, accent = {166, 118, 255}, radius = 6},
|
||||
mono = {background = {255, 255, 255}, color = {0, 0, 0}, accent = {0, 0, 0}, radius = 0},
|
||||
light = { background = { 255, 255, 255 }, color = { 0, 0, 0 }, accent = { 0, 120, 255 }, radius = 6 },
|
||||
dark = { background = { 18, 18, 20 }, color = { 235, 235, 235 }, accent = { 166, 118, 255 }, radius = 6 },
|
||||
mono = { background = { 255, 255, 255 }, color = { 0, 0, 0 }, accent = { 0, 0, 0 }, radius = 0 },
|
||||
}
|
||||
|
||||
local enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||
@@ -59,123 +59,123 @@ local activeScreen
|
||||
local applyPalette
|
||||
|
||||
local function mix(a, b, amount)
|
||||
local out = {}
|
||||
for i = 1, 3 do out[i] = math.floor(a[i] + (b[i] - a[i]) * amount + 0.5) end
|
||||
return out
|
||||
local out = {}
|
||||
for i = 1, 3 do out[i] = math.floor(a[i] + (b[i] - a[i]) * amount + 0.5) end
|
||||
return out
|
||||
end
|
||||
|
||||
local function color(rgb)
|
||||
return gui.color(rgb[1], rgb[2], rgb[3])
|
||||
return gui.color(rgb[1], rgb[2], rgb[3])
|
||||
end
|
||||
|
||||
local function palette(seed)
|
||||
local background, foreground, accent = seed.background, seed.color, seed.accent
|
||||
return {
|
||||
background = color(background),
|
||||
color = color(foreground),
|
||||
muted = color(mix(foreground, background, 0.45)),
|
||||
accent = color(accent),
|
||||
face = color(mix(background, foreground, 0.08)),
|
||||
pressedFace = color(accent),
|
||||
pressedColor = color(background),
|
||||
focusColor = color(accent),
|
||||
radius = seed.radius,
|
||||
}
|
||||
local background, foreground, accent = seed.background, seed.color, seed.accent
|
||||
return {
|
||||
background = color(background),
|
||||
color = color(foreground),
|
||||
muted = color(mix(foreground, background, 0.45)),
|
||||
accent = color(accent),
|
||||
face = color(mix(background, foreground, 0.08)),
|
||||
pressedFace = color(accent),
|
||||
pressedColor = color(background),
|
||||
focusColor = color(accent),
|
||||
radius = seed.radius,
|
||||
}
|
||||
end
|
||||
|
||||
local function loadTheme(name)
|
||||
themeName = THEMES[name] and name or "light"
|
||||
ui.theme = palette(THEMES[themeName])
|
||||
themeName = THEMES[name] and name or "light"
|
||||
ui.theme = palette(THEMES[themeName])
|
||||
end
|
||||
|
||||
---@return string
|
||||
function ui.getTheme()
|
||||
return themeName
|
||||
return themeName
|
||||
end
|
||||
|
||||
---@return string[]
|
||||
function ui.themeNames()
|
||||
local names = {}
|
||||
for name in pairs(THEMES) do names[#names + 1] = name end
|
||||
table.sort(names)
|
||||
return names
|
||||
local names = {}
|
||||
for name in pairs(THEMES) do names[#names + 1] = name end
|
||||
table.sort(names)
|
||||
return names
|
||||
end
|
||||
|
||||
---@param name string
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function ui.setTheme(name)
|
||||
if not THEMES[name] then return nil, "Unknown theme" end
|
||||
local ok, err = fs.writeFile(THEME_PATH, name)
|
||||
if not ok then return nil, err end
|
||||
loadTheme(name)
|
||||
if activeScreen then
|
||||
applyPalette(activeScreen.root)
|
||||
gui.clear(ui.theme.background)
|
||||
node.invalidate(activeScreen.root)
|
||||
end
|
||||
return true
|
||||
if not THEMES[name] then return nil, "Unknown theme" end
|
||||
local ok, err = fs.writeFile(THEME_PATH, name)
|
||||
if not ok then return nil, err end
|
||||
loadTheme(name)
|
||||
if activeScreen then
|
||||
applyPalette(activeScreen.root)
|
||||
gui.clear(ui.theme.background)
|
||||
node.invalidate(activeScreen.root)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local savedTheme = fs.readFile(THEME_PATH, 32)
|
||||
loadTheme(savedTheme and savedTheme:match("^%s*(.-)%s*$") or "light")
|
||||
|
||||
local function clearState()
|
||||
enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||
pressStyles = {}
|
||||
enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||
pressStyles = {}
|
||||
end
|
||||
|
||||
node.setPainter(function(id, x, y, w, h)
|
||||
local painter = painters[id]
|
||||
if painter then painter(id, x, y, w, h) end
|
||||
local painter = painters[id]
|
||||
if painter then painter(id, x, y, w, h) end
|
||||
end)
|
||||
|
||||
local STYLE_KEYS = {
|
||||
"color", "fill", "border", "face", "pressedFace", "pressedColor",
|
||||
"focusColor", "radius", "font", "textStyle",
|
||||
"color", "fill", "border", "face", "pressedFace", "pressedColor",
|
||||
"focusColor", "radius", "font", "textStyle",
|
||||
}
|
||||
|
||||
local function applyStyle(id, spec)
|
||||
local style, hasStyle = {}, false
|
||||
for _, key in ipairs(STYLE_KEYS) do
|
||||
if spec[key] ~= nil then
|
||||
style[key], hasStyle = spec[key], true
|
||||
end
|
||||
end
|
||||
if spec.background ~= nil then
|
||||
style.background, style.fill, hasStyle = spec.background, spec.background, true
|
||||
end
|
||||
if hasStyle then node.setStyle(id, style) end
|
||||
local style, hasStyle = {}, false
|
||||
for _, key in ipairs(STYLE_KEYS) do
|
||||
if spec[key] ~= nil then
|
||||
style[key], hasStyle = spec[key], true
|
||||
end
|
||||
end
|
||||
if spec.background ~= nil then
|
||||
style.background, style.fill, hasStyle = spec.background, spec.background, true
|
||||
end
|
||||
if hasStyle then node.setStyle(id, style) end
|
||||
end
|
||||
|
||||
local function build(spec, kind)
|
||||
spec = spec or {}
|
||||
if laidOut then ui.reset() end
|
||||
spec = spec or {}
|
||||
if laidOut then ui.reset() end
|
||||
|
||||
local children = {}
|
||||
for index, child in ipairs(spec) do
|
||||
children[index] = child
|
||||
spec[index] = nil
|
||||
end
|
||||
local children = {}
|
||||
for index, child in ipairs(spec) do
|
||||
children[index] = child
|
||||
spec[index] = nil
|
||||
end
|
||||
|
||||
spec.type = kind
|
||||
spec.interactive = spec.on_enter ~= nil or spec.on_exit ~= nil or spec.on_click ~= nil
|
||||
local id = node.create(nil, spec)
|
||||
for _, child in ipairs(children) do node.attach(id, child) end
|
||||
spec.type = kind
|
||||
spec.interactive = spec.on_enter ~= nil or spec.on_exit ~= nil or spec.on_click ~= nil
|
||||
local id = node.create(nil, spec)
|
||||
for _, child in ipairs(children) do node.attach(id, child) end
|
||||
|
||||
applyStyle(id, spec)
|
||||
enterHandlers[id] = spec.on_enter
|
||||
exitHandlers[id] = spec.on_exit
|
||||
clickHandlers[id] = spec.on_click
|
||||
painters[id] = spec.paint
|
||||
pressStyles[id] = spec.press_style ~= false
|
||||
return id
|
||||
applyStyle(id, spec)
|
||||
enterHandlers[id] = spec.on_enter
|
||||
exitHandlers[id] = spec.on_exit
|
||||
clickHandlers[id] = spec.on_click
|
||||
painters[id] = spec.paint
|
||||
pressStyles[id] = spec.press_style ~= false
|
||||
return id
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.box(spec)
|
||||
return build(spec, "box")
|
||||
return build(spec, "box")
|
||||
end
|
||||
|
||||
---Side length for a grid of square cards that fits the frame, and the column count that
|
||||
@@ -187,112 +187,112 @@ end
|
||||
---@return integer side
|
||||
---@return integer columns
|
||||
function ui.cardSide(count, pad, gap, reserve)
|
||||
local columns = gui.getWidth() >= gui.getHeight() and 4 or 3
|
||||
local rows = math.ceil(count / columns)
|
||||
local byWidth = (gui.getWidth() - 2 * pad - (columns - 1) * gap) // columns
|
||||
local byHeight = (gui.getHeight() - 2 * pad - (reserve or 0) - (rows - 1) * gap) // rows
|
||||
return math.min(byWidth, byHeight), columns
|
||||
local columns = gui.getWidth() >= gui.getHeight() and 4 or 3
|
||||
local rows = math.ceil(count / columns)
|
||||
local byWidth = (gui.getWidth() - 2 * pad - (columns - 1) * gap) // columns
|
||||
local byHeight = (gui.getHeight() - 2 * pad - (reserve or 0) - (rows - 1) * gap) // rows
|
||||
return math.min(byWidth, byHeight), columns
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.spacer(spec)
|
||||
spec = spec or {}
|
||||
return build({w = spec.w, h = spec.h}, "box")
|
||||
spec = spec or {}
|
||||
return build({ w = spec.w, h = spec.h }, "box")
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param spec? UiSpec
|
||||
---@return NodeId
|
||||
function ui.text(text, spec)
|
||||
spec = spec or {}
|
||||
spec.label = text
|
||||
spec.textStyle, spec.style = spec.style, nil
|
||||
return build(spec, "text")
|
||||
spec = spec or {}
|
||||
spec.label = text
|
||||
spec.textStyle, spec.style = spec.style, nil
|
||||
return build(spec, "text")
|
||||
end
|
||||
|
||||
---@param text string
|
||||
---@param spec? UiSpec
|
||||
---@return NodeId
|
||||
function ui.label(text, spec)
|
||||
spec = spec or {}
|
||||
local font, style = spec.font or gui.FONT_UI, spec.style or gui.STYLE_NORMAL
|
||||
if spec.fit and gui.getTextWidth(font, text, style) > spec.fit then
|
||||
while #text > 1 and gui.getTextWidth(font, text .. "~", style) > spec.fit do
|
||||
text = text:sub(1, -2)
|
||||
end
|
||||
text = text .. "~"
|
||||
end
|
||||
spec.w = gui.getTextWidth(font, text, style)
|
||||
spec.h = gui.getFontHeight(font, style)
|
||||
spec.font, spec.fit = font, nil
|
||||
return ui.text(text, spec)
|
||||
spec = spec or {}
|
||||
local font, style = spec.font or gui.FONT_UI, spec.style or gui.STYLE_NORMAL
|
||||
if spec.fit and gui.getTextWidth(font, text, style) > spec.fit then
|
||||
while #text > 1 and gui.getTextWidth(font, text .. "~", style) > spec.fit do
|
||||
text = text:sub(1, -2)
|
||||
end
|
||||
text = text .. "~"
|
||||
end
|
||||
spec.w = gui.getTextWidth(font, text, style)
|
||||
spec.h = gui.getFontHeight(font, style)
|
||||
spec.font, spec.fit = font, nil
|
||||
return ui.text(text, spec)
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.button(spec)
|
||||
spec = spec or {}
|
||||
spec.pad = spec.pad or 8
|
||||
spec.align = spec.align or "center"
|
||||
local label, font = spec.label, spec.font
|
||||
spec.label = nil
|
||||
local id = build(spec, "button")
|
||||
if label then node.create(id, {type = "text", label = label, font = font or gui.FONT_UI}) end
|
||||
return id
|
||||
spec = spec or {}
|
||||
spec.pad = spec.pad or 8
|
||||
spec.align = spec.align or "center"
|
||||
local label, font = spec.label, spec.font
|
||||
spec.label = nil
|
||||
local id = build(spec, "button")
|
||||
if label then node.create(id, { type = "text", label = label, font = font or gui.FONT_UI }) end
|
||||
return id
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.custom(spec)
|
||||
return build(spec, "custom")
|
||||
return build(spec, "custom")
|
||||
end
|
||||
|
||||
---@param id NodeId
|
||||
---@param text string
|
||||
function ui.setText(id, text)
|
||||
if node.getLabel(id) == text then return end
|
||||
node.setLabel(id, text)
|
||||
node.invalidate(id)
|
||||
if node.getLabel(id) == text then return end
|
||||
node.setLabel(id, text)
|
||||
node.invalidate(id)
|
||||
end
|
||||
|
||||
---@param id NodeId
|
||||
function ui.invalidate(id)
|
||||
node.invalidate(id)
|
||||
node.invalidate(id)
|
||||
end
|
||||
|
||||
---@param spec UiConfirmSpec
|
||||
---@return NodeId
|
||||
function ui.confirm(spec)
|
||||
local card = {
|
||||
w = spec.w or 0.85,
|
||||
pad = 16,
|
||||
gap = 12,
|
||||
background = spec.background or ui.theme.background,
|
||||
border = spec.border or ui.theme.muted,
|
||||
ui.text(spec.title),
|
||||
}
|
||||
if spec.message then card[#card + 1] = ui.text(spec.message, {color = ui.theme.muted}) end
|
||||
local card = {
|
||||
w = spec.w or 0.85,
|
||||
pad = 16,
|
||||
gap = 12,
|
||||
background = spec.background or ui.theme.background,
|
||||
border = spec.border or ui.theme.muted,
|
||||
ui.text(spec.title),
|
||||
}
|
||||
if spec.message then card[#card + 1] = ui.text(spec.message, { color = ui.theme.muted }) end
|
||||
|
||||
local buttons = {row = true, gap = 8, justify = "end"}
|
||||
if spec.cancel ~= false then
|
||||
buttons[#buttons + 1] = ui.button{label = spec.cancel or "cancel", on_click = spec.on_cancel}
|
||||
end
|
||||
buttons[#buttons + 1] = ui.button{label = spec.ok or "ok", on_click = spec.on_ok}
|
||||
card[#card + 1] = ui.box(buttons)
|
||||
local buttons = { row = true, gap = 8, justify = "end" }
|
||||
if spec.cancel ~= false then
|
||||
buttons[#buttons + 1] = ui.button { label = spec.cancel or "cancel", on_click = spec.on_cancel }
|
||||
end
|
||||
buttons[#buttons + 1] = ui.button { label = spec.ok or "ok", on_click = spec.on_ok }
|
||||
card[#card + 1] = ui.box(buttons)
|
||||
|
||||
return ui.box{
|
||||
at = {x = 0, y = 0}, w = "fill", h = "fill", capture = true,
|
||||
align = "center", justify = "center", on_click = spec.on_outside,
|
||||
ui.box(card),
|
||||
}
|
||||
return ui.box {
|
||||
at = { x = 0, y = 0 }, w = "fill", h = "fill", capture = true,
|
||||
align = "center", justify = "center", on_click = spec.on_outside,
|
||||
ui.box(card),
|
||||
}
|
||||
end
|
||||
|
||||
function ui.reset()
|
||||
node.reset()
|
||||
clearState()
|
||||
laidOut = false
|
||||
activeScreen = nil
|
||||
node.reset()
|
||||
clearState()
|
||||
laidOut = false
|
||||
activeScreen = nil
|
||||
end
|
||||
|
||||
---@class UiScreen
|
||||
@@ -300,159 +300,159 @@ local Screen = {}
|
||||
Screen.__index = Screen
|
||||
|
||||
applyPalette = function(root)
|
||||
-- The root is the panel background, not a card: no border, so it takes the fast fillRect
|
||||
-- path rather than the per-pixel roundRect one. Radius stays so cards inherit it.
|
||||
node.setStyle(root, {
|
||||
color = ui.theme.color,
|
||||
background = ui.theme.background,
|
||||
face = ui.theme.face,
|
||||
pressedFace = ui.theme.pressedFace,
|
||||
pressedColor = ui.theme.pressedColor,
|
||||
focusColor = ui.theme.focusColor,
|
||||
radius = ui.theme.radius,
|
||||
font = gui.FONT_UI,
|
||||
})
|
||||
-- The root is the panel background, not a card: no border, so it takes the fast fillRect
|
||||
-- path rather than the per-pixel roundRect one. Radius stays so cards inherit it.
|
||||
node.setStyle(root, {
|
||||
color = ui.theme.color,
|
||||
background = ui.theme.background,
|
||||
face = ui.theme.face,
|
||||
pressedFace = ui.theme.pressedFace,
|
||||
pressedColor = ui.theme.pressedColor,
|
||||
focusColor = ui.theme.focusColor,
|
||||
radius = ui.theme.radius,
|
||||
font = gui.FONT_UI,
|
||||
})
|
||||
end
|
||||
|
||||
---@param root NodeId
|
||||
---@param style? NodeStyle
|
||||
---@return UiScreen
|
||||
function ui.screen(root, style)
|
||||
node.setSize(root, "fill", "fill")
|
||||
applyPalette(root)
|
||||
if style then node.setStyle(root, style) end
|
||||
local screen = setmetatable({root = root}, Screen)
|
||||
activeScreen = screen
|
||||
screen:relayout()
|
||||
return screen
|
||||
node.setSize(root, "fill", "fill")
|
||||
applyPalette(root)
|
||||
if style then node.setStyle(root, style) end
|
||||
local screen = setmetatable({ root = root }, Screen)
|
||||
activeScreen = screen
|
||||
screen:relayout()
|
||||
return screen
|
||||
end
|
||||
|
||||
function Screen:relayout()
|
||||
local ok, err = node.layout(self.root, 0, 0, gui.getWidth(), gui.getHeight())
|
||||
if not ok then error(err, 2) end
|
||||
node.dropScratch()
|
||||
laidOut = true
|
||||
gui.clear(ui.theme.background)
|
||||
local ok, err = node.layout(self.root, 0, 0, gui.getWidth(), gui.getHeight())
|
||||
if not ok then error(err, 2) end
|
||||
node.dropScratch()
|
||||
laidOut = true
|
||||
gui.clear(ui.theme.background)
|
||||
end
|
||||
|
||||
function Screen:draw()
|
||||
node.draw(self.root)
|
||||
node.draw(self.root)
|
||||
end
|
||||
|
||||
local function inside(id, x, y)
|
||||
local rx, ry, rw, rh = node.getRect(id)
|
||||
return x >= rx and x < rx + rw and y >= ry and y < ry + rh
|
||||
local rx, ry, rw, rh = node.getRect(id)
|
||||
return x >= rx and x < rx + rw and y >= ry and y < ry + rh
|
||||
end
|
||||
|
||||
local function enter(id, x, y)
|
||||
if pressStyles[id] then node.setPressed(id, true) end
|
||||
local handler = enterHandlers[id]
|
||||
if handler then handler(id, x, y) end
|
||||
if pressStyles[id] then node.setPressed(id, true) end
|
||||
local handler = enterHandlers[id]
|
||||
if handler then handler(id, x, y) end
|
||||
end
|
||||
|
||||
local function exit(id, x, y)
|
||||
if pressStyles[id] then node.setPressed(id, false) end
|
||||
local handler = exitHandlers[id]
|
||||
if handler then handler(id, x, y) end
|
||||
if pressStyles[id] then node.setPressed(id, false) end
|
||||
local handler = exitHandlers[id]
|
||||
if handler then handler(id, x, y) end
|
||||
end
|
||||
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:down(x, y)
|
||||
local focused = node.getFocus()
|
||||
if focused then
|
||||
node.setFocus(nil)
|
||||
local handler = exitHandlers[focused]
|
||||
if handler then handler(focused) end
|
||||
end
|
||||
local focused = node.getFocus()
|
||||
if focused then
|
||||
node.setFocus(nil)
|
||||
local handler = exitHandlers[focused]
|
||||
if handler then handler(focused) end
|
||||
end
|
||||
|
||||
local target = node.hit(self.root, x, y)
|
||||
if not target then return false end
|
||||
self.captured, self.inside = target, true
|
||||
enter(target, x, y)
|
||||
return true
|
||||
local target = node.hit(self.root, x, y)
|
||||
if not target then return false end
|
||||
self.captured, self.inside = target, true
|
||||
enter(target, x, y)
|
||||
return true
|
||||
end
|
||||
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:move(x, y)
|
||||
local target = self.captured
|
||||
if not target then return false end
|
||||
local isInside = inside(target, x, y)
|
||||
if isInside ~= self.inside then
|
||||
self.inside = isInside
|
||||
if isInside then enter(target, x, y) else exit(target, x, y) end
|
||||
end
|
||||
return true
|
||||
local target = self.captured
|
||||
if not target then return false end
|
||||
local isInside = inside(target, x, y)
|
||||
if isInside ~= self.inside then
|
||||
self.inside = isInside
|
||||
if isInside then enter(target, x, y) else exit(target, x, y) end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:up(x, y)
|
||||
local target = self.captured
|
||||
if not target then return false end
|
||||
local wasActive = self.inside
|
||||
local releasedInside = inside(target, x, y)
|
||||
local handler = releasedInside and clickHandlers[target] or nil
|
||||
self.captured, self.inside = nil, nil
|
||||
if wasActive then exit(target, x, y) end
|
||||
if handler then handler(target, x, y) end
|
||||
return true
|
||||
local target = self.captured
|
||||
if not target then return false end
|
||||
local wasActive = self.inside
|
||||
local releasedInside = inside(target, x, y)
|
||||
local handler = releasedInside and clickHandlers[target] or nil
|
||||
self.captured, self.inside = nil, nil
|
||||
if wasActive then exit(target, x, y) end
|
||||
if handler then handler(target, x, y) end
|
||||
return true
|
||||
end
|
||||
|
||||
local DIRECTIONS = {up = true, down = true, left = true, right = true}
|
||||
local DIRECTIONS = { up = true, down = true, left = true, right = true }
|
||||
|
||||
local function focusFirst(screen)
|
||||
local focused = node.focusFirst(screen.root)
|
||||
if focused then
|
||||
local handler = enterHandlers[focused]
|
||||
if handler then handler(focused) end
|
||||
end
|
||||
return focused
|
||||
local focused = node.focusFirst(screen.root)
|
||||
if focused then
|
||||
local handler = enterHandlers[focused]
|
||||
if handler then handler(focused) end
|
||||
end
|
||||
return focused
|
||||
end
|
||||
|
||||
---@param name string Button name; directions and confirm are handled.
|
||||
---@param pressed boolean
|
||||
---@return boolean handled
|
||||
function Screen:button(name, pressed)
|
||||
if type(pressed) ~= "boolean" then error("button state must be boolean", 2) end
|
||||
if type(pressed) ~= "boolean" then error("button state must be boolean", 2) end
|
||||
|
||||
if DIRECTIONS[name] then
|
||||
if not pressed then return true end
|
||||
local previous = node.getFocus()
|
||||
if not previous then
|
||||
focusFirst(self)
|
||||
return true
|
||||
end
|
||||
local focused = node.moveFocus(self.root, name)
|
||||
if focused ~= previous then
|
||||
local leave = exitHandlers[previous]
|
||||
if leave then leave(previous) end
|
||||
local arrive = enterHandlers[focused]
|
||||
if arrive then arrive(focused) end
|
||||
end
|
||||
return true
|
||||
end
|
||||
if DIRECTIONS[name] then
|
||||
if not pressed then return true end
|
||||
local previous = node.getFocus()
|
||||
if not previous then
|
||||
focusFirst(self)
|
||||
return true
|
||||
end
|
||||
local focused = node.moveFocus(self.root, name)
|
||||
if focused ~= previous then
|
||||
local leave = exitHandlers[previous]
|
||||
if leave then leave(previous) end
|
||||
local arrive = enterHandlers[focused]
|
||||
if arrive then arrive(focused) end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
if name ~= "confirm" then return false end
|
||||
local focused = node.getFocus() or focusFirst(self)
|
||||
if not focused then return false end
|
||||
if pressed then
|
||||
node.setPressed(focused, true)
|
||||
self.confirming = focused
|
||||
else
|
||||
local target = self.confirming
|
||||
self.confirming = nil
|
||||
if target then
|
||||
node.setPressed(target, false)
|
||||
local handler = clickHandlers[target]
|
||||
if handler then handler(target) end
|
||||
end
|
||||
end
|
||||
return true
|
||||
if name ~= "confirm" then return false end
|
||||
local focused = node.getFocus() or focusFirst(self)
|
||||
if not focused then return false end
|
||||
if pressed then
|
||||
node.setPressed(focused, true)
|
||||
self.confirming = focused
|
||||
else
|
||||
local target = self.confirming
|
||||
self.confirming = nil
|
||||
if target then
|
||||
node.setPressed(target, false)
|
||||
local handler = clickHandlers[target]
|
||||
if handler then handler(target) end
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
return ui
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace esp32lua {
|
||||
|
||||
// A platform module compiled to bytecode and linked into the firmware. The module searcher
|
||||
// tries the SD card first, so a local copy shadows the packaged one.
|
||||
struct EmbeddedModule {
|
||||
const char* name;
|
||||
const unsigned char* data;
|
||||
size_t size;
|
||||
};
|
||||
|
||||
extern const EmbeddedModule embedded_modules[];
|
||||
extern const size_t embedded_modules_count;
|
||||
|
||||
} // namespace esp32lua
|
||||
@@ -128,6 +128,7 @@ class Runtime {
|
||||
bool loadScript(const std::string& path);
|
||||
void installLoader(const std::string& appDir);
|
||||
static int searchModule(lua_State* state);
|
||||
static int searchEmbedded(lua_State* state);
|
||||
static int loadFile(lua_State* state);
|
||||
|
||||
// A batch is one visit to the app, however many callbacks it fans out into: a tap fires
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -2,6 +2,9 @@
|
||||
// mount, so every path into the filesystem goes through FsProvider instead.
|
||||
|
||||
#include <lua/runtime.h>
|
||||
#include <lua/embedded_modules.h>
|
||||
|
||||
#include <cstring>
|
||||
|
||||
extern "C" {
|
||||
#include "lauxlib.h"
|
||||
@@ -82,6 +85,22 @@ int Runtime::searchModule(lua_State* state) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
int Runtime::searchEmbedded(lua_State* state) {
|
||||
const char* name = luaL_checkstring(state, 1);
|
||||
for (size_t i = 0; i < embedded_modules_count; i++) {
|
||||
if (std::strcmp(name, embedded_modules[i].name) != 0) continue;
|
||||
const std::string chunkname = std::string("=") + name;
|
||||
if (luaL_loadbuffer(state, reinterpret_cast<const char*>(embedded_modules[i].data),
|
||||
embedded_modules[i].size, chunkname.c_str()) != LUA_OK) {
|
||||
return luaL_error(state, "error loading embedded module '%s': %s", name, lua_tostring(state, -1));
|
||||
}
|
||||
lua_pushstring(state, name);
|
||||
return 2;
|
||||
}
|
||||
lua_pushfstring(state, "\n\tno embedded module '%s'", name);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int Runtime::loadFile(lua_State* state) {
|
||||
Runtime* runtime = Runtime::from(state);
|
||||
if (load(state, *runtime->providers_.fs, luaL_checkstring(state, 1)) == LUA_OK) return 1;
|
||||
@@ -102,10 +121,12 @@ void Runtime::installLoader(const std::string& appDir) {
|
||||
lua_getfield(state_, -1, "searchers");
|
||||
lua_pushcfunction(state_, searchModule);
|
||||
lua_rawseti(state_, -2, 2);
|
||||
for (int at = 3; at <= 4; at++) {
|
||||
lua_pushnil(state_);
|
||||
lua_rawseti(state_, -2, at);
|
||||
}
|
||||
// Embedded bytecode is the fallback after the SD card, so a local /.lua/lib/ui.lua shadows
|
||||
// the packaged one without reflashing.
|
||||
lua_pushcfunction(state_, searchEmbedded);
|
||||
lua_rawseti(state_, -2, 3);
|
||||
lua_pushnil(state_);
|
||||
lua_rawseti(state_, -2, 4);
|
||||
lua_pop(state_, 2);
|
||||
|
||||
lua_pushcfunction(state_, loadFile);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
// Dumps a Lua source file as bytecode, linked against the same vendored Lua the firmware
|
||||
// runs (LUA_32BITS), so the header matches. Stand-in for luac when only the library is
|
||||
// vendored.
|
||||
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
#include "lauxlib.h"
|
||||
#include "lua.h"
|
||||
|
||||
static int writer(lua_State*, const void* p, size_t size, void* ud) {
|
||||
return fwrite(p, 1, size, (FILE*)ud) != size ? 1 : 0;
|
||||
}
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 3) {
|
||||
fprintf(stderr, "usage: %s input.lua output.luac\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
lua_State* L = luaL_newstate();
|
||||
if (luaL_loadfilex(L, argv[1], "t") != LUA_OK) {
|
||||
fprintf(stderr, "%s: %s\n", argv[1], lua_tostring(L, -1));
|
||||
lua_close(L);
|
||||
return 1;
|
||||
}
|
||||
FILE* out = fopen(argv[2], "wb");
|
||||
if (!out) {
|
||||
fprintf(stderr, "cannot open %s\n", argv[2]);
|
||||
lua_close(L);
|
||||
return 1;
|
||||
}
|
||||
const int err = lua_dump(L, writer, out, 1); // strip debug info
|
||||
fclose(out);
|
||||
lua_close(L);
|
||||
return err ? 1 : 0;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Compiles lua/lib/*.lua to LUA_32BITS bytecode and generates a C++ source file embedding
|
||||
the arrays. The runtime's module searcher checks these as a fallback after the SD card, so
|
||||
a local copy always shadows the packaged one.
|
||||
|
||||
Usage:
|
||||
tools/embed.py <luac32> <lua_lib_dir> <output.cpp> [--check]
|
||||
"""
|
||||
|
||||
import subprocess
|
||||
import sys
|
||||
import pathlib
|
||||
|
||||
HEADER = """\
|
||||
// GENERATED by tools/embed.py — do not edit. Run `make embed` to regenerate.
|
||||
#include <lua/embedded_modules.h>
|
||||
|
||||
namespace esp32lua {
|
||||
|
||||
"""
|
||||
|
||||
FOOTER = """
|
||||
|
||||
} // namespace esp32lua
|
||||
"""
|
||||
|
||||
|
||||
def main():
|
||||
check = "--check" in sys.argv
|
||||
args = [a for a in sys.argv[1:] if a != "--check"]
|
||||
if len(args) != 3:
|
||||
sys.exit("usage: embed.py <luac32> <lua_lib_dir> <output.cpp> [--check]")
|
||||
|
||||
luac32, lib_dir, out_path = args
|
||||
lib_dir = pathlib.Path(lib_dir)
|
||||
out_path = pathlib.Path(out_path)
|
||||
|
||||
sources = sorted(lib_dir.glob("*.lua"))
|
||||
arrays = []
|
||||
rows = []
|
||||
for src in sources:
|
||||
import tempfile
|
||||
with tempfile.NamedTemporaryFile(suffix=".luac", delete=False) as tmp:
|
||||
tmp_path = tmp.name
|
||||
subprocess.run([luac32, str(src), tmp_path], check=True)
|
||||
bytecode = pathlib.Path(tmp_path).read_bytes()
|
||||
pathlib.Path(tmp_path).unlink()
|
||||
name = src.stem
|
||||
comma = ", ".join(f"0x{b:02x}" for b in bytecode)
|
||||
arrays.append(f"static const unsigned char {name}[] = {{{comma}}};\n")
|
||||
rows.append(f' {{"{name}", {name}, sizeof({name})}},')
|
||||
|
||||
footer = "const EmbeddedModule embedded_modules[] = {{\n{rows}\n}};\nconst size_t embedded_modules_count = {count};\n"
|
||||
generated = HEADER + "".join(arrays) + footer.format(rows="\n".join(rows), count=len(sources)) + FOOTER
|
||||
|
||||
if check:
|
||||
existing = out_path.read_text() if out_path.exists() else ""
|
||||
if existing != generated:
|
||||
sys.exit(f"{out_path} is stale; run `make embed`")
|
||||
return
|
||||
|
||||
out_path.write_text(generated)
|
||||
print(f"wrote {len(sources)} modules to {out_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user