-- 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