Compare commits
7 Commits
main
..
445a9b2b8f
| Author | SHA1 | Date | |
|---|---|---|---|
| 445a9b2b8f | |||
| 4b89b27947 | |||
| 25b576a3d9 | |||
| 2fc3abc487 | |||
| 3f26c4ff10 | |||
| e3153f57c5 | |||
| e85adfa757 |
@@ -107,10 +107,6 @@ function gui.fillCircle(x, y, radius, color, background) end
|
||||
---@param border? GuiColor Omitted for no border.
|
||||
function gui.roundRect(x, y, w, h, radius, background, top, bottom, border) end
|
||||
|
||||
---Temporarily gives the app the full panel, including firmware chrome.
|
||||
---@param on boolean
|
||||
function gui.setFullscreen(on) end
|
||||
|
||||
---Fills a polygon.
|
||||
---@param xs integer[]
|
||||
---@param ys integer[]
|
||||
|
||||
@@ -2,21 +2,22 @@
|
||||
|
||||
-- Generated from native/src/runtime/runtime.cpp. Do not edit.
|
||||
|
||||
-- Runtime layout:
|
||||
-- /.lua/apps/<AppId>/main.lua application entry point
|
||||
-- /.lua/apps/<AppId>/<Subapp>/main.lua nested route, omitted from the launcher
|
||||
-- /.lua/data/<AppId>/ persistent app data, preserved across updates
|
||||
-- /.lua/lib/<module>.lua shared require() modules
|
||||
-- require() also searches the running application's directory
|
||||
-- The firmware loads /.lua/main.lua into every fresh state and calls these on the
|
||||
-- table it returns. Where apps live, what surrounds them and which of these an app
|
||||
-- itself sees are all main.lua's to decide.
|
||||
--
|
||||
-- Fields the firmware reads: home, the route sys.back() lands on once history is
|
||||
-- empty, and data, the sys.getAppDataPath() template whose ? is the app id.
|
||||
--
|
||||
-- The firmware does not clear the frame before calling draw(), and commits changed
|
||||
-- display content after each callback batch using the panel's own refresh policy.
|
||||
-- Timer callbacks are registered directly with timer.after/every.
|
||||
|
||||
---Required. Runs once before the first draw; failing here stops the app.
|
||||
---Required. Mounts the route; failing here leaves no app running.
|
||||
---@param route string The app path sys.launch, sys.back or the boot recorded.
|
||||
---@param arg? string The string passed to sys.launch or sys.replace.
|
||||
function init(arg) end
|
||||
function start(route, arg) end
|
||||
|
||||
---Optional frame loop, called once after init and then at most 30 FPS, best effort.
|
||||
---Optional frame loop, called once after start and then at most 30 FPS, best effort.
|
||||
---@param deltaMs integer Monotonic milliseconds since the previous draw; zero on the first.
|
||||
function draw(deltaMs) end
|
||||
|
||||
@@ -24,3 +24,15 @@ function settings.getTimezone() end
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function settings.setTimezone(timezone) end
|
||||
|
||||
---Returns the saved palette name. Apps read ui.getTheme() instead; this
|
||||
---is the stored value, which only ui.setTheme() knows how to apply.
|
||||
---@return string
|
||||
function settings.getTheme() end
|
||||
|
||||
---Persists a palette name without applying it. Call ui.setTheme(), which
|
||||
---writes through here and then rebuilds the palette and repaints.
|
||||
---@param theme string
|
||||
---@return true? ok
|
||||
---@return string? error
|
||||
function settings.setTheme(theme) end
|
||||
|
||||
+10
-6
@@ -36,19 +36,23 @@ function sys.getAppDataPath() end
|
||||
---@param title string
|
||||
function sys.setAppTitle(title) end
|
||||
|
||||
---Launches /.lua/apps/<path>/main.lua and pushes the current route.
|
||||
---@param path string App-relative directory path; traversal is rejected.
|
||||
---@param arg? string Passed to init(arg).
|
||||
---Launches a route, which main.lua resolves, and pushes the current one.
|
||||
---@param path string App-relative route; traversal is rejected.
|
||||
---@param arg? string Passed to main.start(route, arg).
|
||||
function sys.launch(path, arg) end
|
||||
|
||||
---Launches an app path without retaining the current route.
|
||||
---@param path string App-relative directory path; traversal is rejected.
|
||||
---@param arg? string Passed to init(arg).
|
||||
---Launches a route without retaining the current one.
|
||||
---@param path string App-relative route; traversal is rejected.
|
||||
---@param arg? string Passed to main.start(route, arg).
|
||||
function sys.replace(path, arg) end
|
||||
|
||||
---Returns to the previous app, or the launcher when history is empty.
|
||||
function sys.back() end
|
||||
|
||||
---Whether sys.back() would return somewhere rather than land on the launcher, which is what chrome needs to decide whether to offer a back control.
|
||||
---@return boolean
|
||||
function sys.canGoBack() end
|
||||
|
||||
---Returns heap statistics.
|
||||
---@return integer freeBytes
|
||||
---@return integer totalBytes
|
||||
|
||||
+77
-58
@@ -42,7 +42,6 @@ local ui = {}
|
||||
---@field on_cancel? UiHandler
|
||||
---@field on_outside? UiHandler
|
||||
|
||||
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 },
|
||||
@@ -55,7 +54,10 @@ local enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||
local pressStyles = {}
|
||||
local laidOut = false
|
||||
local themeName
|
||||
local activeScreen
|
||||
-- One tree per state, built by the function ui.mount() was given. Rebuilding is
|
||||
-- cheap enough that nothing is retained between screens.
|
||||
local builder, root, captured, insideCaptured, confirming
|
||||
local inset = 0
|
||||
local applyPalette
|
||||
|
||||
local function mix(a, b, amount)
|
||||
@@ -113,21 +115,20 @@ function ui.setTheme(name)
|
||||
if not THEMES[name] then
|
||||
return nil, "Unknown theme"
|
||||
end
|
||||
local ok, err = fs.writeFile(THEME_PATH, name)
|
||||
local ok, err = settings.setTheme(name)
|
||||
if not ok then
|
||||
return nil, err
|
||||
end
|
||||
loadTheme(name)
|
||||
if activeScreen then
|
||||
applyPalette(activeScreen.root)
|
||||
if root then
|
||||
applyPalette(root)
|
||||
gui.clear(ui.theme.background)
|
||||
node.invalidate(activeScreen.root)
|
||||
node.invalidate(root)
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local savedTheme = fs.readFile(THEME_PATH, 32)
|
||||
loadTheme(savedTheme and savedTheme:match "^%s*(.-)%s*$" or "light")
|
||||
loadTheme(settings.getTheme())
|
||||
|
||||
local function clearState()
|
||||
enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||
@@ -171,8 +172,10 @@ end
|
||||
|
||||
local function build(spec, kind)
|
||||
spec = spec or {}
|
||||
-- Nodes outside a build would reset the arena under the screen already on the
|
||||
-- panel, chrome included. Rebuilding is the only way to change one.
|
||||
if laidOut then
|
||||
ui.reset()
|
||||
error("build nodes from the function ui.mount() was given, then ui.rebuild()", 3)
|
||||
end
|
||||
|
||||
local children = {}
|
||||
@@ -212,13 +215,28 @@ end
|
||||
---@return integer side
|
||||
---@return integer columns
|
||||
function ui.cardSide(count, pad, gap, reserve)
|
||||
local columns = gui.getWidth() >= gui.getHeight() and 3 or 2
|
||||
local width, height = ui.frame()
|
||||
local columns = width >= height and 3 or 2
|
||||
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
|
||||
local byWidth = (width - 2 * pad - (columns - 1) * gap) // columns
|
||||
local byHeight = (height - 2 * pad - (reserve or 0) - (rows - 1) * gap) // rows
|
||||
return math.min(byWidth, byHeight), columns
|
||||
end
|
||||
|
||||
---How much of the panel chrome took before the app was built. Set by whatever mounts the
|
||||
---tree, because layout has not run yet when an app sizes itself.
|
||||
---@param px integer
|
||||
function ui.setInset(px)
|
||||
inset = px
|
||||
end
|
||||
|
||||
---The box the app is built into, which is the panel minus the chrome above it.
|
||||
---@return integer w
|
||||
---@return integer h
|
||||
function ui.frame()
|
||||
return gui.getWidth(), gui.getHeight() - inset
|
||||
end
|
||||
|
||||
---@param spec UiSpec
|
||||
---@return NodeId
|
||||
function ui.spacer(spec)
|
||||
@@ -328,13 +346,9 @@ function ui.reset()
|
||||
node.reset()
|
||||
clearState()
|
||||
laidOut = false
|
||||
activeScreen = nil
|
||||
root, captured, insideCaptured, confirming = nil, nil, nil, nil
|
||||
end
|
||||
|
||||
---@class UiScreen
|
||||
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.
|
||||
@@ -350,33 +364,39 @@ applyPalette = function(root)
|
||||
})
|
||||
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
|
||||
---Registers the function that builds the whole tree and shows what it returns.
|
||||
---@param fn fun(): NodeId
|
||||
function ui.mount(fn)
|
||||
builder = fn
|
||||
ui.rebuild()
|
||||
end
|
||||
|
||||
function Screen:relayout()
|
||||
local ok, err = node.layout(self.root, 0, 0, gui.getWidth(), gui.getHeight())
|
||||
---Rebuilds the tree from scratch and repaints. Screens are not retained, so this
|
||||
---is how a screen changes, a rotation is answered and a dialog opens.
|
||||
function ui.rebuild()
|
||||
ui.reset()
|
||||
root = builder()
|
||||
node.setSize(root, "fill", "fill")
|
||||
applyPalette(root)
|
||||
local ok, err = node.layout(root, 0, 0, gui.getWidth(), gui.getHeight())
|
||||
if not ok then
|
||||
error(err, 2)
|
||||
end
|
||||
node.dropScratch()
|
||||
laidOut = true
|
||||
gui.clear(ui.theme.background)
|
||||
node.draw(root)
|
||||
-- A build allocates a spec table per node and drops them all here, and the next thing an
|
||||
-- app does may be the one that needs a contiguous WiFi buffer. Collecting now costs a few
|
||||
-- milliseconds on a screen change nobody can see, and leaves the heap in a known state
|
||||
-- instead of one that depends on when the incremental GC last ran.
|
||||
collectgarbage()
|
||||
end
|
||||
|
||||
function Screen:draw()
|
||||
node.draw(self.root)
|
||||
function ui.draw()
|
||||
if root then
|
||||
node.draw(root)
|
||||
end
|
||||
end
|
||||
|
||||
local function inside(id, x, y)
|
||||
@@ -407,7 +427,7 @@ end
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:down(x, y)
|
||||
function ui.down(x, y)
|
||||
local focused = node.getFocus()
|
||||
if focused then
|
||||
node.setFocus(nil)
|
||||
@@ -417,11 +437,11 @@ function Screen:down(x, y)
|
||||
end
|
||||
end
|
||||
|
||||
local target = node.hit(self.root, x, y)
|
||||
local target = root and node.hit(root, x, y)
|
||||
if not target then
|
||||
return false
|
||||
end
|
||||
self.captured, self.inside = target, true
|
||||
captured, insideCaptured = target, true
|
||||
enter(target, x, y)
|
||||
return true
|
||||
end
|
||||
@@ -429,18 +449,17 @@ end
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:move(x, y)
|
||||
local target = self.captured
|
||||
if not target then
|
||||
function ui.move(x, y)
|
||||
if not captured then
|
||||
return false
|
||||
end
|
||||
local isInside = inside(target, x, y)
|
||||
if isInside ~= self.inside then
|
||||
self.inside = isInside
|
||||
local isInside = inside(captured, x, y)
|
||||
if isInside ~= insideCaptured then
|
||||
insideCaptured = isInside
|
||||
if isInside then
|
||||
enter(target, x, y)
|
||||
enter(captured, x, y)
|
||||
else
|
||||
exit(target, x, y)
|
||||
exit(captured, x, y)
|
||||
end
|
||||
end
|
||||
return true
|
||||
@@ -449,15 +468,15 @@ end
|
||||
---@param x integer
|
||||
---@param y integer
|
||||
---@return boolean handled
|
||||
function Screen:up(x, y)
|
||||
local target = self.captured
|
||||
function ui.up(x, y)
|
||||
local target = captured
|
||||
if not target then
|
||||
return false
|
||||
end
|
||||
local wasActive = self.inside
|
||||
local wasActive = insideCaptured
|
||||
local releasedInside = inside(target, x, y)
|
||||
local handler = releasedInside and clickHandlers[target] or nil
|
||||
self.captured, self.inside = nil, nil
|
||||
captured, insideCaptured = nil, nil
|
||||
if wasActive then
|
||||
exit(target, x, y)
|
||||
end
|
||||
@@ -469,8 +488,8 @@ end
|
||||
|
||||
local DIRECTIONS = { up = true, down = true, left = true, right = true }
|
||||
|
||||
local function focusFirst(screen)
|
||||
local focused = node.focusFirst(screen.root)
|
||||
local function focusFirst()
|
||||
local focused = node.focusFirst(root)
|
||||
if focused then
|
||||
local handler = enterHandlers[focused]
|
||||
if handler then
|
||||
@@ -483,7 +502,7 @@ end
|
||||
---@param name string Button name; directions and confirm are handled.
|
||||
---@param pressed boolean
|
||||
---@return boolean handled
|
||||
function Screen:button(name, pressed)
|
||||
function ui.buttonPress(name, pressed)
|
||||
if type(pressed) ~= "boolean" then
|
||||
error("button state must be boolean", 2)
|
||||
end
|
||||
@@ -494,10 +513,10 @@ function Screen:button(name, pressed)
|
||||
end
|
||||
local previous = node.getFocus()
|
||||
if not previous then
|
||||
focusFirst(self)
|
||||
focusFirst()
|
||||
return true
|
||||
end
|
||||
local focused = node.moveFocus(self.root, name)
|
||||
local focused = node.moveFocus(root, name)
|
||||
if focused ~= previous then
|
||||
local leave = exitHandlers[previous]
|
||||
if leave then
|
||||
@@ -514,16 +533,16 @@ function Screen:button(name, pressed)
|
||||
if name ~= "confirm" then
|
||||
return false
|
||||
end
|
||||
local focused = node.getFocus() or focusFirst(self)
|
||||
local focused = node.getFocus() or focusFirst()
|
||||
if not focused then
|
||||
return false
|
||||
end
|
||||
if pressed then
|
||||
node.setPressed(focused, true)
|
||||
self.confirming = focused
|
||||
confirming = focused
|
||||
else
|
||||
local target = self.confirming
|
||||
self.confirming = nil
|
||||
local target = confirming
|
||||
confirming = nil
|
||||
if target then
|
||||
node.setPressed(target, false)
|
||||
local handler = clickHandlers[target]
|
||||
|
||||
+62
-31
@@ -17,6 +17,17 @@ fs = {
|
||||
end,
|
||||
}
|
||||
|
||||
local savedTheme = "light"
|
||||
settings = {
|
||||
getTheme = function()
|
||||
return savedTheme
|
||||
end,
|
||||
setTheme = function(name)
|
||||
savedTheme = name
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
local frameWidth, frameHeight = 320, 480
|
||||
|
||||
gui = {
|
||||
@@ -166,7 +177,7 @@ 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")
|
||||
assert(savedTheme == "dark" and ui.getTheme() == "dark")
|
||||
|
||||
local events = {}
|
||||
local function handler(name)
|
||||
@@ -175,27 +186,30 @@ local function handler(name)
|
||||
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 })
|
||||
local first, second
|
||||
ui.mount(function()
|
||||
first = ui.button {
|
||||
label = "one",
|
||||
on_enter = handler "enter",
|
||||
on_exit = handler "exit",
|
||||
on_click = handler "click",
|
||||
}
|
||||
second = ui.button {
|
||||
label = "two",
|
||||
on_enter = handler "enter",
|
||||
on_exit = handler "exit",
|
||||
on_click = handler "click",
|
||||
}
|
||||
return ui.box { row = true, first, second }
|
||||
end)
|
||||
|
||||
assert(screen:down(10, 10))
|
||||
assert(ui.down(10, 10))
|
||||
assert(node.isPressed(first))
|
||||
assert(screen:move(95, 10))
|
||||
assert(ui.move(95, 10))
|
||||
assert(not node.isPressed(first))
|
||||
assert(screen:move(10, 10))
|
||||
assert(ui.move(10, 10))
|
||||
assert(node.isPressed(first))
|
||||
assert(screen:up(10, 10))
|
||||
assert(ui.up(10, 10))
|
||||
assert(not node.isPressed(first))
|
||||
|
||||
local expectedTouch = { "enter", "exit", "enter", "exit", "click" }
|
||||
@@ -207,16 +221,16 @@ for index, name in ipairs(expectedTouch) do
|
||||
end
|
||||
|
||||
events = {}
|
||||
assert(screen:button("right", true))
|
||||
assert(screen:button("right", false))
|
||||
assert(ui.buttonPress("right", true))
|
||||
assert(ui.buttonPress("right", false))
|
||||
assert(node.getFocus() == first)
|
||||
assert(screen:button("right", true))
|
||||
assert(ui.buttonPress("right", true))
|
||||
assert(node.getFocus() == second)
|
||||
assert(screen:button("confirm", true))
|
||||
assert(ui.buttonPress("confirm", true))
|
||||
assert(node.isPressed(second))
|
||||
assert(screen:button("confirm", false))
|
||||
assert(ui.buttonPress("confirm", false))
|
||||
assert(not node.isPressed(second))
|
||||
assert(screen:button("back", true) == false)
|
||||
assert(ui.buttonPress("back", true) == false)
|
||||
|
||||
local expectedButtons = {
|
||||
{ "enter", first },
|
||||
@@ -235,7 +249,13 @@ assert(ui.setTheme "mono" == true)
|
||||
assert(ui.getTheme() == "mono" and #invalidated == before + 1)
|
||||
assert(cleared == ui.theme.background)
|
||||
|
||||
screen:draw()
|
||||
ui.draw()
|
||||
|
||||
-- The inset chrome took comes off the height budget before anything else.
|
||||
ui.setInset(44)
|
||||
assert(select(2, ui.frame()) == 436, "the frame is the panel minus the chrome")
|
||||
assert(select(1, ui.cardSide(5, 12, 8)) == 132, "a grid fits the app's box, not the panel")
|
||||
ui.setInset(0)
|
||||
|
||||
-- 320x480 portrait: two columns, and the reserve comes off the height budget.
|
||||
local side, columns = ui.cardSide(5, 12, 8)
|
||||
@@ -254,21 +274,32 @@ local pressedCalls = {}
|
||||
node.setPressed = function(_, on)
|
||||
pressedCalls[#pressedCalls + 1] = on
|
||||
end
|
||||
local own = ui.custom { h = 20, press_style = false, on_click = function() end }
|
||||
local styled = ui.button { h = 20, label = "ok", on_click = function() end }
|
||||
local board = ui.screen(ui.box { own, styled })
|
||||
local own, styled
|
||||
ui.mount(function()
|
||||
own = ui.custom { h = 20, press_style = false, on_click = function() end }
|
||||
styled = ui.button { h = 20, label = "ok", on_click = function() end }
|
||||
return ui.box { own, styled }
|
||||
end)
|
||||
local target
|
||||
node.hit = function()
|
||||
return target
|
||||
end
|
||||
|
||||
target = own
|
||||
board:down(0, 0)
|
||||
board:up(0, 0)
|
||||
ui.down(0, 0)
|
||||
ui.up(0, 0)
|
||||
assert(#pressedCalls == 0, "a self-painting widget is not styled on press")
|
||||
|
||||
target = styled
|
||||
board:down(0, 0)
|
||||
ui.down(0, 0)
|
||||
assert(pressedCalls[1] == true, "an ordinary widget still gets its pressed style")
|
||||
|
||||
-- Building outside a rebuild would reset the arena under the screen on the panel.
|
||||
local built = pcall(ui.button, { label = "stray" })
|
||||
assert(not built, "a node built after layout is refused")
|
||||
local rebuilt = false
|
||||
ui.rebuild()
|
||||
rebuilt = true
|
||||
assert(rebuilt and ui.down(0, 0), "a rebuild replaces the screen and keeps dispatch live")
|
||||
|
||||
print "ok"
|
||||
|
||||
@@ -42,6 +42,10 @@ public:
|
||||
virtual Status setRotation(int32_t degrees) = 0;
|
||||
virtual std::string timezone() const = 0;
|
||||
virtual Status setTimezone(const std::string& timezone) = 0;
|
||||
// Only the name of a palette; the colours themselves live in Lua, so the
|
||||
// firmware can read the saved theme before a lua_State exists.
|
||||
virtual std::string theme() const = 0;
|
||||
virtual Status setTheme(const std::string& theme) = 0;
|
||||
};
|
||||
|
||||
// Only what the firmware alone can answer. App identity, titles, data paths,
|
||||
@@ -132,9 +136,6 @@ public:
|
||||
virtual void roundRect(int32_t x, int32_t y, int32_t w, int32_t h,
|
||||
int32_t radius, int32_t background, const int32_t* top,
|
||||
const int32_t* bottom, const int32_t* border) = 0;
|
||||
// Hands the app the whole panel, including whatever chrome the firmware
|
||||
// paints.
|
||||
virtual void setFullscreen(bool on) = 0;
|
||||
// Applies everything drawn since the last commit. The runtime supplies only
|
||||
// the timing -- the end of a callback batch -- because that is the one fact a
|
||||
// driver cannot know; which region to touch, which waveform, and whether to
|
||||
|
||||
@@ -15,14 +15,10 @@ namespace esp32lua {
|
||||
// build.
|
||||
constexpr int32_t API_VERSION = 1;
|
||||
|
||||
// Where the runtime looks for apps, their data, and shared modules.
|
||||
struct Paths {
|
||||
std::string apps = "/.lua/apps";
|
||||
std::string data = "/.lua/data";
|
||||
std::string lib = "/.lua/lib";
|
||||
// Where sys.back() lands once history is empty. It is an app like any other.
|
||||
std::string home = "Home";
|
||||
};
|
||||
// The one path the firmware knows. Everything below it -- where apps live,
|
||||
// where their data goes, what chrome surrounds them -- is decided by the table
|
||||
// this file returns.
|
||||
constexpr const char* MAIN_PATH = "/.lua/main.lua";
|
||||
|
||||
// Firmware supplies every core provider; a null feature provider is how
|
||||
// sys.hasFeature() answers false, and its namespace additions are simply never
|
||||
@@ -44,7 +40,7 @@ struct Providers {
|
||||
|
||||
class Runtime {
|
||||
public:
|
||||
explicit Runtime(const Providers& providers, const Paths& paths = Paths());
|
||||
explicit Runtime(const Providers& providers);
|
||||
~Runtime();
|
||||
|
||||
Runtime(const Runtime&) = delete;
|
||||
@@ -55,9 +51,9 @@ public:
|
||||
void close();
|
||||
lua_State* state() const { return state_; }
|
||||
|
||||
// Replaces the running app with a fresh lua_State, loads
|
||||
// <apps>/<path>/main.lua, and calls init(arg). A failure leaves no app
|
||||
// running rather than a half-built one.
|
||||
// Replaces the running app with a fresh lua_State, loads main.lua, and hands
|
||||
// it the route through start(route, arg). A failure leaves no app running
|
||||
// rather than a half-built one.
|
||||
bool startApp(const std::string& path,
|
||||
const std::string& arg = std::string());
|
||||
bool hasApp() const { return !appPath_.empty(); }
|
||||
@@ -99,11 +95,11 @@ public:
|
||||
|
||||
ui::Tree& tree() { return tree_; }
|
||||
|
||||
// Entry points into the app. The firmware decides whether an event reaches
|
||||
// the app at all -- jitter, chrome and debouncing are its business -- and the
|
||||
// runtime decides what the app sees. Only a failed init() stops an app; every
|
||||
// other callback logs and carries on.
|
||||
bool callInit(const std::string& arg);
|
||||
// Entry points into main.lua, which forwards whatever the app it mounted
|
||||
// defines. The firmware decides whether an event happens at all -- jitter and
|
||||
// debouncing are its business -- and main.lua decides who sees it. Only a
|
||||
// failed start() stops an app; every other callback logs and carries on.
|
||||
bool callStart(const std::string& route, const std::string& arg);
|
||||
void callDraw(int32_t deltaMs);
|
||||
// An Up phase also fires the on_touch tap alias, in that order.
|
||||
void callTouch(TouchPhase phase, int32_t x, int32_t y);
|
||||
@@ -136,7 +132,12 @@ private:
|
||||
};
|
||||
|
||||
bool loadScript(const std::string& path);
|
||||
void installLoader(const std::string& appDir);
|
||||
// Runs main.lua and keeps the table it returns; the app is mounted by it, not
|
||||
// by the runtime.
|
||||
bool loadMain();
|
||||
void installLoader();
|
||||
// A field of the main table, or the fallback when main.lua names none.
|
||||
std::string mainField(const char* key, const char* fallback);
|
||||
static int searchModule(lua_State* state);
|
||||
static int searchEmbedded(lua_State* state);
|
||||
static int loadFile(lua_State* state);
|
||||
@@ -155,9 +156,10 @@ private:
|
||||
Runtime& runtime_;
|
||||
};
|
||||
|
||||
// Pushes the named global, or returns false when the app does not define it.
|
||||
// Pushes main.<name>, or returns false when main.lua defines no such handler.
|
||||
bool beginCall(const char* name);
|
||||
bool finishCall(const char* name, int argc);
|
||||
bool finishCallValue(const char* name);
|
||||
void cancelAllTimers();
|
||||
|
||||
Providers providers_;
|
||||
@@ -167,7 +169,12 @@ private:
|
||||
TimerId nextTimerId_ = 1;
|
||||
int batchDepth_ = 0;
|
||||
|
||||
Paths paths_;
|
||||
// Registry reference to the table main.lua returned, or 0 before one loads.
|
||||
int mainRef_ = 0;
|
||||
// Read from main.lua once per load, because sys.back() out of the last app
|
||||
// needs the route after that app's state is gone.
|
||||
std::string home_;
|
||||
std::string dataTemplate_;
|
||||
std::string appPath_;
|
||||
std::string appTitle_;
|
||||
std::vector<Route> history_;
|
||||
|
||||
@@ -125,12 +125,6 @@ int roundRect(lua_State* state) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int setFullscreen(lua_State* state) {
|
||||
luaL_checkany(state, 1);
|
||||
provider(state).setFullscreen(lua_toboolean(state, 1) != 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
void readIntegers(lua_State* state, int index, std::vector<int32_t>& out) {
|
||||
const lua_Integer count = luaL_len(state, index);
|
||||
for (lua_Integer at = 1; at <= count; at++) {
|
||||
@@ -291,9 +285,6 @@ const luaL_Reg FUNCTIONS[] = {
|
||||
// without a gradient use top.
|
||||
// @param border GuiColor|nil Omitted for no border.
|
||||
{"roundRect", roundRect},
|
||||
// ---Temporarily gives the app the full panel, including firmware chrome.
|
||||
// @param on boolean
|
||||
{"setFullscreen", setFullscreen},
|
||||
// --- Fills a polygon.
|
||||
// @param xs integer[]
|
||||
// @param ys integer[]
|
||||
|
||||
@@ -47,6 +47,19 @@ int setTimezone(lua_State* state) {
|
||||
state, Runtime::from(state)->settings().setTimezone({value, length}));
|
||||
}
|
||||
|
||||
int getTheme(lua_State* state) {
|
||||
const std::string theme = Runtime::from(state)->settings().theme();
|
||||
lua_pushlstring(state, theme.data(), theme.size());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int setTheme(lua_State* state) {
|
||||
size_t length = 0;
|
||||
const char* value = luaL_checklstring(state, 1, &length);
|
||||
return pushStatus(state,
|
||||
Runtime::from(state)->settings().setTheme({value, length}));
|
||||
}
|
||||
|
||||
const luaL_Reg FUNCTIONS[] = {
|
||||
// --- Returns the saved rotation in degrees clockwise.
|
||||
// @return integer
|
||||
@@ -64,6 +77,16 @@ const luaL_Reg FUNCTIONS[] = {
|
||||
// @return true|nil ok
|
||||
// @return string|nil error
|
||||
{"setTimezone", setTimezone},
|
||||
// --- Returns the saved palette name. Apps read ui.getTheme() instead; this
|
||||
// --- is the stored value, which only ui.setTheme() knows how to apply.
|
||||
// @return string
|
||||
{"getTheme", getTheme},
|
||||
// --- Persists a palette name without applying it. Call ui.setTheme(), which
|
||||
// --- writes through here and then rebuilds the palette and repaints.
|
||||
// @param theme string
|
||||
// @return true|nil ok
|
||||
// @return string|nil error
|
||||
{"setTheme", setTheme},
|
||||
{nullptr, nullptr},
|
||||
};
|
||||
|
||||
|
||||
@@ -50,6 +50,10 @@ int back(lua_State* state) {
|
||||
Runtime::from(state)->requestBack();
|
||||
return 0;
|
||||
}
|
||||
int canGoBack(lua_State* state) {
|
||||
lua_pushboolean(state, Runtime::from(state)->canGoBack());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int getMemory(lua_State* state) {
|
||||
const MemoryInfo memory = Runtime::from(state)->sys().memory();
|
||||
@@ -88,16 +92,22 @@ const luaL_Reg FUNCTIONS[] = {
|
||||
// --- Changes the running app's display title.
|
||||
// @param title string
|
||||
{"setAppTitle", setAppTitle},
|
||||
// --- Launches /.lua/apps/<path>/main.lua and pushes the current route.
|
||||
// @param path string App-relative directory path; traversal is rejected.
|
||||
// @param arg string|nil Passed to init(arg).
|
||||
// --- Launches a route, which main.lua resolves, and pushes the current
|
||||
// one.
|
||||
// @param path string App-relative route; traversal is rejected.
|
||||
// @param arg string|nil Passed to main.start(route, arg).
|
||||
{"launch", launch},
|
||||
// --- Launches an app path without retaining the current route.
|
||||
// @param path string App-relative directory path; traversal is rejected.
|
||||
// @param arg string|nil Passed to init(arg).
|
||||
// --- Launches a route without retaining the current one.
|
||||
// @param path string App-relative route; traversal is rejected.
|
||||
// @param arg string|nil Passed to main.start(route, arg).
|
||||
{"replace", replace},
|
||||
// --- Returns to the previous app, or the launcher when history is empty.
|
||||
{"back", back},
|
||||
// --- Whether sys.back() would return somewhere rather than land on the
|
||||
// launcher, which is what chrome needs to decide whether to offer a back
|
||||
// control.
|
||||
// @return boolean
|
||||
{"canGoBack", canGoBack},
|
||||
// --- Returns heap statistics.
|
||||
// @return integer freeBytes
|
||||
// @return integer totalBytes
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -123,13 +123,11 @@ int Runtime::loadFile(lua_State* state) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
void Runtime::installLoader(const std::string& appDir) {
|
||||
// package.path is left to main.lua, which is loaded by absolute path and knows
|
||||
// where its libraries and its apps are.
|
||||
void Runtime::installLoader() {
|
||||
lua_getglobal(state_, "package");
|
||||
|
||||
const std::string path = appDir + "/?.lua;" + paths_.lib + "/?.lua";
|
||||
lua_pushlstring(state_, path.data(), path.size());
|
||||
lua_setfield(state_, -2, "path");
|
||||
|
||||
// Keep the preload searcher, drop the C loaders: they can only report
|
||||
// misleading errors about shared objects that were never there.
|
||||
lua_getfield(state_, -1, "searchers");
|
||||
|
||||
@@ -45,8 +45,7 @@ bool isSafeRoute(const std::string& path) {
|
||||
|
||||
} // namespace
|
||||
|
||||
Runtime::Runtime(const Providers& providers, const Paths& paths)
|
||||
: providers_(providers), paths_(paths) {}
|
||||
Runtime::Runtime(const Providers& providers) : providers_(providers) {}
|
||||
|
||||
Runtime::~Runtime() { close(); }
|
||||
|
||||
@@ -91,6 +90,7 @@ void Runtime::close() {
|
||||
cancelAllTimers();
|
||||
lua_close(state_);
|
||||
state_ = nullptr;
|
||||
mainRef_ = 0; // the registry it referenced went with the state
|
||||
// Node handles mean nothing to the next lua_State, so an app that inherited
|
||||
// the previous tree would build onto its nodes.
|
||||
tree_.reset();
|
||||
@@ -108,13 +108,40 @@ Runtime::Batch::~Batch() {
|
||||
}
|
||||
|
||||
bool Runtime::beginCall(const char* name) {
|
||||
lua_getglobal(state_, name);
|
||||
if (!mainRef_)
|
||||
return false;
|
||||
lua_rawgeti(state_, LUA_REGISTRYINDEX, mainRef_);
|
||||
lua_getfield(state_, -1, name);
|
||||
lua_remove(state_, -2);
|
||||
if (lua_isfunction(state_, -1))
|
||||
return true;
|
||||
lua_pop(state_, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string Runtime::mainField(const char* key, const char* fallback) {
|
||||
if (!mainRef_)
|
||||
return fallback;
|
||||
lua_rawgeti(state_, LUA_REGISTRYINDEX, mainRef_);
|
||||
lua_getfield(state_, -1, key);
|
||||
const char* value = lua_tostring(state_, -1);
|
||||
const std::string result = value ? value : fallback;
|
||||
lua_pop(state_, 2);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Calls a chunk or handler that leaves one value on the stack; the caller owns
|
||||
// it.
|
||||
bool Runtime::finishCallValue(const char* name) {
|
||||
if (lua_pcall(state_, 0, 1, 0) == LUA_OK)
|
||||
return true;
|
||||
const char* message = lua_tostring(state_, -1);
|
||||
providers_.log->write(LogLevel::Error, std::string(name) + ": " +
|
||||
(message ? message : "failed"));
|
||||
lua_pop(state_, 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Runtime::finishCall(const char* name, int argc) {
|
||||
if (lua_pcall(state_, argc, 0, 0) == LUA_OK)
|
||||
return true;
|
||||
@@ -126,17 +153,16 @@ bool Runtime::finishCall(const char* name, int argc) {
|
||||
}
|
||||
|
||||
// @lua-global core/runtime
|
||||
// @lua-preamble -- Runtime layout:
|
||||
// @lua-preamble -- /.lua/apps/<AppId>/main.lua application entry
|
||||
// point
|
||||
// @lua-preamble -- /.lua/apps/<AppId>/<Subapp>/main.lua nested route,
|
||||
// omitted from the launcher
|
||||
// @lua-preamble -- /.lua/data/<AppId>/ persistent app
|
||||
// data, preserved across updates
|
||||
// @lua-preamble -- /.lua/lib/<module>.lua shared require()
|
||||
// modules
|
||||
// @lua-preamble -- require() also searches the running application's
|
||||
// directory
|
||||
// @lua-preamble -- The firmware loads /.lua/main.lua into every fresh state and
|
||||
// calls these on the
|
||||
// @lua-preamble -- table it returns. Where apps live, what surrounds them and
|
||||
// which of these an app
|
||||
// @lua-preamble -- itself sees are all main.lua's to decide.
|
||||
// @lua-preamble --
|
||||
// @lua-preamble -- Fields the firmware reads: home, the route sys.back() lands
|
||||
// on once history is
|
||||
// @lua-preamble -- empty, and data, the sys.getAppDataPath() template whose ?
|
||||
// is the app id.
|
||||
// @lua-preamble --
|
||||
// @lua-preamble -- The firmware does not clear the frame before calling draw(),
|
||||
// and commits changed
|
||||
@@ -145,20 +171,22 @@ bool Runtime::finishCall(const char* name, int argc) {
|
||||
// @lua-preamble -- Timer callbacks are registered directly with
|
||||
// timer.after/every.
|
||||
|
||||
// ---Required. Runs once before the first draw; failing here stops the app.
|
||||
// ---Required. Mounts the route; failing here leaves no app running.
|
||||
// @param route string The app path sys.launch, sys.back or the boot recorded.
|
||||
// @param arg string|nil The string passed to sys.launch or sys.replace.
|
||||
// @lua-fn init
|
||||
bool Runtime::callInit(const std::string& arg) {
|
||||
// @lua-fn start
|
||||
bool Runtime::callStart(const std::string& route, const std::string& arg) {
|
||||
const Batch batch(*this);
|
||||
if (!beginCall("init")) {
|
||||
providers_.log->write(LogLevel::Error, "init: the app defines none");
|
||||
if (!beginCall("start")) {
|
||||
providers_.log->write(LogLevel::Error, "start: main.lua defines none");
|
||||
return false;
|
||||
}
|
||||
lua_pushlstring(state_, route.data(), route.size());
|
||||
lua_pushlstring(state_, arg.data(), arg.size());
|
||||
return finishCall("init", 1);
|
||||
return finishCall("start", 2);
|
||||
}
|
||||
|
||||
// ---Optional frame loop, called once after init and then at most 30 FPS, best
|
||||
// ---Optional frame loop, called once after start and then at most 30 FPS, best
|
||||
// effort.
|
||||
// @param deltaMs integer Monotonic milliseconds since the previous draw; zero
|
||||
// on the first.
|
||||
@@ -280,7 +308,15 @@ std::string Runtime::appId() const {
|
||||
return slash == std::string::npos ? appPath_ : appPath_.substr(0, slash);
|
||||
}
|
||||
|
||||
std::string Runtime::appDataPath() const { return paths_.data + "/" + appId(); }
|
||||
// The template is main.lua's; substituting the app id here keeps every app on
|
||||
// its own directory whatever tree the card uses.
|
||||
std::string Runtime::appDataPath() const {
|
||||
const size_t mark = dataTemplate_.find('?');
|
||||
if (mark == std::string::npos)
|
||||
return dataTemplate_;
|
||||
return dataTemplate_.substr(0, mark) + appId() +
|
||||
dataTemplate_.substr(mark + 1);
|
||||
}
|
||||
|
||||
bool Runtime::hasFeature(const std::string& feature) const {
|
||||
if (feature == "touch")
|
||||
@@ -302,21 +338,33 @@ bool Runtime::startApp(const std::string& path, const std::string& arg) {
|
||||
appPath_ = path;
|
||||
appTitle_ = appId();
|
||||
|
||||
const std::string directory = paths_.apps + "/" + path;
|
||||
installLoader(directory);
|
||||
if (!loadScript(directory + "/main.lua")) {
|
||||
installLoader();
|
||||
// main.lua runs first and start() mounts the route, so an app that fails
|
||||
// either way leaves nothing behind.
|
||||
if (!loadMain() || !callStart(path, arg)) {
|
||||
close();
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Runtime::loadMain() {
|
||||
if (!loadScript(MAIN_PATH)) {
|
||||
providers_.log->write(LogLevel::Error, lua_tostring(state_, -1)
|
||||
? lua_tostring(state_, -1)
|
||||
: "load failed");
|
||||
close();
|
||||
: "cannot load main.lua");
|
||||
return false;
|
||||
}
|
||||
// The chunk body runs first, then init(), so an app that fails either way
|
||||
// leaves nothing behind.
|
||||
if (!finishCall("main.lua", 0) || !callInit(arg)) {
|
||||
close();
|
||||
if (!finishCallValue("main.lua"))
|
||||
return false;
|
||||
if (!lua_istable(state_, -1)) {
|
||||
providers_.log->write(LogLevel::Error, "main.lua returned no table");
|
||||
lua_pop(state_, 1);
|
||||
return false;
|
||||
}
|
||||
mainRef_ = luaL_ref(state_, LUA_REGISTRYINDEX);
|
||||
home_ = mainField("home", "Home");
|
||||
dataTemplate_ = mainField("data", "/.lua/data/?");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -341,7 +389,7 @@ bool Runtime::applyPendingNavigation() {
|
||||
if (pending.kind == Pending::Back) {
|
||||
// An empty history means the launcher, which is an app like any other.
|
||||
Route target;
|
||||
target.path = paths_.home;
|
||||
target.path = home_;
|
||||
if (!history_.empty()) {
|
||||
target = history_.back();
|
||||
history_.pop_back();
|
||||
|
||||
@@ -26,6 +26,7 @@ struct Log : LogProvider {
|
||||
struct Settings : SettingsProvider {
|
||||
int32_t degrees = 0;
|
||||
std::string tz = "UTC0";
|
||||
std::string themeName = "light";
|
||||
int32_t rotation() const override { return degrees; }
|
||||
Status setRotation(int32_t value) override {
|
||||
degrees = value;
|
||||
@@ -36,6 +37,11 @@ struct Settings : SettingsProvider {
|
||||
tz = value;
|
||||
return Status::success();
|
||||
}
|
||||
std::string theme() const override { return themeName; }
|
||||
Status setTheme(const std::string& value) override {
|
||||
themeName = value;
|
||||
return Status::success();
|
||||
}
|
||||
};
|
||||
|
||||
struct Sys : SysProvider {
|
||||
@@ -148,7 +154,6 @@ struct Fs : FsProvider {
|
||||
struct Gui : GuiProvider {
|
||||
std::string trace;
|
||||
int32_t degrees = 0;
|
||||
bool fullscreen = false;
|
||||
bool gradient = false;
|
||||
|
||||
FontIds fonts() const override {
|
||||
@@ -189,7 +194,6 @@ struct Gui : GuiProvider {
|
||||
trace += border ? ",border" : ",-";
|
||||
trace += ");";
|
||||
}
|
||||
void setFullscreen(bool on) override { fullscreen = on; }
|
||||
// Stands in for an e-ink panel, where a second commit is a second visible
|
||||
// refresh.
|
||||
void commit() override { commits++; }
|
||||
|
||||
@@ -129,12 +129,10 @@ int main() {
|
||||
"'confirm')");
|
||||
assert(bench.touch.calibration[3] == 400);
|
||||
|
||||
// Chrome control and gradients are core: an e-ink provider flattens what it
|
||||
// cannot show.
|
||||
run(state, "gui.setFullscreen(true)\n"
|
||||
"gui.roundRect(0, 0, 10, 10, 4, 0, 0xFF, 0x00)\n"
|
||||
// Gradients are core: an e-ink provider flattens what it cannot show.
|
||||
run(state, "gui.roundRect(0, 0, 10, 10, 4, 0, 0xFF, 0x00)\n"
|
||||
"gui.roundRect(0, 0, 10, 10, 4, 0, nil, nil, 0xFF)");
|
||||
assert(bench.gui.fullscreen && bench.gui.gradient);
|
||||
assert(bench.gui.gradient);
|
||||
assert(bench.gui.trace.find("roundRect(-,border);") != std::string::npos);
|
||||
|
||||
bench.gui.trace.clear();
|
||||
@@ -178,35 +176,6 @@ int main() {
|
||||
"node.draw(root)\n"
|
||||
"assert(painted == 320)");
|
||||
|
||||
// Callbacks: only init failing stops an app, and a release fires the tap
|
||||
// alias after the up.
|
||||
run(state,
|
||||
"events = {}\n"
|
||||
"local function note(name) return function(a) events[#events + 1] = name "
|
||||
".. ':' .. tostring(a) end end\n"
|
||||
"function init(arg) events[#events + 1] = 'init:' .. tostring(arg) end\n"
|
||||
"function draw(delta) events[#events + 1] = 'draw:' .. delta end\n"
|
||||
"on_touch_down = note('down')\n"
|
||||
"on_touch_up = note('up')\n"
|
||||
"on_touch = note('tap')\n"
|
||||
"on_button_up = note('bup')\n"
|
||||
"on_button = note('btap')");
|
||||
bench.gui.commits = 0;
|
||||
assert(runtime.callInit("book.epub"));
|
||||
runtime.callDraw(33);
|
||||
runtime.callTouch(esp32lua::TouchPhase::Down, 5, 6);
|
||||
runtime.callTouch(esp32lua::TouchPhase::Move, 5,
|
||||
7); // the app defines no on_touch_move
|
||||
runtime.callTouch(esp32lua::TouchPhase::Up, 5, 8);
|
||||
runtime.callButton("confirm", false);
|
||||
run(state,
|
||||
"assert(table.concat(events, ' ') == "
|
||||
"'init:book.epub draw:33 down:5 up:5 tap:5 bup:confirm btap:confirm')");
|
||||
// One commit per visit to the app, so six calls and not seven: the release
|
||||
// and its tap alias are one visible change, and the move nobody handled still
|
||||
// ends a batch.
|
||||
assert(bench.gui.commits == 6);
|
||||
|
||||
// A timer firing inside draw is still one batch.
|
||||
run(state, "function draw() timer.after(1, function() end) end\n"
|
||||
"nested = timer.after(1, function() draw() end)");
|
||||
@@ -214,12 +183,57 @@ int main() {
|
||||
runtime.callTimer(bench.timer.scheduled.back());
|
||||
assert(bench.gui.commits == 1);
|
||||
|
||||
run(state, "function init() error('boom') end");
|
||||
assert(!runtime.callInit(""));
|
||||
assert(bench.log.message.find("init: ") == 0);
|
||||
run(state, "function draw() error('kaboom') end");
|
||||
runtime.callDraw(1); // a failed frame logs and the app keeps running
|
||||
assert(bench.log.message.find("draw: ") == 0);
|
||||
// Callbacks land on the table main.lua returns, only start() failing stops an
|
||||
// app, and a release fires the tap alias after the up.
|
||||
{
|
||||
fake::Bench chrome;
|
||||
chrome.fs.files["/.lua/main.lua"] =
|
||||
"events = {}\n"
|
||||
"local function note(name) return function(a) events[#events + 1] = "
|
||||
"name .. ':' .. tostring(a) end end\n"
|
||||
"return {\n"
|
||||
" start = function(route, arg) events[#events + 1] = 'start:' .. "
|
||||
"route .. ':' .. arg end,\n"
|
||||
" draw = function(delta) if delta < 0 then error('kaboom') end\n"
|
||||
" events[#events + 1] = 'draw:' .. delta end,\n"
|
||||
" on_touch_down = note('down'),\n"
|
||||
" on_touch_up = note('up'),\n"
|
||||
" on_touch = note('tap'),\n"
|
||||
" on_button_up = note('bup'),\n"
|
||||
" on_button = note('btap'),\n"
|
||||
"}\n";
|
||||
esp32lua::Runtime hosted(chrome.providers());
|
||||
assert(hosted.startApp("Reader", "book.epub"));
|
||||
chrome.gui.commits = 0;
|
||||
hosted.callDraw(33);
|
||||
hosted.callTouch(esp32lua::TouchPhase::Down, 5, 6);
|
||||
hosted.callTouch(esp32lua::TouchPhase::Move, 5,
|
||||
7); // main.lua defines no on_touch_move
|
||||
hosted.callTouch(esp32lua::TouchPhase::Up, 5, 8);
|
||||
hosted.callButton("confirm", false);
|
||||
run(hosted.state(),
|
||||
"assert(table.concat(events, ' ') == "
|
||||
"'start:Reader:book.epub draw:33 down:5 up:5 tap:5 bup:confirm "
|
||||
"btap:confirm')");
|
||||
// One commit per visit, so five calls and not six: the release and its tap
|
||||
// alias are one visible change, and the move nobody handled still ends a
|
||||
// batch.
|
||||
assert(chrome.gui.commits == 5);
|
||||
|
||||
hosted.callDraw(-1); // a failed frame logs and the app keeps running
|
||||
assert(chrome.log.message.find("draw: ") == 0);
|
||||
assert(hosted.hasApp());
|
||||
|
||||
chrome.fs.files["/.lua/main.lua"] =
|
||||
"return { start = function() error('boom') end }";
|
||||
assert(!hosted.startApp("Reader"));
|
||||
assert(chrome.log.message.find("start: ") == 0);
|
||||
assert(!hosted.hasApp());
|
||||
|
||||
chrome.fs.files["/.lua/main.lua"] = "return 7";
|
||||
assert(!hosted.startApp("Reader"));
|
||||
assert(chrome.log.message == "main.lua returned no table");
|
||||
}
|
||||
|
||||
// A feature callback without its provider is a wiring bug, not a silent
|
||||
// no-op.
|
||||
@@ -235,22 +249,36 @@ int main() {
|
||||
"assert(input.getTouch == nil and input.isPressed ~= nil)");
|
||||
}
|
||||
|
||||
// App loading: a fresh state per app, require reaching the app directory and
|
||||
// /.lua/lib, and navigation applied between batches rather than inside a
|
||||
// App loading: a fresh state per app, main.lua deciding where apps and
|
||||
// modules live, and navigation applied between batches rather than inside a
|
||||
// callback.
|
||||
{
|
||||
fake::Bench host;
|
||||
// The tree is main.lua's, so the test states it the way a card would.
|
||||
host.fs.files["/.lua/main.lua"] =
|
||||
"package.path = '/.lua/lib/?.lua'\n"
|
||||
"return {\n"
|
||||
" home = 'Home',\n"
|
||||
" data = '/.lua/data/?',\n"
|
||||
" start = function(route, arg)\n"
|
||||
" local dir = '/.lua/apps/' .. route\n"
|
||||
" package.path = dir .. '/?.lua;/.lua/lib/?.lua'\n"
|
||||
" app = assert(loadfile(dir .. '/main.lua'))()\n"
|
||||
" app.init(arg)\n"
|
||||
" end,\n"
|
||||
"}\n";
|
||||
host.fs.files["/.lua/lib/greet.lua"] =
|
||||
"return {hello = function() return 'hi' end}";
|
||||
host.fs.files["/.lua/apps/Home/main.lua"] =
|
||||
"local greet = require('greet')\n"
|
||||
"function init(arg) started = greet.hello() .. ':' .. tostring(arg) "
|
||||
"end";
|
||||
"return {init = function(arg) started = greet.hello() .. ':' .. "
|
||||
"tostring(arg) end}";
|
||||
host.fs.files["/.lua/apps/Reader/main.lua"] =
|
||||
"local page = require('page')\n"
|
||||
"function init(arg) started = page.name .. ':' .. arg end";
|
||||
"return {init = function(arg) started = page.name .. ':' .. arg end}";
|
||||
host.fs.files["/.lua/apps/Reader/page.lua"] = "return {name = 'page'}";
|
||||
host.fs.files["/.lua/apps/Reader/Notes/main.lua"] = "function init() end";
|
||||
host.fs.files["/.lua/apps/Reader/Notes/main.lua"] =
|
||||
"return {init = function() end}";
|
||||
|
||||
esp32lua::Runtime app(host.providers());
|
||||
assert(app.startApp("Home"));
|
||||
@@ -289,7 +317,7 @@ int main() {
|
||||
assert(!app.startApp("Absent"));
|
||||
assert(!app.hasApp() && app.state() == nullptr);
|
||||
host.fs.files["/.lua/apps/Broken/main.lua"] =
|
||||
"function init() error('nope') end";
|
||||
"return {init = function() error('nope') end}";
|
||||
assert(!app.startApp("Broken"));
|
||||
assert(!app.hasApp());
|
||||
assert(!app.startApp("../secrets"));
|
||||
|
||||
Reference in New Issue
Block a user