Skip to content

VContainer Widget

The VContainer is the lean vertical-scroll container: vertical scroll plus a right-edge bar, no horizontal axis and no keypad focus-scope overhead — the small-footprint choice for a plain scrollable column or list. Init installs the whole look (surface fill, themed 1 px border, accent thumb over a subtle track groove) and set_area() places it; parent a column of rows taller than the panel and the bar appears automatically. set_style() restyles the frame (fill / border / width / corner radius) and set_scrollbar_colors() restyles the bar. Unlike the full Container, each child keeps its OWN keypad focus stop — TAB moves between them and the vcontainer's per-frame tick auto-scrolls the focused one into view, no focus-scope code needed.

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 VContainer

Two calls make it usable: ugui_widget_vcontainer_init() installs the default look and set_area() places it. A column of rows taller than the panel (row count derived from the panel height at init) makes the scrollbar appear automatically — drag the panel (touch) or TAB to it and press up/down (keypad) to drive it.

Default VContainer

/* --- 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_VCONTAINER)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t          s_vc;
static ugui_vcontainer_ext_t  s_vc_ext;
#define MAX_ROWS    ((uint8_t)UGUI_MAX_CHILDREN)
#define ROW_EXTRA   2u
static ugui_widget_t   s_row[MAX_ROWS];
static ugui_box_ext_t  s_row_ext[MAX_ROWS];
#endif
#if defined(UGUI_WIDGET_ENABLE_VCONTAINER)
static ugui_text_cfg_t s_row_txt[MAX_ROWS];
static char            s_row_label[MAX_ROWS][12];
#endif
#if defined(UGUI_WIDGET_ENABLE_VCONTAINER)
#define PAN_W   ((int16_t)200)
#define PAN_X   ((int16_t)((UGUI_SCREEN_W - PAN_W) / 2))
#define PAN_Y   ((int16_t)(38 + 2))
#define PAN_H   ((int16_t)(UGUI_SCREEN_H - PAN_Y - 8))
#define ROW_X       8
#define ROW_W       ((int16_t)(PAN_W - ROW_X - 12))   /* leave room for the bar */
#define ROW_H       30
#define ROW_TOP     8
#define ROW_STEP    36
#endif

/* --- local helpers & event callbacks --- */
static uint8_t fill_row_count(int16_t avail_px, int16_t step_px, uint8_t extra, uint8_t cap)
{
    int32_t fit = (step_px > 0) ? ((int32_t)avail_px / (int32_t)step_px) : 0;
    if (fit < 0) { fit = 0; }
    int32_t n = fit + (int32_t)extra;
    if (n < 1) { n = 1; }
    if (n > (int32_t)cap) { n = (int32_t)cap; }
    return (uint8_t)n;
}

/* --- build it (in your screen's init) --- */
ugui_widget_vcontainer_init(&s_root, &s_vc, &s_vc_ext);
ugui_widget_set_area(&s_vc, PAN_X, PAN_Y, PAN_W, PAN_H);
ugui_widget_set_id(&s_vc, WDGT_ID_DEMO);
ugui_screen_add_widget(&s_screen, &s_vc);

const ugui_color_t row_fill =
    ugui_color_blend(UGUI_THEME_BORDER, UGUI_THEME_SURFACE, 56u);
