Radio Button Widget¶
The Radio Button widget is a labelled, mutually-exclusive selector.
Init installs the on-theme look AND a default event mask (group 0), so a
fresh set of radios already forms one exclusive group with zero
set_group() calls — selecting one clears its same-group siblings
automatically. set_group() splits widgets into independent groups sharing
one parent; set_colors() overrides the theme accent per-instance;
set_state(true) selects a button programmatically. Unlike a checkbox, a
tap can only MOVE the selection, never clear it to "none".
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 Group¶
Four radio buttons that init installs on-theme AND interactive, all defaulting to group 0 — zero set_group() calls needed for one mutually-exclusive group. set_state() establishes the initial selection without disturbing the others.

/* --- 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
};
/* --- 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_RADIO_BUTTON)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
#define RB_N 4
static ugui_widget_t s_rb[RB_N];
static ugui_radio_button_ext_t s_rb_ext[RB_N];
#endif
#if defined(UGUI_WIDGET_ENABLE_RADIO_BUTTON)
static ugui_text_cfg_t s_rb_txt[RB_N];
static const char* const s_rb_lbl[RB_N] =
{ "Small", "Medium", "Large", "Extra large" };
#endif
#if defined(UGUI_WIDGET_ENABLE_RADIO_BUTTON)
#define RB_X 40
#define RB_W (UGUI_SCREEN_W - 2 * RB_X)
#define RB_H 28
#define RB_Y0 (38 + 12)
#define RB_STEP 40
#endif
/* --- local helpers & event callbacks --- */
static void on_rb_change(bool selected, void* ctx)
{
if (!selected) { return; }
printf("[radio.default] selected: %s\n", (ctx != NULL) ? (const char*)ctx : "?");
}
/* --- build it (in your screen's init) --- */
for (uint8_t i = 0u; i < (uint8_t)RB_N; i++)
{
ugui_widget_radio_init(&s_root, &s_rb[i], &s_rb_ext[i]);
ugui_widget_set_area(&s_rb[i], RB_X, (int16_t)(RB_Y0 + (int16_t)i * RB_STEP),
RB_W, RB_H);
ugui_widget_set_id(&s_rb[i], (uint8_t)(WDGT_ID_SLOT0 + i));
#ifdef UGUI_ENABLE_FONT
ugui_text_cfg_init(&s_rb_txt[i], s_rb_lbl[i], &lexend_14pt_2bpp, UGUI_THEME_TEXT,
(ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
(ugui_point_t){ 0, 0 }, 0u);
ugui_widget_radio_set_text(&s_rb[i], &s_rb_txt[i]);
ugui_widget_radio_set_callback(&s_rb[i], on_rb_change, (void*)s_rb_lbl[i]);
#endif
ugui_screen_add_widget(&s_screen, &s_rb[i]);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_rb[i]);
#endif
}
/* Establish the initial selection (Medium) — set_state does not disturb
* the others, so it is the right call for a startup default. */
ugui_widget_radio_set_state(&s_rb[1], true);
States & Colour Overrides¶
The full visual vocabulary: an On/Off pair sharing one real group, three set_colors() overrides (success/warning/danger, each its own group so the dot stays put purely for display), and a disabled + selected button, greyed and non-interactive.

