SpinBox Widget¶
The SpinBox is a numeric stepper — a themed [-] / value / [+] control for
setpoints (rate, temperature, volume) that need to stay in physical units,
not raw dropdown items. Init installs the whole look (theme-tracked SURFACE
plate, ACCENT buttons, a press highlight) at a settable size and AUTO
orientation — wide areas lay out horizontally, tall ones stack + / value / −
vertically. set_range() sets the min/max/step (all int32, so a "0.5 °C
step" is really an integer step over a tenths-scaled value) and
set_format() gives it a font, a unit suffix and a decimal-point position;
set_value() seeds the display. Tap a button for one step; hold it and the
input layer's click-hold engine repeats the step, accelerating ×10 after
~0.8 s — reaching a wide range one tap at a time is exactly what that is
for. set_on_change() fires with the new int32 value on every step.
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 SpinBox: Setpoint + Vertical Stepper¶
A target-temperature spinbox (16.0..30.0 °C in 0.5° steps, one decimal shown) beside a vertical fan-level stepper (0..5) — nothing is configured for the fan's orientation, the area is just taller than wide, so AUTO stacks [+] on top and [−] at the bottom. Tapping [+] steps the temperature and set_on_change() writes the readout below.

/* --- 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_SLOT0
};
/* --- 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_SPINBOX)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_readout;
#endif
#if defined(UGUI_WIDGET_ENABLE_SPINBOX)
static char s_readout_str[36];
#endif
#if defined(UGUI_WIDGET_ENABLE_SPINBOX)
static ugui_widget_t s_temp;
static ugui_spinbox_ext_t s_temp_ext;
static ugui_widget_t s_fan; /* vertical (AUTO) */
static ugui_spinbox_ext_t s_fan_ext;
static ugui_widget_t s_fan_cap;
static ugui_label_ext_t s_fan_cap_ext;
#define SB_W 220
#define SB_H 44
#define SB_X ((UGUI_SCREEN_W - SB_W) / 2 - 40)
#define SB_Y (UGUI_SCREEN_H / 2 - SB_H)
#define FAN_W 64
#define FAN_H 150
#define FAN_X (SB_X + SB_W + 46)
#define FAN_Y (SB_Y + SB_H / 2 - FAN_H / 2)
#endif
/* --- local helpers & event callbacks --- */
static void on_fan_change(ugui_widget_t* w, int32_t value, void* ctx)
{
(void)w; (void)ctx;
printf("[spinbox.default] fan -> %ld\n", (long)value);
}
static void on_temp_change(ugui_widget_t* w, int32_t value, void* ctx)
{
(void)w; (void)ctx;
#ifdef UGUI_ENABLE_FONT
(void)snprintf(s_readout_str, sizeof(s_readout_str),
"setpoint: %ld.%ld C", (long)(value / 10), (long)(value % 10));
ugui_widget_invalidate(&s_readout);
#endif
printf("[spinbox.default] temp -> %ld (tenths C)\n", (long)value);
}
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) --- */
ugui_widget_spinbox_init(&s_root, &s_temp, &s_temp_ext);
ugui_widget_set_area(&s_temp, SB_X, SB_Y, SB_W, SB_H);
ugui_widget_set_id(&s_temp, WDGT_ID_DEMO);
ugui_widget_spinbox_set_range(&s_temp, 160, 300, 5);
ugui_widget_spinbox_set_value(&s_temp, 215);
#ifdef UGUI_ENABLE_FONT
ugui_widget_spinbox_set_format(&s_temp, &lexend_14pt_2bpp, "C", 1u);
#endif
ugui_widget_spinbox_set_on_change(&s_temp, on_temp_change, NULL);
ugui_screen_add_widget(&s_screen, &s_temp);
/* Vertical stepper: tall area → AUTO stacks + / value / −. */
ugui_widget_spinbox_init(&s_root, &s_fan, &s_fan_ext);
ugui_widget_set_area(&s_fan, FAN_X, FAN_Y, FAN_W, FAN_H);
ugui_widget_set_id(&s_fan, WDGT_ID_SLOT0);
ugui_widget_spinbox_set_range(&s_fan, 0, 5, 1);
ugui_widget_spinbox_set_value(&s_fan, 2);
#ifdef UGUI_ENABLE_FONT
ugui_widget_spinbox_set_format(&s_fan, &lexend_14pt_2bpp, NULL, 0u);
#endif
ugui_widget_spinbox_set_on_change(&s_fan, on_fan_change, NULL);
ugui_screen_add_widget(&s_screen, &s_fan);
/* Caption under the fan stepper. */
add_label(&s_fan_cap, &s_fan_cap_ext, FAN_X - 18, FAN_Y + FAN_H + 4,
FAN_W + 36, 20, "Fan");
Infusion Pump: Rate + VTBI¶
Two spinboxes the way a pump programs an infusion: Rate (0.1..999.9 ml/h in 0.1 steps) and VTBI (1..2000 ml in 1 ml steps). Reaching 999.9 one tap at a time is what hold-acceleration is for — hold + and after ~0.8 s of repeats the step becomes 1.0, then 10.0. The footer echoes both values on every change.

