Skip to content

Fold Widget

The Fold is an accordion / tree menu — collapsible sections with an animated reveal, the shape of a real device settings tree. Init installs the whole look (theme-tracked colours, default size, tracker bar, and the slide/key/click/confirm/cancel event mask) at a settable size; set_font() gives it text and set_items() attaches the tree via a caller-owned ugui_fold_item_t array — expanded state and reveal-animation height live IN the array, so it must never be shared between two folds. Tapping (or ENTER on) a header expands or collapses it, animated; a leaf fires on_select. Rows can even carry an EMBEDDED child widget (a slider, a toggle) — parented to the fold, positioned by its own draw loop, hit-tested directly by the input layer, with a tap on the row's label forwarding to the embed. set_theme() replaces every colour with a fixed palette that ignores the app theme, and set_expanded() opens a section programmatically (e.g. the default section on screen entry).

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 Fold

Three calls make a working accordion menu: ugui_widget_fold_init() installs the whole look, size and event mask, set_font() gives it text, set_items() attaches the tree with every section collapsed. Tapping a header expands or collapses it with an animated reveal — the widget's own tick drives it, no per-frame app code.

Default Fold

/* --- 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_FOLD_MENU)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t   s_fold;
static ugui_fold_ext_t s_fold_ext;
static ugui_fold_item_t s_items[] = {
    { "Display",       0u, true,  2u,  0u, false, 0, NULL, 0u },
    { " Brightness",   1u, false, 0u, 10u, false, 0, NULL, 0u },
    { " Night mode",   1u, false, 0u, 11u, false, 0, NULL, 0u },
    { "Audio",         0u, true,  2u,  0u, false, 0, NULL, 0u },
    { " Volume",       1u, false, 0u, 20u, false, 0, NULL, 0u },
    { " Key click",    1u, false, 0u, 21u, false, 0, NULL, 0u },
    { "Network",       0u, true,  2u,  0u, false, 0, NULL, 0u },
    { " Wi-Fi",        1u, false, 0u, 30u, false, 0, NULL, 0u },
    { " Bluetooth",    1u, false, 0u, 31u, false, 0, NULL, 0u },
};
#define ITEM_N  ((uint8_t)(sizeof(s_items) / sizeof(s_items[0])))
#endif

/* --- local helpers & event callbacks --- */
static void on_select(uint8_t item_id, void* ctx)
{
    (void)ctx;
    printf("[fold.default] leaf selected: id=%u\n", (unsigned)item_id);
}

/* --- build it (in your screen's init) --- */
ugui_widget_fold_init(&s_root, &s_fold, &s_fold_ext);
ugui_widget_set_align(&s_fold, UGUI_ALIGN_CENTER, 0, 10);
ugui_widget_set_id(&s_fold, WDGT_ID_DEMO);
#ifdef UGUI_ENABLE_FONT
ugui_widget_fold_set_font(&s_fold, &lexend_14pt_2bpp);
#endif
ugui_widget_fold_set_items(&s_fold, s_items, ITEM_N);
ugui_widget_fold_set_on_select(&s_fold, on_select, NULL);
ugui_screen_add_widget(&s_screen, &s_fold);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_fold);
#endif

Deep Tree & Scroll

Six sections / 26 rows in a fixed-height viewport, so expanding a couple of sections overflows it. The tracker bar appears automatically on overflow and its thumb length/position track the content; key navigation auto-scrolls so the focused row always stays inside the viewport; collapsed sections cost no rows.

