Dropdown Widget¶
The Dropdown is a single-select combo box — a themed header row that
opens a scrollable overlay list, the compact alternative to a row of radio
buttons or a full picker screen. Init installs the whole look (theme-tracked
colours, rounded box, chevron) at a settable size; set_font() gives it
text and set_items() fills the list from a caller-owned string array
(never copied) with a starting selection. Tapping the header (or CONFIRM
while focused) opens the popup panel — a root-level overlay so it can spill
past its column — Up/Down (or a drag) moves the highlight, and picking a row
(or CONFIRM again) commits and closes; the header always shows the current
choice. set_theme() replaces every colour with a fixed six-colour palette
that ignores the app theme, set_geometry() tunes row height / visible-row
count / corner radius / popup width, and the popup auto-flips ABOVE the
header when there is no room below — it never paints off-screen.
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 Dropdown¶
Four calls make a working select box: ugui_widget_dropdown_init() installs the whole look, set_font() gives it text, set_items() fills the list (started on "Medium"), set_area() places it. Tapping the header opens the popup list overlay — no colour or geometry overrides anywhere.

/* --- 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_DROPDOWN)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_dd;
static ugui_dropdown_ext_t s_dd_ext;
static const char* const k_items[] = { "Low", "Medium", "High" };
#define DD_W 200
#define DD_H 36
#define DD_X ((UGUI_SCREEN_W - DD_W) / 2)
#define DD_Y (38 + 46)
#endif
/* --- build it (in your screen's init) --- */
ugui_widget_dropdown_init(&s_root, &s_dd, &s_dd_ext, &s_screen);
ugui_widget_set_area(&s_dd, DD_X, DD_Y, DD_W, DD_H);
ugui_widget_set_id(&s_dd, WDGT_ID_DEMO);
#ifdef UGUI_ENABLE_FONT
ugui_widget_dropdown_set_font(&s_dd, &lexend_14pt_2bpp);
#endif
ugui_widget_dropdown_set_items(&s_dd, k_items, 3u, 1u /* start on "Medium" */);
ugui_screen_add_widget(&s_screen, &s_dd);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_dd);
#endif
Long Lists: Windowing & Scrollbar¶
Two dropdowns whose item counts exceed what fits: Month (12 items) and Time zone (24 items) each open showing only max_visible rows (5 by default) with a scrollbar thumb, windowing to keep the highlight visible. Scroll with the arrow keys or by dragging a finger over the open 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_SLOT2 = 10, WDGT_ID_SLOT3
};
/* --- 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_DROPDOWN)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_month, s_zone;
static ugui_dropdown_ext_t s_month_ext, s_zone_ext;
static const char* const k_month[] = {
"January", "February", "March", "April", "May", "June",
"July", "August", "September", "October", "November", "December"
};
static const char* const k_zone[] = {
"UTC-11", "UTC-10", "UTC-09", "UTC-08", "UTC-07", "UTC-06",
"UTC-05", "UTC-04", "UTC-03", "UTC-02", "UTC-01", "UTC+00",
"UTC+01", "UTC+02", "UTC+03", "UTC+04", "UTC+05", "UTC+06",
"UTC+07", "UTC+08", "UTC+09", "UTC+10", "UTC+11", "UTC+12"
};
#define COL_W ((int16_t)((UGUI_SCREEN_W - 3 * 30) / 2)) /* 30px gutters */
#define COL_A_X 30
#define COL_B_X ((int16_t)(COL_A_X + COL_W + 30))
#define DD_Y (38 + 34)
#define DD_H 34
#endif
/* --- local helpers & event callbacks --- */
static void dropdown_add(ugui_widget_t* w, ugui_dropdown_ext_t* ext, uint8_t id,
int16_t x, const char* const* items, uint16_t count)
{
ugui_widget_dropdown_init(&s_root, w, ext, &s_screen);
ugui_widget_set_area(w, x, DD_Y, COL_W, DD_H);
ugui_widget_set_id(w, id);
#ifdef UGUI_ENABLE_FONT
ugui_widget_dropdown_set_font(w, &lexend_14pt_2bpp);
#endif
ugui_widget_dropdown_set_items(w, items, count, 0u);
ugui_screen_add_widget(&s_screen, w);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, w);
#endif
}
/* --- build it (in your screen's init) --- */
dropdown_add(&s_month, &s_month_ext, WDGT_ID_SLOT2, COL_A_X, k_month, 12u);
dropdown_add(&s_zone, &s_zone_ext, WDGT_ID_SLOT3, COL_B_X, k_zone, 24u);
Themes, Geometry & Open-Upward¶
Three rows exercise the styling and geometry knobs: a full custom six-colour palette via set_theme(), taller rows with fewer visible rows plus key-wrap via set_geometry(), and — anchored near the bottom edge — a popup that FLIPS above the header with a wider-than-header popup_w. The flip is the boundary-handling contract: the list never paints off-screen.