/* --- 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
};
/* --- 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_RADIO_BUTTON)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
#define RB_N 6
static ugui_widget_t s_rb[RB_N];
static ugui_radio_button_ext_t s_rb_ext[RB_N];
#endif
#if defined(UGUI_WIDGET_ENABLE_RADIO_BUTTON)
static ugui_text_cfg_t s_rb_txt[RB_N];
#endif
#if defined(UGUI_WIDGET_ENABLE_RADIO_BUTTON)
#define RB_X 28
#define RB_W (UGUI_SCREEN_W - 2 * RB_X)
#define RB_H 26
#define RB_Y0 (38 + 4)
#define RB_STEP 31
#endif
/* --- local helpers & event callbacks --- */
static void rb_add(uint8_t i, uint8_t group, const char* label, bool selected,
bool enabled, bool custom, ugui_color_t color)
{
ugui_widget_radio_init(&s_root, &s_rb[i], &s_rb_ext[i]);
ugui_widget_set_area(&s_rb[i], RB_X, (int16_t)(RB_Y0 + (int16_t)i * RB_STEP),
RB_W, RB_H);
ugui_widget_set_id (&s_rb[i], (uint8_t)(WDGT_ID_SLOT0 + i));
ugui_widget_radio_set_group(&s_rb[i], group);
if (custom)
{
/* ring = dot = the accent colour; bg = the surface (interior). */
ugui_widget_radio_set_colors(&s_rb[i], color, color, UGUI_THEME_SURFACE);
}
if (selected) { ugui_widget_radio_set_state(&s_rb[i], true); }
if (!enabled) { ugui_widget_set_enabled(&s_rb[i], false); }
#ifdef UGUI_ENABLE_FONT
ugui_text_cfg_init(&s_rb_txt[i], label, &lexend_14pt_2bpp,
enabled ? UGUI_THEME_TEXT : UGUI_COLOR_GRAY,
(ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
(ugui_point_t){ 0, 0 }, 0u);
ugui_widget_radio_set_text(&s_rb[i], &s_rb_txt[i]);
#else
(void)label;
#endif
ugui_screen_add_widget(&s_screen, &s_rb[i]);
#if UGUI_ENABLE_KEYPAD
if (enabled) { ugui_screen_add_focus(&s_screen, &s_rb[i]); }
#endif
}
/* --- build it (in your screen's init) --- */
/* idx group label select enabled custom colour */
/* rows 0/1 = ONE real On/Off group (tapping moves the single selection). */
rb_add(0u, 0u, "On", true, true, false, UGUI_THEME_ACCENT);
rb_add(1u, 0u, "Off", false, true, false, UGUI_THEME_ACCENT);
/* colour + disabled rows: each its own group, so the dot stays for display. */
rb_add(2u, 2u, "Custom: success", true, true, true, UGUI_THEME_OK);
rb_add(3u, 3u, "Custom: warning", true, true, true, UGUI_COLOR_ORANGE);
rb_add(4u, 4u, "Custom: danger", true, true, true, UGUI_THEME_CANCEL);
rb_add(5u, 5u, "Disabled (selected)", true, false, false, UGUI_THEME_ACCENT);
Independent Groups (live summary)¶
Two independent radio groups under one parent, kept apart purely by set_group() id — picking a Quality option never touches Subtitles. A live status line rebuilds from the current selection of each group 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_HDR_QUALITY, WDGT_ID_HDR_SUBTITLES,
WDGT_ID_DEMO
};
/* --- 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_RADIO_BUTTON) && defined(UGUI_WIDGET_ENABLE_LABEL) && defined(UGUI_ENABLE_FONT)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
#define RB_N 5
static ugui_widget_t s_rb[RB_N];
static ugui_radio_button_ext_t s_rb_ext[RB_N];
static ugui_text_cfg_t s_rb_txt[RB_N];
static const char* const s_name[RB_N] = { "Low", "Medium", "High", "Off", "On" };
static const uint8_t s_grp [RB_N] = { 0u, 0u, 0u, 1u, 1u };
static ugui_widget_t s_hdr[2];
static ugui_label_ext_t s_hdr_ext[2];
static ugui_widget_t s_status;
static ugui_label_ext_t s_status_ext;
static char s_status_buf[56];
#define RB_X 24
#define RB_W (UGUI_SCREEN_W - 2 * RB_X)
#define RB_H 24
#define HDR_H 16
#endif
/* --- local helpers & event callbacks --- */
static const char* selected_name(uint8_t group)
{
for (uint8_t i = 0u; i < (uint8_t)RB_N; i++)
{
if ((s_grp[i] == group) && ugui_widget_radio_get_state(&s_rb[i]))
{
return s_name[i];
}
}
return "-";
}
static void update_status(void)
{
(void)snprintf(s_status_buf, sizeof(s_status_buf), "Quality: %s Subtitles: %s",
selected_name(0u), selected_name(1u));
ugui_label_set_text(&s_status, s_status_buf);
}
static void on_rb_change(bool selected, void* ctx)
{
(void)ctx;
if (!selected) { return; }
update_status();
}
static void hdr_add(uint8_t i, int16_t y, const char* text, uint8_t id)
{
ugui_widget_label_init(&s_root, &s_hdr[i], &s_hdr_ext[i]);
ugui_widget_set_area(&s_hdr[i], RB_X, y, RB_W, HDR_H);
ugui_widget_set_id (&s_hdr[i], id);
ugui_widget_label_set_style(&s_hdr[i], false, UGUI_THEME_BG, text, &lexend_14pt_2bpp,
UGUI_THEME_ACCENT,
(ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER), 1u);
ugui_screen_add_widget(&s_screen, &s_hdr[i]);
}
static void rb_add(uint8_t i, int16_t y, bool selected)
{
ugui_widget_radio_init(&s_root, &s_rb[i], &s_rb_ext[i]);
ugui_widget_set_area(&s_rb[i], RB_X, y, RB_W, RB_H);
ugui_widget_set_id (&s_rb[i], (uint8_t)(WDGT_ID_SLOT0 + i));
ugui_widget_radio_set_group(&s_rb[i], s_grp[i]);
ugui_widget_radio_set_callback(&s_rb[i], on_rb_change, NULL);
if (selected) { ugui_widget_radio_set_state(&s_rb[i], true); }
ugui_text_cfg_init(&s_rb_txt[i], s_name[i], &lexend_14pt_2bpp, UGUI_THEME_TEXT,
(ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
(ugui_point_t){ 0, 0 }, 0u);
ugui_widget_radio_set_text(&s_rb[i], &s_rb_txt[i]);
ugui_screen_add_widget(&s_screen, &s_rb[i]);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_rb[i]);
#endif
}
/* --- build it (in your screen's init) --- */
hdr_add(0u, (int16_t)(38 + 2), "PLAYBACK QUALITY", WDGT_ID_HDR_QUALITY);
rb_add (0u, 58, false); /* Low */
rb_add (1u, 84, true); /* Medium (default)*/
rb_add (2u, 110, false); /* High */
hdr_add(1u, 138, "SUBTITLES", WDGT_ID_HDR_SUBTITLES);
rb_add (3u, 156, true); /* Off (default) */
rb_add (4u, 182, false); /* On */
/* Live status line. */
ugui_widget_label_init(&s_root, &s_status, &s_status_ext);
ugui_widget_set_area(&s_status, RB_X, 210, RB_W, 22);
ugui_widget_set_id (&s_status, WDGT_ID_DEMO);
ugui_widget_label_set_style(&s_status, false, UGUI_THEME_BG, s_status_buf,
&lexend_14pt_2bpp, UGUI_THEME_TEXT,
(ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER), 1u);
ugui_screen_add_widget(&s_screen, &s_status);
update_status(); /* seed the summary from the initial selections */
Live Preview (drives other widgets)¶
An accent-colour picker: four radio buttons, each tinted with the colour it represents, drive a large preview box (ugui_widget_box_set_style() + invalidate()) and a name label. The callback context carries the colour + name for the chosen option, so one shared handler serves all four buttons with no per-button branching.

