1adf121dc6
sys.startApp(path, args) tears the runtime down and starts over from a file, and keeps nothing else: no history, no title, no app identity. All of that can ride in the arguments, so it does, and sdcard/.lua/lib/nav.lua is the single writer of that table. nav owns the back stack, the bar's title and /.lua/data/<AppId>, which means the launcher, what "back" reaches and how deep a route nests are editable on the card rather than in a reflash. Apps call nav.launch, nav.replace and nav.back; sys.startApp has one caller. Boot passes no arguments at all, which is how main.lua tells a cold start from a navigation and opens its own launcher, replacing the home field the runtime used to read. Arguments cross as JSON, so history is a list of tables rather than a packed string, and an app passing something JSON cannot carry sees the error at its own nav call.
180 lines
5.2 KiB
Lua
180 lines
5.2 KiB
Lua
-- The top strip, built into the same tree as the running app by /.lua/main.lua.
|
|
--
|
|
-- It keeps no cache of what it painted: each field is its own node, so ui.setText()
|
|
-- repaints a clock that ticked and leaves the rest of the bar alone. The title is the
|
|
-- exception -- a longer one needs a wider box -- so a change to it rebuilds the screen.
|
|
|
|
local ui = require "ui"
|
|
local nav = require "nav"
|
|
|
|
local FONT = screen.FONT_SMALL
|
|
-- The app name reads as a heading, matching a card title; the clock and memory
|
|
-- stay small so the right-hand stack still fits two rows.
|
|
local TITLE_FONT = screen.FONT_UI
|
|
|
|
local BAR_H = 44
|
|
local PAD, GAP = 6, 8
|
|
local WIFI_W, WIFI_HEIGHTS = 11, { 3, 5, 8 }
|
|
|
|
local M = { height = BAR_H }
|
|
|
|
local ids = {}
|
|
local title, shownBars, shownOffline
|
|
local hidden = false
|
|
|
|
local function clock()
|
|
return sys.isClockSynced() and os.date "%H:%M:%S" or "--:--:--"
|
|
end
|
|
|
|
local function signalBars()
|
|
local status = wifi.getStatus()
|
|
if status.state ~= "connected" then
|
|
return 0, true
|
|
end
|
|
return status.rssi >= -60 and 3 or status.rssi >= -75 and 2 or 1, false
|
|
end
|
|
|
|
local function memory()
|
|
local free, total = sys.getMemory()
|
|
local used = math.max(0, (total or 0) - (free or 0))
|
|
return (used + 512) // 1024 .. "kB"
|
|
end
|
|
|
|
local function paintWifi(_, x, y)
|
|
local theme = ui.theme
|
|
local bars, offline = signalBars()
|
|
local empty = offline and theme.disabled or theme.muted
|
|
for i, h in ipairs(WIFI_HEIGHTS) do
|
|
screen.fillRect(x + (i - 1) * 4, y + 8 - h, 3, h, i <= bars and theme.color or empty)
|
|
end
|
|
if offline then
|
|
screen.drawLine(x, y, x + WIFI_W - 1, y + 8, theme.muted)
|
|
end
|
|
end
|
|
|
|
-- Painted rather than assembled from a button and a glyph: the chevron is four lines at
|
|
-- this size, and a custom node keeps the press feedback to one repaint of one box.
|
|
local function paintBack(id, x, y, w, h)
|
|
local theme = ui.theme
|
|
local pressed = tree.isPressed(id)
|
|
local face = pressed and theme.pressedFace or theme.face
|
|
local color = pressed and theme.pressedColor or theme.color
|
|
screen.roundRect(x, y, w, h, 4, theme.background, face)
|
|
local cx, cy = x + w // 2 + 1, y + h // 2
|
|
for offset = 0, 1 do -- two passes, because a one pixel chevron reads as a speck
|
|
screen.drawLine(cx + offset, cy - 4, cx + offset - 4, cy, color)
|
|
screen.drawLine(cx + offset - 4, cy, cx + offset, cy + 4, color)
|
|
end
|
|
end
|
|
|
|
local function backNode()
|
|
local side = BAR_H - 6
|
|
return ui.custom {
|
|
w = side,
|
|
h = side,
|
|
paint = paintBack,
|
|
press_style = false, -- paintBack draws its own, so nothing else should repaint
|
|
on_enter = function(id)
|
|
tree.setPressed(id, true)
|
|
ui.invalidate(id)
|
|
end,
|
|
on_exit = function(id)
|
|
tree.setPressed(id, false)
|
|
ui.invalidate(id)
|
|
end,
|
|
on_click = function()
|
|
nav.back()
|
|
end,
|
|
}
|
|
end
|
|
|
|
---@return NodeId
|
|
function M.node()
|
|
local theme = ui.theme
|
|
title = nav.getTitle()
|
|
local clockW = screen.getTextWidth(FONT, "00:00:00")
|
|
local memW = screen.getTextWidth(FONT, "0000kB")
|
|
local fontH = screen.getFontHeight(FONT)
|
|
|
|
ids = {}
|
|
ids.clock = ui.text(clock(), { w = clockW, h = fontH, font = FONT, color = theme.muted })
|
|
ids.memory = ui.text(memory(), { w = memW, h = fontH, font = FONT, color = theme.muted })
|
|
ids.wifi = ui.custom { w = WIFI_W, h = 9, paint = paintWifi }
|
|
shownBars, shownOffline = signalBars() -- what the first paint is about to show
|
|
|
|
-- Every slot is sized here rather than left to fill: a fill in a row takes the whole
|
|
-- width, and the three slots have to share it.
|
|
local statusW = math.max(clockW, memW + GAP + WIFI_W)
|
|
local backW = nav.canGoBack() and BAR_H - 6 or 0
|
|
local titleW = screen.getWidth() - 2 * PAD - 2 * GAP - backW - statusW
|
|
|
|
local status = ui.box {
|
|
w = statusW,
|
|
gap = 4,
|
|
align = "end",
|
|
ids.clock,
|
|
ui.box { row = true, w = memW + GAP + WIFI_W, gap = GAP, align = "center", ids.memory, ids.wifi },
|
|
}
|
|
|
|
-- A rule under the strip, so the bar reads as chrome rather than as the top of the app.
|
|
return ui.box {
|
|
w = "fill",
|
|
h = BAR_H,
|
|
background = theme.background,
|
|
ui.box {
|
|
w = "fill",
|
|
h = BAR_H - 1,
|
|
row = true,
|
|
pad = PAD,
|
|
gap = GAP,
|
|
align = "center",
|
|
backW > 0 and backNode() or ui.spacer { w = 0, h = 1 },
|
|
ui.box {
|
|
row = true,
|
|
w = titleW,
|
|
justify = "center",
|
|
align = "center",
|
|
ui.label(title, { font = TITLE_FONT, fit = titleW }),
|
|
},
|
|
status,
|
|
},
|
|
ui.box { w = "fill", h = 1, background = theme.muted },
|
|
}
|
|
end
|
|
|
|
---Repaints only the fields that changed. A new title needs a new box, so that one is a
|
|
---rebuild of the screen rather than a repaint of the bar.
|
|
function M.tick()
|
|
if hidden then
|
|
return
|
|
end
|
|
if nav.getTitle() ~= title then
|
|
ui.rebuild()
|
|
return
|
|
end
|
|
ui.setText(ids.clock, clock())
|
|
ui.setText(ids.memory, memory())
|
|
local bars, offline = signalBars()
|
|
if bars ~= shownBars or offline ~= shownOffline then
|
|
shownBars, shownOffline = bars, offline
|
|
ui.invalidate(ids.wifi)
|
|
end
|
|
end
|
|
|
|
---@return boolean
|
|
function M.isVisible()
|
|
return not hidden
|
|
end
|
|
|
|
---Hands the whole panel to the app, or takes the strip back.
|
|
---@param on boolean
|
|
function M.setFullscreen(on)
|
|
if hidden == on then
|
|
return
|
|
end
|
|
hidden = on
|
|
ui.rebuild()
|
|
end
|
|
|
|
return M
|