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.

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

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

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