/* --- 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_PREVIEW, WDGT_ID_NAME
};
/* --- 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_RADIO_BUTTON) && defined(UGUI_WIDGET_ENABLE_LABEL) && defined(UGUI_ENABLE_FONT)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
typedef struct { ugui_color_t color; const char* name; } accent_t;
#define RB_N 4
static const accent_t s_accent[RB_N] = {
{ UGUI_COLOR_PURPLE, "Purple" },
{ UGUI_COLOR_ORANGE, "Orange" },
{ UGUI_COLOR_TEAL, "Teal" },
{ UGUI_COLOR_GREEN, "Green" },
};
static ugui_widget_t s_rb[RB_N];
static ugui_radio_button_ext_t s_rb_ext[RB_N];
static ugui_text_cfg_t s_rb_txt[RB_N];
static ugui_widget_t s_preview;
static ugui_box_ext_t s_preview_ext;
static ugui_widget_t s_name;
static ugui_label_ext_t s_name_ext;
#define RB_X 24
#define RB_W 150
#define RB_H 28
#define RB_Y0 56
#define RB_STEP 34
#define PV_X 198
#define PV_Y 58
#define PV_W 98
#define PV_H 98
#define NAME_Y (PV_Y + PV_H + 6)
#endif
/* --- local helpers & event callbacks --- */
static void apply_accent(const accent_t* a)
{
if (a == NULL) { return; }
ugui_widget_box_set_style(&s_preview, true, 10u, 1u, a->color, UGUI_THEME_ACCENT);
ugui_widget_invalidate(&s_preview);
ugui_label_set_text(&s_name, a->name);
}
static void on_accent_change(bool selected, void* ctx)
{
if (!selected) { return; }
apply_accent((const accent_t*)ctx);
}
/* --- build it (in your screen's init) --- */
/* The radio group — one group (0); each tinted with its own colour. */
for (uint8_t i = 0u; i < (uint8_t)RB_N; i++)
{
ugui_widget_radio_init(&s_root, &s_rb[i], &s_rb_ext[i]);
ugui_widget_set_area(&s_rb[i], RB_X, (int16_t)(RB_Y0 + (int16_t)i * RB_STEP),
RB_W, RB_H);
ugui_widget_set_id(&s_rb[i], (uint8_t)(WDGT_ID_SLOT0 + i));
ugui_widget_radio_set_colors(&s_rb[i], s_accent[i].color, s_accent[i].color,
UGUI_THEME_SURFACE);
ugui_widget_radio_set_callback(&s_rb[i], on_accent_change,
(void*)&s_accent[i]);
ugui_text_cfg_init(&s_rb_txt[i], s_accent[i].name, &lexend_14pt_2bpp, UGUI_THEME_TEXT,
(ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
(ugui_point_t){ 0, 0 }, 0u);
ugui_widget_radio_set_text(&s_rb[i], &s_rb_txt[i]);
ugui_screen_add_widget(&s_screen, &s_rb[i]);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_rb[i]);
#endif
}
/* Preview box (recoloured on selection). */
ugui_widget_box_init(&s_root, &s_preview, &s_preview_ext);
ugui_widget_set_area(&s_preview, PV_X, PV_Y, PV_W, PV_H);
ugui_widget_set_id (&s_preview, WDGT_ID_PREVIEW);
ugui_widget_box_set_style(&s_preview, true, 10u, 1u, s_accent[0].color, UGUI_THEME_ACCENT);
ugui_screen_add_widget(&s_screen, &s_preview);
/* Name label under the preview. */
ugui_widget_label_init(&s_root, &s_name, &s_name_ext);
ugui_widget_set_area(&s_name, PV_X - 10, NAME_Y, PV_W + 20, 22);
ugui_widget_set_id (&s_name, WDGT_ID_NAME);
ugui_widget_label_set_style(&s_name, false, UGUI_THEME_BG, s_accent[0].name,
&lexend_14pt_2bpp, UGUI_THEME_TEXT, UGUI_ALIGN_CENTER, 1u);
ugui_screen_add_widget(&s_screen, &s_name);
/* Seed the initial selection + preview (Purple). */
ugui_widget_radio_set_state(&s_rb[0], true);
apply_accent(&s_accent[0]);
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.