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.

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

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

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

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