Deep Tree & 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_FOLD_MENU)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t   s_fold;
static ugui_fold_ext_t s_fold_ext;
static ugui_fold_item_t s_items[] = {
    { "Motor",           0u, true,  4u,  0u, false, 0, NULL, 0u },
    { " Speed limit",    1u, false, 0u, 10u, false, 0, NULL, 0u },
    { " Acceleration",   1u, false, 0u, 11u, false, 0, NULL, 0u },
    { " Direction",      1u, false, 0u, 12u, false, 0, NULL, 0u },
    { " Brake mode",     1u, false, 0u, 13u, false, 0, NULL, 0u },
    { "Sensors",         0u, true,  3u,  0u, false, 0, NULL, 0u },
    { " Calibrate",      1u, false, 0u, 20u, false, 0, NULL, 0u },
    { " Sample rate",    1u, false, 0u, 21u, false, 0, NULL, 0u },
    { " Filter",         1u, false, 0u, 22u, false, 0, NULL, 0u },
    { "Limits",          0u, true,  3u,  0u, false, 0, NULL, 0u },
    { " Max current",    1u, false, 0u, 30u, false, 0, NULL, 0u },
    { " Max temp",       1u, false, 0u, 31u, false, 0, NULL, 0u },
    { " Soft stop",      1u, false, 0u, 32u, false, 0, NULL, 0u },
    { "Comms",           0u, true,  4u,  0u, false, 0, NULL, 0u },
    { " CAN bitrate",    1u, false, 0u, 40u, false, 0, NULL, 0u },
    { " Node ID",        1u, false, 0u, 41u, false, 0, NULL, 0u },
    { " Heartbeat",      1u, false, 0u, 42u, false, 0, NULL, 0u },
    { " Timeout",        1u, false, 0u, 43u, false, 0, NULL, 0u },
    { "Logging",         0u, true,  3u,  0u, false, 0, NULL, 0u },
    { " Log level",      1u, false, 0u, 50u, false, 0, NULL, 0u },
    { " Export",         1u, false, 0u, 51u, false, 0, NULL, 0u },
    { " Clear log",      1u, false, 0u, 52u, false, 0, NULL, 0u },
    { "Service",         0u, true,  3u,  0u, false, 0, NULL, 0u },
    { " Self test",      1u, false, 0u, 60u, false, 0, NULL, 0u },
    { " Firmware",       1u, false, 0u, 61u, false, 0, NULL, 0u },
    { " Factory reset",  1u, false, 0u, 62u, false, 0, NULL, 0u },
};
#define ITEM_N  ((uint8_t)(sizeof(s_items) / sizeof(s_items[0])))
#define FOLD_X   10
#define FOLD_Y   (38 + 2)
#define FOLD_W   260
#define FOLD_H   ((int16_t)(UGUI_SCREEN_H - FOLD_Y - 10))
#endif

/* --- local helpers & event callbacks --- */
static void on_select(uint8_t item_id, void* ctx)
{
    (void)ctx;
    printf("[fold.scroll] leaf selected: id=%u\n", (unsigned)item_id);
}

/* --- build it (in your screen's init) --- */
ugui_widget_fold_init(&s_root, &s_fold, &s_fold_ext);
ugui_widget_set_area(&s_fold, FOLD_X, FOLD_Y, FOLD_W, FOLD_H);
ugui_widget_set_id(&s_fold, WDGT_ID_DEMO);
#ifdef UGUI_ENABLE_FONT
ugui_widget_fold_set_font(&s_fold, &lexend_14pt_2bpp);
#endif
ugui_widget_fold_set_items(&s_fold, s_items, ITEM_N);
ugui_widget_fold_set_on_select(&s_fold, on_select, NULL);
ugui_screen_add_widget(&s_screen, &s_fold);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_fold);
#endif

Styling & Programmatic State

LEFT: the whole look comes from init() (it tracks the active theme); the only extra call is set_expanded(0, true) so the first section plays its reveal animation as the screen appears. RIGHT: full custom skin — set_theme() replaces all seven colours with a fixed amber industrial palette and set_scrollbar() matches the tracker bar to it.

Styling & Programmatic State

