Skip to content

Drum Widget

The Drum is a kinetic rotary picker — a scrolling belt of items with a centre selection band, the classic "spin to a value" control (time, rate, setpoint). Init installs the whole look (theme-tracked colours, centre band, default size, and the slide/key/confirm event mask) at a settable size; set_font() gives it text and set_items() fills the belt — the item array is caller-owned RAM (expanded state lives in it, never copied). Swiping (or the keypad up/down) spins the belt with inertia and snaps to an item; set_kinetics() tunes the flick distance per swipe. get_focus() / set_focus() poll and programmatically snap the selection, and set_callbacks() fires on confirm (ENTER/OK) and cancel (the CANCEL key) — committing resolves whichever item is nearest centre even mid-spin.

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 Drum

Three calls make a working rotary picker: ugui_widget_drum_init() installs the whole look, size and event mask, set_font() gives it text, set_items() fills the belt (started on "Medium"). set_align() only centres the default-sized widget — no geometry, colour or event wiring anywhere.

Default Drum

/* --- 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_DRUM_PICKER)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t   s_drum;
static ugui_drum_ext_t s_drum_ext;
static const char* const k_items[] = { "Off", "Low", "Medium", "High", "Turbo" };
#define ITEM_N  ((uint16_t)(sizeof(k_items) / sizeof(k_items[0])))
#endif

/* --- build it (in your screen's init) --- */
ugui_widget_drum_init(&s_root, &s_drum, &s_drum_ext);
ugui_widget_set_align(&s_drum, UGUI_ALIGN_CENTER, 0, 10);
ugui_widget_set_id(&s_drum, WDGT_ID_DEMO);
#ifdef UGUI_ENABLE_FONT
ugui_widget_drum_set_font(&s_drum, &lexend_14pt_2bpp);
#endif
ugui_widget_drum_set_items(&s_drum, k_items, ITEM_N, 2u /* "Medium" */);
ugui_screen_add_widget(&s_screen, &s_drum);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_drum);
#endif

Three-Wheel Time Picker

The classic drum job: hours, minutes and AM/PM as three independent drums side by side — each is just init + font + items, all library defaults otherwise. A live readout bar proves get_focus(): a per-frame feed polls the three wheels and rewrites the bar the moment any one settles on a new item. set_kinetics() gives the 12-item minutes wheel a heavier ~3-item-per-swipe flick.

Three-Wheel Time Picker

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

/* --- 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_DRUM_PICKER)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t   s_hour,     s_min,     s_ampm;
static ugui_drum_ext_t s_hour_ext, s_min_ext, s_ampm_ext;
static const char* const k_hours[] = {
    "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12"
};
static const char* const k_mins[] = {
    "00", "05", "10", "15", "20", "25", "30", "35", "40", "45", "50", "55"
};
static const char* const k_ampm[] = { "AM", "PM" };
#define HOUR_N  ((uint16_t)(sizeof(k_hours) / sizeof(k_hours[0])))
#define MIN_N   ((uint16_t)(sizeof(k_mins)  / sizeof(k_mins[0])))
#define AMPM_N  ((uint16_t)(sizeof(k_ampm)  / sizeof(k_ampm[0])))
#define INIT_HOUR_IDX  6u
#define INIT_MIN_IDX   6u
#define INIT_AMPM_IDX  1u
#define WHEEL_W   110
#define WHEEL_H   144      /* taller than the 108 px slot stack: the widget
                            * centres the stack, extra height reads as air  */
#define WHEEL_GAP 18
#define GROUP_W   (3 * WHEEL_W + 2 * WHEEL_GAP)
#define WHEEL_X0  ((UGUI_SCREEN_W - GROUP_W) / 2)
#define WHEEL_Y   (38 + 18)
#endif