/* --- 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_SLOT3 = 10, WDGT_ID_SLOT4, WDGT_ID_SLOT5
};
/* --- 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_DROPDOWN)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_theme, s_geom, s_up;
static ugui_dropdown_ext_t s_theme_ext, s_geom_ext, s_up_ext;
static const char* const k_mode[] = { "Off", "Eco", "Comfort", "Boost", "Auto" };
static const char* const k_color[] = { "Red", "Green", "Blue", "Cyan",
"Magenta", "Yellow" };
static const char* const k_chan[] = { "Channel 1", "Channel 2", "Channel 3",
"Channel 4", "Channel 5", "Channel 6",
"Channel 7", "Channel 8" };
#define DD_X 190
#define DD_W 240
#define DD_H 34
#define R1_Y (38 + 16)
#define R2_Y (38 + 74)
#define R3_Y ((int16_t)(UGUI_SCREEN_H - 54)) /* near the bottom → opens up */
#endif
/* --- local helpers & event callbacks --- */
static void dropdown_base(ugui_widget_t* w, ugui_dropdown_ext_t* ext, uint8_t id,
int16_t x, int16_t y, int16_t w_px,
const char* const* items, uint16_t count)
{
ugui_widget_dropdown_init(&s_root, w, ext, &s_screen);
ugui_widget_set_area(w, x, y, w_px, DD_H);
ugui_widget_set_id(w, id);
#ifdef UGUI_ENABLE_FONT
ugui_widget_dropdown_set_font(w, &lexend_14pt_2bpp);
#endif
ugui_widget_dropdown_set_items(w, items, count, 0u);
ugui_screen_add_widget(&s_screen, w);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, w);
#endif
}
/* --- build it (in your screen's init) --- */
dropdown_base(&s_theme, &s_theme_ext, WDGT_ID_SLOT3, DD_X, R1_Y, DD_W, k_mode, 5u);
ugui_widget_dropdown_set_theme(&s_theme,
ugui_color_hex(0x06231Au), /* bg */
ugui_color_hex(0x00E0A0u), /* border */
ugui_color_hex(0xD0FFE8u), /* text */
ugui_color_hex(0x00A070u), /* focus fill */
UGUI_COLOR_WHITE, /* focus text */
ugui_color_hex(0x00E0A0u)); /* chevron/bar */
/* 2. Taller rows, only 4 visible (so 6 items scroll), and key-wrap on. */
dropdown_base(&s_geom, &s_geom_ext, WDGT_ID_SLOT4, DD_X, R2_Y, DD_W, k_color, 6u);
ugui_widget_dropdown_set_geometry(&s_geom, 34u /* row_h */, 4u /* max_visible */,
0xFFu /* keep corner radius */, 0 /* popup=hdr */);
s_geom_ext.key_wrap = 1u;
/* 3. Near the bottom edge → the popup flips ABOVE the header; popup_w is set
* wider than the header to show the width override + screen clamp. */
dropdown_base(&s_up, &s_up_ext, WDGT_ID_SLOT5, DD_X, R3_Y, DD_W, k_chan, 8u);
ugui_widget_dropdown_set_geometry(&s_up, 0u, 0u, 0xFFu, (int16_t)(DD_W + 40));
Settings Form with Live Status¶
The realistic case: four dropdowns coexist as a preferences form, each wired to an on_select callback that writes the committed choice into a shared status bar — the header shows the selection, the bar shows the app REACTING to it. All four use plain library defaults, so the whole form tracks the active theme.

