Skip to content

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.

Default Dropdown

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

Long Lists: Windowing & Scrollbar

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

Themes, Geometry & Open-Upward

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

Settings Form with Live Status

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