/* --- local helpers & event callbacks --- */
static void wheel_add(ugui_widget_t* w, ugui_drum_ext_t* ext, uint8_t col,
                      const char* const* items, uint16_t count, uint16_t init_idx)
{
    ugui_widget_drum_init(&s_root, w, ext);
    ugui_widget_set_area(w, (int16_t)(WHEEL_X0 + (int16_t)col * (WHEEL_W + WHEEL_GAP)),
                         WHEEL_Y, WHEEL_W, WHEEL_H);
    ugui_widget_set_id(w, (uint8_t)(WDGT_ID_SLOT4 + col));
#ifdef UGUI_ENABLE_FONT
    ugui_widget_drum_set_font(w, &lexend_14pt_2bpp);
#endif
    ugui_widget_drum_set_items(w, items, count, init_idx);
    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) --- */
wheel_add(&s_hour, &s_hour_ext, 0u, k_hours, HOUR_N, INIT_HOUR_IDX);
wheel_add(&s_min,  &s_min_ext,  1u, k_mins,  MIN_N,  INIT_MIN_IDX);
wheel_add(&s_ampm, &s_ampm_ext, 2u, k_ampm,  AMPM_N, INIT_AMPM_IDX);

/* Per-wheel touch feel: the long minutes wheel gets a heavier flick
 * (~3 items per swipe: 2*ITEM_H px8 gain, 92 % glide); hours and the
 * 2-item AM/PM wheel keep the stock one-item-per-swipe kinetics. */
ugui_widget_drum_set_kinetics(&s_min, (uint16_t)(2u * UGUI_DRUM_ITEM_H), 92u);

Themes & Non-Default Heights

Three drums, same content, three looks: left keeps the theme-tracked library defaults at the natural slot-stack height; middle is a custom "midnight" palette via set_theme() (fixed colours, ignores the app theme) at a TALLER-than-stack height; right is a custom "amber terminal" palette, SHORTER than the stack. Any height renders balanced — the widget centres the slots.

Themes & Non-Default Heights

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

/* --- 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_DRUM_PICKER)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
#define DRUM_N 3
static ugui_widget_t    s_drum[DRUM_N];
static ugui_drum_ext_t  s_drum_ext[DRUM_N];
static ugui_widget_t    s_cap[DRUM_N];
static ugui_label_ext_t s_cap_ext[DRUM_N];
static const char* const k_items[] = {
    "10 %", "25 %", "50 %", "75 %", "100 %"
};
#define ITEM_N  ((uint16_t)(sizeof(k_items) / sizeof(k_items[0])))
#define COL_W     130
#define COL_GAP   22
#define GRID_W    (3 * COL_W + 2 * COL_GAP)
#define COL_X(i)  ((int16_t)((UGUI_SCREEN_W - GRID_W) / 2 + (i) * (COL_W + COL_GAP)))
#define DRUM_Y    (38 + 14)
#define CAP_Y     (DRUM_Y + 152)
static const int16_t k_heights[DRUM_N] = { 108, 144, 76 };
static const char* const k_cap_text[DRUM_N] = {
    "Theme default", "Midnight 144px", "Compact 76px"
};
#endif

/* --- local helpers & event callbacks --- */
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_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, UGUI_ALIGN_CENTER, 1u);
    ugui_screen_add_widget(&s_screen, w);
}

/* --- build it (in your screen's init) --- */
for (uint8_t i = 0u; i < (uint8_t)DRUM_N; i++) {
    int16_t h = k_heights[i];
    /* Centre each drum vertically on the same axis so the three centre
     * bands line up whatever the height. */
    int16_t y = (int16_t)(DRUM_Y + (int16_t)((152 - h) / 2));

    ugui_widget_drum_init(&s_root, &s_drum[i], &s_drum_ext[i]);
    ugui_widget_set_area(&s_drum[i], COL_X(i), y, COL_W, h);
    ugui_widget_set_id(&s_drum[i], (uint8_t)(WDGT_ID_SLOT4 + i));
#ifdef UGUI_ENABLE_FONT
    ugui_widget_drum_set_font(&s_drum[i], &lexend_14pt_2bpp);
#endif
    ugui_widget_drum_set_items(&s_drum[i], k_items, ITEM_N, 2u);
    ugui_screen_add_widget(&s_screen, &s_drum[i]);
#if UGUI_ENABLE_KEYPAD
    ugui_screen_add_focus(&s_screen, &s_drum[i]);
#endif

    add_label(&s_cap[i], &s_cap_ext[i], COL_X(i), CAP_Y, COL_W, 20, k_cap_text[i]);
}

