Skip to content

Toggle Widget

The Toggle is an animated ON/OFF switch — a pill-shaped track with a sliding knob. Init installs a theme-tracked look (a neutral BORDER-coloured OFF track, an accent ON track, a themed knob) at a default 64×32 size, wired for both touch (tap) and keypad (focus + ENTER) with no extra event wiring. set_state() / get_state() drive and read the value; set_colors() restyles the OFF / ON / knob colours independently; the knob slides itself once a change is applied — the widget's own tick animates it, no per-frame app code.

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 Toggle

Two calls make a working switch: ugui_widget_toggle_init() installs the whole look, size and event mask, and set_callback() hooks the state change. Tapping anywhere on the pill flips it; the knob slides itself and the live readout below tracks on_change()'s value.

Default Toggle

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

/* --- 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_TOGGLE_BUTTON)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t     s_toggle;
static ugui_toggle_ext_t s_toggle_ext;
static ugui_widget_t     s_state;       /* live ON/OFF readout under the toggle */
static ugui_label_ext_t  s_state_ext;
#endif

/* --- local helpers & event callbacks --- */
static void on_change(bool state, void* ctx)
{
    (void)ctx;
    printf("[toggle.default] state -> %s\n", state ? "ON" : "OFF");
    ugui_label_set_text(&s_state, state ? "ON" : "OFF");
}

/* --- build it (in your screen's init) --- */
ugui_widget_toggle_init(&s_root, &s_toggle, &s_toggle_ext);
ugui_widget_set_align(&s_toggle, UGUI_ALIGN_CENTER, 0, -6);
ugui_widget_set_id(&s_toggle, WDGT_ID_DEMO);
ugui_widget_toggle_set_callback(&s_toggle, on_change, NULL);
ugui_screen_add_widget(&s_screen, &s_toggle);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_toggle);
#endif

/* Live ON/OFF readout, centred just below the toggle. */
ugui_widget_label_init(&s_root, &s_state, &s_state_ext);
ugui_widget_set_area(&s_state, 0, (int16_t)((UGUI_SCREEN_H / 2) + 24),
                     (int16_t)UGUI_SCREEN_W, 22);
ugui_widget_set_id(&s_state, WDGT_ID_STATE);
ugui_widget_label_set_style(&s_state, false, UGUI_THEME_BG, "OFF",
                            &lexend_14pt_2bpp, UGUI_THEME_TEXT, UGUI_ALIGN_CENTER, 1u);
ugui_screen_add_widget(&s_screen, &s_state);

Start Value, Disabled

Four rows show the whole state API: a plain default (OFF), set_state(true) before display (starts ON, no animation), and two disabled rows (set_enabled(false)) — one locked ON, one locked OFF, both ignoring taps and skipped by the keypad focus ring.

Start Value, Disabled

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

/* --- 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_TOGGLE_BUTTON)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
#define ROW_N  4u
static ugui_widget_t     s_toggle_w  [ROW_N];
static ugui_toggle_ext_t s_toggle_ext[ROW_N];
static ugui_widget_t     s_label     [ROW_N];
static ugui_label_ext_t  s_label_ext [ROW_N];
static ugui_widget_t     s_status;                    /* live readout */
static ugui_label_ext_t  s_status_ext;
static const char* const k_row_label[ROW_N] = {
    "Starts OFF", "Starts ON", "Disabled (ON)", "Disabled (OFF)"
};
#define TOG_W    60
#define TOG_H    30
#define TOG_X    ((int16_t)(UGUI_SCREEN_W - TOG_W - 10))
#define LBL_X    10
#define LBL_W    ((int16_t)(TOG_X - LBL_X - 8))
#define ROW0_Y   ((int16_t)(38 + 6))
#define ROW_STEP 40
#define STATUS_H 26
#define STATUS_Y ((int16_t)(UGUI_SCREEN_H - STATUS_H - 10))
static char s_status_buf[48];
#endif

/* --- local helpers & event callbacks --- */
static void on_change(bool state, void* ctx)
{
    (void)snprintf(s_status_buf, sizeof(s_status_buf), "%s -> %s",
                   (const char*)ctx, state ? "ON" : "OFF");
    ugui_label_set_text(&s_status, s_status_buf);
}

static void add_label(ugui_widget_t* w, ugui_label_ext_t* ext, uint8_t id,
                      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_set_id  (w, id);
    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) --- */
