Skip to content

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.

Default Group

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

States & Colour Overrides

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

Independent Groups (live summary)

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

Live Preview (drives other widgets)

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