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.
|
---@param border? GuiColor Omitted for no border.
|
||||||
function gui.roundRect(x, y, w, h, radius, background, top, bottom, border) end
|
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.
|
---Fills a polygon.
|
||||||
---@param xs integer[]
|
---@param xs integer[]
|
||||||
---@param ys integer[]
|
---@param ys integer[]
|
||||||
|
|||||||
@@ -2,21 +2,22 @@
|
|||||||
|
|
||||||
-- Generated from native/src/runtime/runtime.cpp. Do not edit.
|
-- Generated from native/src/runtime/runtime.cpp. Do not edit.
|
||||||
|
|
||||||
-- Runtime layout:
|
-- The firmware loads /.lua/main.lua into every fresh state and calls these on the
|
||||||
-- /.lua/apps/<AppId>/main.lua application entry point
|
-- table it returns. Where apps live, what surrounds them and which of these an app
|
||||||
-- /.lua/apps/<AppId>/<Subapp>/main.lua nested route, omitted from the launcher
|
-- itself sees are all main.lua's to decide.
|
||||||
-- /.lua/data/<AppId>/ persistent app data, preserved across updates
|
--
|
||||||
-- /.lua/lib/<module>.lua shared require() modules
|
-- Fields the firmware reads: home, the route sys.back() lands on once history is
|
||||||
-- require() also searches the running application's directory
|
-- 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
|
-- 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.
|
-- display content after each callback batch using the panel's own refresh policy.
|
||||||
-- Timer callbacks are registered directly with timer.after/every.
|
-- 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.
|
---@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.
|
---@param deltaMs integer Monotonic milliseconds since the previous draw; zero on the first.
|
||||||
function draw(deltaMs) end
|
function draw(deltaMs) end
|
||||||
|
|||||||
@@ -24,3 +24,15 @@ function settings.getTimezone() end
|
|||||||
---@return true? ok
|
---@return true? ok
|
||||||
---@return string? error
|
---@return string? error
|
||||||
function settings.setTimezone(timezone) end
|
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
|
---@param title string
|
||||||
function sys.setAppTitle(title) end
|
function sys.setAppTitle(title) end
|
||||||
|
|
||||||
---Launches /.lua/apps/<path>/main.lua and pushes the current route.
|
---Launches a route, which main.lua resolves, and pushes the current one.
|
||||||
---@param path string App-relative directory path; traversal is rejected.
|
---@param path string App-relative route; traversal is rejected.
|
||||||
---@param arg? string Passed to init(arg).
|
---@param arg? string Passed to main.start(route, arg).
|
||||||
function sys.launch(path, arg) end
|
function sys.launch(path, arg) end
|
||||||
|
|
||||||
---Launches an app path without retaining the current route.
|
---Launches a route without retaining the current one.
|
||||||
---@param path string App-relative directory path; traversal is rejected.
|
---@param path string App-relative route; traversal is rejected.
|
||||||
---@param arg? string Passed to init(arg).
|
---@param arg? string Passed to main.start(route, arg).
|
||||||
function sys.replace(path, arg) end
|
function sys.replace(path, arg) end
|
||||||
|
|
||||||
---Returns to the previous app, or the launcher when history is empty.
|
---Returns to the previous app, or the launcher when history is empty.
|
||||||
function sys.back() end
|
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.
|
---Returns heap statistics.
|
||||||
---@return integer freeBytes
|
---@return integer freeBytes
|
||||||
---@return integer totalBytes
|
---@return integer totalBytes
|
||||||
|
|||||||
+77
-58
@@ -42,7 +42,6 @@ local ui = {}
|
|||||||
---@field on_cancel? UiHandler
|
---@field on_cancel? UiHandler
|
||||||
---@field on_outside? UiHandler
|
---@field on_outside? UiHandler
|
||||||
|
|
||||||
local THEME_PATH = "/.lua/theme"
|
|
||||||
local THEMES = {
|
local THEMES = {
|
||||||
light = { background = { 255, 255, 255 }, color = { 0, 0, 0 }, accent = { 0, 120, 255 }, radius = 6 },
|
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 },
|
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 pressStyles = {}
|
||||||
local laidOut = false
|
local laidOut = false
|
||||||
local themeName
|
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 applyPalette
|
||||||
|
|
||||||
local function mix(a, b, amount)
|
local function mix(a, b, amount)
|
||||||
@@ -113,21 +115,20 @@ function ui.setTheme(name)
|
|||||||
if not THEMES[name] then
|
if not THEMES[name] then
|
||||||
return nil, "Unknown theme"
|
return nil, "Unknown theme"
|
||||||
end
|
end
|
||||||
local ok, err = fs.writeFile(THEME_PATH, name)
|
local ok, err = settings.setTheme(name)
|
||||||
if not ok then
|
if not ok then
|
||||||
return nil, err
|
return nil, err
|
||||||
end
|
end
|
||||||
loadTheme(name)
|
loadTheme(name)
|
||||||
if activeScreen then
|
if root then
|
||||||
applyPalette(activeScreen.root)
|
applyPalette(root)
|
||||||
gui.clear(ui.theme.background)
|
gui.clear(ui.theme.background)
|
||||||
node.invalidate(activeScreen.root)
|
node.invalidate(root)
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
|
|
||||||
local savedTheme = fs.readFile(THEME_PATH, 32)
|
loadTheme(settings.getTheme())
|
||||||
loadTheme(savedTheme and savedTheme:match "^%s*(.-)%s*$" or "light")
|
|
||||||
|
|
||||||
local function clearState()
|
local function clearState()
|
||||||
enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
enterHandlers, exitHandlers, clickHandlers, painters = {}, {}, {}, {}
|
||||||
@@ -171,8 +172,10 @@ end
|
|||||||
|
|
||||||
local function build(spec, kind)
|
local function build(spec, kind)
|
||||||
spec = spec or {}
|
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
|
if laidOut then
|
||||||
ui.reset()
|
error("build nodes from the function ui.mount() was given, then ui.rebuild()", 3)
|
||||||
end
|
end
|
||||||
|
|
||||||
local children = {}
|
local children = {}
|
||||||
@@ -212,13 +215,28 @@ end
|
|||||||
---@return integer side
|
---@return integer side
|
||||||
---@return integer columns
|
---@return integer columns
|
||||||
function ui.cardSide(count, pad, gap, reserve)
|
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 rows = math.ceil(count / columns)
|
||||||
local byWidth = (gui.getWidth() - 2 * pad - (columns - 1) * gap) // columns
|
local byWidth = (width - 2 * pad - (columns - 1) * gap) // columns
|
||||||
local byHeight = (gui.getHeight() - 2 * pad - (reserve or 0) - (rows - 1) * gap) // rows
|
local byHeight = (height - 2 * pad - (reserve or 0) - (rows - 1) * gap) // rows
|
||||||
return math.min(byWidth, byHeight), columns
|
return math.min(byWidth, byHeight), columns
|
||||||
end
|
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
|
---@param spec UiSpec
|
||||||
---@return NodeId
|
---@return NodeId
|
||||||
function ui.spacer(spec)
|
function ui.spacer(spec)
|
||||||
@@ -328,13 +346,9 @@ function ui.reset()
|
|||||||
node.reset()
|
node.reset()
|
||||||
clearState()
|
clearState()
|
||||||
laidOut = false
|
laidOut = false
|
||||||
activeScreen = nil
|
root, captured, insideCaptured, confirming = nil, nil, nil, nil
|
||||||
end
|
end
|
||||||
|
|
||||||
---@class UiScreen
|
|
||||||
local Screen = {}
|
|
||||||
Screen.__index = Screen
|
|
||||||
|
|
||||||
applyPalette = function(root)
|
applyPalette = function(root)
|
||||||
-- The root is the panel background, not a card: no border, so it takes the fast fillRect
|
-- 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.
|
-- path rather than the per-pixel roundRect one. Radius stays so cards inherit it.
|
||||||
@@ -350,33 +364,39 @@ applyPalette = function(root)
|
|||||||
})
|
})
|
||||||
end
|
end
|
||||||
|
|
||||||
---@param root NodeId
|
---Registers the function that builds the whole tree and shows what it returns.
|
||||||
---@param style? NodeStyle
|
---@param fn fun(): NodeId
|
||||||
---@return UiScreen
|
function ui.mount(fn)
|
||||||
function ui.screen(root, style)
|
builder = fn
|
||||||
node.setSize(root, "fill", "fill")
|
ui.rebuild()
|
||||||
applyPalette(root)
|
|
||||||
if style then
|
|
||||||
node.setStyle(root, style)
|
|
||||||
end
|
|
||||||
local screen = setmetatable({ root = root }, Screen)
|
|
||||||
activeScreen = screen
|
|
||||||
screen:relayout()
|
|
||||||
return screen
|
|
||||||
end
|
end
|
||||||
|
|
||||||
function Screen:relayout()
|
---Rebuilds the tree from scratch and repaints. Screens are not retained, so this
|
||||||
local ok, err = node.layout(self.root, 0, 0, gui.getWidth(), gui.getHeight())
|
---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
|
if not ok then
|
||||||
error(err, 2)
|
error(err, 2)
|
||||||
end
|
end
|
||||||
node.dropScratch()
|
node.dropScratch()
|
||||||
laidOut = true
|
laidOut = true
|
||||||
gui.clear(ui.theme.background)
|
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
|
end
|
||||||
|
|
||||||
function Screen:draw()
|
function ui.draw()
|
||||||
node.draw(self.root)
|
if root then
|
||||||
|
node.draw(root)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local function inside(id, x, y)
|
local function inside(id, x, y)
|
||||||
@@ -407,7 +427,7 @@ end
|
|||||||
---@param x integer
|
---@param x integer
|
||||||
---@param y integer
|
---@param y integer
|
||||||
---@return boolean handled
|
---@return boolean handled
|
||||||
function Screen:down(x, y)
|
function ui.down(x, y)
|
||||||
local focused = node.getFocus()
|
local focused = node.getFocus()
|
||||||
if focused then
|
if focused then
|
||||||
node.setFocus(nil)
|
node.setFocus(nil)
|
||||||
@@ -417,11 +437,11 @@ function Screen:down(x, y)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local target = node.hit(self.root, x, y)
|
local target = root and node.hit(root, x, y)
|
||||||
if not target then
|
if not target then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
self.captured, self.inside = target, true
|
captured, insideCaptured = target, true
|
||||||
enter(target, x, y)
|
enter(target, x, y)
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -429,18 +449,17 @@ end
|
|||||||
---@param x integer
|
---@param x integer
|
||||||
---@param y integer
|
---@param y integer
|
||||||
---@return boolean handled
|
---@return boolean handled
|
||||||
function Screen:move(x, y)
|
function ui.move(x, y)
|
||||||
local target = self.captured
|
if not captured then
|
||||||
if not target then
|
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local isInside = inside(target, x, y)
|
local isInside = inside(captured, x, y)
|
||||||
if isInside ~= self.inside then
|
if isInside ~= insideCaptured then
|
||||||
self.inside = isInside
|
insideCaptured = isInside
|
||||||
if isInside then
|
if isInside then
|
||||||
enter(target, x, y)
|
enter(captured, x, y)
|
||||||
else
|
else
|
||||||
exit(target, x, y)
|
exit(captured, x, y)
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return true
|
return true
|
||||||
@@ -449,15 +468,15 @@ end
|
|||||||
---@param x integer
|
---@param x integer
|
||||||
---@param y integer
|
---@param y integer
|
||||||
---@return boolean handled
|
---@return boolean handled
|
||||||
function Screen:up(x, y)
|
function ui.up(x, y)
|
||||||
local target = self.captured
|
local target = captured
|
||||||
if not target then
|
if not target then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local wasActive = self.inside
|
local wasActive = insideCaptured
|
||||||
local releasedInside = inside(target, x, y)
|
local releasedInside = inside(target, x, y)
|
||||||
local handler = releasedInside and clickHandlers[target] or nil
|
local handler = releasedInside and clickHandlers[target] or nil
|
||||||
self.captured, self.inside = nil, nil
|
captured, insideCaptured = nil, nil
|
||||||
if wasActive then
|
if wasActive then
|
||||||
exit(target, x, y)
|
exit(target, x, y)
|
||||||
end
|
end
|
||||||
@@ -469,8 +488,8 @@ 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 function focusFirst()
|
||||||
local focused = node.focusFirst(screen.root)
|
local focused = node.focusFirst(root)
|
||||||
if focused then
|
if focused then
|
||||||
local handler = enterHandlers[focused]
|
local handler = enterHandlers[focused]
|
||||||
if handler then
|
if handler then
|
||||||
@@ -483,7 +502,7 @@ end
|
|||||||
---@param name string Button name; directions and confirm are handled.
|
---@param name string Button name; directions and confirm are handled.
|
||||||
---@param pressed boolean
|
---@param pressed boolean
|
||||||
---@return boolean handled
|
---@return boolean handled
|
||||||
function Screen:button(name, pressed)
|
function ui.buttonPress(name, pressed)
|
||||||
if type(pressed) ~= "boolean" then
|
if type(pressed) ~= "boolean" then
|
||||||
error("button state must be boolean", 2)
|
error("button state must be boolean", 2)
|
||||||
end
|
end
|
||||||
@@ -494,10 +513,10 @@ function Screen:button(name, pressed)
|
|||||||
end
|
end
|
||||||
local previous = node.getFocus()
|
local previous = node.getFocus()
|
||||||
if not previous then
|
if not previous then
|
||||||
focusFirst(self)
|
focusFirst()
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
local focused = node.moveFocus(self.root, name)
|
local focused = node.moveFocus(root, name)
|
||||||
if focused ~= previous then
|
if focused ~= previous then
|
||||||
local leave = exitHandlers[previous]
|
local leave = exitHandlers[previous]
|
||||||
if leave then
|
if leave then
|
||||||
@@ -514,16 +533,16 @@ function Screen:button(name, pressed)
|
|||||||
if name ~= "confirm" then
|
if name ~= "confirm" then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
local focused = node.getFocus() or focusFirst(self)
|
local focused = node.getFocus() or focusFirst()
|
||||||
if not focused then
|
if not focused then
|
||||||
return false
|
return false
|
||||||
end
|
end
|
||||||
if pressed then
|
if pressed then
|
||||||
node.setPressed(focused, true)
|
node.setPressed(focused, true)
|
||||||
self.confirming = focused
|
confirming = focused
|
||||||
else
|
else
|
||||||
local target = self.confirming
|
local target = confirming
|
||||||
self.confirming = nil
|
confirming = nil
|
||||||
if target then
|
if target then
|
||||||
node.setPressed(target, false)
|
node.setPressed(target, false)
|
||||||
local handler = clickHandlers[target]
|
local handler = clickHandlers[target]
|
||||||
|
|||||||
+62
-31
@@ -17,6 +17,17 @@ fs = {
|
|||||||
end,
|
end,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
local savedTheme = "light"
|
||||||
|
settings = {
|
||||||
|
getTheme = function()
|
||||||
|
return savedTheme
|
||||||
|
end,
|
||||||
|
setTheme = function(name)
|
||||||
|
savedTheme = name
|
||||||
|
return true
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
|
||||||
local frameWidth, frameHeight = 320, 480
|
local frameWidth, frameHeight = 320, 480
|
||||||
|
|
||||||
gui = {
|
gui = {
|
||||||
@@ -166,7 +177,7 @@ assert(table.concat(ui.themeNames(), ",") == "dark,light,mono")
|
|||||||
local ok, err = ui.setTheme "missing"
|
local ok, err = ui.setTheme "missing"
|
||||||
assert(ok == nil and err == "Unknown theme")
|
assert(ok == nil and err == "Unknown theme")
|
||||||
assert(ui.setTheme "dark" == true)
|
assert(ui.setTheme "dark" == true)
|
||||||
assert(files["/.lua/theme"] == "dark" and ui.getTheme() == "dark")
|
assert(savedTheme == "dark" and ui.getTheme() == "dark")
|
||||||
|
|
||||||
local events = {}
|
local events = {}
|
||||||
local function handler(name)
|
local function handler(name)
|
||||||
@@ -175,27 +186,30 @@ local function handler(name)
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
local first = ui.button {
|
local first, second
|
||||||
label = "one",
|
ui.mount(function()
|
||||||
on_enter = handler "enter",
|
first = ui.button {
|
||||||
on_exit = handler "exit",
|
label = "one",
|
||||||
on_click = handler "click",
|
on_enter = handler "enter",
|
||||||
}
|
on_exit = handler "exit",
|
||||||
local second = ui.button {
|
on_click = handler "click",
|
||||||
label = "two",
|
}
|
||||||
on_enter = handler "enter",
|
second = ui.button {
|
||||||
on_exit = handler "exit",
|
label = "two",
|
||||||
on_click = handler "click",
|
on_enter = handler "enter",
|
||||||
}
|
on_exit = handler "exit",
|
||||||
local screen = ui.screen(ui.box { row = true, first, second })
|
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(node.isPressed(first))
|
||||||
assert(screen:move(95, 10))
|
assert(ui.move(95, 10))
|
||||||
assert(not node.isPressed(first))
|
assert(not node.isPressed(first))
|
||||||
assert(screen:move(10, 10))
|
assert(ui.move(10, 10))
|
||||||
assert(node.isPressed(first))
|
assert(node.isPressed(first))
|
||||||
assert(screen:up(10, 10))
|
assert(ui.up(10, 10))
|
||||||
assert(not node.isPressed(first))
|
assert(not node.isPressed(first))
|
||||||
|
|
||||||
local expectedTouch = { "enter", "exit", "enter", "exit", "click" }
|
local expectedTouch = { "enter", "exit", "enter", "exit", "click" }
|
||||||
@@ -207,16 +221,16 @@ for index, name in ipairs(expectedTouch) do
|
|||||||
end
|
end
|
||||||
|
|
||||||
events = {}
|
events = {}
|
||||||
assert(screen:button("right", true))
|
assert(ui.buttonPress("right", true))
|
||||||
assert(screen:button("right", false))
|
assert(ui.buttonPress("right", false))
|
||||||
assert(node.getFocus() == first)
|
assert(node.getFocus() == first)
|
||||||
assert(screen:button("right", true))
|
assert(ui.buttonPress("right", true))
|
||||||
assert(node.getFocus() == second)
|
assert(node.getFocus() == second)
|
||||||
assert(screen:button("confirm", true))
|
assert(ui.buttonPress("confirm", true))
|
||||||
assert(node.isPressed(second))
|
assert(node.isPressed(second))
|
||||||
assert(screen:button("confirm", false))
|
assert(ui.buttonPress("confirm", false))
|
||||||
assert(not node.isPressed(second))
|
assert(not node.isPressed(second))
|
||||||
assert(screen:button("back", true) == false)
|
assert(ui.buttonPress("back", true) == false)
|
||||||
|
|
||||||
local expectedButtons = {
|
local expectedButtons = {
|
||||||
{ "enter", first },
|
{ "enter", first },
|
||||||
@@ -235,7 +249,13 @@ assert(ui.setTheme "mono" == true)
|
|||||||
assert(ui.getTheme() == "mono" and #invalidated == before + 1)
|
assert(ui.getTheme() == "mono" and #invalidated == before + 1)
|
||||||
assert(cleared == ui.theme.background)
|
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.
|
-- 320x480 portrait: two columns, and the reserve comes off the height budget.
|
||||||
local side, columns = ui.cardSide(5, 12, 8)
|
local side, columns = ui.cardSide(5, 12, 8)
|
||||||
@@ -254,21 +274,32 @@ local pressedCalls = {}
|
|||||||
node.setPressed = function(_, on)
|
node.setPressed = function(_, on)
|
||||||
pressedCalls[#pressedCalls + 1] = on
|
pressedCalls[#pressedCalls + 1] = on
|
||||||
end
|
end
|
||||||
local own = ui.custom { h = 20, press_style = false, on_click = function() end }
|
local own, styled
|
||||||
local styled = ui.button { h = 20, label = "ok", on_click = function() end }
|
ui.mount(function()
|
||||||
local board = ui.screen(ui.box { own, styled })
|
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
|
local target
|
||||||
node.hit = function()
|
node.hit = function()
|
||||||
return target
|
return target
|
||||||
end
|
end
|
||||||
|
|
||||||
target = own
|
target = own
|
||||||
board:down(0, 0)
|
ui.down(0, 0)
|
||||||
board:up(0, 0)
|
ui.up(0, 0)
|
||||||
assert(#pressedCalls == 0, "a self-painting widget is not styled on press")
|
assert(#pressedCalls == 0, "a self-painting widget is not styled on press")
|
||||||
|
|
||||||
target = styled
|
target = styled
|
||||||
board:down(0, 0)
|
ui.down(0, 0)
|
||||||
assert(pressedCalls[1] == true, "an ordinary widget still gets its pressed style")
|
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"
|
print "ok"
|
||||||
|
|||||||
@@ -42,6 +42,10 @@ public:
|
|||||||
virtual Status setRotation(int32_t degrees) = 0;
|
virtual Status setRotation(int32_t degrees) = 0;
|
||||||
virtual std::string timezone() const = 0;
|
virtual std::string timezone() const = 0;
|
||||||
virtual Status setTimezone(const std::string& timezone) = 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,
|
// 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,
|
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,
|
int32_t radius, int32_t background, const int32_t* top,
|
||||||
const int32_t* bottom, const int32_t* border) = 0;
|
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
|
// 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
|
// 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
|
// driver cannot know; which region to touch, which waveform, and whether to
|
||||||
|
|||||||
@@ -15,14 +15,10 @@ namespace esp32lua {
|
|||||||
// build.
|
// build.
|
||||||
constexpr int32_t API_VERSION = 1;
|
constexpr int32_t API_VERSION = 1;
|
||||||
|
|
||||||
// Where the runtime looks for apps, their data, and shared modules.
|
// The one path the firmware knows. Everything below it -- where apps live,
|
||||||
struct Paths {
|
// where their data goes, what chrome surrounds them -- is decided by the table
|
||||||
std::string apps = "/.lua/apps";
|
// this file returns.
|
||||||
std::string data = "/.lua/data";
|
constexpr const char* MAIN_PATH = "/.lua/main.lua";
|
||||||
std::string lib = "/.lua/lib";
|
|
||||||
// Where sys.back() lands once history is empty. It is an app like any other.
|
|
||||||
std::string home = "Home";
|
|
||||||
};
|
|
||||||
|
|
||||||
// Firmware supplies every core provider; a null feature provider is how
|
// Firmware supplies every core provider; a null feature provider is how
|
||||||
// sys.hasFeature() answers false, and its namespace additions are simply never
|
// sys.hasFeature() answers false, and its namespace additions are simply never
|
||||||
@@ -44,7 +40,7 @@ struct Providers {
|
|||||||
|
|
||||||
class Runtime {
|
class Runtime {
|
||||||
public:
|
public:
|
||||||
explicit Runtime(const Providers& providers, const Paths& paths = Paths());
|
explicit Runtime(const Providers& providers);
|
||||||
~Runtime();
|
~Runtime();
|
||||||
|
|
||||||
Runtime(const Runtime&) = delete;
|
Runtime(const Runtime&) = delete;
|
||||||
@@ -55,9 +51,9 @@ public:
|
|||||||
void close();
|
void close();
|
||||||
lua_State* state() const { return state_; }
|
lua_State* state() const { return state_; }
|
||||||
|
|
||||||
// Replaces the running app with a fresh lua_State, loads
|
// Replaces the running app with a fresh lua_State, loads main.lua, and hands
|
||||||
// <apps>/<path>/main.lua, and calls init(arg). A failure leaves no app
|
// it the route through start(route, arg). A failure leaves no app running
|
||||||
// running rather than a half-built one.
|
// rather than a half-built one.
|
||||||
bool startApp(const std::string& path,
|
bool startApp(const std::string& path,
|
||||||
const std::string& arg = std::string());
|
const std::string& arg = std::string());
|
||||||
bool hasApp() const { return !appPath_.empty(); }
|
bool hasApp() const { return !appPath_.empty(); }
|
||||||
@@ -99,11 +95,11 @@ public:
|
|||||||
|
|
||||||
ui::Tree& tree() { return tree_; }
|
ui::Tree& tree() { return tree_; }
|
||||||
|
|
||||||
// Entry points into the app. The firmware decides whether an event reaches
|
// Entry points into main.lua, which forwards whatever the app it mounted
|
||||||
// the app at all -- jitter, chrome and debouncing are its business -- and the
|
// defines. The firmware decides whether an event happens at all -- jitter and
|
||||||
// runtime decides what the app sees. Only a failed init() stops an app; every
|
// debouncing are its business -- and main.lua decides who sees it. Only a
|
||||||
// other callback logs and carries on.
|
// failed start() stops an app; every other callback logs and carries on.
|
||||||
bool callInit(const std::string& arg);
|
bool callStart(const std::string& route, const std::string& arg);
|
||||||
void callDraw(int32_t deltaMs);
|
void callDraw(int32_t deltaMs);
|
||||||
// An Up phase also fires the on_touch tap alias, in that order.
|
// An Up phase also fires the on_touch tap alias, in that order.
|
||||||
void callTouch(TouchPhase phase, int32_t x, int32_t y);
|
void callTouch(TouchPhase phase, int32_t x, int32_t y);
|
||||||
@@ -136,7 +132,12 @@ private:
|
|||||||
};
|
};
|
||||||
|
|
||||||
bool loadScript(const std::string& path);
|
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 searchModule(lua_State* state);
|
||||||
static int searchEmbedded(lua_State* state);
|
static int searchEmbedded(lua_State* state);
|
||||||
static int loadFile(lua_State* state);
|
static int loadFile(lua_State* state);
|
||||||
@@ -155,9 +156,10 @@ private:
|
|||||||
Runtime& runtime_;
|
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 beginCall(const char* name);
|
||||||
bool finishCall(const char* name, int argc);
|
bool finishCall(const char* name, int argc);
|
||||||
|
bool finishCallValue(const char* name);
|
||||||
void cancelAllTimers();
|
void cancelAllTimers();
|
||||||
|
|
||||||
Providers providers_;
|
Providers providers_;
|
||||||
@@ -167,7 +169,12 @@ private:
|
|||||||
TimerId nextTimerId_ = 1;
|
TimerId nextTimerId_ = 1;
|
||||||
int batchDepth_ = 0;
|
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 appPath_;
|
||||||
std::string appTitle_;
|
std::string appTitle_;
|
||||||
std::vector<Route> history_;
|
std::vector<Route> history_;
|
||||||
|
|||||||
@@ -125,12 +125,6 @@ int roundRect(lua_State* state) {
|
|||||||
return 0;
|
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) {
|
void readIntegers(lua_State* state, int index, std::vector<int32_t>& out) {
|
||||||
const lua_Integer count = luaL_len(state, index);
|
const lua_Integer count = luaL_len(state, index);
|
||||||
for (lua_Integer at = 1; at <= count; at++) {
|
for (lua_Integer at = 1; at <= count; at++) {
|
||||||
@@ -291,9 +285,6 @@ const luaL_Reg FUNCTIONS[] = {
|
|||||||
// without a gradient use top.
|
// without a gradient use top.
|
||||||
// @param border GuiColor|nil Omitted for no border.
|
// @param border GuiColor|nil Omitted for no border.
|
||||||
{"roundRect", roundRect},
|
{"roundRect", roundRect},
|
||||||
// ---Temporarily gives the app the full panel, including firmware chrome.
|
|
||||||
// @param on boolean
|
|
||||||
{"setFullscreen", setFullscreen},
|
|
||||||
// --- Fills a polygon.
|
// --- Fills a polygon.
|
||||||
// @param xs integer[]
|
// @param xs integer[]
|
||||||
// @param ys integer[]
|
// @param ys integer[]
|
||||||
|
|||||||
@@ -47,6 +47,19 @@ int setTimezone(lua_State* state) {
|
|||||||
state, Runtime::from(state)->settings().setTimezone({value, length}));
|
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[] = {
|
const luaL_Reg FUNCTIONS[] = {
|
||||||
// --- Returns the saved rotation in degrees clockwise.
|
// --- Returns the saved rotation in degrees clockwise.
|
||||||
// @return integer
|
// @return integer
|
||||||
@@ -64,6 +77,16 @@ const luaL_Reg FUNCTIONS[] = {
|
|||||||
// @return true|nil ok
|
// @return true|nil ok
|
||||||
// @return string|nil error
|
// @return string|nil error
|
||||||
{"setTimezone", setTimezone},
|
{"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},
|
{nullptr, nullptr},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ int back(lua_State* state) {
|
|||||||
Runtime::from(state)->requestBack();
|
Runtime::from(state)->requestBack();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
int canGoBack(lua_State* state) {
|
||||||
|
lua_pushboolean(state, Runtime::from(state)->canGoBack());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
int getMemory(lua_State* state) {
|
int getMemory(lua_State* state) {
|
||||||
const MemoryInfo memory = Runtime::from(state)->sys().memory();
|
const MemoryInfo memory = Runtime::from(state)->sys().memory();
|
||||||
@@ -88,16 +92,22 @@ const luaL_Reg FUNCTIONS[] = {
|
|||||||
// --- Changes the running app's display title.
|
// --- Changes the running app's display title.
|
||||||
// @param title string
|
// @param title string
|
||||||
{"setAppTitle", setAppTitle},
|
{"setAppTitle", setAppTitle},
|
||||||
// --- Launches /.lua/apps/<path>/main.lua and pushes the current route.
|
// --- Launches a route, which main.lua resolves, and pushes the current
|
||||||
// @param path string App-relative directory path; traversal is rejected.
|
// one.
|
||||||
// @param arg string|nil Passed to init(arg).
|
// @param path string App-relative route; traversal is rejected.
|
||||||
|
// @param arg string|nil Passed to main.start(route, arg).
|
||||||
{"launch", launch},
|
{"launch", launch},
|
||||||
// --- Launches an app path without retaining the current route.
|
// --- Launches a route without retaining the current one.
|
||||||
// @param path string App-relative directory path; traversal is rejected.
|
// @param path string App-relative route; traversal is rejected.
|
||||||
// @param arg string|nil Passed to init(arg).
|
// @param arg string|nil Passed to main.start(route, arg).
|
||||||
{"replace", replace},
|
{"replace", replace},
|
||||||
// --- Returns to the previous app, or the launcher when history is empty.
|
// --- Returns to the previous app, or the launcher when history is empty.
|
||||||
{"back", back},
|
{"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.
|
// --- Returns heap statistics.
|
||||||
// @return integer freeBytes
|
// @return integer freeBytes
|
||||||
// @return integer totalBytes
|
// @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;
|
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");
|
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
|
// Keep the preload searcher, drop the C loaders: they can only report
|
||||||
// misleading errors about shared objects that were never there.
|
// misleading errors about shared objects that were never there.
|
||||||
lua_getfield(state_, -1, "searchers");
|
lua_getfield(state_, -1, "searchers");
|
||||||
|
|||||||
@@ -45,8 +45,7 @@ bool isSafeRoute(const std::string& path) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Runtime::Runtime(const Providers& providers, const Paths& paths)
|
Runtime::Runtime(const Providers& providers) : providers_(providers) {}
|
||||||
: providers_(providers), paths_(paths) {}
|
|
||||||
|
|
||||||
Runtime::~Runtime() { close(); }
|
Runtime::~Runtime() { close(); }
|
||||||
|
|
||||||
@@ -91,6 +90,7 @@ void Runtime::close() {
|
|||||||
cancelAllTimers();
|
cancelAllTimers();
|
||||||
lua_close(state_);
|
lua_close(state_);
|
||||||
state_ = nullptr;
|
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
|
// Node handles mean nothing to the next lua_State, so an app that inherited
|
||||||
// the previous tree would build onto its nodes.
|
// the previous tree would build onto its nodes.
|
||||||
tree_.reset();
|
tree_.reset();
|
||||||
@@ -108,13 +108,40 @@ Runtime::Batch::~Batch() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool Runtime::beginCall(const char* name) {
|
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))
|
if (lua_isfunction(state_, -1))
|
||||||
return true;
|
return true;
|
||||||
lua_pop(state_, 1);
|
lua_pop(state_, 1);
|
||||||
return false;
|
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) {
|
bool Runtime::finishCall(const char* name, int argc) {
|
||||||
if (lua_pcall(state_, argc, 0, 0) == LUA_OK)
|
if (lua_pcall(state_, argc, 0, 0) == LUA_OK)
|
||||||
return true;
|
return true;
|
||||||
@@ -126,17 +153,16 @@ bool Runtime::finishCall(const char* name, int argc) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @lua-global core/runtime
|
// @lua-global core/runtime
|
||||||
// @lua-preamble -- Runtime layout:
|
// @lua-preamble -- The firmware loads /.lua/main.lua into every fresh state and
|
||||||
// @lua-preamble -- /.lua/apps/<AppId>/main.lua application entry
|
// calls these on the
|
||||||
// point
|
// @lua-preamble -- table it returns. Where apps live, what surrounds them and
|
||||||
// @lua-preamble -- /.lua/apps/<AppId>/<Subapp>/main.lua nested route,
|
// which of these an app
|
||||||
// omitted from the launcher
|
// @lua-preamble -- itself sees are all main.lua's to decide.
|
||||||
// @lua-preamble -- /.lua/data/<AppId>/ persistent app
|
// @lua-preamble --
|
||||||
// data, preserved across updates
|
// @lua-preamble -- Fields the firmware reads: home, the route sys.back() lands
|
||||||
// @lua-preamble -- /.lua/lib/<module>.lua shared require()
|
// on once history is
|
||||||
// modules
|
// @lua-preamble -- empty, and data, the sys.getAppDataPath() template whose ?
|
||||||
// @lua-preamble -- require() also searches the running application's
|
// is the app id.
|
||||||
// directory
|
|
||||||
// @lua-preamble --
|
// @lua-preamble --
|
||||||
// @lua-preamble -- The firmware does not clear the frame before calling draw(),
|
// @lua-preamble -- The firmware does not clear the frame before calling draw(),
|
||||||
// and commits changed
|
// and commits changed
|
||||||
@@ -145,20 +171,22 @@ bool Runtime::finishCall(const char* name, int argc) {
|
|||||||
// @lua-preamble -- Timer callbacks are registered directly with
|
// @lua-preamble -- Timer callbacks are registered directly with
|
||||||
// timer.after/every.
|
// 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.
|
// @param arg string|nil The string passed to sys.launch or sys.replace.
|
||||||
// @lua-fn init
|
// @lua-fn start
|
||||||
bool Runtime::callInit(const std::string& arg) {
|
bool Runtime::callStart(const std::string& route, const std::string& arg) {
|
||||||
const Batch batch(*this);
|
const Batch batch(*this);
|
||||||
if (!beginCall("init")) {
|
if (!beginCall("start")) {
|
||||||
providers_.log->write(LogLevel::Error, "init: the app defines none");
|
providers_.log->write(LogLevel::Error, "start: main.lua defines none");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
lua_pushlstring(state_, route.data(), route.size());
|
||||||
lua_pushlstring(state_, arg.data(), arg.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.
|
// effort.
|
||||||
// @param deltaMs integer Monotonic milliseconds since the previous draw; zero
|
// @param deltaMs integer Monotonic milliseconds since the previous draw; zero
|
||||||
// on the first.
|
// on the first.
|
||||||
@@ -280,7 +308,15 @@ std::string Runtime::appId() const {
|
|||||||
return slash == std::string::npos ? appPath_ : appPath_.substr(0, slash);
|
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 {
|
bool Runtime::hasFeature(const std::string& feature) const {
|
||||||
if (feature == "touch")
|
if (feature == "touch")
|
||||||
@@ -302,21 +338,33 @@ bool Runtime::startApp(const std::string& path, const std::string& arg) {
|
|||||||
appPath_ = path;
|
appPath_ = path;
|
||||||
appTitle_ = appId();
|
appTitle_ = appId();
|
||||||
|
|
||||||
const std::string directory = paths_.apps + "/" + path;
|
installLoader();
|
||||||
installLoader(directory);
|
// main.lua runs first and start() mounts the route, so an app that fails
|
||||||
if (!loadScript(directory + "/main.lua")) {
|
// 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)
|
providers_.log->write(LogLevel::Error, lua_tostring(state_, -1)
|
||||||
? lua_tostring(state_, -1)
|
? lua_tostring(state_, -1)
|
||||||
: "load failed");
|
: "cannot load main.lua");
|
||||||
close();
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// The chunk body runs first, then init(), so an app that fails either way
|
if (!finishCallValue("main.lua"))
|
||||||
// leaves nothing behind.
|
return false;
|
||||||
if (!finishCall("main.lua", 0) || !callInit(arg)) {
|
if (!lua_istable(state_, -1)) {
|
||||||
close();
|
providers_.log->write(LogLevel::Error, "main.lua returned no table");
|
||||||
|
lua_pop(state_, 1);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
mainRef_ = luaL_ref(state_, LUA_REGISTRYINDEX);
|
||||||
|
home_ = mainField("home", "Home");
|
||||||
|
dataTemplate_ = mainField("data", "/.lua/data/?");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -341,7 +389,7 @@ bool Runtime::applyPendingNavigation() {
|
|||||||
if (pending.kind == Pending::Back) {
|
if (pending.kind == Pending::Back) {
|
||||||
// An empty history means the launcher, which is an app like any other.
|
// An empty history means the launcher, which is an app like any other.
|
||||||
Route target;
|
Route target;
|
||||||
target.path = paths_.home;
|
target.path = home_;
|
||||||
if (!history_.empty()) {
|
if (!history_.empty()) {
|
||||||
target = history_.back();
|
target = history_.back();
|
||||||
history_.pop_back();
|
history_.pop_back();
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ struct Log : LogProvider {
|
|||||||
struct Settings : SettingsProvider {
|
struct Settings : SettingsProvider {
|
||||||
int32_t degrees = 0;
|
int32_t degrees = 0;
|
||||||
std::string tz = "UTC0";
|
std::string tz = "UTC0";
|
||||||
|
std::string themeName = "light";
|
||||||
int32_t rotation() const override { return degrees; }
|
int32_t rotation() const override { return degrees; }
|
||||||
Status setRotation(int32_t value) override {
|
Status setRotation(int32_t value) override {
|
||||||
degrees = value;
|
degrees = value;
|
||||||
@@ -36,6 +37,11 @@ struct Settings : SettingsProvider {
|
|||||||
tz = value;
|
tz = value;
|
||||||
return Status::success();
|
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 {
|
struct Sys : SysProvider {
|
||||||
@@ -148,7 +154,6 @@ struct Fs : FsProvider {
|
|||||||
struct Gui : GuiProvider {
|
struct Gui : GuiProvider {
|
||||||
std::string trace;
|
std::string trace;
|
||||||
int32_t degrees = 0;
|
int32_t degrees = 0;
|
||||||
bool fullscreen = false;
|
|
||||||
bool gradient = false;
|
bool gradient = false;
|
||||||
|
|
||||||
FontIds fonts() const override {
|
FontIds fonts() const override {
|
||||||
@@ -189,7 +194,6 @@ struct Gui : GuiProvider {
|
|||||||
trace += border ? ",border" : ",-";
|
trace += border ? ",border" : ",-";
|
||||||
trace += ");";
|
trace += ");";
|
||||||
}
|
}
|
||||||
void setFullscreen(bool on) override { fullscreen = on; }
|
|
||||||
// Stands in for an e-ink panel, where a second commit is a second visible
|
// Stands in for an e-ink panel, where a second commit is a second visible
|
||||||
// refresh.
|
// refresh.
|
||||||
void commit() override { commits++; }
|
void commit() override { commits++; }
|
||||||
|
|||||||
@@ -129,12 +129,10 @@ int main() {
|
|||||||
"'confirm')");
|
"'confirm')");
|
||||||
assert(bench.touch.calibration[3] == 400);
|
assert(bench.touch.calibration[3] == 400);
|
||||||
|
|
||||||
// Chrome control and gradients are core: an e-ink provider flattens what it
|
// Gradients are core: an e-ink provider flattens what it cannot show.
|
||||||
// cannot show.
|
run(state, "gui.roundRect(0, 0, 10, 10, 4, 0, 0xFF, 0x00)\n"
|
||||||
run(state, "gui.setFullscreen(true)\n"
|
|
||||||
"gui.roundRect(0, 0, 10, 10, 4, 0, 0xFF, 0x00)\n"
|
|
||||||
"gui.roundRect(0, 0, 10, 10, 4, 0, nil, nil, 0xFF)");
|
"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);
|
assert(bench.gui.trace.find("roundRect(-,border);") != std::string::npos);
|
||||||
|
|
||||||
bench.gui.trace.clear();
|
bench.gui.trace.clear();
|
||||||
@@ -178,35 +176,6 @@ int main() {
|
|||||||
"node.draw(root)\n"
|
"node.draw(root)\n"
|
||||||
"assert(painted == 320)");
|
"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.
|
// A timer firing inside draw is still one batch.
|
||||||
run(state, "function draw() timer.after(1, function() end) end\n"
|
run(state, "function draw() timer.after(1, function() end) end\n"
|
||||||
"nested = timer.after(1, function() draw() end)");
|
"nested = timer.after(1, function() draw() end)");
|
||||||
@@ -214,12 +183,57 @@ int main() {
|
|||||||
runtime.callTimer(bench.timer.scheduled.back());
|
runtime.callTimer(bench.timer.scheduled.back());
|
||||||
assert(bench.gui.commits == 1);
|
assert(bench.gui.commits == 1);
|
||||||
|
|
||||||
run(state, "function init() error('boom') end");
|
// Callbacks land on the table main.lua returns, only start() failing stops an
|
||||||
assert(!runtime.callInit(""));
|
// app, and a release fires the tap alias after the up.
|
||||||
assert(bench.log.message.find("init: ") == 0);
|
{
|
||||||
run(state, "function draw() error('kaboom') end");
|
fake::Bench chrome;
|
||||||
runtime.callDraw(1); // a failed frame logs and the app keeps running
|
chrome.fs.files["/.lua/main.lua"] =
|
||||||
assert(bench.log.message.find("draw: ") == 0);
|
"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
|
// A feature callback without its provider is a wiring bug, not a silent
|
||||||
// no-op.
|
// no-op.
|
||||||
@@ -235,22 +249,36 @@ int main() {
|
|||||||
"assert(input.getTouch == nil and input.isPressed ~= nil)");
|
"assert(input.getTouch == nil and input.isPressed ~= nil)");
|
||||||
}
|
}
|
||||||
|
|
||||||
// App loading: a fresh state per app, require reaching the app directory and
|
// App loading: a fresh state per app, main.lua deciding where apps and
|
||||||
// /.lua/lib, and navigation applied between batches rather than inside a
|
// modules live, and navigation applied between batches rather than inside a
|
||||||
// callback.
|
// callback.
|
||||||
{
|
{
|
||||||
fake::Bench host;
|
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"] =
|
host.fs.files["/.lua/lib/greet.lua"] =
|
||||||
"return {hello = function() return 'hi' end}";
|
"return {hello = function() return 'hi' end}";
|
||||||
host.fs.files["/.lua/apps/Home/main.lua"] =
|
host.fs.files["/.lua/apps/Home/main.lua"] =
|
||||||
"local greet = require('greet')\n"
|
"local greet = require('greet')\n"
|
||||||
"function init(arg) started = greet.hello() .. ':' .. tostring(arg) "
|
"return {init = function(arg) started = greet.hello() .. ':' .. "
|
||||||
"end";
|
"tostring(arg) end}";
|
||||||
host.fs.files["/.lua/apps/Reader/main.lua"] =
|
host.fs.files["/.lua/apps/Reader/main.lua"] =
|
||||||
"local page = require('page')\n"
|
"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/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());
|
esp32lua::Runtime app(host.providers());
|
||||||
assert(app.startApp("Home"));
|
assert(app.startApp("Home"));
|
||||||
@@ -289,7 +317,7 @@ int main() {
|
|||||||
assert(!app.startApp("Absent"));
|
assert(!app.startApp("Absent"));
|
||||||
assert(!app.hasApp() && app.state() == nullptr);
|
assert(!app.hasApp() && app.state() == nullptr);
|
||||||
host.fs.files["/.lua/apps/Broken/main.lua"] =
|
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.startApp("Broken"));
|
||||||
assert(!app.hasApp());
|
assert(!app.hasApp());
|
||||||
assert(!app.startApp("../secrets"));
|
assert(!app.startApp("../secrets"));
|
||||||
|
|||||||
Reference in New Issue
Block a user