/* --- 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_FOLD_MENU)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t   s_fold_a;            /* left: theme defaults        */
static ugui_fold_ext_t s_fold_a_ext;
static ugui_widget_t   s_fold_b;            /* right: custom set_theme     */
static ugui_fold_ext_t s_fold_b_ext;
static ugui_fold_item_t s_items_a[] = {
    { "Presets",       0u, true,  3u,  0u, false, 0, NULL, 0u },
    { " Eco",          1u, false, 0u, 10u, false, 0, NULL, 0u },
    { " Normal",       1u, false, 0u, 11u, false, 0, NULL, 0u },
    { " Sport",        1u, false, 0u, 12u, false, 0, NULL, 0u },
    { "Advanced",      0u, true,  2u,  0u, false, 0, NULL, 0u },
    { " Tuning",       1u, false, 0u, 20u, false, 0, NULL, 0u },
    { " Telemetry",    1u, false, 0u, 21u, false, 0, NULL, 0u },
};
#define ITEM_A_N  ((uint8_t)(sizeof(s_items_a) / sizeof(s_items_a[0])))
static ugui_fold_item_t s_items_b[] = {
    { "Furnace",       0u, true,  3u,  0u, false, 0, NULL, 0u },
    { " Setpoint",     1u, false, 0u, 10u, false, 0, NULL, 0u },
    { " Ramp rate",    1u, false, 0u, 11u, false, 0, NULL, 0u },
    { " Soak time",    1u, false, 0u, 12u, false, 0, NULL, 0u },
    { "Alarms",        0u, true,  2u,  0u, false, 0, NULL, 0u },
    { " Over-temp",    1u, false, 0u, 20u, false, 0, NULL, 0u },
    { " Door open",    1u, false, 0u, 21u, false, 0, NULL, 0u },
};
#define ITEM_B_N  ((uint8_t)(sizeof(s_items_b) / sizeof(s_items_b[0])))
#define COL_W    ((int16_t)((UGUI_SCREEN_W - 3 * 10) / 2))
#define COL0_X   ((int16_t)10)
#define COL1_X   ((int16_t)(2 * 10 + COL_W))
#define FOLD_Y   (38 + 24)
#define FOLD_H   ((int16_t)(UGUI_SCREEN_H - FOLD_Y - 10))
#endif

/* --- build it (in your screen's init) --- */
/* LEFT — library look untouched; open the first section programmatically
 * so the screen appears with its default section already revealed. */
ugui_widget_fold_init(&s_root, &s_fold_a, &s_fold_a_ext);
ugui_widget_set_area(&s_fold_a, COL0_X, FOLD_Y, COL_W, FOLD_H);
ugui_widget_set_id(&s_fold_a, WDGT_ID_SLOT2);
#ifdef UGUI_ENABLE_FONT
ugui_widget_fold_set_font(&s_fold_a, &lexend_14pt_2bpp);
#endif
ugui_widget_fold_set_items(&s_fold_a, s_items_a, ITEM_A_N);
ugui_widget_fold_set_expanded(&s_fold_a, 0u, true);
ugui_screen_add_widget(&s_screen, &s_fold_a);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_fold_a);
#endif

/* RIGHT — replace every colour with an amber industrial palette (fixed
 * hex values on purpose: this fold must NOT follow the theme switcher),
 * then match the tracker bar to it. */
ugui_widget_fold_init(&s_root, &s_fold_b, &s_fold_b_ext);
ugui_widget_set_area(&s_fold_b, COL1_X, FOLD_Y, COL_W, FOLD_H);
ugui_widget_set_id(&s_fold_b, WDGT_ID_SLOT3);
ugui_widget_fold_set_theme(&s_fold_b,
                           ugui_color_hex(0x201608u),   /* surface       */
                           ugui_color_hex(0x3A2A10u),   /* header fill   */
                           ugui_color_hex(0xFFC060u),   /* header text   */
                           ugui_color_hex(0xD0B080u),   /* leaf text     */
                           ugui_color_hex(0xFFA020u),   /* focus fill    */
                           ugui_color_hex(0x201608u),   /* focus text    */
                           ugui_color_hex(0xFFC060u),   /* chevron       */
#ifdef UGUI_ENABLE_FONT
                           &lexend_14pt_2bpp);
#else
                           NULL);
#endif
#if UGUI_FOLD_ENABLE_SCROLLBAR
ugui_widget_fold_set_scrollbar(&s_fold_b, true,
                               ugui_color_hex(0xFFA020u),   /* thumb */
                               ugui_color_hex(0x2E2110u));  /* track */
#endif
ugui_widget_fold_set_items(&s_fold_b, s_items_b, ITEM_B_N);
ugui_widget_fold_set_expanded(&s_fold_b, 4u, true);   /* open "Alarms" */
ugui_screen_add_widget(&s_screen, &s_fold_b);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_fold_b);
#endif

Settings Tree with Live Controls

