Graphics API
Drawing primitives
Every call takes the gfx_t* passed to the redraw callback. 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:
GFX_COLOR_CLEAR(0) - background: clears them (black)GFX_COLOR_SET(1) - foreground: sets them (white). This is the normal drawing colour.GFX_COLOR_XOR(2) - inverts the existing pixels; used to overlay a cursor or a playhead without erasing what is underneath.
Every draw callback (redraw, a hint painter, a menu painter...) starts from the same default state: colour 1, transparent bitmaps, the default font, and the full drawing area (the screen, or the tapp's window). Any change lasts until that callback returns - nothing needs to be restored.
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_coord_t, SIGNED: a shape may start left of or above the panel, and every primitive clips it against the display and the clip window - partly off-screen draws the visible part.
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 = os_app_get_model(app);
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, GFX_COLOR_XOR); // XOR the playhead in
gfx_draw_vline(gfx, 20 + m->cursor, 60, 20);
}Paint a single pixel in the current colour
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x | X coordinate |
| gfx_coord_t | y | Y coordinate |
Horizontal line running right from (x, y)
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x | Left end |
| gfx_coord_t | y | Row |
| gfx_coord_t | w | Length in pixels |
Note — This is the fastest primitive - it writes whole framebuffer bytes at a time. A fill built from hlines is substantially faster than one built from pixels.
Vertical line running down from (x, y)
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x | Column |
| gfx_coord_t | y | Top end |
| gfx_coord_t | h | Length in pixels |
Line between two arbitrary points (Bresenham)
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x1 | First point X |
| gfx_coord_t | y1 | First point Y |
| gfx_coord_t | x2 | Second point X |
| gfx_coord_t | y2 | Second point Y |
Line with a thickness, drawn as a stack of parallel lines
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x1 | First point X |
| gfx_coord_t | y1 | First point Y |
| gfx_coord_t | x2 | Second point X |
| gfx_coord_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_coord_t | x0 | Start X |
| gfx_coord_t | y0 | Start Y |
| gfx_coord_t | x1 | Control point X - the curve is pulled toward it but does not touch it |
| gfx_coord_t | y1 | Control point Y |
| gfx_coord_t | x2 | End X |
| gfx_coord_t | y2 | End Y |
Rectangle outline
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x | Left edge |
| gfx_coord_t | y | Top edge |
| gfx_coord_t | w | Width in pixels |
| gfx_coord_t | h | Height in pixels |
Rectangle outline with rounded corners
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x | Left edge |
| gfx_coord_t | y | Top edge |
| gfx_coord_t | w | Width in pixels |
| gfx_coord_t | h | Height in pixels |
| uint16_t | r | Corner radius; must not exceed half the shorter side |
Solid rectangle
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x | Left edge |
| gfx_coord_t | y | Top edge |
| gfx_coord_t | w | Width in pixels |
| gfx_coord_t | h | Height in pixels |
Note — Filling with colour 0 erases a region before it is redrawn.
Solid rectangle with rounded corners
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x | Left edge |
| gfx_coord_t | y | Top edge |
| gfx_coord_t | w | Width in pixels |
| gfx_coord_t | h | Height in pixels |
| uint16_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_coord_t | x | Left edge |
| gfx_coord_t | y | Top edge |
| gfx_coord_t | w | Width in pixels |
| gfx_coord_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_coord_t | x | Left end |
| gfx_coord_t | y | Row |
| gfx_coord_t | w | Length in pixels |
| uint8_t | shade | 0-7, see gfx_shades |
Dithered vertical line
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x | Column |
| gfx_coord_t | y | Top end |
| gfx_coord_t | h | Length in pixels |
| uint8_t | shade | 0-7, see gfx_shades |
Dithered filled circle
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | cx | Centre X |
| gfx_coord_t | cy | Centre Y |
| gfx_coord_t | r | Radius in pixels |
| uint8_t | shade | 0-7, see gfx_shades |
Dithered rounded rectangle
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x | Left edge |
| gfx_coord_t | y | Top edge |
| gfx_coord_t | w | Width in pixels |
| gfx_coord_t | h | Height in pixels |
| gfx_coord_t | r | Corner radius |
| uint8_t | shade | 0-7, see gfx_shades |
Rectangle with a vertical dither gradient
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x | Left edge |
| gfx_coord_t | y | Top edge |
| gfx_coord_t | w | Width in pixels |
| gfx_coord_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 a gradient is animated in and out.
Rectangle with a horizontal dither gradient
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x | Left edge |
| gfx_coord_t | y | Top edge |
| gfx_coord_t | w | Width in pixels |
| gfx_coord_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_coord_t | x0 | Centre X |
| gfx_coord_t | y0 | Centre Y |
| uint16_t | r | Radius in pixels |
| uint8_t | opt | Quadrant mask - GFX_DRAW_ALL, or an OR of the GFX_DRAW_* family |
Filled circle, whole or by quadrant
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x0 | Centre X |
| gfx_coord_t | y0 | Centre Y |
| uint16_t | r | Radius in pixels |
| uint8_t | opt | Quadrant mask - GFX_DRAW_ALL, or an OR of the GFX_DRAW_UPPER_RIGHT family |
Select what subsequent draw calls do to the pixels they touch
| gfx_t* | gfx | Graphics context |
| uint8_t | color | GFX_COLOR_CLEAR, GFX_COLOR_SET or GFX_COLOR_XOR |
Note — Lasts until the draw callback returns; the next one starts at GFX_COLOR_SET.
Draw a string in the current font and colour
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x | Left edge of the first glyph |
| gfx_coord_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_coord_t | x | Left edge of the first glyph |
| gfx_coord_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 redrawn every frame, scale to an int explicitly: `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.
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 uint16_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_coord_t | x | Left edge |
| gfx_coord_t | y | Top edge |
| gfx_coord_t | w | Bitmap width in pixels |
| gfx_coord_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 — TRANSPARENT by default: 1 bits are painted in the current colour, 0 bits are left alone. gfx_set_bitmap_mode(gfx, false) makes blits opaque (0 bits in the opposite colour) for the rest of the callback. For assets, prefer ui_draw_img(), which takes a gfx_img_t from the asset pipeline and knows its own dimensions; this is the blit for a bitmap built at runtime.
Transparent (default) or opaque bitmap blits, until the draw callback returns
| gfx_t* | gfx | Graphics context |
| uint8_t | is_transparent | true: 0 bits are left alone; false: they are painted in the opposite colour, overwriting the bitmap's whole bounding box |
Restrict drawing to a rectangular window
| gfx_t* | gfx | Graphics context |
| gfx_coord_t | x0 | Left edge of the window |
| gfx_coord_t | y0 | Top edge of the window |
| gfx_coord_t | x1 | Right edge of the window (a coordinate, not a width) |
| gfx_coord_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. It narrows the callback's drawing area (the screen, or the tapp's window/cell - it can never widen it) and lasts until gfx_clip_reset() or the callback's return.
Remove the clip window: restore the whole area this callback draws into (the screen, or the tapp's window / hint cell - never beyond it)
| gfx_t* | gfx | Graphics context |
How many frames have been PAINTED
| gfx_t* | gfx | Graphics context |
Returns — Frame counter, incremented once per frame actually pushed to the display
Default UI body font
Headings / emphasis
Monospace - counters, numeric readouts