uint8_t i;
for (i = 0u; i < ROW_N; i++) {
    int16_t row_y = (int16_t)(ROW0_Y + (int16_t)i * ROW_STEP);

    add_label(&s_label[i], &s_label_ext[i], (uint8_t)(WDGT_ID_SLOT0 + i),
             LBL_X, row_y, LBL_W, TOG_H,
             k_row_label[i], (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER));

    ugui_widget_toggle_init(&s_root, &s_toggle_w[i], &s_toggle_ext[i]);
    ugui_widget_set_area(&s_toggle_w[i], TOG_X, row_y, TOG_W, TOG_H);
    ugui_widget_set_id(&s_toggle_w[i], (uint8_t)(WDGT_ID_SLOT4 + i));

    switch (i) {
        case 1u:  /* Starts ON */
            ugui_widget_toggle_set_state(&s_toggle_w[i], true);
            break;
        case 2u:  /* Disabled, locked ON */
            ugui_widget_toggle_set_state(&s_toggle_w[i], true);
            ugui_widget_set_enabled(&s_toggle_w[i], false);
            break;
        case 3u:  /* Disabled, locked OFF */
            ugui_widget_set_enabled(&s_toggle_w[i], false);
            break;
        default:  /* case 0: plain default (OFF) */
            break;
    }

    ugui_screen_add_widget(&s_screen, &s_toggle_w[i]);

    /* Only the two enabled rows take part in touch callbacks + keypad
     * focus; the disabled rows are display-only. */
    if (i < 2u) {
        ugui_widget_toggle_set_callback(&s_toggle_w[i], on_change,
                                        (void*)k_row_label[i]);
#if UGUI_ENABLE_KEYPAD
        ugui_screen_add_focus(&s_screen, &s_toggle_w[i]);
#endif
    }
}

/* Live status line across the foot. */
(void)snprintf(s_status_buf, sizeof(s_status_buf), "flip a switch - state lands here");
ugui_widget_label_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_STATUS);
ugui_widget_label_set_style(&s_status, false, UGUI_THEME_BG, s_status_buf,
                            &lexend_14pt_2bpp, UGUI_THEME_ACCENT, UGUI_ALIGN_CENTER, 1u);
ugui_screen_add_widget(&s_screen, &s_status);

Colours & Sizes

Left: five toggles differing only in set_colors(off, on, knob) — four preset ON to show a semantic ON colour (Accent / Success / Caution / Alert), the fifth preset OFF with a custom OFF track too, so all three colour fields get a row. Right: the same widget at three pixel sizes — the pill radius and knob scale off the height automatically.

Colours & Sizes

/* --- 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_SLOT7, WDGT_ID_SLOT12,
    WDGT_ID_SLOT15
};

/* --- 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_TOGGLE_BUTTON)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
#define COL_N  5u
static ugui_widget_t     s_col_w  [COL_N];
static ugui_toggle_ext_t s_col_ext[COL_N];
static ugui_widget_t     s_col_lbl[COL_N];
static ugui_label_ext_t  s_col_lbl_ext[COL_N];
static const char* const k_col_name[COL_N] = {
    "Accent", "Success", "Caution", "Alert", "Custom off"
};
#define SIZE_N  3u
static ugui_widget_t     s_size_w  [SIZE_N];
static ugui_toggle_ext_t s_size_ext[SIZE_N];
static ugui_widget_t     s_size_lbl[SIZE_N];
static ugui_label_ext_t  s_size_lbl_ext[SIZE_N];
static const char* const k_size_name[SIZE_N] = { "Small", "Medium", "Large" };
static const int16_t     k_size_w   [SIZE_N] = { 44, 64, 90 };
static const int16_t     k_size_h   [SIZE_N] = { 22, 32, 44 };
#define HDR_Y     38
#define COL_LBL_X 10
#define COL_LBL_W 82
#define COL_TOG_X ((int16_t)(COL_LBL_X + COL_LBL_W + 4))
#define COL_TOG_W 56
#define COL_TOG_H 26
#define COL_ROW0  ((int16_t)(HDR_Y + 24))
#define COL_STEP  34    /* 5 rows fit a 240-px-tall panel (COL_ROW0 + 4*STEP + H) */
#define SIZE_CX   ((int16_t)(3 * UGUI_SCREEN_W / 4))   /* centre of right half */
#endif