/* --- 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_SLOT4 = 10, WDGT_ID_DEMO
};
/* --- 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_DROPDOWN)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_status;
static ugui_box_ext_t s_status_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_DROPDOWN)
static ugui_text_cfg_t s_status_text;
#endif
#if defined(UGUI_WIDGET_ENABLE_DROPDOWN)
#define ROW_N 4
static ugui_widget_t s_cap[ROW_N];
static ugui_label_ext_t s_cap_ext[ROW_N];
static ugui_widget_t s_dd[ROW_N];
static ugui_dropdown_ext_t s_dd_ext[ROW_N];
static const char* const k_units[] = { "Celsius", "Fahrenheit", "Kelvin" };
static const char* const k_lang[] = { "English", "Spanish", "German", "French" };
static const char* const k_bright[] = { "25%", "50%", "75%", "100%" };
static const char* const k_refresh[]= { "30 Hz", "60 Hz", "90 Hz", "120 Hz" };
static const char* const k_row_name[ROW_N] = { "Units", "Language",
"Brightness", "Refresh" };
static const char* const* const k_row_items[ROW_N] = {
k_units, k_lang, k_bright, k_refresh
};
static const uint16_t k_row_count[ROW_N] = { 3u, 4u, 4u, 4u };
static uint8_t k_row_idx[ROW_N] = { 0u, 1u, 2u, 3u };
static char s_status_buf[48] = "Pick any setting to see it here";
#define CAP_X 34
#define CAP_W 150
#define DD_X 200
#define DD_W 240
#define DD_H 30
#define ROW0_Y (38 + 8)
#define ROW_STEP 40
#define ROW_Y(i) ((int16_t)(ROW0_Y + (i) * ROW_STEP))
#endif
/* --- local helpers & event callbacks --- */
static void on_pick(uint16_t idx, void* ctx)
{
if (ctx == NULL) { return; }
uint8_t row = *(const uint8_t*)ctx;
if (row >= (uint8_t)ROW_N) { return; }
if (idx >= k_row_count[row]) { return; }
#ifdef UGUI_ENABLE_FONT
(void)snprintf(s_status_buf, sizeof(s_status_buf), "%s: %s",
k_row_name[row], k_row_items[row][idx]);
ugui_widget_box_update_text_str(&s_status, s_status_buf);
#endif
}
static void add_label(ugui_widget_t* w, ugui_label_ext_t* ext, int16_t x, int16_t y,
int16_t cw, int16_t ch, const char* str, ugui_align_t align)
{
ugui_widget_label_init(&s_root, w, ext);
ugui_widget_set_area(w, x, y, cw, ch);
ugui_widget_label_set_style(w, false, UGUI_THEME_BG, str,
&lexend_14pt_2bpp, UGUI_THEME_TEXT, align, 1u);
ugui_screen_add_widget(&s_screen, w);
}
static void row_add(uint8_t i)
{
const ugui_align_t la = (ugui_align_t)((uint16_t)UGUI_ALIGN_LEFT |
(uint16_t)UGUI_ALIGN_V_CENTER);
add_label(&s_cap[i], &s_cap_ext[i], CAP_X, ROW_Y(i), CAP_W, DD_H,
k_row_name[i], la);
ugui_widget_dropdown_init(&s_root, &s_dd[i], &s_dd_ext[i], &s_screen);
ugui_widget_set_area(&s_dd[i], DD_X, ROW_Y(i), DD_W, DD_H);
ugui_widget_set_id(&s_dd[i], (uint8_t)(WDGT_ID_SLOT4 + i));
#ifdef UGUI_ENABLE_FONT
ugui_widget_dropdown_set_font(&s_dd[i], &lexend_14pt_2bpp);
#endif
ugui_widget_dropdown_set_items(&s_dd[i], k_row_items[i], k_row_count[i], 0u);
ugui_widget_dropdown_set_on_select(&s_dd[i], on_pick, &k_row_idx[i]);
ugui_screen_add_widget(&s_screen, &s_dd[i]);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_dd[i]);
#endif
}
/* --- build it (in your screen's init) --- */
for (uint8_t i = 0u; i < (uint8_t)ROW_N; i++) { row_add(i); }
/* Live status bar (filled card) across the foot — on_pick (above) writes
* the committed choice here, proving the callback path fires. */
ugui_widget_box_init(&s_root, &s_status, &s_status_ext);
ugui_widget_set_area(&s_status, 10, (int16_t)(UGUI_SCREEN_H - 34),
(int16_t)(UGUI_SCREEN_W - 20), 26);
ugui_widget_set_id(&s_status, WDGT_ID_DEMO);
ugui_widget_box_set_style(&s_status, true, 4u, 1u, UGUI_THEME_SURFACE, UGUI_THEME_ACCENT);
#ifdef UGUI_ENABLE_FONT
ugui_text_cfg_init(&s_status_text, s_status_buf, &lexend_14pt_2bpp, UGUI_THEME_ACCENT,
UGUI_ALIGN_CENTER, (ugui_point_t){ 0, 0 }, 1u);
ugui_widget_box_set_text(&s_status, &s_status_text);
#endif
ugui_widget_set_enabled(&s_status, false); /* display-only */
ugui_screen_add_widget(&s_screen, &s_status);
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.