host.g records drawing ops inside a
draw node’s on_draw painter. Outside a
painter the ops are inert; inside one, up to 256 ops are recorded per
paint and replayed onto a crisp, DPI-aware canvas.
-- displays.lua
{ type = "draw", w = 64, h = 64, bind = "reading", on_draw = M.draw_gauge }
-- logic.lua
function M.draw_gauge(host, d, w, h) -- d = the full payload; w,h = design px
local g = host.g
g.circle(w/2, h/2, 28, { fill = "raised", stroke = "rim" })
g.arc(w/2, h/2, 24, -2.36, -2.36 + 4.72 * d.reading.frac,
{ stroke = "accent", width = 4, cap = "round" })
g.text(d.reading.label, w/2 - 10, h - 6, { size = 10, fill = "half" })
end
The painter runs only when the bound field changes — never per frame. That’s the whole idle-cost story: an unchanging gauge costs zero.
The ops
Every op takes coordinates in design px, with an optional style table last.
g.line(x1, y1, x2, y2, style?)
g.line(32, 32, 32 + 14 * math.sin(a), 32 - 14 * math.cos(a),
{ stroke = "primary", width = 3, cap = "round" })
g.rect(x, y, w, h, style?)
g.rect(4, 20, 56, 8, { fill = "well" })
g.circle(cx, cy, r, style?)
g.circle(32, 32, 30, { fill = "raised", stroke = "rim", width = 1 })
g.arc(cx, cy, r, a0, a1, style?)
Angles in radians, standard canvas orientation (0 = 3 o’clock, increasing clockwise).
g.arc(32, 32, 24, 0, math.pi * 1.5, { stroke = "@brand", width = 4 })
g.path(points, style?)
A polyline from an array of {x, y} points.
g.path({ {4, 28}, {18, 12}, {32, 20}, {60, 6} }, { stroke = "live", width = 2 })
g.text(str, x, y, style?)
Bold Inter, baseline at y.
g.text("42%", 22, 38, { size = 13, fill = "primary" })
The style table
| Key | Applies to | Values |
|---|---|---|
fill | closed shapes, text | any color: roles, @palette, hex |
stroke | outlines, lines, arcs, paths | same |
width | strokes | px, default 1 |
cap | line ends | butt (default) round square |
size | text | px, default 13 |
Role and palette colors resolve against the active theme at paint
time — a draw gauge recolors with a theme switch exactly like every
declarative node.