-- 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 WIFI_HEIGHTS = {3, 5, 8} local GAP = 8 local ROW_INSET = 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 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, h in ipairs(WIFI_HEIGHTS) do gui.fillRect(x + (i - 1) * 4, y + 8 - 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 gui.setTextSize(1) local frameKey = gui.getRotation() if geometry.key ~= frameKey then local w = gui.getWidth() local fontH = gui.getFontHeight() local clockW = gui.getTextWidth("00:00:00") local memW = gui.getTextWidth("000kB") 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 - fontH) // 2, topY = ROW_INSET, bottomY = BAR_H - 1 - ROW_INSET - fontH, fontH = fontH, 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 fontH, clockW, memW = geometry.fontH, 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) > available do name = name:sub(1, -2) end local nameW = gui.getTextWidth(name) local nameX = (w - nameW) // 2 nameX = math.max(titleLeft, math.min(nameX, titleRight - nameW)) gui.drawText(name, nameX, titleY, theme.fg, theme.bg) end local mem = memory() if mem ~= shown.mem then shown.mem = mem gui.fillRect(memX, bottomY, memW, fontH, theme.bg) gui.drawText(mem, memX, bottomY, theme.muted, theme.bg) end local bars = signalBars() if bars ~= shown.bars then shown.bars = bars gui.fillRect(signalX, bottomY, WIFI_W, math.max(fontH + 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, fontH, theme.bg) gui.drawText(time, clockX, topY, theme.muted, theme.bg) end end return M