The realistic case: sliders (Brightness, Volume) and toggles (Night mode, Mute, Wi-Fi, Bluetooth) live INSIDE fold rows as embedded child widgets — the fold positions and hit-tests them directly, and tapping a row's label forwards the click to its embed. Plain leaves (Profiles) confirm the classic on_select menu-command path. Every interaction writes a line into the status bar.

Settings Tree with Live Controls

/* --- 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_SLOT4
};

/* --- 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_FOLD_MENU) && defined(UGUI_WIDGET_ENABLE_TOGGLE_BUTTON) && defined(UGUI_WIDGET_ENABLE_SLIDER)
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_FOLD_MENU) && defined(UGUI_WIDGET_ENABLE_TOGGLE_BUTTON) && defined(UGUI_WIDGET_ENABLE_SLIDER)
static ugui_text_cfg_t   s_status_text;
#endif
#if defined(UGUI_WIDGET_ENABLE_FOLD_MENU) && defined(UGUI_WIDGET_ENABLE_TOGGLE_BUTTON) && defined(UGUI_WIDGET_ENABLE_SLIDER)
static ugui_widget_t   s_fold;
static ugui_fold_ext_t s_fold_ext;
#define SLIDER_N  2u
#define TOGGLE_N  4u
static ugui_widget_t     s_slider_w  [SLIDER_N];
static ugui_slider_ext_t s_slider_ext[SLIDER_N];
static ugui_widget_t     s_toggle_w  [TOGGLE_N];
static ugui_toggle_ext_t s_toggle_ext[TOGGLE_N];
static ugui_fold_item_t s_items[] = {
    { "Display",         0u, true,  2u,  0u, false, 0, NULL, 0u },
    { " Brightness",     1u, false, 0u, 10u, false, 0, NULL, 0u },  /* slider */
    { " Night mode",     1u, false, 0u, 11u, false, 0, NULL, 0u },  /* toggle */
    { "Sound",           0u, true,  2u,  0u, false, 0, NULL, 0u },
    { " Volume",         1u, false, 0u, 20u, false, 0, NULL, 0u },  /* slider */
    { " Mute",           1u, false, 0u, 21u, false, 0, NULL, 0u },  /* toggle */
    { "Network",         0u, true,  2u,  0u, false, 0, NULL, 0u },
    { " Wi-Fi",          1u, false, 0u, 30u, false, 0, NULL, 0u },  /* toggle */
    { " Bluetooth",      1u, false, 0u, 31u, false, 0, NULL, 0u },  /* toggle */
    { "Profiles",        0u, true,  2u,  0u, false, 0, NULL, 0u },
    { " Day profile",    1u, false, 0u, 40u, false, 0, NULL, 0u },  /* plain  */
    { " Night profile",  1u, false, 0u, 41u, false, 0, NULL, 0u },  /* plain  */
};
#define ITEM_N  ((uint8_t)(sizeof(s_items) / sizeof(s_items[0])))
#define FOLD_X    10
#define FOLD_Y    (38 + 4)
#define FOLD_W    290
#define STATUS_H  28
#define STATUS_Y  ((int16_t)(UGUI_SCREEN_H - STATUS_H - 10))
#define FOLD_H    ((int16_t)(STATUS_Y - FOLD_Y - 8))
#endif
#if defined(UGUI_WIDGET_ENABLE_FOLD_MENU) && defined(UGUI_WIDGET_ENABLE_TOGGLE_BUTTON) && defined(UGUI_WIDGET_ENABLE_SLIDER)
static char s_status_buf[64];
#endif

/* --- local helpers & event callbacks --- */
static void status_show(const char* name, const char* val)
{
#ifdef UGUI_ENABLE_FONT
    if (*name == ' ') { name++; }
    (void)snprintf(s_status_buf, sizeof(s_status_buf), "%s: %s", name, val);
    ugui_widget_box_update_text_str(&s_status, s_status_buf);
#else
    (void)name; (void)val;
#endif
}

static void on_embed_slider(int16_t val_lo, int16_t val_hi, void* ctx)
{
    (void)val_hi;
#ifdef UGUI_ENABLE_FONT
    char num[8];
    (void)snprintf(num, sizeof(num), "%d%%", (int)val_lo);
    status_show((const char*)ctx, num);
#else
    (void)val_lo; (void)ctx;
#endif
}

