9a88d03d23
Cache font heights, release FreeType glyph state between apps, and catch renderer allocation failures before they cross Lua and abort the device. Individual low-memory draws degrade to allocation-free Font 1. Show disconnected WiFi as three faded zero-signal bars without expanding the icon footprint.
140 lines
5.2 KiB
Lua
140 lines
5.2 KiB
Lua
-- The top bar, painted by the firmware once a second in every app. It draws in panel
|
|
-- coordinates (the firmware drops the app viewport around the call), so gui.getWidth()
|
|
-- here is the whole screen.
|
|
--
|
|
-- Invalidation is entirely local: draw() compares what it is about to paint against what
|
|
-- it last painted, so the firmware never has to tell the bar that anything changed.
|
|
|
|
local ui = require("ui")
|
|
|
|
local PAD = 6
|
|
|
|
-- The firmware reads both: `height` is the strip it keeps apps out of, `interval` is how
|
|
-- often it calls draw().
|
|
local M = {height = 44, interval = 1000}
|
|
local BAR_H = M.height
|
|
local WIFI_W = 11
|
|
local GAP = 8
|
|
local TITLE_FONT_SIZE = 14
|
|
local INFO_FONT_SIZE = 10
|
|
|
|
-- What is currently on the panel. Every field repaints only when its value changes, so
|
|
-- the once-a-second tick costs one clock rectangle instead of a whole bar.
|
|
local shown = {}
|
|
local geometry = {}
|
|
|
|
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 end
|
|
return status.rssi >= -60 and 3 or status.rssi >= -75 and 2 or 1
|
|
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 drawWifi(x, y, bars, color, muted)
|
|
for i = 1, 3 do
|
|
local h = i * 3
|
|
gui.fillRect(x + (i - 1) * 4, y + 9 - h, 3, h, i <= bars and color or muted)
|
|
end
|
|
|
|
end
|
|
|
|
-- Fills the leading square of the bar, which is the rect the firmware treats as back.
|
|
-- Drawn as a button rather than left as a hidden hit region: a target nobody can see is
|
|
-- one nobody finds.
|
|
local function drawBack(theme)
|
|
local inset = 3
|
|
local side = BAR_H - 1 - inset * 2
|
|
gui.roundRect(inset, inset, side, side, 4, theme.bg, theme.face[1], theme.face[2], theme.muted)
|
|
local x, y = inset + side // 2 + 1, inset + side // 2
|
|
for offset = 0, 1 do -- two passes, because a one pixel chevron reads as a speck
|
|
gui.drawLine(x + offset, y - 4, x + offset - 4, y, theme.fg)
|
|
gui.drawLine(x + offset - 4, y, x + offset, y + 4, theme.fg)
|
|
end
|
|
end
|
|
|
|
-- Leaving fullscreen is the one change the bar cannot observe: no draw happens while an
|
|
-- app owns the panel, so the state it last painted still matches the state it would paint
|
|
-- now, while the pixels are gone. Apps toggle fullscreen through here for that reason.
|
|
function M.setFullscreen(on)
|
|
gui.setFullscreen(on)
|
|
if not on then shown = {} end
|
|
end
|
|
|
|
function M.draw(hasBack)
|
|
local theme = ui.theme
|
|
local rotation, font = gui.getRotation(), gui.getFont()
|
|
local frameKey = rotation .. "|" .. font
|
|
if geometry.key ~= frameKey then
|
|
local w = gui.getWidth()
|
|
local titleH = gui.getFontHeight(TITLE_FONT_SIZE)
|
|
local infoH = gui.getFontHeight(INFO_FONT_SIZE)
|
|
local clockW = gui.getTextWidth("00:00:00", INFO_FONT_SIZE)
|
|
local memW = gui.getTextWidth("000kB", INFO_FONT_SIZE)
|
|
local clockX = w - PAD - clockW
|
|
local signalX = w - PAD - WIFI_W
|
|
local memX = signalX - GAP - memW
|
|
geometry = {
|
|
key = frameKey, w = w, titleY = (BAR_H - 1 - titleH) // 2,
|
|
topY = PAD, bottomY = BAR_H - 1 - PAD - infoH, infoH = infoH,
|
|
clockW = clockW, memW = memW, clockX = clockX, signalX = signalX,
|
|
memX = memX, rightX = math.min(clockX, memX),
|
|
}
|
|
end
|
|
local w, titleY, topY, bottomY = geometry.w, geometry.titleY, geometry.topY, geometry.bottomY
|
|
local infoH, clockW, memW = geometry.infoH, geometry.clockW, geometry.memW
|
|
local clockX, signalX, memX, rightX = geometry.clockX, geometry.signalX, geometry.memX, geometry.rightX
|
|
|
|
-- Rotation moves every slot, the theme recolors them, and an app may rename itself at
|
|
-- any time. A new app gets a fresh Lua state, so an empty cache already means "repaint
|
|
-- everything".
|
|
local name = sys.getAppName()
|
|
local key = frameKey .. "|" .. ui.themeName .. "|" .. name
|
|
if key ~= shown.key then
|
|
shown = {key = key}
|
|
gui.fillRect(0, 0, w, BAR_H, theme.bg)
|
|
gui.fillRect(0, BAR_H - 1, w, 1, theme.muted) -- a rule, so the bar reads as chrome
|
|
if hasBack then drawBack(theme) end
|
|
|
|
local titleLeft = hasBack and BAR_H + PAD or PAD
|
|
local titleRight = rightX - GAP
|
|
local available = titleRight - titleLeft
|
|
while #name > 1 and gui.getTextWidth(name, TITLE_FONT_SIZE) > available do name = name:sub(1, -2) end
|
|
local nameW = gui.getTextWidth(name, TITLE_FONT_SIZE)
|
|
local nameX = (w - nameW) // 2
|
|
nameX = math.max(titleLeft, math.min(nameX, titleRight - nameW))
|
|
gui.drawText(name, nameX, titleY, theme.fg, theme.bg, TITLE_FONT_SIZE)
|
|
end
|
|
|
|
local mem = memory()
|
|
if mem ~= shown.mem then
|
|
shown.mem = mem
|
|
gui.fillRect(memX, bottomY, memW, infoH, theme.bg)
|
|
gui.drawText(mem, memX, bottomY, theme.muted, theme.bg, INFO_FONT_SIZE)
|
|
end
|
|
|
|
local bars = signalBars()
|
|
if bars ~= shown.bars then
|
|
shown.bars = bars
|
|
gui.fillRect(signalX, bottomY, WIFI_W, math.max(infoH + 1, 11), theme.bg)
|
|
drawWifi(signalX, bottomY, bars, theme.fg, theme.muted)
|
|
end
|
|
|
|
local time = clock()
|
|
if time ~= shown.time then
|
|
shown.time = time
|
|
gui.fillRect(clockX, topY, clockW, infoH, theme.bg)
|
|
gui.drawText(time, clockX, topY, theme.muted, theme.bg, INFO_FONT_SIZE)
|
|
end
|
|
end
|
|
|
|
return M
|