uList Widget¶
The uList is a lean, virtual, scrollable list of uniform rows — the
small-footprint replacement for the older List (kinetic) and Fold widgets:
rows are virtual (a 5-row and a 64-row list cost the same RAM), there is no
physics, no per-row draw callback, no reveal animation, and no embedded
child widgets beyond one real widget per row via the embed slot. Init
installs the whole look (theme-tracked colours, row height, default
geometry) at a settable size; set_font() gives it text, set_items()
attaches a caller-owned ugui_ulist_item_t[] — each row a label, an
optional right-aligned value, and an optional accessory (chevron / switch /
check / radio) — and set_on_action() wires activation. Rows can be
grouped under HEADER rows, and a header flagged EXPANDABLE becomes a
collapsible accordion section; a RADIO accessory auto-clears its section
peers when one is picked. The widget applies ALL of this itself (flips
CHECKED, expands a header, clears radio peers) BEFORE calling on_action —
the callback only needs to read the item back to report it. A row's
embed field can point at a real child widget (a Slider, say): init it
with parent = the uList, set its x/width/height, and the list takes over
from there — positioning it vertically so it scrolls with the row, showing
and hiding it with the row, and forwarding LEFT/RIGHT to it when the
keypad cursor is on that row.
Every example below is complete — the file-scope storage, the callbacks and
the build calls — beside the exact frame it renders on a 480×320 panel. Each
compiles against nothing but the released library: drop it into the
Application Skeleton and it runs as-is. The icons come from
inc/icons/, the lexend fonts from inc/fonts/ — both ship with the library.
Examples¶
Default: A Settings Menu¶
Four calls make a working scrollable menu: ugui_widget_ulist_init() installs the whole look, set_font() gives it text, set_items() attaches a caller-owned row array (label + optional right-aligned value + accessory), set_on_action() wires activation. Tapping a row (or CONFIRM while focused) fires on_action; the status line below echoes which row and its id.

/* --- widget IDs: only need to be unique within the screen (10+ leaves
* room for the IDs your app already uses, e.g. the root's) --- */
enum {
WDGT_ID_DEMO = 10
};
/* --- fonts: each is DEFINED in inc/fonts/<name>.h — #include that
* header in exactly one .c of your app; elsewhere just: --- */
extern const ugui_font_t lexend_14pt_2bpp;
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_ULIST)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_status;
#endif
#if defined(UGUI_WIDGET_ENABLE_ULIST)
static char s_status_str[40];
#endif
#if defined(UGUI_WIDGET_ENABLE_ULIST)
static ugui_widget_t s_list;
static ugui_ulist_ext_t s_list_ext;
#define LIST_X 10
#define LIST_Y 38
#define LIST_W (UGUI_SCREEN_W - 2 * LIST_X)
#define LIST_H (UGUI_SCREEN_H - LIST_Y - 40)
static ugui_ulist_item_t s_items[] = {
{ "Wi-Fi", "Connected", 1u, UGUI_ULIST_ACC_CHEVRON, 0u, NULL },
{ "Bluetooth", "Off", 2u, UGUI_ULIST_ACC_CHEVRON, 0u, NULL },
{ "Software update", "Up to date", 3u, UGUI_ULIST_ACC_CHEVRON, 0u, NULL },
{ "About", NULL, 4u, UGUI_ULIST_ACC_CHEVRON, 0u, NULL },
};
#define ITEM_N ((uint16_t)(sizeof(s_items) / sizeof(s_items[0])))
#endif
/* --- local helpers & event callbacks --- */
static void on_action(ugui_widget_t* w, uint16_t idx, void* ctx)
{
(void)w; (void)ctx;
if (idx >= ITEM_N) { return; }
#ifdef UGUI_ENABLE_FONT
(void)snprintf(s_status_str, sizeof(s_status_str), "opened: %s (id %u)",
s_items[idx].label, (unsigned)s_items[idx].id);
ugui_widget_invalidate(&s_status);
#endif
printf("[ulist.default] row %u ('%s') activated\n",
(unsigned)idx, s_items[idx].label);
}
/* --- build it (in your screen's init) --- */
ugui_widget_ulist_init(&s_root, &s_list, &s_list_ext);
ugui_widget_set_area(&s_list, LIST_X, LIST_Y, LIST_W, LIST_H);
ugui_widget_set_id(&s_list, WDGT_ID_DEMO);
ugui_widget_ulist_set_font(&s_list, &lexend_14pt_2bpp);
ugui_widget_ulist_set_items(&s_list, s_items, ITEM_N);
ugui_widget_ulist_set_on_action(&s_list, on_action, NULL);
ugui_screen_add_widget(&s_screen, &s_list);
Sections, Accessories & an Accordion Header¶
Three HEADER-grouped sections: two SWITCH rows under DISPLAY, an EXPANDABLE header under SOUND (collapsed by default — tapping it here reveals its member rows, the contiguous non-header rows that follow it), and a RADIO group under THEME. The widget flips CHECKED / expands the header / clears radio peers itself before on_action fires — the callback only reads the (already-updated) item back.

