Files
slate32/sdcard/lib/statusbar.lua
T
evan b5146b8642 refactor(lua): give the status bar its own dirty tracking and split settings out of sys
The bar repainted itself whole every second. It now compares each field against what
it last painted, adds seconds and a memory percentage, and keys the cache on
gui.getRotation() and ui.themeName so rotation and theme changes still repaint it.
Invalidation lives entirely in Lua; the firmware's push flag and gfx/statusbar.h are gone.

Bindings follow getName/setName/isName, persisted preferences move from sys to a settings
table, and gui.setRotation takes degrees like settings does. A bar that dies mid-run now
keeps its rows reserved rather than silently resizing the running app.
2026-08-02 10:49:01 -04:00

120 lines
4.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 = 22, interval = 1000}
local BAR_H = M.height
local SIGNAL_W = 11
local GAP = 8
-- 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 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 memPercent()
local free, total = sys.getMemory()
if not total or total <= 0 then return 0 end
return (total - free) * 100 // total
end
-- Bars rather than glyphs: there is no icon font, and signal strength is a scale.
local function drawSignal(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 home.
-- Drawn as a button rather than left as a hidden hit region: a target nobody can see is
-- one nobody finds.
local function drawHome(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(home)
local theme = ui.theme
gui.setTextSize(1) -- panel state: the app may have left it scaled up
local w = gui.getWidth()
local textY = math.floor((BAR_H - 1 - gui.getFontHeight()) / 2)
-- Slots sized for their widest value, so a shorter one never shifts its neighbours.
local clockW, memW = gui.getTextWidth("00:00:00"), gui.getTextWidth("100%")
local clockX = w - PAD - clockW
local signalX = clockX - GAP - SIGNAL_W
local memX = signalX - GAP - memW
-- Rotation moves every slot and the theme recolors them; a new app gets a fresh Lua
-- state, so an empty cache already means "repaint everything".
local key = gui.getRotation() .. "|" .. ui.themeName
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
local nameX = PAD
if home then
drawHome(theme)
nameX = BAR_H + PAD
end
gui.drawText(sys.getAppName(), nameX, textY, theme.fg, theme.bg)
end
local mem = memPercent()
if mem ~= shown.mem then
shown.mem = mem
local text = mem .. "%"
gui.fillRect(memX, 0, memW, BAR_H - 1, theme.bg)
gui.drawText(text, memX + memW - gui.getTextWidth(text), textY, theme.muted, theme.bg)
end
local bars = signalBars()
if bars ~= shown.bars then
shown.bars = bars
gui.fillRect(signalX, 0, SIGNAL_W, BAR_H - 1, theme.bg)
drawSignal(signalX, textY, bars, theme.fg, theme.muted)
end
local time = clock()
if time ~= shown.time then
shown.time = time
gui.fillRect(clockX, 0, clockW, BAR_H - 1, theme.bg)
gui.drawText(time, clockX, textY, theme.muted, theme.bg)
end
end
return M