/* --- local helpers & event callbacks --- */
static void add_label(ugui_widget_t* w, ugui_label_ext_t* ext, uint8_t id,
                      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_set_id  (w, id);
    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) --- */
ugui_color_t off_col[COL_N];
ugui_color_t on_col [COL_N];
bool         start_on[COL_N];

off_col[0] = UGUI_THEME_BORDER;  on_col[0] = UGUI_THEME_ACCENT;             start_on[0] = true;   /* Accent  */
off_col[1] = UGUI_THEME_BORDER;  on_col[1] = UGUI_THEME_OK;             start_on[1] = true;   /* Success */
off_col[2] = UGUI_THEME_BORDER;  on_col[2] = ugui_color_hex(0xF5A623u); start_on[2] = true;   /* Caution */
off_col[3] = UGUI_THEME_BORDER;  on_col[3] = UGUI_THEME_CANCEL;         start_on[3] = true;   /* Alert   */
/* Custom deselect: OFF track = slate blue, ON track = cyan, shown OFF. */
off_col[4] = ugui_color_hex(0x3A4A63u);
on_col [4] = ugui_color_hex(0x22B8E6u);
start_on[4] = false;

uint8_t i;
for (i = 0u; i < COL_N; i++) {
    int16_t row_y = (int16_t)(COL_ROW0 + (int16_t)i * COL_STEP);

    add_label(&s_col_lbl[i], &s_col_lbl_ext[i], (uint8_t)(WDGT_ID_SLOT2 + i),
             COL_LBL_X, row_y, COL_LBL_W, COL_TOG_H,
             k_col_name[i], (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER));

    ugui_widget_toggle_init(&s_root, &s_col_w[i], &s_col_ext[i]);
    ugui_widget_set_area(&s_col_w[i], COL_TOG_X, row_y, COL_TOG_W, COL_TOG_H);
    ugui_widget_set_id(&s_col_w[i], (uint8_t)(WDGT_ID_SLOT7 + i));
    ugui_widget_toggle_set_colors(&s_col_w[i], off_col[i], on_col[i], UGUI_THEME_KNOB);
    ugui_widget_toggle_set_state(&s_col_w[i], start_on[i]);
    ugui_screen_add_widget(&s_screen, &s_col_w[i]);
#if UGUI_ENABLE_KEYPAD
    ugui_screen_add_focus(&s_screen, &s_col_w[i]);
#endif
}

for (i = 0u; i < SIZE_N; i++) {
    int16_t tw    = k_size_w[i];
    int16_t th    = k_size_h[i];
    int16_t tx    = (int16_t)(SIZE_CX - tw / 2);
    int16_t ty    = (int16_t)(COL_ROW0 + (int16_t)i * 52);
    int16_t lbl_y = (int16_t)(ty + th + 1);

    ugui_widget_toggle_init(&s_root, &s_size_w[i], &s_size_ext[i]);
    ugui_widget_set_area(&s_size_w[i], tx, ty, tw, th);
    ugui_widget_set_id(&s_size_w[i], (uint8_t)(WDGT_ID_SLOT12 + i));
    ugui_widget_toggle_set_state(&s_size_w[i], true);
    ugui_screen_add_widget(&s_screen, &s_size_w[i]);
#if UGUI_ENABLE_KEYPAD
    ugui_screen_add_focus(&s_screen, &s_size_w[i]);
#endif

    add_label(&s_size_lbl[i], &s_size_lbl_ext[i], (uint8_t)(WDGT_ID_SLOT15 + i),
             (int16_t)(UGUI_SCREEN_W / 2), lbl_y,
             (int16_t)(UGUI_SCREEN_W / 2 - 10), 16,
             k_size_name[i], UGUI_ALIGN_CENTER);
}

Settings Form (Wi-Fi, Bluetooth, ...)

The realistic case: a card panel of five labeled switch rows, each toggle carrying its row name as callback context so ONE handler formats every change into the status bar at the foot — the callback path scales to an arbitrary number of controls with no per-row wiring.

Settings Form (Wi-Fi, Bluetooth, ...)

