Files
slate32/sdcard/.lua/lib/ui/header.lua
T
2026-08-07 20:12:54 -04:00

188 lines
5.6 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.
--
-- Navigation lives in the footer; this strip is status only.
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 stacked rows still fit.
local TITLE_FONT = screen.FONT_UI
-- Just the right-hand stack plus padding and the rule: the clock, a 4px gap, the icon row.
local BAR_H = 32
local PAD, GAP = 5, 8
local WIFI_W, WIFI_HEIGHTS = 11, { 3, 5, 8 }
local BT_W, ICON_H = 7, 9
local M = { height = BAR_H }
local ids = {}
local title, shownBars, shownOffline, shownBt
local hidden, built = false, 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 luaMemory()
return math.floor(collectgarbage "count" + 0.5) .. "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
-- The Bluetooth rune, drawn rather than glyphed: five strokes at this size, and the
-- crossed-out off state matches how the WiFi icon reports the radio being down.
local function paintBt(_, x, y)
local theme = ui.theme
local on = ble.isInitialized()
local color = on and theme.color or theme.disabled
local function line(x1, y1, x2, y2)
screen.drawLine(x + x1, y + y1, x + x2, y + y2, color)
end
line(3, 0, 3, 8)
line(3, 0, 6, 2)
line(6, 2, 0, 6)
line(3, 8, 6, 6)
line(6, 6, 0, 2)
if not on then
screen.drawLine(x, y, x + BT_W - 1, y + 8, theme.muted)
end
end
---@return NodeId
function M.node()
-- Reached only through ui.mount()'s rebuild, so a tree now exists and setFullscreen()
-- may repaint.
built = true
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.luaMemory = ui.text(luaMemory(), { w = memW, h = fontH, font = FONT, color = theme.muted })
ids.wifi = ui.custom { w = WIFI_W, h = ICON_H, paint = paintWifi }
ids.bt = ui.custom { w = BT_W, h = ICON_H, paint = paintBt }
shownBars, shownOffline = signalBars() -- what the first paint is about to show
shownBt = ble.isInitialized()
-- 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 iconsW = BT_W + GAP + WIFI_W
local statusW = math.max(clockW, iconsW)
local titleW = screen.getWidth() - 2 * PAD - 2 * GAP - memW - statusW
local memoryStack = ui.box { w = memW, gap = 4, ids.memory, ids.luaMemory }
local status = ui.box {
w = statusW,
gap = 4,
align = "end",
ids.clock,
ui.box { row = true, w = iconsW, gap = GAP, align = "center", ids.bt, 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",
memoryStack,
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())
ui.setText(ids.luaMemory, luaMemory())
local bars, offline = signalBars()
if bars ~= shownBars or offline ~= shownOffline then
shownBars, shownOffline = bars, offline
ui.invalidate(ids.wifi)
end
if ble.isInitialized() ~= shownBt then
shownBt = not shownBt
ui.invalidate(ids.bt)
end
end
---@return boolean
function M.isVisible()
return not hidden
end
---Hands the whole panel to the app, or takes the chrome back. The footer follows, because
---main.lua builds it only while this strip is visible.
---@param on boolean
function M.setFullscreen(on)
if hidden == on then
return
end
hidden = on
-- Rebuild only once a tree exists: an app init() runs before ui.mount(), and its
-- fullscreen request is picked up by that mount instead.
if built then
ui.rebuild()
end
end
return M