#ifdef UGUI_ENABLE_FONT
/* Middle: "midnight" — cool blues on near-black (msgbox-style popup). */
ugui_widget_drum_set_theme(&s_drum[1],
                           ugui_color_hex(0x1A1A2Eu),   /* surface     */
                           ugui_color_hex(0x1E3A5Fu),   /* band fill   */
                           ugui_color_hex(0x00C8FFu),   /* band border */
                           ugui_color_hex(0x607080u),   /* normal text */
                           UGUI_COLOR_WHITE,            /* focus text  */
                           &lexend_14pt_2bpp);

/* Right: "amber terminal" — warm monochrome instrument look. */
ugui_widget_drum_set_theme(&s_drum[2],
                           ugui_color_hex(0x201400u),   /* surface     */
                           ugui_color_hex(0x3A2600u),   /* band fill   */
                           ugui_color_hex(0xFFB000u),   /* band border */
                           ugui_color_hex(0x8A6A20u),   /* normal text */
                           ugui_color_hex(0xFFD060u),   /* focus text  */
                           &lexend_14pt_2bpp);
#endif

Setpoint Editor with Commit

The realistic case: a pump-rate drum wired the way a real instrument would use it. set_callbacks() fires on_confirm on ENTER/OK and on_cancel on the CANCEL key, both writing the status bar; the Apply button reads get_focus() on demand — the poll-style alternative to callbacks; the Reset button uses set_focus() to snap back to the default setpoint with the same animation a key press uses.

Setpoint Editor with Commit

/* --- 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, WDGT_ID_SLOT5,
    WDGT_ID_SLOT1
};

/* --- 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_DRUM_PICKER)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t   s_drum;
static ugui_drum_ext_t s_drum_ext;
static ugui_widget_t     s_apply;    static ugui_button_ext_t s_apply_ext;
static ugui_widget_t     s_reset;    static ugui_button_ext_t s_reset_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_DRUM_PICKER)
static ugui_text_cfg_t   s_apply_txt;
static ugui_text_cfg_t   s_reset_txt;
#endif
#if defined(UGUI_WIDGET_ENABLE_DRUM_PICKER)
static ugui_widget_t     s_status;   static ugui_box_ext_t    s_status_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_DRUM_PICKER)
static ugui_text_cfg_t   s_status_text;
#endif
#if defined(UGUI_WIDGET_ENABLE_DRUM_PICKER)
static const char* const k_rates[] = {
    "0.5", "1.0", "2.0", "5.0", "10", "25", "50", "100", "150", "200"
};
#define RATE_N        ((uint16_t)(sizeof(k_rates) / sizeof(k_rates[0])))
#define RATE_INIT_IDX 3u                    /* default setpoint: 5.0       */
#endif
#if defined(UGUI_WIDGET_ENABLE_DRUM_PICKER)
static char s_status_buf[48];
#endif
#if defined(UGUI_WIDGET_ENABLE_DRUM_PICKER)
#define DRUM_W   150
#define DRUM_H   144
#define DRUM_X   ((int16_t)(UGUI_SCREEN_W / 2 - DRUM_W - 20))
#define DRUM_Y   (38 + 16)
#define BTN_X    ((int16_t)(UGUI_SCREEN_W / 2 + 20))
#define BTN_W    150
#define BTN_H    32
#endif