/* --- widget IDs: only need to be unique within the screen (10+ leaves
* room for the IDs your app already uses, e.g. the root's) --- */
enum {
WDGT_ID_DEMO = 10
};
/* --- fonts: each is DEFINED in inc/fonts/<name>.h — #include that
* header in exactly one .c of your app; elsewhere just: --- */
extern const ugui_font_t lexend_14pt_2bpp;
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_ULIST)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_status;
#endif
#if defined(UGUI_WIDGET_ENABLE_ULIST)
static char s_status_str[48];
#endif
#if defined(UGUI_WIDGET_ENABLE_ULIST)
static ugui_widget_t s_list;
static ugui_ulist_ext_t s_list_ext;
#define LIST_X 10
#define LIST_Y 38
#define LIST_W (UGUI_SCREEN_W - 2 * LIST_X)
#define LIST_H (UGUI_SCREEN_H - LIST_Y - 40)
static ugui_ulist_item_t s_items[] = {
{ "DISPLAY", NULL, 0u, UGUI_ULIST_ACC_NONE, UGUI_ULIST_HEADER, NULL },
{ "Dark mode", NULL, 1u, UGUI_ULIST_ACC_SWITCH, UGUI_ULIST_CHECKED, NULL },
{ "Auto-lock", NULL, 2u, UGUI_ULIST_ACC_SWITCH, 0u, NULL },
{ "SOUND", NULL, 0u, UGUI_ULIST_ACC_NONE,
(uint8_t)(UGUI_ULIST_HEADER | UGUI_ULIST_EXPANDABLE), NULL },
{ "Volume", "70%", 3u, UGUI_ULIST_ACC_CHEVRON, 0u, NULL },
{ "Notifications", NULL, 4u, UGUI_ULIST_ACC_SWITCH, UGUI_ULIST_CHECKED, NULL },
{ "THEME", NULL, 0u, UGUI_ULIST_ACC_NONE, UGUI_ULIST_HEADER, NULL },
{ "Purple", NULL, 5u, UGUI_ULIST_ACC_RADIO, UGUI_ULIST_CHECKED, NULL },
{ "Teal", NULL, 6u, UGUI_ULIST_ACC_RADIO, 0u, NULL },
{ "Green", NULL, 7u, UGUI_ULIST_ACC_RADIO, 0u, NULL },
};
#define ITEM_N ((uint16_t)(sizeof(s_items) / sizeof(s_items[0])))
#endif
/* --- local helpers & event callbacks --- */
static void on_action(ugui_widget_t* w, uint16_t idx, void* ctx)
{
(void)w; (void)ctx;
if (idx >= ITEM_N) { return; }
const ugui_ulist_item_t* it = &s_items[idx];
#ifdef UGUI_ENABLE_FONT
if ((it->flags & UGUI_ULIST_HEADER) != 0u) {
(void)snprintf(s_status_str, sizeof(s_status_str), "%s: %s", it->label,
((it->flags & UGUI_ULIST_EXPANDED) != 0u) ? "expanded" : "collapsed");
} else if ((it->acc == UGUI_ULIST_ACC_SWITCH) || (it->acc == UGUI_ULIST_ACC_RADIO)
|| (it->acc == UGUI_ULIST_ACC_CHECK)) {
(void)snprintf(s_status_str, sizeof(s_status_str), "%s: %s", it->label,
((it->flags & UGUI_ULIST_CHECKED) != 0u) ? "on" : "off");
} else {
(void)snprintf(s_status_str, sizeof(s_status_str), "opened: %s", it->label);
}
ugui_widget_invalidate(&s_status);
#endif
printf("[ulist.accessories] row %u ('%s') activated\n", (unsigned)idx, it->label);
}
/* --- build it (in your screen's init) --- */
ugui_widget_ulist_init(&s_root, &s_list, &s_list_ext);
ugui_widget_set_area(&s_list, LIST_X, LIST_Y, LIST_W, LIST_H);
ugui_widget_set_id(&s_list, WDGT_ID_DEMO);
ugui_widget_ulist_set_font(&s_list, &lexend_14pt_2bpp);
ugui_widget_ulist_set_items(&s_list, s_items, ITEM_N);
ugui_widget_ulist_set_on_action(&s_list, on_action, NULL);
ugui_screen_add_widget(&s_screen, &s_list);
A Real Child Widget Hosted in a Row¶
The "Brightness" row's embed field points at a real Slider: init it with parent == the uList widget, set its x/width/height, and the list takes over — positioning it vertically so it scrolls with the row, showing/hiding it with the row, and forwarding LEFT/RIGHT to it when the keypad cursor is on that row (touch always hits the slider directly). The embedded widget must ALSO be screen-registered so its dirty flags reach the render pass.

