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.

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

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

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

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