Graphics API
Drawing primitives for the 400x240 monochrome display
Every call takes the gfx_t* your redraw callback was handed. There is no cursor and no transform stack: each call takes absolute screen coordinates and paints in the current draw colour.
Color model
The panel is 1-bit and the frame starts cleared to black, so drawing means turning pixels on. gfx_set_color() selects what a draw call does to the pixels it touches:
0— background: clears them (black)1— foreground: sets them (white). This is the normal drawing colour.2— XOR: inverts whatever is already there, which is how you overlay a cursor or a playhead without erasing what is underneath.
The colour is sticky: it persists across calls and into the next frame. If you change it, set it back to 1 before you return.
Coordinate system
Origin (0,0) is the top-left pixel, x grows right, y grows down, and the screen is SCREEN_WIDTH x SCREEN_HEIGHT = 400x240. Coordinates are gfx_uint_t (uint16_t) and every primitive clips against the display and the clip window, so drawing partly off-screen is safe.
Dithering — grey on a 1-bit panel
The _dithered calls paint an ordered 8x8 Bayer pattern in the current colour instead of a solid fill, which the eye reads as grey. shade is 0-7 and picks how many pixels get painted:
| shade | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
|---|---|---|---|---|---|---|---|---|
| pixels set | 6% | 12% | 25% | 37% | 50% | 62% | 75% | 87% |
In the usual colour 1 that runs from nearly black to nearly white. The pattern is anchored to screen coordinates rather than to the shape, so neighbouring fills of the same shade line up seamlessly.
A minimal redraw
static void my_redraw(gfx_t* gfx, const os_app_t* app) {
my_model_t* m = app->data->model;
gfx_set_font(gfx, gfx_nunito_bold_18);
gfx_draw_str(gfx, 20, 40, "hello");
gfx_fill_rect_dithered(gfx, 20, 60, 360, 20, 3); // grey bar
gfx_draw_rect_r(gfx, 20, 60, 360, 20, 4); // outline over it
gfx_set_color(gfx, 2); // XOR the playhead in
gfx_draw_vline(gfx, 20 + m->cursor, 60, 20);
gfx_set_color(gfx, 1); // leave it as you found it
}Paint a single pixel in the current colour
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | X coordinate |
| gfx_uint_t | y | Y coordinate |
Horizontal line running right from (x, y)
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left end |
| gfx_uint_t | y | Row |
| gfx_uint_t | w | Length in pixels |
Note — This is the fastest primitive there is — it writes whole framebuffer bytes at a time. Building a fill out of hlines beats building it out of pixels by a wide margin.
Vertical line running down from (x, y)
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Column |
| gfx_uint_t | y | Top end |
| gfx_uint_t | h | Length in pixels |
Axis-aligned line in one of four directions
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Start X |
| gfx_uint_t | y | Start Y |
| gfx_uint_t | len | Length in pixels |
| uint8_t | dir | 0 = right, 1 = down, 2 = left, 3 = up |
Note — `dir` 0 and 1 are gfx_draw_hline() / gfx_draw_vline() exactly; 2 and 3 are the same lines drawn backwards from (x, y). Call the hline/vline pair directly unless the direction is a runtime value.
Line between two arbitrary points (Bresenham)
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x1 | First point X |
| gfx_uint_t | y1 | First point Y |
| gfx_uint_t | x2 | Second point X |
| gfx_uint_t | y2 | Second point Y |
Line with a thickness, drawn as a stack of parallel lines
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x1 | First point X |
| gfx_uint_t | y1 | First point Y |
| gfx_uint_t | x2 | Second point X |
| gfx_uint_t | y2 | Second point Y |
| uint16_t | thickness | Width in pixels; 1 is gfx_draw_line() |
Quadratic Bezier curve through 16 straight segments
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x0 | Start X |
| gfx_uint_t | y0 | Start Y |
| gfx_uint_t | x1 | Control point X — the curve is pulled toward it but does not touch it |
| gfx_uint_t | y1 | Control point Y |
| gfx_uint_t | x2 | End X |
| gfx_uint_t | y2 | End Y |
Rectangle outline
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge |
| gfx_uint_t | y | Top edge |
| gfx_uint_t | w | Width in pixels |
| gfx_uint_t | h | Height in pixels |
Rectangle outline with rounded corners
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge |
| gfx_uint_t | y | Top edge |
| gfx_uint_t | w | Width in pixels |
| gfx_uint_t | h | Height in pixels |
| gfx_uint_t | r | Corner radius; must not exceed half the shorter side |
Solid rectangle
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge |
| gfx_uint_t | y | Top edge |
| gfx_uint_t | w | Width in pixels |
| gfx_uint_t | h | Height in pixels |
Note — Filling with colour 0 is how you erase a region before redrawing it.
Solid rectangle with rounded corners
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge |
| gfx_uint_t | y | Top edge |
| gfx_uint_t | w | Width in pixels |
| gfx_uint_t | h | Height in pixels |
| gfx_uint_t | r | Corner radius; must not exceed half the shorter side |
Rectangle filled with a dither pattern instead of solid colour
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge |
| gfx_uint_t | y | Top edge |
| gfx_uint_t | w | Width in pixels |
| gfx_uint_t | h | Height in pixels |
| uint8_t | shade | 0-7, see gfx_shades — 0 is the sparsest, 7 the densest |
Dithered horizontal line
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left end |
| gfx_uint_t | y | Row |
| gfx_uint_t | w | Length in pixels |
| uint8_t | shade | 0-7, see gfx_shades |
Dithered vertical line
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Column |
| gfx_uint_t | y | Top end |
| gfx_uint_t | h | Length in pixels |
| uint8_t | shade | 0-7, see gfx_shades |
Dithered filled circle
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | cx | Centre X |
| gfx_uint_t | cy | Centre Y |
| gfx_uint_t | r | Radius in pixels |
| uint8_t | shade | 0-7, see gfx_shades |
Dithered rounded rectangle
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge |
| gfx_uint_t | y | Top edge |
| gfx_uint_t | w | Width in pixels |
| gfx_uint_t | h | Height in pixels |
| gfx_uint_t | r | Corner radius |
| uint8_t | shade | 0-7, see gfx_shades |
Rectangle with a vertical dither gradient
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge |
| gfx_uint_t | y | Top edge |
| gfx_uint_t | w | Width in pixels |
| gfx_uint_t | h | Height in pixels |
| uint8_t | shade_top | Shade at the top row, 0-7 |
| uint8_t | shade_bottom | Shade at the bottom row, 0-7 |
Note — Equal top and bottom shades give a flat fill, so this doubles as gfx_fill_rect_dithered() when you are animating a gradient in and out.
Rectangle with a horizontal dither gradient
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge |
| gfx_uint_t | y | Top edge |
| gfx_uint_t | w | Width in pixels |
| gfx_uint_t | h | Height in pixels |
| uint8_t | shade_left | Shade at the left column, 0-7 |
| uint8_t | shade_right | Shade at the right column, 0-7 |
Circle outline, whole or by quadrant
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x0 | Centre X |
| gfx_uint_t | y0 | Centre Y |
| gfx_uint_t | r | Radius in pixels |
| uint8_t | opt | Quadrant mask — GFX_DRAW_ALL, or an OR of the GFX_DRAW_* family |
Note — GFX_DRAW_ALL draws full circle and matches GFX_DRAW_UPPER_LEFT | GFX_DRAW_UPPER_RIGHT | GFX_DRAW_LOWER_RIGHT | GFX_DRAW_LOWER_LEFT
Filled circle, whole or by quadrant
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x0 | Centre X |
| gfx_uint_t | y0 | Centre Y |
| gfx_uint_t | r | Radius in pixels |
| uint8_t | opt | Quadrant mask — GFX_DRAW_ALL, or an OR of the GFX_DRAW_UPPER_RIGHT family |
Note — GFX_DRAW_ALL draws full circle and matches GFX_DRAW_UPPER_LEFT | GFX_DRAW_UPPER_RIGHT | GFX_DRAW_LOWER_RIGHT | GFX_DRAW_LOWER_LEFT
Circle outline drawn as a dotted ring
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x0 | Centre X |
| gfx_uint_t | y0 | Centre Y |
| gfx_uint_t | r | Radius in pixels |
| uint8_t | opt | Quadrant mask — GFX_DRAW_ALL, or an OR of the GFX_DRAW_UPPER_RIGHT family |
Note — GFX_DRAW_ALL draws full circle and matches GFX_DRAW_UPPER_LEFT | GFX_DRAW_UPPER_RIGHT | GFX_DRAW_LOWER_RIGHT | GFX_DRAW_LOWER_LEFT
Select what subsequent draw calls do to the pixels they touch
| gfx_t* | gfx | Graphics context |
| uint8_t | color | 0 = clear (black), 1 = set (white), 2 = XOR |
Note — Sticky — it survives into the next frame. See gfx_color.
Draw a string in the current font and colour
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge of the first glyph |
| gfx_uint_t | y | Text baseline — glyphs sit *above* this row, so y is the bottom of the line |
| const char* | str | NUL-terminated ASCII/UTF-8 string |
Returns — Width drawn, in pixels — add it to x to continue on the same line
printf-style gfx_draw_str()
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge of the first glyph |
| gfx_uint_t | y | Text baseline |
| const char* | fmt | printf format string |
| ... |
Returns — Width drawn, in pixels
Note — `%f` costs far more than the integer conversions. For a value you redraw every frame, scale to an int yourself: `gfx_draw_strf(gfx, x, y, "%d%%", (int)(v * 100))`.
Warning — The formatted result is built in a shared 128-byte buffer and truncated to fit. Do not call it from an audio or timer callback while the UI thread may also be drawing.
Draw one glyph by character code
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge |
| gfx_uint_t | y | Baseline |
| uint16_t | encoding | Character code (ASCII, or a Unicode code point in a font that has it) |
Returns — Advance width of that glyph in pixels
Select the font for subsequent text calls
| gfx_t* | gfx | Graphics context |
| const uint8_t* | font | One of the exported font blobs — gfx_nunito_semibold_14, gfx_nunito_bold_18 or DepartureMono_Regular_10 |
Note — Sticky, like the colour. Setting a font is cheap; measuring text is not, so hoist gfx_get_str_width() out of loops rather than the gfx_set_font() call.
Measure a string in the current font without drawing it
| gfx_t* | gfx | Graphics context |
| const char* | str | NUL-terminated string |
Returns — Width in pixels
// centre a label const gfx_uint_t w = gfx_get_str_width(gfx, label); gfx_draw_str(gfx, SCREEN_CENTER_X - (w >> 1), 120, label);
Blit a 1-bit XBM bitmap
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge |
| gfx_uint_t | y | Top edge |
| gfx_uint_t | w | Bitmap width in pixels |
| gfx_uint_t | h | Bitmap height in pixels |
| const uint8_t* | bitmap | XBM data: one bit per pixel, LSB first, each row padded to a whole byte |
Note — The blit is opaque — 0 bits are painted in the opposite colour, so the bitmap's bounding box is fully overwritten. Prefer ui_draw_img(), which takes a gfx_img_t from the asset pipeline and knows its own dimensions.
Blit a bitmap rotated about its own centre
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge of the unrotated bounding box |
| gfx_uint_t | y | Top edge of the unrotated bounding box |
| gfx_uint_t | w | Bitmap width in pixels |
| gfx_uint_t | h | Bitmap height in pixels |
| const uint8_t* | bitmap | XBM data |
| float | angle | Rotation in radians, clockwise |
Note — Per-pixel and transparent, unlike gfx_draw_xbm(): only set bits are painted, and corners can spill outside the original w x h box.
Warning — This variant reads the bitmap MSB first — the opposite of gfx_draw_xbm() and gfx_draw_xbm_scaled(). Asset-pipeline bitmaps come out mirrored within each byte, so check on device before you build a UI on it.
Blit a bitmap shrunk by an integer factor
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x | Left edge |
| gfx_uint_t | y | Top edge |
| const uint8_t* | bitmap | XBM data — note this comes *before* the dimensions here, unlike gfx_draw_xbm() |
| gfx_uint_t | w | Source bitmap width in pixels |
| gfx_uint_t | h | Source bitmap height in pixels |
| gfx_uint_t | scale | Divisor: 2 draws at half size, 3 at a third. 1 is 1:1. |
Warning — `scale` shrinks, it does not magnify — there is no scale-up entry point in the TAPP API. Sampling is nearest-neighbour, so thin 1px features can vanish entirely.
Restrict drawing to a rectangular window
| gfx_t* | gfx | Graphics context |
| gfx_uint_t | x0 | Left edge of the window |
| gfx_uint_t | y0 | Top edge of the window |
| gfx_uint_t | x1 | Right edge of the window (a coordinate, not a width) |
| gfx_uint_t | y1 | Bottom edge of the window (a coordinate, not a height) |
gfx_set_clip(gfx, 10, 40, 390, 200); // scrolling list body draw_rows(gfx, m); gfx_clip_reset(gfx);
Note — Corner-to-corner, not x/y/w/h. The window is sticky and applies to every primitive, so a forgotten clip makes the rest of your UI silently disappear — always pair it with gfx_clip_reset().
Drop the clip window and draw to the whole screen again
| gfx_t* | gfx | Graphics context |
Get the current frame counter (for animations)
| gfx_t* | gfx | Graphics context |
Returns — Frame counter value, incremented on each display refresh
Note — This is the clock to drive animation from — a tapp has no wall time. Divide it down for slower motion: `ui_draw_anim(gfx, x, y, ui_get_frame(gfx) >> 2, &my_anim)`.
Default UI body font
Headings / emphasis
Monospace — counters, numeric readouts