uint8_t row_count = fill_row_count(PAN_H, ROW_STEP, ROW_EXTRA, MAX_ROWS);
for (uint8_t i = 0u; i < row_count; i++) {
    ugui_widget_box_init(&s_vc, &s_row[i], &s_row_ext[i]);
    ugui_widget_set_area(&s_row[i], ROW_X, (int16_t)(ROW_TOP + i * ROW_STEP),
                         ROW_W, ROW_H);
    ugui_widget_set_id(&s_row[i], (uint8_t)(WDGT_ID_SLOT0 + i));
    ugui_widget_box_set_style(&s_row[i], true, 5u, 0u, row_fill, UGUI_THEME_ACCENT);
#ifdef UGUI_ENABLE_FONT
    (void)snprintf(s_row_label[i], sizeof(s_row_label[i]), "Item %u",
                   (unsigned)(i + 1u));
    ugui_text_cfg_init(&s_row_txt[i], s_row_label[i], &lexend_14pt_2bpp,
                       UGUI_THEME_TEXT,
                       (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
                       (ugui_point_t){ 10, 0 }, 1u);
    ugui_widget_box_set_text(&s_row[i], &s_row_txt[i]);
#endif
    ugui_screen_add_widget(&s_screen, &s_row[i]);
}

ugui_widget_vcontainer_relayout(&s_vc);

Styled (rounded, framed, custom bar)

The same scrollable column, restyled: set_style() gives it a soft framed-card look (2 px border, 14 px corner radius) and set_scrollbar_colors() recolours the bar. The large corner radius makes the scrollbar geometry visible — the bar sits just inside the border and its ends are trimmed by the radius, never painting over the rounded corners or frame.

Styled (rounded, framed, custom bar)

/* --- 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_VCONTAINER)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t          s_vc;
static ugui_vcontainer_ext_t  s_vc_ext;
#define MAX_ROWS    ((uint8_t)UGUI_MAX_CHILDREN)
#define ROW_EXTRA   2u
static ugui_widget_t   s_row[MAX_ROWS];
static ugui_box_ext_t  s_row_ext[MAX_ROWS];
#endif
#if defined(UGUI_WIDGET_ENABLE_VCONTAINER)
static ugui_text_cfg_t s_row_txt[MAX_ROWS];
static char            s_row_label[MAX_ROWS][12];
#endif
#if defined(UGUI_WIDGET_ENABLE_VCONTAINER)
#define PAN_W   ((int16_t)220)
#define PAN_X   ((int16_t)((UGUI_SCREEN_W - PAN_W) / 2))
#define PAN_Y   ((int16_t)(38 + 2))
#define PAN_H   ((int16_t)(UGUI_SCREEN_H - PAN_Y - 8))
#define ROW_X       14
#define ROW_W       ((int16_t)(PAN_W - ROW_X - 16))
#define ROW_H       30
#define ROW_TOP     14
#define ROW_STEP    36
#endif

/* --- local helpers & event callbacks --- */
static uint8_t fill_row_count(int16_t avail_px, int16_t step_px, uint8_t extra, uint8_t cap)
{
    int32_t fit = (step_px > 0) ? ((int32_t)avail_px / (int32_t)step_px) : 0;
    if (fit < 0) { fit = 0; }
    int32_t n = fit + (int32_t)extra;
    if (n < 1) { n = 1; }
    if (n > (int32_t)cap) { n = (int32_t)cap; }
    return (uint8_t)n;
}

/* --- build it (in your screen's init) --- */
const ugui_color_t groove =
    ugui_color_blend(UGUI_THEME_BORDER, UGUI_THEME_SURFACE, 96u);
const ugui_color_t row_fill =
    ugui_color_blend(UGUI_THEME_BORDER, UGUI_THEME_SURFACE, 56u);

ugui_widget_vcontainer_init(&s_root, &s_vc, &s_vc_ext);
ugui_widget_set_area(&s_vc, PAN_X, PAN_Y, PAN_W, PAN_H);
ugui_widget_set_id(&s_vc, WDGT_ID_DEMO);
ugui_widget_vcontainer_set_style(&s_vc, UGUI_THEME_SURFACE, UGUI_THEME_ACCENT, 2u, 14u);
ugui_widget_vcontainer_set_scrollbar_colors(&s_vc, UGUI_THEME_ACCENT, groove);
s_vc_ext.scrollbar_w = 6u;                  /* wider thumb (no dedicated setter) */
ugui_screen_add_widget(&s_screen, &s_vc);

uint8_t row_count = fill_row_count(PAN_H, ROW_STEP, ROW_EXTRA, MAX_ROWS);
for (uint8_t i = 0u; i < row_count; i++) {
    ugui_widget_box_init(&s_vc, &s_row[i], &s_row_ext[i]);
    ugui_widget_set_area(&s_row[i], ROW_X, (int16_t)(ROW_TOP + i * ROW_STEP),
                         ROW_W, ROW_H);
    ugui_widget_set_id(&s_row[i], (uint8_t)(WDGT_ID_SLOT0 + i));
    ugui_widget_box_set_style(&s_row[i], true, 6u, 0u, row_fill, UGUI_THEME_ACCENT);
#ifdef UGUI_ENABLE_FONT
    (void)snprintf(s_row_label[i], sizeof(s_row_label[i]), "Item %u",
                   (unsigned)(i + 1u));
    ugui_text_cfg_init(&s_row_txt[i], s_row_label[i], &lexend_14pt_2bpp,
                       UGUI_THEME_TEXT,
                       (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
                       (ugui_point_t){ 10, 0 }, 1u);
    ugui_widget_box_set_text(&s_row[i], &s_row_txt[i]);
#endif
    ugui_screen_add_widget(&s_screen, &s_row[i]);
}

ugui_widget_vcontainer_relayout(&s_vc);

Mixed-Control Settings List

The complex case: a scrollable column of REAL, varied controls — toggles, sliders, checkboxes, an arc knob and a button — taller than the panel. Each control is its own keypad focus stop (unlike the full Container's single proxy stop); TAB moves between them and the vcontainer's tick auto-scrolls the focused one into view. Touch works too: use a control directly, or drag the panel to scroll.

Mixed-Control Settings List

/* --- 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_VCONTAINER)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t          s_vc;
static ugui_vcontainer_ext_t  s_vc_ext;
static ugui_widget_t     s_wifi;   static ugui_toggle_ext_t s_wifi_ext;
static ugui_widget_t     s_dark;   static ugui_toggle_ext_t s_dark_ext;
static ugui_widget_t     s_bright; static ugui_slider_ext_t s_bright_ext;
static ugui_widget_t     s_vol;    static ugui_slider_ext_t s_vol_ext;
static ugui_widget_t     s_apply;  static ugui_box_ext_t    s_apply_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_VCONTAINER)
static ugui_text_cfg_t   s_apply_txt;
#endif
#if defined(UGUI_WIDGET_ENABLE_VCONTAINER)
static ugui_widget_t     s_bt;     static ugui_checkbox_ext_t s_bt_ext;
static ugui_widget_t     s_sync;   static ugui_checkbox_ext_t s_sync_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_VCONTAINER)
static ugui_text_cfg_t   s_bt_txt, s_sync_txt;
#endif
#if defined(UGUI_WIDGET_ENABLE_VCONTAINER)
static ugui_widget_t     s_arc;    static ugui_arc_ext_t s_arc_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_VCONTAINER)
#define PAN_W   ((int16_t)236)
#define PAN_X   ((int16_t)((UGUI_SCREEN_W - PAN_W) / 2))
#define PAN_Y   ((int16_t)(38 + 2))
#define PAN_H   ((int16_t)(UGUI_SCREEN_H - PAN_Y - 8))
#define ROW_TOP   10
#define ROW_STEP  40
#define LBL_X     12
#define LBL_W     108
#define FULL_W    ((int16_t)(PAN_W - LBL_X - 16))   /* checkbox / button width */
#endif

/* --- local helpers & event callbacks --- */
static void on_bool(bool state, void* ctx)
{
    printf("[vcontainer.list] %s = %s\n", (const char*)ctx, state ? "ON" : "OFF");
}

static void on_slider(int16_t lo, int16_t hi, void* ctx)
{
    (void)hi;
    printf("[vcontainer.list] %s = %d\n", (const char*)ctx, (int)lo);
}

static void on_apply(uint8_t id, ugui_event_t ev)
{
    (void)id;
    if (ev == UGUI_EVENT_CLICK) { printf("[vcontainer.list] APPLY clicked\n"); }
}

static void on_arc(int16_t value, void* ctx)
{
    printf("[vcontainer.list] %s = %d\n", (const char*)ctx, (int)value);
}

static void row_label(int16_t y, const char* text) { (void)y; (void)text; }

/* --- build it (in your screen's init) --- */
/* Style BEFORE adding children so each captures the surface colour for AA. */
ugui_widget_vcontainer_init(&s_root, &s_vc, &s_vc_ext);
ugui_widget_set_area(&s_vc, PAN_X, PAN_Y, PAN_W, PAN_H);
ugui_widget_set_id(&s_vc, WDGT_ID_DEMO);
ugui_widget_vcontainer_set_style(&s_vc, UGUI_THEME_SURFACE, UGUI_THEME_ACCENT, 1u, 8u);
ugui_screen_add_widget(&s_screen, &s_vc);

const int16_t tgl_x = (int16_t)(PAN_W - 52 - 16);
const int16_t sld_x = (int16_t)(LBL_X + LBL_W - 4);
const int16_t sld_w = (int16_t)(PAN_W - sld_x - 16);
int16_t y = ROW_TOP;

/* 1. toggle — Wi-Fi */
row_label(y, "Wi-Fi");
ugui_widget_toggle_init(&s_vc, &s_wifi, &s_wifi_ext);
ugui_widget_set_area(&s_wifi, tgl_x, y, 52, 28);
ugui_widget_toggle_set_colors(&s_wifi,
                              ugui_color_blend(UGUI_THEME_BORDER, UGUI_THEME_SURFACE, 96u),
                              UGUI_THEME_OK, UGUI_THEME_KNOB);
ugui_widget_toggle_set_state(&s_wifi, true);
ugui_widget_toggle_set_callback(&s_wifi, on_bool, (void*)"Wi-Fi");
ugui_screen_add_widget(&s_screen, &s_wifi);
y = (int16_t)(y + ROW_STEP);

/* 2. slider — Brightness */
row_label(y, "Brightness");
ugui_widget_slider_init(&s_vc, &s_bright, &s_bright_ext);
ugui_widget_set_area(&s_bright, sld_x, (int16_t)(y + 2), sld_w, 24);
ugui_widget_slider_set_range(&s_bright, 0, 100);
ugui_widget_slider_set_value(&s_bright, 70);
ugui_widget_slider_set_callback(&s_bright, on_slider, (void*)"Brightness");
ugui_screen_add_widget(&s_screen, &s_bright);
y = (int16_t)(y + ROW_STEP);

/* 3. checkbox — Bluetooth (built-in text) */
#ifdef UGUI_WIDGET_ENABLE_CHECKBOX
ugui_widget_checkbox_init(&s_vc, &s_bt, &s_bt_ext);
ugui_widget_set_area(&s_bt, LBL_X, y, FULL_W, 28);
ugui_widget_checkbox_set_callback(&s_bt, on_bool, (void*)"Bluetooth");
#ifdef UGUI_ENABLE_FONT
ugui_text_cfg_init(&s_bt_txt, "Bluetooth", &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                   (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
                   (ugui_point_t){ 0, 0 }, 1u);
ugui_widget_checkbox_set_text(&s_bt, &s_bt_txt);
#endif
ugui_screen_add_widget(&s_screen, &s_bt);
y = (int16_t)(y + ROW_STEP);
#endif

/* 4. arc knob — Level */
#ifdef UGUI_WIDGET_ENABLE_ARC
row_label(y, "Level");
ugui_widget_arc_init(&s_vc, &s_arc, &s_arc_ext);
ugui_widget_set_area(&s_arc, (int16_t)(PAN_W - 34 - 18), (int16_t)(y - 3), 34, 34);
ugui_widget_arc_set_range(&s_arc, 0, 100);
ugui_widget_arc_set_value(&s_arc, 45);
ugui_widget_arc_set_thickness(&s_arc, 5u);
ugui_widget_arc_set_step(&s_arc, 5);
ugui_widget_arc_set_colors(&s_arc, UGUI_THEME_ACCENT,
                           ugui_color_blend(UGUI_THEME_BORDER, UGUI_THEME_SURFACE, 96u),
                           UGUI_THEME_SURFACE);
ugui_widget_arc_set_callback(&s_arc, on_arc, (void*)"Level");
ugui_screen_add_widget(&s_screen, &s_arc);
y = (int16_t)(y + ROW_STEP);
#endif

/* 5. toggle — Dark mode */
row_label(y, "Dark mode");
ugui_widget_toggle_init(&s_vc, &s_dark, &s_dark_ext);
ugui_widget_set_area(&s_dark, tgl_x, y, 52, 28);
ugui_widget_toggle_set_colors(&s_dark,
                              ugui_color_blend(UGUI_THEME_BORDER, UGUI_THEME_SURFACE, 96u),
                              UGUI_THEME_OK, UGUI_THEME_KNOB);
ugui_widget_toggle_set_callback(&s_dark, on_bool, (void*)"Dark mode");
ugui_screen_add_widget(&s_screen, &s_dark);
y = (int16_t)(y + ROW_STEP);

/* 6. slider — Volume */
row_label(y, "Volume");
ugui_widget_slider_init(&s_vc, &s_vol, &s_vol_ext);
ugui_widget_set_area(&s_vol, sld_x, (int16_t)(y + 2), sld_w, 24);
ugui_widget_slider_set_range(&s_vol, 0, 100);
ugui_widget_slider_set_value(&s_vol, 40);
ugui_widget_slider_set_callback(&s_vol, on_slider, (void*)"Volume");
ugui_screen_add_widget(&s_screen, &s_vol);
y = (int16_t)(y + ROW_STEP);

/* 7. checkbox — Auto-sync (built-in text) */
#ifdef UGUI_WIDGET_ENABLE_CHECKBOX
ugui_widget_checkbox_init(&s_vc, &s_sync, &s_sync_ext);
ugui_widget_set_area(&s_sync, LBL_X, y, FULL_W, 28);
ugui_widget_checkbox_set_state(&s_sync, true);
ugui_widget_checkbox_set_callback(&s_sync, on_bool, (void*)"Auto-sync");
#ifdef UGUI_ENABLE_FONT
ugui_text_cfg_init(&s_sync_txt, "Auto-sync", &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                   (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
                   (ugui_point_t){ 0, 0 }, 1u);
ugui_widget_checkbox_set_text(&s_sync, &s_sync_txt);
#endif
ugui_screen_add_widget(&s_screen, &s_sync);
y = (int16_t)(y + ROW_STEP);
#endif

/* 8. button — Reset (built-in text) */
ugui_widget_box_init(&s_vc, &s_apply, &s_apply_ext);
ugui_widget_set_area(&s_apply, LBL_X, (int16_t)(y + 1), FULL_W, 30);
ugui_widget_box_set_style(&s_apply, true, 6u, 1u, UGUI_THEME_ACCENT, UGUI_THEME_ACCENT);
ugui_widget_set_event_cb(&s_apply, on_apply,
                         UGUI_EMASK_CLICK | UGUI_EMASK_DOWN | UGUI_EMASK_UP
                         | UGUI_EMASK_KEY_CONFIRM);
#ifdef UGUI_ENABLE_FONT
ugui_text_cfg_init(&s_apply_txt, "Reset", &lexend_14pt_2bpp, UGUI_THEME_KNOB,
                   UGUI_ALIGN_CENTER, (ugui_point_t){ 0, 0 }, 1u);
ugui_widget_box_set_text(&s_apply, &s_apply_txt);
#endif
ugui_screen_add_widget(&s_screen, &s_apply);

ugui_widget_vcontainer_relayout(&s_vc);

#if UGUI_ENABLE_KEYPAD
/* Each control is its own focus stop; the vcontainer's tick scrolls the
 * focused one into view. Order = visual top-to-bottom. */
ugui_screen_add_focus(&s_screen, &s_wifi);
ugui_screen_add_focus(&s_screen, &s_bright);
#ifdef UGUI_WIDGET_ENABLE_CHECKBOX
ugui_screen_add_focus(&s_screen, &s_bt);
#endif
#ifdef UGUI_WIDGET_ENABLE_ARC
ugui_screen_add_focus(&s_screen, &s_arc);
#endif
ugui_screen_add_focus(&s_screen, &s_dark);
ugui_screen_add_focus(&s_screen, &s_vol);
#ifdef UGUI_WIDGET_ENABLE_CHECKBOX
ugui_screen_add_focus(&s_screen, &s_sync);
#endif
ugui_screen_add_focus(&s_screen, &s_apply);
#endif


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.