/* --- local helpers & event callbacks --- */
static void status_show(const char* verb, uint16_t idx)
{
#ifdef UGUI_ENABLE_FONT
    (void)snprintf(s_status_buf, sizeof(s_status_buf), "%s: %s ml/h",
                   verb, k_rates[(idx < RATE_N) ? idx : (uint16_t)(RATE_N - 1u)]);
    ugui_widget_box_update_text_str(&s_status, s_status_buf);
#else
    (void)verb; (void)idx;
#endif
}

static void on_rate_confirm(uint16_t idx, void* ctx)
{
    (void)ctx;
    status_show("Committed", idx);
}

static void on_rate_cancel(void* ctx)
{
    (void)ctx;
    ugui_widget_drum_set_focus(&s_drum, RATE_INIT_IDX);
    status_show("Cancelled - back to", RATE_INIT_IDX);
}

static void on_apply(uint8_t widget_id, ugui_event_t event)
{
    (void)widget_id;
    if (event == UGUI_EVENT_CLICK) {
        status_show("Applied", ugui_widget_drum_get_focus(&s_drum));
    }
}

static void on_reset(uint8_t widget_id, ugui_event_t event)
{
    (void)widget_id;
    if (event == UGUI_EVENT_CLICK) {
        ugui_widget_drum_set_focus(&s_drum, RATE_INIT_IDX);
        status_show("Reset", RATE_INIT_IDX);
    }
}

static void add_button(ugui_widget_t* w, ugui_button_ext_t* ext, ugui_text_cfg_t* text,
                       uint8_t id, int16_t x, int16_t y, int16_t bw, int16_t bh,
                       ugui_color_t fill, const char* str, ugui_event_cb_t cb)
{
    ugui_widget_button_init(&s_root, w, ext);
    ugui_widget_set_area(w, x, y, bw, bh);
    ugui_widget_set_id(w, id);
    ugui_widget_button_set_style(w, true, 5u, 1u, fill, UGUI_THEME_ACCENT);
#ifdef UGUI_ENABLE_FONT
    ugui_text_cfg_init(text, str, &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                       UGUI_ALIGN_CENTER, (ugui_point_t){ 0, 0 }, 1u);
    ugui_widget_button_set_text(w, text);
#else
    (void)str;
#endif
    ugui_widget_set_event_cb(w, cb,
                             UGUI_EMASK_CLICK | UGUI_EMASK_DOWN | UGUI_EMASK_UP
                             | UGUI_EMASK_KEY_CONFIRM);
    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) --- */
ugui_widget_drum_init(&s_root, &s_drum, &s_drum_ext);
ugui_widget_set_area(&s_drum, DRUM_X, (int16_t)(DRUM_Y + 18), DRUM_W, DRUM_H);
ugui_widget_set_id(&s_drum, WDGT_ID_DEMO);
#ifdef UGUI_ENABLE_FONT
ugui_widget_drum_set_font(&s_drum, &lexend_14pt_2bpp);
#endif
ugui_widget_drum_set_items(&s_drum, k_rates, RATE_N, RATE_INIT_IDX);
ugui_widget_drum_set_callbacks(&s_drum, on_rate_confirm, on_rate_cancel, NULL);
ugui_screen_add_widget(&s_screen, &s_drum);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_drum);
#endif

add_button(&s_apply, &s_apply_ext, &s_apply_txt, WDGT_ID_SLOT4,
          BTN_X, (int16_t)(DRUM_Y + 34), BTN_W, BTN_H,
          UGUI_THEME_SURFACE, "Apply", on_apply);
add_button(&s_reset, &s_reset_ext, &s_reset_txt, WDGT_ID_SLOT5,
          BTN_X, (int16_t)(DRUM_Y + 34 + BTN_H + 14), BTN_W, BTN_H,
          ugui_color_blend(UGUI_THEME_SURFACE, UGUI_THEME_BG, 96u), "Reset", on_reset);

/* 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, (int16_t)(UGUI_SCREEN_H - 40),
                     (int16_t)(UGUI_SCREEN_W - 20), 30);
ugui_widget_set_id(&s_status, WDGT_ID_SLOT1);
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),
               "Spin to a rate, then ENTER / Apply");
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.