Skip to content

Text & UI

These modules turn strings and button presses into pixels: render text from any font, drop in a HUD label, pop a dialog box, or run a cursor menu. For bare signatures see the reference; for end-to-end games see the tutorials.

Renders a string with any fontio font (typically the bundled terminalio.FONT) into a picogame.Bitmap - no extra asset files, no reflash. Reach for it when you want crisp text from the font you already have and don’t mind a HUD background box.

  • render_text(pg, font, text, fg, bg=None) - composes text into a PAL8 bitmap and returns the tuple (bmp, w, h) (bitmap plus pixel size). fg/bg are wire colours from pg.rgb565(...). bg=None leaves the background transparent (palette index 0); an opaque bg means a redraw fully overwrites the old text, so HUD updates need no separate clear.
  • render_text_pal(pg, font, text, fg, bg=None) - same as above but returns (bmp, w, h, palette). Keep the palette (an array('H')) and mutate palette[1] (the fg colour) for a live colour shimmer without rebuilding the bitmap - the C Bitmap reads the same buffer.
  • Label(pg, font, x, y, fg, bg) - a positioned label drawn immediately (good for a HUD over a screen you render yourself).
    • .set(text) - re-renders only if the text changed; returns True if it did, False if skipped. Coerces non-strings via str().
    • .move(x, y) - repositions and forces a re-render at the new spot on the next set/draw.
    • .draw(display, buffer) - repaints just the label’s rectangle via pg.render (a single present).
    • .w, .h - pixel size of the last rendered text.
import picogame_font, terminalio
hud = picogame_font.Label(pg, terminalio.FONT, 4, 4,
pg.rgb565(255, 255, 255), BG)
# each frame, after you draw the screen:
hud.set("SCORE %06d" % score) # rebuilds only on change
hud.draw(board.DISPLAY, bufA) # repaints just its rect

Gotchas: Bitmap.palette is not a Python attribute on-device, so for a shimmer you must mutate the palette array from render_text_pal, not bmp.palette. Over a live, scrolling scene use picogame_ui.SceneLabel instead - an immediate Label fights scene.refresh().

A self-contained 8x8, 2-bit (4-shade, anti-aliased/outlined) bitmap font with game glyphs (codes 0..31 are arrows/hearts/star/note/pipes; 32+ is ASCII). Its built-in dark outline gives contrast over gameplay without a HUD box, so index 0 is transparent and text reads cleanly over the world. Reach for it for floating combat text, “+10”, hearts, or any label you want without a background panel.

  • render_text(pg, text, fg=None, outline=None, mid=None, bg=None) - renders text to a PAL8 bitmap and returns (bitmap, w, h). The four shades map to: 0 -> bg/transparent, 1 -> outline, 2 -> mid, 3 -> fg. Colour defaults: fg white, outline black, mid mid-grey; all are rgb565 wire colours. Supports \n for multi-line. Pass bg for an opaque background (else index 0 is transparent).
  • Symbol constants (1-char strings you concatenate into text): ARROW_U, ARROW_D, ARROW_R, ARROW_L, BOXX, STAR, HEART, BALL, NOTE.
  • GLYPH_W, GLYPH_H - both 8, the per-glyph cell size.
import picogame_bitfont as bf
bmp, w, h = bf.render_text(pg, "LIVES " + bf.HEART * 3) # white, outlined, transparent
spr = pg.Sprite(bmp, x, y) # place anywhere
spr.scale = 2 # scale up for big text

Gotchas: there’s no Label/widget class here, just render_text -> you manage the Sprite yourself. This font is an ecosystem layer (art derived from circuitpython-stage, MIT) rather than the pristine bundle; if RAM is tight, fall back to picogame_font + terminalio. See memory for the RAM trade-off.

The UI scaffolding: a camera-independent HUD label, multi-line text boxes, and cursor menus. The key split is scene-layer vs. immediate:

  • Scene* widgets (SceneLabel, SceneBox, SceneMenu) live in the scene as fixed (camera-independent) layers, so scene.refresh() paints them every frame with no extra draw call. Use these over a live, scrolling scene.
  • Immediate widgets (Label in picogame_font, TextBox, Menu, HudBar) draw via pg.render and are for a static screen you render entirely yourself - over a live scene they fight scene.refresh() and flicker.

