Skip to content

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.

Default: A Settings Menu

/* --- 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.

Sections, Accessories & an Accordion Header

/* --- 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.

A Real Child Widget Hosted in a Row

/* --- 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.