static void on_embed_toggle(bool state, void* ctx)
{
    status_show((const char*)ctx, state ? "ON" : "OFF");
}

static void on_select(uint8_t item_id, void* ctx)
{
    (void)ctx;
    uint8_t i;
    for (i = 0u; i < ITEM_N; i++) {
        if ((s_items[i].depth == 1u) && (s_items[i].id == item_id)) {
            status_show(s_items[i].label, "loaded");
            return;
        }
    }
}

/* --- build it (in your screen's init) --- */
ugui_widget_fold_init(&s_root, &s_fold, &s_fold_ext);
ugui_widget_set_area(&s_fold, FOLD_X, FOLD_Y, FOLD_W, FOLD_H);
ugui_widget_set_id(&s_fold, WDGT_ID_DEMO);
#ifdef UGUI_ENABLE_FONT
ugui_widget_fold_set_font(&s_fold, &lexend_14pt_2bpp);
#endif
ugui_widget_fold_set_items(&s_fold, s_items, ITEM_N);
ugui_widget_fold_set_on_select(&s_fold, on_select, NULL);

/* Slider embeds (Brightness, Volume). Children of the FOLD, not of the
 * screen: the fold positions them inside their rows and the input layer
 * hit-tests them directly. Theme colours come from slider init. */
{
    static const uint8_t k_slider_rows[SLIDER_N] = { 1u, 4u };
    static const int16_t k_slider_init[SLIDER_N] = { 70, 40 };
    uint8_t si;
    for (si = 0u; si < SLIDER_N; si++) {
        ugui_fold_item_t* row = &s_items[k_slider_rows[si]];

        ugui_widget_slider_init(&s_fold, &s_slider_w[si], &s_slider_ext[si]);
        ugui_widget_slider_set_range(&s_slider_w[si], 0, 100);
        ugui_widget_slider_set_value(&s_slider_w[si], k_slider_init[si]);
        ugui_widget_slider_set_callback(&s_slider_w[si], on_embed_slider,
                                        (void*)row->label);
        ugui_widget_set_enabled(&s_slider_w[si], true);

        row->embed   = &s_slider_w[si];
        row->embed_w = 110u;
    }
}

/* Toggle embeds (Night mode, Mute, Wi-Fi, Bluetooth). Wi-Fi starts ON. */
{
    static const uint8_t k_toggle_rows[TOGGLE_N] = { 2u, 5u, 7u, 8u };
    uint8_t ti;
    for (ti = 0u; ti < TOGGLE_N; ti++) {
        ugui_fold_item_t* row = &s_items[k_toggle_rows[ti]];

        ugui_widget_toggle_init(&s_fold, &s_toggle_w[ti], &s_toggle_ext[ti]);
        ugui_widget_toggle_set_callback(&s_toggle_w[ti], on_embed_toggle,
                                        (void*)row->label);
        ugui_widget_set_enabled(&s_toggle_w[ti], true);

        row->embed   = &s_toggle_w[ti];
        row->embed_w = 52u;
    }
    ugui_widget_toggle_set_state(&s_toggle_w[2], true);   /* Wi-Fi ON */
}

/* Default state: Display section open when the screen appears. */
ugui_widget_fold_set_expanded(&s_fold, 0u, true);

ugui_screen_add_widget(&s_screen, &s_fold);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_fold);
#endif

/* Live status bar (filled card) across the foot. */
ugui_widget_box_init(&s_root, &s_status, &s_status_ext);
ugui_widget_set_area(&s_status, 10, STATUS_Y,
                     (int16_t)(UGUI_SCREEN_W - 20), STATUS_H);
ugui_widget_set_id(&s_status, WDGT_ID_SLOT4);
ugui_widget_box_set_style(&s_status, true, 4u, 1u, UGUI_THEME_SURFACE, UGUI_THEME_ACCENT);
#ifdef UGUI_ENABLE_FONT
(void)snprintf(s_status_buf, sizeof(s_status_buf),
               "Interact with a row - every change lands here");
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.