Skip to content

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);
HAL: implement 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);
HAL: 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:

ugui_widget_set_layer(&s_widget, UGUI_LAYER_BACK);   /* or _MID / _TOP */
HAL: same as mode 1. No framebuffers.

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);
HAL: same 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);
HAL: implement 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]);
}
HAL: implement 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 */
}
HAL: same 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 no ugui_msec(); the library calls ugui_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 call ugui_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 least ugui_display_init()) before you build screens, because ugui_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.