Skip to content

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.

Default SpinBox: Setpoint + Vertical Stepper

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

Infusion Pump: Rate + VTBI

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

Settings Review (Embedded in a uList)

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