Application Skeleton — the code every μGUI app shares¶
Whatever widgets you use, every μGUI application has the same shape. This
page is that skeleton with the widget-specific parts left as // ... comments —
copy it, drop in the widgets from any widget page, and you have a working app.
Nothing here is widget-specific — this is the scaffolding every widget sits inside.
Jump to: The skeleton · Bring-up per display mode · Do I need widget IDs? · Things people miss · More than one screen
The whole skeleton¶
#include "ugui.h" /* core + every widget header + convenience API, one include */
/* ------------------------------------------------------------------ *
* 1. Widget IDs (optional — see "Do I need IDs?" below)
* ------------------------------------------------------------------ */
enum {
WDGT_ID_ROOT = 1,
// WDGT_ID_OK_BUTTON,
// ... one per widget you want to identify in a shared callback
};
/* ------------------------------------------------------------------ *
* 2. Storage — everything is app-static, the library never allocates.
* One ugui_screen_t + one root ugui_widget_t per screen, plus a
* ugui_widget_t (and its *_ext) per widget on that screen.
* ------------------------------------------------------------------ */
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
// static ugui_widget_t s_ok_button;
// static ugui_button_ext_t s_ok_button_ext;
/* ------------------------------------------------------------------ *
* 3. Event callbacks (only for interactive widgets).
* The library passes back the widget id so one callback can serve
* many widgets — see "Do I need IDs?".
* ------------------------------------------------------------------ */
// static void on_event(uint8_t widget_id, ugui_event_t event) {
// switch (widget_id) {
// case WDGT_ID_OK_BUTTON: /* ... */ break;
// }
// }
/* ------------------------------------------------------------------ *
* 4. Build one screen. Call this once, before ugui_screen_switch().
* ------------------------------------------------------------------ */
static void screen_init(void)
{
/* Register the screen (background + lifecycle hooks) AND build the
* full-screen root every widget below is parented to — one call.
* Root size comes from the runtime geometry set by ugui_init(). */
ugui_screen_init(&s_screen, &s_root, WDGT_ID_ROOT, UGUI_COLOR_BLACK, NULL, NULL);
/* --- your widgets go here --- e.g.:
ugui_widget_button_init(&s_root, &s_ok_button, &s_ok_button_ext);
ugui_widget_set_id(&s_ok_button, WDGT_ID_OK_BUTTON);
ugui_widget_set_event_cb(&s_ok_button, on_event, UGUI_EMASK_CLICK);
ugui_screen_add_widget(&s_screen, &s_ok_button); // <-- don't forget this
*/
}
/* ================================================================== *
* 5. THE HAL — the functions YOU implement so uGui can reach your
* hardware. All of these are mandatory; a missing one is a linker
* error. On the Linux/WSL simulator these are provided for you.
* ================================================================== */
void ugui_hal_init(void* bsp) { /* bring up SPI/panel/backlight */ (void)bsp; }
void ugui_hal_deinit(void) { /* release hardware */ }
void ugui_hal_draw_pixel(int16_t x, int16_t y, ugui_color_t c) { /* rare fallback */ (void)x;(void)y;(void)c; }
/* Hot path: push one finished band of pixels to the panel (DMA here). */
void ugui_hal_flush_buffer(const ugui_color_t* buf, uint32_t size,
int16_t x, int16_t y, int16_t w, int16_t h)
{
/* set the panel address window to (x,y,w,h) and stream `size` pixels. */
(void)buf;(void)size;(void)x;(void)y;(void)w;(void)h;
}
void ugui_hal_flush_done(void) { /* wait for DMA / TE pin if used */ }
/* The millisecond tick — uGui reads this itself for animations and
* click-hold timing. Return a monotonic ms counter. This is the piece
* people forget: there is no "ugui_msec()" — this IS the ms source. */
uint32_t ugui_hal_get_tick_ms(void) { return board_millis(); }
void ugui_hal_debug_print(const char* s) { /* UART/SWO puts(); */ (void)s; }
/* ================================================================== *
* 6. Entry point
* ================================================================== */
int main(void)
{
/* --- board-specific bring-up (clocks, GPIO, panel, touch) --- */
ugui_hal_init(NULL);
/* --- uGui bring-up. Two lines cover the common (band) case. HERE YOU:
* 1. declare the band buffer — it's YOUR RAM, so one library build
* fits any panel size;
* 2. hand it + the screen size to ugui_init(), which also starts the
* task system.
* The drawing method is chosen ONCE in ugui_config.h, NOT here:
* #define DISPLAY_MODE DISPLAY_MODE_BAND_FLUSH
* The default (DISPLAY_MODE_LAYERS) and every band mode use this one
* small buffer. Other modes change ONLY these two lines — the
* "Bring-up for each display mode" section below has all seven. --- */
static ugui_color_t s_band[UGUI_SCREEN_W * UGUI_BUF_LINES];
ugui_init(UGUI_SCREEN_W, UGUI_SCREEN_H, UGUI_BUF_LINES, s_band, NULL);
/* --- build the UI and show the first screen --- */
screen_init(); /* repeat for every screen you have */
ugui_screen_switch(&s_screen);
/* --- main loop --- */
while (running())
{
/* Feed input: read your touch controller / keypad and inject it.
* The library dispatches it to widgets and fires their callbacks. */
// ugui_input_touch((ugui_point_t){ tx, ty }, UGUI_PRESS_DOWN/DRAG/UP);
// ugui_input_key(UGUI_KEY_NEXT); // keypad/encoder builds
/* Render whatever is dirty. This is also where the library calls
* your ugui_hal_flush_buffer(); nothing else to call for band modes. */
ugui_task_execute();
}
/* --- teardown, mirroring bring-up order --- */
ugui_deinit(); /* undoes ugui_init() (stops the task system) */
ugui_hal_deinit(); /* your hardware teardown */
return 0;
}
Replace board_millis(), running(), and the touch read with your platform's
equivalents. On the WSL/Linux simulator the HAL and the input are provided for
you, so only the screen and widget code above is yours to write.
Bring-up for each display mode¶
Everything in the skeleton stays the same for all seven
drawing methods — only the buffer setup changes: what you declare at file
scope, the ugui_init() call, what you wire to the screen after building it, and
which HAL present function you implement. To use a mode, drop its block below in
place of the two band-buffer lines in the skeleton's bring-up step.
Set the mode once in ugui_config.h (or via -DDISPLAY_MODE=…); the default is
DISPLAY_MODE_LAYERS. Not sure which to pick? Mode 1 (band flush) is the
safe starting point for SPI/8080 GRAM panels and uses the least RAM.
1 · DISPLAY_MODE_BAND_FLUSH — GRAM SPI/8080 panels (lowest RAM)¶
static ugui_color_t s_band[UGUI_SCREEN_W * UGUI_BUF_LINES]; /* one strip */
ugui_init(UGUI_SCREEN_W, UGUI_SCREEN_H, UGUI_BUF_LINES, s_band, NULL);
ugui_hal_flush_buffer() + ugui_hal_flush_done(). No
framebuffers.
2 · DISPLAY_MODE_BAND_PINGPONG — band flush + DMA overlap¶
static ugui_color_t s_band_a[UGUI_SCREEN_W * UGUI_BUF_LINES]; /* two strips */
static ugui_color_t s_band_b[UGUI_SCREEN_W * UGUI_BUF_LINES];
ugui_init(UGUI_SCREEN_W, UGUI_SCREEN_H, UGUI_BUF_LINES, s_band_a, s_band_b);
ugui_hal_flush_buffer() must start a non-blocking (DMA) transfer and
return; wait for it in ugui_hal_flush_done().
3 · DISPLAY_MODE_LAYERS — band flush, painted in BACK→MID→TOP order (default)¶
Same one band buffer and ugui_init() as mode 1 — the only difference is you tag
each widget with a layer:
4 · DISPLAY_MODE_LAYER_FRAMEBUF — per-layer RAM buffers, software composite¶
/* file scope: one full-screen buffer per layer + one composite + a band strip */
static ugui_color_t g_layer_fb[UGUI_LAYER_COUNT][UGUI_SCREEN_W * UGUI_SCREEN_H];
static ugui_color_t g_display_fb[UGUI_SCREEN_W * UGUI_SCREEN_H];
static ugui_color_t s_band[UGUI_SCREEN_W * UGUI_BUF_LINES];
ugui_init(UGUI_SCREEN_W, UGUI_SCREEN_H, UGUI_BUF_LINES, s_band, NULL);
/* after screen_init(), before ugui_screen_switch(): wire the buffers.
* (If you skip this, the library falls back to plain band flush.) */
for (uint8_t i = 0u; i < (uint8_t)UGUI_LAYER_COUNT; i++) {
ugui_screen_set_layer_framebuf(&s_screen, i, g_layer_fb[i]);
}
ugui_screen_set_display_framebuf(&s_screen, g_display_fb);
flush_buffer() + flush_done() (the composite is staged through the
band).
5 · DISPLAY_MODE_DOUBLE_BUFFER — HW double buffer, VSYNC flip (no band buffer)¶
static ugui_color_t g_layer_fb[UGUI_LAYER_COUNT][UGUI_SCREEN_W * UGUI_SCREEN_H];
static ugui_color_t g_display_front[UGUI_SCREEN_W * UGUI_SCREEN_H];
static ugui_color_t g_display_back [UGUI_SCREEN_W * UGUI_SCREEN_H];
ugui_init(UGUI_SCREEN_W, UGUI_SCREEN_H, 0u, NULL, NULL); /* 0 band lines */
for (uint8_t i = 0u; i < (uint8_t)UGUI_LAYER_COUNT; i++) {
ugui_screen_set_layer_framebuf(&s_screen, i, g_layer_fb[i]);
}
ugui_screen_set_display_framebuf (&s_screen, g_display_front);
ugui_screen_set_display_framebuf_back(&s_screen, g_display_back);
ugui_hal_present(fb) — point the scanout base at fb and wait
for VSYNC. flush_buffer() is not used.
6 · DISPLAY_MODE_HW_LAYERS — hardware compositor, two buffers/layer¶
static ugui_color_t g_layer_shadow [UGUI_LAYER_COUNT][UGUI_SCREEN_W * UGUI_SCREEN_H]; /* CPU draws */
static ugui_color_t g_layer_display[UGUI_LAYER_COUNT][UGUI_SCREEN_W * UGUI_SCREEN_H]; /* HW scans */
ugui_init(UGUI_SCREEN_W, UGUI_SCREEN_H, 0u, NULL, NULL);
for (uint8_t i = 0u; i < (uint8_t)UGUI_LAYER_COUNT; i++) {
ugui_screen_set_layer_framebuf (&s_screen, i, g_layer_shadow[i]);
ugui_screen_set_layer_display_framebuf(&s_screen, i, g_layer_display[i]);
}
ugui_hal_present_layers(bufs, count) — reload every layer base
at one VSYNC; colour-key UGUI_LAYER_KEY_COLOR for transparency. flush_buffer()
is not used.
7 · DISPLAY_MODE_HW_LAYERS_SINGLE — hardware compositor, one buffer/layer (leanest)¶
Like mode 6 but each layer has a single buffer (the CPU draws straight into the buffer the controller scans) — half the layer RAM, no shadow/display pair:
static ugui_color_t g_layer_fb[UGUI_LAYER_COUNT][UGUI_SCREEN_W * UGUI_SCREEN_H];
ugui_init(UGUI_SCREEN_W, UGUI_SCREEN_H, 0u, NULL, NULL);
for (uint8_t i = 0u; i < (uint8_t)UGUI_LAYER_COUNT; i++) {
ugui_screen_set_layer_framebuf(&s_screen, i, g_layer_fb[i]); /* no _display pair */
}
ugui_hal_present_layers() as mode 6. Trade-off: a layer can tear if
you rewrite it while it's scanned, so it suits static/slow layers — pair it
with mode 6's double buffer for the one layer that animates.
Do I need widget IDs?¶
An ID is just a name tag you put on a widget. The library never uses it to
find or manage widgets (it always uses the pointer, e.g. &s_ok_button). The
only place an ID is read functionally is when a widget fires an event: the
library calls your callback with (widget_id, event), so you can tell which
widget it was.
If you never call ugui_widget_set_id(), the ID defaults to 0.
| Pros | Cons / when it bites | |
|---|---|---|
| Set IDs | One shared callback can serve many widgets (switch (id)); debug logs read as "widget 7" not "widget 0"; the code self-documents what each widget is. |
A tiny bit more boilerplate; IDs must be unique within a screen and fit in a uint8_t (0–255). |
| Skip IDs (leave at 0) | Less code. Perfectly fine for decorative widgets (a plain box, a label) with no callback, for widgets that each have their own callback, or when there's only one interactive widget. | If several widgets share one callback and all keep the default 0, you can't tell them apart — they all report id == 0. Debug logs also all say "widget 0". |
Rule of thumb: give an ID to any widget you'll identify inside a shared callback; skip it for purely decorative widgets.
Things people miss¶
- The HAL is mandatory. All seven
ugui_hal_*functions (section 5) must exist or the link fails — that's by design (SRS-HAL-001). Scanout display modes add one more (ugui_hal_present/ugui_hal_present_layers). ugui_hal_get_tick_ms()is the millisecond source. There is nougui_msec(); the library callsugui_hal_get_tick_ms()itself for animations and click-hold timing. If it returns a constant (or you forget it), animations freeze and press-and-hold never fires.- Inject input every loop.
ugui_task_execute()only renders. Read your touch/keypad and callugui_input_touch()/ugui_input_key()— otherwise nothing is interactive. - Register every widget with
ugui_screen_add_widget()after its*_init(), or it won't be drawn or hit-tested. - Order matters at startup:
ugui_init()(or at leastugui_display_init()) before you build screens, becauseugui_screen_init()reads the runtime geometry to size each root.
More than one screen¶
Give each screen its own ugui_screen_t + root widget + an *_init() that calls
ugui_screen_init(), call every *_init() once at startup, then move between
them with ugui_screen_switch(&other_screen) (typically from a button's event
callback). The task system and HAL are set up once for the whole app — only
ugui_screen_init() is per screen.