tick()-based widgets return: a chosen index/cell on A (confirm), ui.CANCEL (-2) on B (back), or None while navigating. See scene-format for the fixed layer and hardware for the buttons.

SceneLabel(scene, pg, font, x, y, fg, bg) - one line of text pinned over a scrolling world.

  • .set(text) - re-renders only on change (swaps the sprite’s bitmap; dirty-rect handles old/new bounds). A blank/empty string hides the sprite, leaving no leftover bg patch.

SceneBox(scene, pg, font, x, y, w, h, fg, bg, nlines=3, key=None, border=None) - a multi-line dialog/status panel (a Canvas + SceneLabel rows) over a live scene, in one flicker-free present. key is the transparent-when-hidden colour (defaults to magenta); pass border for a 3D frame.

  • .show(lines) - fill the panel and set text, then reveal. Call once, not per frame.
  • .hide() - make the panel fully transparent and blank the rows.
  • .set_line(i, text) - update one row in place (no Canvas/border redraw).

HudBar(pg, display, buffer, x, y, w, h, bg) - a HUD strip drawn outside the scene, in a region the scene reserves with Scene(..., top=/bottom=). The scene never paints there, so it costs nothing per frame; you call redraw() only on HUD changes. buffer is any scratch strip buffer (e.g. the scene’s bufA); bg is a flat colour.

  • .add(sprite) - store a sprite (hearts, gauges) in the bar; returns it.
  • .label(font, x, y, fg, text=" ") - create a text sprite stored in the bar; returns the Sprite. Remembers its font/fg for set_text.
  • .set_text(sprite, text) - re-render a label sprite created by .label().
  • .redraw() - repaint the strip (flat bg + visible sprites) in one pg.render.
hud = ui.HudBar(pg, board.DISPLAY, bufA, 0, 0, W, BAR, pg.rgb565(10, 12, 24))
hud_l = hud.label(terminalio.FONT, 4, 3, INK, "SCORE 0 LIVES 3")
hud.redraw()
# later, only when it changes:
hud.set_text(hud_l, "SCORE %d LIVES %d" % (score, lives))
hud.redraw()

TextBox(pg, font, x, y, w, h, fg, bg, maxlines=6) - a screen-space multi-line box (filled rect + text rows) for static dialog/battle/menu screens.

  • .draw(display, buffer, lines, force=False) - skips the repaint when lines are unchanged; when it does draw, the bg and every row go out in one pg.render (no blank-fill flash). Pass force=True after the screen under it was wiped (e.g. a full-screen pg.render).
  • .draw_line(display, buffer, i, text) - repaint a single row in place, atomically.

Menu(pg, font, x, y, items, fg, bg, *, title=None, rows=None, width=None, paged=True) - a cursor menu over a TextBox (immediate; for static screens). UP/DOWN navigate with auto-repeat; draw() renders a > marker. rows=None shows all items (no scroll); set it to scroll a long list. paged=True (default) jumps a whole page at the edges (cheap; full repaint only on a page boundary) - much snappier than the line-scroll paged=False on near-full-screen lists, since this hardware can’t hardware-scroll the panel. The keyword args are keyword-only (after *).

  • .tick(btn) - returns the chosen index on A, ui.CANCEL on B, else None.
  • .draw(display, buffer, force=False) - repaints only what changed (nothing / the 2 affected rows on a cursor move / the whole box on scroll). force=True repaints unconditionally after a wipe.
bmenu = ui.Menu(pg, terminalio.FONT, 8, H - 72,
["ATTACK", "MAGIC", "HEAL", "FLEE"], WHITE, NAVY)
# each frame:
act = bmenu.tick(btn) # index on A, ui.CANCEL on B, else None
bmenu.draw(board.DISPLAY, bufA)

