Hints and Statusbar
The button-hint band at the top of the screen, and the statusbar it lives in
The defaults for each role are in Screen defaults per app type; the BTN4/BTN5 hold gestures that reveal and dismiss the band are described under Input Handling.
| HintsActionsTotal | 0 |
| uint8_t btn) { return (HintActionType)(btn * | 2u |
The press / hold cell of an on_input() button id (BTN1..BTN5).
| const char* | label | REFERENCED, not copied: a literal or the model's storage |
| const gfx_img_t* | icon_on | non-NULL makes the cell a two-position switch (icons only) |
| uint8_t | flags | HINT_ACTIVE | HINT_LOCKED | HINT_ON | HINT_WIDE | HINT_PARAM | HINT_SPAN |
Set the tapp's button hints: the whole band, in one call
| const tapp_hint_cell_t cells[HintsActionsTotal] | ||
| cells | HintsActionsTotal cells indexed by HintActionType; NULL = an empty band |
Every call states every cell - labels, icons, switches, lit and locked - so call it again whenever the app state changes (from on_input, right after the change). There is no order between calls and nothing carries over from the previous band; the firmware keeps the lit/lock animations running across calls, so re-stating an unchanged band causes no flicker. It does NOT change visibility - see ui_hints_show().
The hints render at the TOP of the screen, where the buttons are, with the system styling and slide-in: press cells on the top row, holds beneath. Draw from TAPP_CONTENT_Y down; a tapp must never draw its own hint bar.
Which cells belong to the tapp depends on its role:
- utility: all ten. In WINDOW mode the cells left empty keep the menu's labels, drawn as unavailable - those buttons reach the tapp's window, not the menu.
- standalone: all but HintHold4/HintHold5, the firmware's menu/close pill. The BTN4/BTN5 PRESS cells are the narrow 50px columns: keep those labels to ~5 characters.
- source: BTN1..BTN3; both BTN4/BTN5 rows are the firmware's prev/next and back/exit. Content placed on a firmware cell is ignored.
The row grid
Every hint band uses one grid, so nothing changes width or position when one replaces another:
| columns | width | notes |
|---|---|---|
| BTN1-BTN3 | 100px | the tapp's wide label columns |
| BTN4/BTN5 | 50px | narrow; pinned to the right edge |
3 + 3*100 + 2*50 = 403 is the row's nominal end: each cell draws its body 5px narrower than its column, so the last lit pixel is 397 and the trailing margin mirrors the 3px lead.
The BTN4/BTN5 pair is RIGHT-PINNED, so those two cells stay under their buttons however wide the columns before them are. The widths need not add up exactly - but columns that fall short leave a visible gap before the pinned pair, which is why the grid should be followed.
50px is a floor, not a preference: it is the narrowest width the switch pill renders correctly at.
Each session starts with an empty band (plus the role's firmware cells): nothing carries over from the previously run app. The firmware owns the hint objects - only the tapp's cell array crosses the ABI.
const tapp_hint_cell_t c[HintsActionsTotal] = {
[HintPress1] = {"play", gfx_icon(GfxIconPlay), NULL, m->playing ? HINT_ACTIVE : 0},
[HintHold1] = {"loop"},
[HintPress3] = {NULL, gfx_icon(GfxIconJack), gfx_icon(GfxIconMic), m->mic ? HINT_ON : 0},
};
ui_hints_set(c);Flash a hint cell the way the device flashes its own - solid plate the instant it is called, 2px depress, fading out on its own after ~240ms. Use it for MOMENTARY actions (a shuffle, a retrigger) that have no on/off state.
| HintActionType | action |
This is the mechanism for press feedback, NOT HINT_ACTIVE: the device emits the PUSH and RELEASE events together at the release edge, so there is no finger-down window for an on/off pair to cover, and the ACTIVE fill ramps over ~4 UI ticks rather than snapping. HINT_ACTIVE is for SUSTAINED state (a mode that stays on); this is for a tap.
Self-timed: call it once from on_input; no follow-up call is needed. Repeat taps re-flash. A LOCKED cell is skipped, matching the rest of the device.
Put the app's own GRAPHICS in the system statusbar (left side)
| tapp_statusbar_gfx_cb | cb | Draw callback, or NULL to remove the item |
| uint16_t | width | Cell width in pixels |
| void* | user | Context handed back to cb |
A tapp's transport icon or meter sits in the bar rather than eating a strip of the app panel.
Draw ONLY inside the item's width x cell. The bar erases each item's own box right before calling it and nothing else, so anything outside would never be cleared; the call is clipped to the cell, which means overdraw is silently cut off rather than smeared. Nothing sets a font for status items, so call gfx_set_font() before drawing text.TAPP_STATUSBAR_H
Sizing: ~176 px is always safe. The bar is 400 px wide with the USB indicator parked at x 332 and battery beyond it, so ~330 is the practical ceiling. Call ONCE, in init(); registering again updates it.
This is the surface for a whole app strip. It is TAPP_STATUSBAR_H tall - deeper than the bar's own 24 px - so it reaches a little into the panel, and it costs the app nothing: the firmware stops drawing status items the moment the hints go up, which is the one thing an app cannot detect for itself. Transport, meters and readouts drawn here rather than on the app's own canvas move out of the hints' way automatically.
The callback runs on the UI task during the statusbar's redraw - the same context as the app's redraw(), not the audio thread. Keep it cheap and read a snapshot, do not block. The item is owner-stamped: it hides when the app loses focus and is dropped when it exits, so deinit() needs no cleanup.
Draw a hint cell's content from a tapp callback (procedural graphics in the cell)
| HintActionType | action | Which cell (HintPress1..HintHold5) |
| tapp_statusbar_gfx_cb | cb | Draw callback (same shape as the statusbar one), or NULL to restore the label |
| void* | user | Context handed back to cb |
The firmware still draws the standard framed cell body (and the press animation); the callback draws the content on top and is CLIPPED to the cell interior (w x 22 px at the given x/y - overdraw is cut off, not smeared). Runs on the UI task like redraw(); set the font before drawing text. Stays across ui_hints_set() calls until NULL is passed, and makes its cell exist; the cell's label/icon are not drawn while a callback is installed. Dropped automatically when the app exits. Works on every cell EXCEPT HintHold4/HintHold5, the system's gesture cells, where it is refused.
Show or hide the tapp's hints (ui_hints_set())
| bool | stat | true to show hints, false to hide |
The role sets the default (Screen defaults per app type); this overrides it. THE CALL IS REMEMBERED: the firmware records it against the tapp's image and replays it whenever it re-derives the screen's chrome - a menu round-trip, a refocus, a co-resident image taking and handing back the screen. So "always visible" is one call in init(), with nothing to re-assert from on_resume() or tick(), and a BTN5 hold will not take down a band the tapp requested (see Input Handling). The same applies to ui_statusbar_show().
Two idioms:
- always visible: call once with true in init(), then re-call ui_hints_set() whenever the app state changes.
- reveal on one of the tapp's OWN buttons (BTN1..BTN3): true on KEY_STATE_HOLD, false on KEY_STATE_RELEASED, from on_input.
SOURCE tapps: init() runs OFF-SCREEN (the picker loads the tapp and backgrounds it immediately), so state the chrome from the first tick() or on_resume() instead; otherwise the call tears down the chrome of whatever is actually on screen. A STANDALONE tapp is already on screen by init() and can state it there.
Install one VOLUME cell in the hints - the same cells the device's own hold-PLAY volume hints are made of (icon + name + live VU + cursor, active cell enlarged with the same animation).
| uint8_t | slot | Physical button, 1..5 - slots 4 and 5 included. The volume cells are the tapp's OWN array, not the label hints, so the system's BTN4/BTN5 columns do not overwrite it and cells installed there DO render. (The BTN4/BTN5 hold GESTURES are unaffected - they stay the firmware's, whatever the cell draws.) |
| uint8_t | row | 0 = press row, 1 = hold row |
| const char* | name | Cell label - REFERENCED, must outlive the app state showing it |
| params_t* | param | The params_t this cell displays and the firmware edits while it is selected (encoder, dB taper for ParamValDBType). Its callback fires with ctx NULL - reach the model from the param, not from ctx. NULL clears the cell - unless `name` is non-empty, which installs a plain LABEL cell instead (e.g. "<" / ">" nav arrows on the buttons that switch the app's selection). Use ParamValDBType for gain-style 0..2.0 params - the linear draw branch assumes 0..1. |
| uint8_t | icon_id | Icon AND VU source: HintVolOutput (volume icon, output bus), HintVolInput (jack/mic icon following the live source, input bus), HintVolFx, HintVolFeedback (sound-on-sound) |
Installing volume cells REPLACES the tapp's ui_hints_set() band and vice versa - hold-to-reveal apps install the cells on KEY_STATE_HOLD and re-state their band on KEY_STATE_RELEASED (see examples/recorder.c). Each call clears the selection, so call ui_hints_volume_select() after the cells are in. The firmware owns the cell objects; only the name string and the params_t cross the ABI, and both are dropped automatically when the app exits.
The band is FIRMWARE-DRIVEN, like the device's own: while it is up, BTN4/BTN5 presses step the selection and the encoder edits the selected level - the app never receives those events. A running source's (radio, slicer, a source tapp) level is appended after the app's own, exactly as the tape decks list it.
GEOMETRY: every volume cell is 60px wide and grows to 120px while selected - one pair, no per-icon special case. Four cells plus the grid's 50px BTN4/BTN5 pair is exactly the row (3 + 3*60 + 120 + 2*50 = 403). Install FEWER than four and the row simply starts short: the nav cells stay pinned under BTN4/BTN5 either way, so a 2- or 3-cell band is a first-class layout, not a broken one.
Select one volume cell (slot 0 = none) - the band's INITIAL selection, once the cells are in. From then on BTN4/BTN5 step it (see ui_hints_set_volume()).
| uint8_t | slot | |
| uint8_t | row |
Show or hide the whole system statusbar (battery, status items, hints)
| bool | state | true to show, false to hide |
When shown and hints are not active it displays the idle status items (battery etc). A standalone tapp launches with it shown, matching the rest of the device - call with false to take the full screen. A source tapp launches with it hidden and can call true to restore it (recommended for a tapp left running, so that the battery level stays visible).
REMEMBERED against the tapp's image and replayed on every refocus, exactly like ui_hints_show() - state it once and it stays. The role supplies the default; making no call keeps it.
The hints live INSIDE the bar, so hiding it does not hide them: a tapp with the statusbar off and the hints on gets a band and nothing else, which is the shape examples/recorder.c uses.