/* --- 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_SLOT0 = 10, 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_SPINBOX)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_cap_rate;
static ugui_label_ext_t s_cap_rate_ext;
static ugui_widget_t s_cap_vtbi;
static ugui_label_ext_t s_cap_vtbi_ext;
static ugui_widget_t s_footer;
#endif
#if defined(UGUI_WIDGET_ENABLE_SPINBOX)
static char s_footer_str[64];
#endif
#if defined(UGUI_WIDGET_ENABLE_SPINBOX)
static ugui_widget_t s_rate, s_vtbi;
static ugui_spinbox_ext_t s_rate_ext, s_vtbi_ext;
#define SB_W 230
#define SB_H 40
#define CAP_W 100
#define ROW_X ((UGUI_SCREEN_W - (CAP_W + 10 + SB_W)) / 2)
#define ROW0_Y (38 + 28)
#define ROW1_Y (ROW0_Y + SB_H + 24)
#endif
/* --- local helpers & event callbacks --- */
static void pump_update_footer(void)
{
#ifdef UGUI_ENABLE_FONT
int32_t r = ugui_widget_spinbox_get_value(&s_rate);
int32_t v = ugui_widget_spinbox_get_value(&s_vtbi);
(void)snprintf(s_footer_str, sizeof(s_footer_str),
"program: %ld.%ld ml/h, VTBI %ld ml",
(long)(r / 10), (long)(r % 10), (long)v);
ugui_widget_invalidate(&s_footer);
#endif
}
static void on_pump_change(ugui_widget_t* w, int32_t value, void* ctx)
{
(void)w; (void)value; (void)ctx;
pump_update_footer();
}
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);
}
/* --- build it (in your screen's init) --- */
add_label(&s_cap_rate, &s_cap_rate_ext, ROW_X, ROW0_Y, CAP_W, SB_H, "Rate",
(ugui_align_t)((uint16_t)UGUI_ALIGN_LEFT
| (uint16_t)UGUI_ALIGN_V_CENTER));
ugui_widget_spinbox_init(&s_root, &s_rate, &s_rate_ext);
ugui_widget_set_area(&s_rate, (int16_t)(ROW_X + CAP_W + 10), ROW0_Y, SB_W, SB_H);
ugui_widget_set_id(&s_rate, WDGT_ID_SLOT0);
ugui_widget_spinbox_set_range(&s_rate, 1, 9999, 1); /* tenths of ml/h */
ugui_widget_spinbox_set_value(&s_rate, 125); /* 12.5 ml/h */
#ifdef UGUI_ENABLE_FONT
ugui_widget_spinbox_set_format(&s_rate, &lexend_14pt_2bpp, "ml/h", 1u);
#endif
ugui_widget_spinbox_set_on_change(&s_rate, on_pump_change, NULL);
ugui_screen_add_widget(&s_screen, &s_rate);
add_label(&s_cap_vtbi, &s_cap_vtbi_ext, ROW_X, ROW1_Y, CAP_W, SB_H, "VTBI",
(ugui_align_t)((uint16_t)UGUI_ALIGN_LEFT
| (uint16_t)UGUI_ALIGN_V_CENTER));
ugui_widget_spinbox_init(&s_root, &s_vtbi, &s_vtbi_ext);
ugui_widget_set_area(&s_vtbi, (int16_t)(ROW_X + CAP_W + 10), ROW1_Y, SB_W, SB_H);
ugui_widget_set_id(&s_vtbi, WDGT_ID_SLOT1);
ugui_widget_spinbox_set_range(&s_vtbi, 1, 2000, 1);
ugui_widget_spinbox_set_value(&s_vtbi, 500);
#ifdef UGUI_ENABLE_FONT
ugui_widget_spinbox_set_format(&s_vtbi, &lexend_14pt_2bpp, "ml", 0u);
#endif
ugui_widget_spinbox_set_on_change(&s_vtbi, on_pump_change, NULL);
ugui_screen_add_widget(&s_screen, &s_vtbi);
Settings Review (Embedded in a uList)¶
The pre-use checklist pattern: a uList groups parameters under section headers, read-only rows show label + value, booleans are SWITCH accessory rows, and the ADJUSTABLE row hosts a real SpinBox via the uList embed slot — the list scrolls it into view and forwards LEFT/RIGHT to it when the row is focused, while touch hits it directly.