/* --- widget IDs: only need to be unique within the screen (10+ leaves
* room for the IDs your app already uses, e.g. the root's) --- */
enum {
WDGT_ID_DEMO = 10, WDGT_ID_SLOT0
};
/* --- fonts: each is DEFINED in inc/fonts/<name>.h — #include that
* header in exactly one .c of your app; elsewhere just: --- */
extern const ugui_font_t lexend_14pt_2bpp;
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_ULIST) && defined(UGUI_WIDGET_ENABLE_SLIDER)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_status;
#endif
#if defined(UGUI_WIDGET_ENABLE_ULIST) && defined(UGUI_WIDGET_ENABLE_SLIDER)
static char s_status_str[40];
#endif
#if defined(UGUI_WIDGET_ENABLE_ULIST) && defined(UGUI_WIDGET_ENABLE_SLIDER)
static ugui_widget_t s_list;
static ugui_ulist_ext_t s_list_ext;
static ugui_widget_t s_bright; /* embedded in the Brightness row */
static ugui_slider_ext_t s_bright_ext;
#define LIST_X 10
#define LIST_Y 38
#define LIST_W (UGUI_SCREEN_W - 2 * LIST_X)
#define LIST_H (UGUI_SCREEN_H - LIST_Y - 40)
#define ROW_H 34
static ugui_ulist_item_t s_items[] = {
{ "Brightness", NULL, 1u, UGUI_ULIST_ACC_NONE, 0u, NULL },
{ "Auto-brightness", NULL, 2u, UGUI_ULIST_ACC_SWITCH, UGUI_ULIST_CHECKED, NULL },
{ "Night mode", NULL, 3u, UGUI_ULIST_ACC_SWITCH, 0u, NULL },
};
#define ITEM_N ((uint16_t)(sizeof(s_items) / sizeof(s_items[0])))
#endif
/* --- local helpers & event callbacks --- */
static void on_bright_change(int16_t val_lo, int16_t val_hi, void* ctx)
{
(void)val_hi; (void)ctx;
#ifdef UGUI_ENABLE_FONT
(void)snprintf(s_status_str, sizeof(s_status_str), "brightness: %d%%", (int)val_lo);
ugui_widget_invalidate(&s_status);
#else
(void)val_lo;
#endif
}
static void on_action(ugui_widget_t* w, uint16_t idx, void* ctx)
{
(void)w; (void)ctx;
if ((idx == 0u) || (idx >= ITEM_N)) { return; }
const ugui_ulist_item_t* it = &s_items[idx];
#ifdef UGUI_ENABLE_FONT
(void)snprintf(s_status_str, sizeof(s_status_str), "%s: %s", it->label,
((it->flags & UGUI_ULIST_CHECKED) != 0u) ? "on" : "off");
ugui_widget_invalidate(&s_status);
#endif
printf("[ulist.embed] row %u ('%s') activated\n", (unsigned)idx, it->label);
}
/* --- build it (in your screen's init) --- */
ugui_widget_ulist_init(&s_root, &s_list, &s_list_ext);
ugui_widget_set_area(&s_list, LIST_X, LIST_Y, LIST_W, LIST_H);
ugui_widget_set_id(&s_list, WDGT_ID_DEMO);
ugui_widget_ulist_set_font(&s_list, &lexend_14pt_2bpp);
ugui_widget_ulist_set_geometry(&s_list, ROW_H, 0u, 0u, 0u);
/* The embedded brightness slider: parent = the LIST, x/width/height set
* here, vertical position + show/hide owned by the list (uList embed
* contract). LEFT/RIGHT reach it when its row is focused. */
ugui_widget_slider_init(&s_list, &s_bright, &s_bright_ext);
ugui_widget_set_area(&s_bright, (int16_t)(LIST_W - 190), 0, 180, (int16_t)(ROW_H - 6));
ugui_widget_set_id(&s_bright, WDGT_ID_SLOT0);
ugui_widget_slider_set_range(&s_bright, 0, 100);
ugui_widget_slider_set_value(&s_bright, 65);
ugui_widget_slider_set_callback(&s_bright, on_bright_change, NULL);
ugui_widget_ulist_set_items(&s_list, s_items, ITEM_N);
ugui_widget_ulist_set_on_action(&s_list, on_action, NULL);
ugui_screen_add_widget(&s_screen, &s_list);
/* Embedded widgets must ALSO be screen-registered so their dirty flags
* reach the render pass (uList embed convention — the list still owns
* their position and drawing order). */
ugui_screen_add_widget(&s_screen, &s_bright);
From this widget to a working app
Everything above is the widget-specific code. The scaffolding it drops into — storage, HAL, main loop, display-mode bring-up — is the same for every app: copy it from the Application Skeleton.