SceneMenu(scene, pg, font, x, y, items, fg, bg, title=None, rows=None, width=None, border=None, paged=True) - the same menu but built on SceneBox, for use over a live scene (battle actions, an in-game popup). Same navigation/paging as Menu.

  • .show(sel=0) - reveal it (resets the cursor). The scene paints it from then on - no draw() call.
  • .hide() - hide it.
  • .tick(btn) - same return contract as Menu; repaints only the rows that changed.

GridCursor(cols, rows, tx=0, ty=0, wrap=False) - logic-only 2D cursor for a battlefield, tile inventory, or match-3. It owns movement (D-pad auto-repeat) and confirm/cancel; you draw the grid and a highlight at (cursor.tx, cursor.ty). wrap=True wraps at edges, else it clamps.

  • .tick(btn) - returns the (tx, ty) tuple on A, ui.CANCEL on B, else None.
  • .tx, .ty - current cell.
  • .index (property) - ty * cols + tx, handy for indexing a flat list.
cur = ui.GridCursor(N, N) # N x N board
# each frame:
pick = cur.tick(btn) # (tx, ty) on A, ui.CANCEL on B, else None
# you draw the highlight yourself at (cur.tx, cur.ty)

Gotchas: the biggest trap is mixing the two families - an immediate Menu/TextBox over a live scene gets erased by the Display pushing strips over it (it “vanishes”); use the Scene* twin there. The default menu width heuristic (~11 px/char) over-sizes for a narrow font and can run off-screen - pass width= for long labels or full-screen lists. SceneBox.show() is call-once, not per-frame.

A settings/value menu built on picogame_ui.SceneBox, for the settings / shop / recruit pattern. Reach for it when a plain ui.Menu (pick-an-index) isn’t enough because each row carries an adjustable value: a difficulty choice, a volume stepper, a sound toggle, plus a plain action like “Start”. It’s a scene-layer widget, so use it over a live scene; value edits show immediately. It’s deliberately provisional - kept out of the frozen ui core so it can evolve.

  • OptionsMenu(scene, pg, font, x, y, w, rows, fg, bg, title=None, border=None) - rows is a list of dicts, each with a kind:
    • choice - {"key", "label", "kind": "choice", "choices": [...]}; cycles through the list. (A non-empty choices is required - it raises ValueError up front otherwise.)
    • stepper - {"key", "label", "kind": "stepper", "value", "min", "max"}; also honours an optional "step" (default 1). Clamps to min/max.
    • toggle - {"key", "label", "kind": "toggle", "value": True/False}.
    • action - {"key", "label", "kind": "action"}; no value, just returns its key on A.
  • .show(sel=0) - reveal and render (call once); scene.refresh() paints it after.
  • .hide() - hide the panel.
  • .tick(btn) - UP/DOWN move the cursor; LEFT/RIGHT change the selected row’s value live (steppers/choices auto-repeat while held; a toggle flips only on a fresh press, so it can’t oscillate). Returns the selected row’s key on A, ui.CANCEL on B, else None.
  • .value(key) - read a row’s current value any time: a choice returns the chosen string, a stepper an int, a toggle a bool; None if no such key.
import picogame_options as opt
menu = opt.OptionsMenu(scene, pg, font, 40, 40, 240, [
{"key": "diff", "label": "Difficulty", "kind": "choice", "choices": ["Easy", "Normal", "Hard"]},
{"key": "vol", "label": "Volume", "kind": "stepper", "value": 7, "min": 0, "max": 10},
{"key": "snd", "label": "Sound", "kind": "toggle", "value": True},
{"key": "done", "label": "Start", "kind": "action"},
], WHITE, NAVY, title="OPTIONS")
menu.show()
while True:
btn.poll()
k = menu.tick(btn)
if k == "done":
diff = menu.value("diff") # read live values on the action row
elif k == opt.CANCEL:
menu.hide()
scene.refresh() # paints the menu - no draw() call

Gotchas: it imports CANCEL from picogame_ui, so opt.CANCEL and ui.CANCEL are the same -2. Being a SceneBox widget, it needs a live scene.refresh() under it - it won’t paint on a static screen drawn purely with pg.render.