/* --- 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_SLOT0
};
/* --- 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_SPINBOX) && defined(UGUI_WIDGET_ENABLE_ULIST)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_list;
static ugui_ulist_ext_t s_list_ext;
static ugui_widget_t s_rate; /* embedded in the Rate row */
static ugui_spinbox_ext_t s_rate_ext;
#define LIST_X 10
#define LIST_Y 38
#define LIST_W (UGUI_SCREEN_W - 2 * LIST_X)
#define LIST_H (UGUI_SCREEN_H - LIST_Y - 8)
#define ROW_H 34
static ugui_ulist_item_t s_items[] = {
{ "VENTILATION", NULL, 1u, UGUI_ULIST_ACC_NONE, UGUI_ULIST_HEADER, NULL },
{ "Mode", "PC-SIMV", 2u, UGUI_ULIST_ACC_CHEVRON, 0u, NULL },
{ "Rate", NULL, 3u, UGUI_ULIST_ACC_NONE, 0u, &s_rate },
{ "Tidal volume", "450 ml", 4u, UGUI_ULIST_ACC_CHEVRON, 0u, NULL },
{ "PEEP", "5 cmH2O", 5u, UGUI_ULIST_ACC_CHEVRON, 0u, NULL },
{ "FiO2", "40 %", 6u, UGUI_ULIST_ACC_CHEVRON, 0u, NULL },
{ "ALARMS", NULL, 7u, UGUI_ULIST_ACC_NONE, UGUI_ULIST_HEADER, NULL },
{ "Apnea alarm", NULL, 8u, UGUI_ULIST_ACC_SWITCH, UGUI_ULIST_CHECKED, NULL },
{ "Pmax", "35 cmH2O", 9u, UGUI_ULIST_ACC_CHEVRON, 0u, NULL },
{ "MV low", "2.0 l/min", 10u, UGUI_ULIST_ACC_CHEVRON, 0u, NULL },
{ "Checks done", NULL, 11u, UGUI_ULIST_ACC_CHECK, 0u, NULL },
};
#endif
/* --- local helpers & event callbacks --- */
static void on_rate_change(ugui_widget_t* w, int32_t value, void* ctx)
{
(void)w; (void)ctx;
printf("[settings] rate -> %ld /min\n", (long)value);
}
static void on_list_action(ugui_widget_t* w, uint16_t idx, void* ctx)
{
(void)w; (void)ctx;
printf("[settings] row %u ('%s') activated\n",
(unsigned)idx, (s_items[idx].label != NULL) ? s_items[idx].label : "");
}
/* --- build it (in your screen's init) --- */
ugui_widget_ulist_init(&s_root, &s_list, &s_list_ext);
ugui_widget_set_area(&s_list, LIST_X, LIST_Y, LIST_W, LIST_H);
ugui_widget_set_id(&s_list, WDGT_ID_DEMO);
ugui_widget_ulist_set_font(&s_list, &lexend_14pt_2bpp);
ugui_widget_ulist_set_geometry(&s_list, ROW_H, 0u, 6u, 1u);
/* The embedded setpoint editor: parent = the LIST, x/width/height set
* here, vertical position + show/hide owned by the list (uList embed
* contract). LEFT/RIGHT reach it when its row is focused. */
ugui_widget_spinbox_init(&s_list, &s_rate, &s_rate_ext);
ugui_widget_set_area(&s_rate, (int16_t)(LIST_W - 190), 0,
180, (int16_t)(ROW_H - 6));
ugui_widget_set_id(&s_rate, WDGT_ID_SLOT0);
ugui_widget_spinbox_set_range(&s_rate, 4, 60, 1); /* breaths/min */
ugui_widget_spinbox_set_value(&s_rate, 14);
#ifdef UGUI_ENABLE_FONT
ugui_widget_spinbox_set_format(&s_rate, &lexend_14pt_2bpp, "/min", 0u);
#endif
ugui_widget_spinbox_set_on_change(&s_rate, on_rate_change, NULL);
ugui_widget_ulist_set_items(&s_list, s_items,
(uint16_t)(sizeof(s_items) / sizeof(s_items[0])));
ugui_widget_ulist_set_on_action(&s_list, on_list_action, NULL);
ugui_screen_add_widget(&s_screen, &s_list);
/* Embedded widgets must ALSO be screen-registered so their dirty flags
* reach the render pass (uList embed convention — the list still owns
* their position and drawing order). */
ugui_screen_add_widget(&s_screen, &s_rate);
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.