/* --- 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_PANEL = 10, WDGT_ID_SLOT5, WDGT_ID_SLOT0,
    WDGT_ID_STATUS
};

/* --- 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_TOGGLE_BUTTON)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t    s_panel;             /* card behind the rows      */
static ugui_box_ext_t   s_panel_ext;
static ugui_widget_t    s_status;            /* live readout              */
static ugui_label_ext_t s_status_ext;
#define ROW_N  5u
static ugui_widget_t     s_toggle_w  [ROW_N];
static ugui_toggle_ext_t s_toggle_ext[ROW_N];
static ugui_widget_t     s_label     [ROW_N];
static ugui_label_ext_t  s_label_ext [ROW_N];
static const char* const k_row_label[ROW_N] = {
    "Wi-Fi", "Bluetooth", "Notifications", "Location", "Airplane mode"
};
#define TOG_W     58
#define TOG_H     28
#define STATUS_H  26
#define STATUS_Y  ((int16_t)(UGUI_SCREEN_H - STATUS_H - 10))
#define PANEL_X   10
#define PANEL_Y   ((int16_t)(38 + 4))
#define PANEL_W   ((int16_t)(UGUI_SCREEN_W - 2 * 10))
#define PANEL_H   ((int16_t)(STATUS_Y - PANEL_Y - 8))
#define ROW_STEP  ((int16_t)(PANEL_H / (int16_t)ROW_N))
#define LBL_X     ((int16_t)(PANEL_X + 12))
#define TOG_X     ((int16_t)(PANEL_X + PANEL_W - TOG_W - 12))
#define LBL_W     ((int16_t)(TOG_X - LBL_X - 8))
static char s_status_buf[48];
#endif

/* --- local helpers & event callbacks --- */
static void on_change(bool state, void* ctx)
{
    (void)snprintf(s_status_buf, sizeof(s_status_buf), "%s: %s",
                   (const char*)ctx, state ? "ON" : "OFF");
    ugui_label_set_text(&s_status, s_status_buf);
}

static void add_label(ugui_widget_t* w, ugui_label_ext_t* ext, uint8_t id,
                      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_set_id  (w, id);
    ugui_widget_label_set_style(w, false, UGUI_THEME_SURFACE, str,
                                &lexend_14pt_2bpp, UGUI_THEME_TEXT, align, 1u);
    ugui_screen_add_widget(&s_screen, w);
}

/* --- build it (in your screen's init) --- */
/* Card panel behind the rows (display-only, added first so it sits under
 * the switches). */
ugui_widget_box_init(&s_root, &s_panel, &s_panel_ext);
ugui_widget_set_area(&s_panel, PANEL_X, PANEL_Y, PANEL_W, PANEL_H);
ugui_widget_set_id(&s_panel, WDGT_ID_PANEL);
ugui_widget_box_set_style(&s_panel, true, 6u, 1u, UGUI_THEME_SURFACE, UGUI_THEME_ACCENT);
ugui_widget_set_enabled(&s_panel, false);
ugui_screen_add_widget(&s_screen, &s_panel);

uint8_t i;
for (i = 0u; i < ROW_N; i++) {
    int16_t row_y = (int16_t)(PANEL_Y + (int16_t)i * ROW_STEP
                              + (ROW_STEP - TOG_H) / 2);

    add_label(&s_label[i], &s_label_ext[i], (uint8_t)(WDGT_ID_SLOT5 + i),
             LBL_X, row_y, LBL_W, TOG_H,
             k_row_label[i], (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER));

    ugui_widget_toggle_init(&s_root, &s_toggle_w[i], &s_toggle_ext[i]);
    ugui_widget_set_area(&s_toggle_w[i], TOG_X, row_y, TOG_W, TOG_H);
    ugui_widget_set_id(&s_toggle_w[i], (uint8_t)(WDGT_ID_SLOT0 + i));
    ugui_widget_toggle_set_callback(&s_toggle_w[i], on_change,
                                    (void*)k_row_label[i]);
    ugui_screen_add_widget(&s_screen, &s_toggle_w[i]);
#if UGUI_ENABLE_KEYPAD
    ugui_screen_add_focus(&s_screen, &s_toggle_w[i]);
#endif
}
ugui_widget_toggle_set_state(&s_toggle_w[0], true);   /* Wi-Fi starts ON */

/* Live status line across the foot. */
(void)snprintf(s_status_buf, sizeof(s_status_buf), "toggle a setting to see it here");
ugui_widget_label_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_STATUS);
ugui_widget_label_set_style(&s_status, false, UGUI_THEME_BG, s_status_buf,
                            &lexend_14pt_2bpp, UGUI_THEME_ACCENT, UGUI_ALIGN_CENTER, 1u);
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.