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.
picogame_font
Section titled “picogame_font”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)- composestextinto aPAL8bitmap and returns the tuple(bmp, w, h)(bitmap plus pixel size).fg/bgare wire colours frompg.rgb565(...).bg=Noneleaves the background transparent (palette index 0); an opaquebgmeans 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 thepalette(anarray('H')) and mutatepalette[1](the fg colour) for a live colour shimmer without rebuilding the bitmap - the CBitmapreads 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; returnsTrueif it did,Falseif skipped. Coerces non-strings viastr()..move(x, y)- repositions and forces a re-render at the new spot on the nextset/draw..draw(display, buffer)- repaints just the label’s rectangle viapg.render(a single present)..w,.h- pixel size of the last rendered text.
import picogame_font, terminaliohud = 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 changehud.draw(board.DISPLAY, bufA) # repaints just its rectGotchas: 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().
picogame_bitfont
Section titled “picogame_bitfont”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)- renderstextto aPAL8bitmap and returns(bitmap, w, h). The four shades map to:0 -> bg/transparent,1 -> outline,2 -> mid,3 -> fg. Colour defaults:fgwhite,outlineblack,midmid-grey; all arergb565wire colours. Supports\nfor multi-line. Passbgfor 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- both8, the per-glyph cell size.
import picogame_bitfont as bfbmp, w, h = bf.render_text(pg, "LIVES " + bf.HEART * 3) # white, outlined, transparentspr = pg.Sprite(bmp, x, y) # place anywherespr.scale = 2 # scale up for big textGotchas: 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.
picogame_ui
Section titled “picogame_ui”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 asfixed(camera-independent) layers, soscene.refresh()paints them every frame with no extra draw call. Use these over a live, scrolling scene.- Immediate widgets (
Labelinpicogame_font,TextBox,Menu,HudBar) draw viapg.renderand are for a static screen you render entirely yourself - over a live scene they fightscene.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 theSprite. Remembers its font/fg forset_text..set_text(sprite, text)- re-render a label sprite created by.label()..redraw()- repaint the strip (flat bg + visible sprites) in onepg.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 whenlinesare unchanged; when it does draw, the bg and every row go out in onepg.render(no blank-fill flash). Passforce=Trueafter the screen under it was wiped (e.g. a full-screenpg.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.CANCELon B, elseNone..draw(display, buffer, force=False)- repaints only what changed (nothing / the 2 affected rows on a cursor move / the whole box on scroll).force=Truerepaints 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 Nonebmenu.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 - nodraw()call..hide()- hide it..tick(btn)- same return contract asMenu; 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.CANCELon B, elseNone..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.
picogame_options
Section titled “picogame_options”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)-rowsis a list of dicts, each with akind:choice-{"key", "label", "kind": "choice", "choices": [...]}; cycles through the list. (A non-emptychoicesis required - it raisesValueErrorup front otherwise.)stepper-{"key", "label", "kind": "stepper", "value", "min", "max"}; also honours an optional"step"(default 1). Clamps tomin/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’skeyon A,ui.CANCELon B, elseNone..value(key)- read a row’s current value any time: achoicereturns the chosen string, astepperan int, atogglea bool;Noneif no such key.
import picogame_options as optmenu = 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() callGotchas: 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.