Radial Widget¶
The Radial is a radial menu / shortcut hub — a ring of selectable
bubbles around a centre hub, for round screens and shortcut launchers.
Items are nodes in a caller-declared tree (ugui_radial_node_t, often
flash-resident); the widget owns ALL multi-level navigation itself:
selecting a node with children DESCENDS into a new ring (the current menu
is pushed onto a bounded ancestor stack), a node flagged UGUI_RADIAL_BACK
ASCENDS one level, and a leaf node fires its own on_activate — where the
application opens an existing widget or switches screen. Init installs the
whole look (theme-tracked colours, auto radii, hub + rim track shown, key
wrap on) at a settable size; set_font() gives it text and
set_root() attaches the tree — set_root()'s argument is ONE node whose
CHILDREN are the visible bubbles, so the root itself is never drawn. Driven
equally by touch (tap a bubble — hit-tests and activates in one step) and a
rotary encoder / keypad (rotate = move around the ring, push = select); the
hub always shows the focused node's label + value. set_theme() replaces
every colour with a fixed palette independent of the active app theme,
set_hub_font() gives the hub's VALUE a different (often larger) font than
the bubble labels, and set_on_focus() fires every time the cursor moves —
even while just browsing, without confirming.
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: A Single Ring of Shortcuts¶
Three calls make a working radial hub: ugui_widget_radial_init() installs the whole look (theme-tracked colours, auto radii, hub + rim track), set_font() gives it text, set_root() attaches a flash-resident node tree whose six children become the bubbles. Rotate with the keypad (or drag/tap a bubble) to move focus — the hub always shows the focused bubble's label; confirming a leaf fires its on_activate.

/* --- 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
};
/* --- 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_RADIAL)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_status;
#endif
#if defined(UGUI_WIDGET_ENABLE_RADIAL)
static char s_status_str[32];
#endif
#if defined(UGUI_WIDGET_ENABLE_RADIAL)
static ugui_widget_t s_radial;
static ugui_radial_ext_t s_radial_ext;
static void on_leaf(ugui_widget_t* w, const ugui_radial_node_t* node, void* ctx);
static const ugui_radial_node_t k_bubbles[] = {
{ .label = "Home", .on_activate = on_leaf, .id = 1u },
{ .label = "Music", .on_activate = on_leaf, .id = 2u },
{ .label = "Camera", .on_activate = on_leaf, .id = 3u },
{ .label = "Settings", .on_activate = on_leaf, .id = 4u },
{ .label = "Wi-Fi", .on_activate = on_leaf, .id = 5u },
{ .label = "Info", .on_activate = on_leaf, .id = 6u },
};
#define BUBBLE_N ((uint8_t)(sizeof(k_bubbles) / sizeof(k_bubbles[0])))
static const ugui_radial_node_t k_root = {
.children = k_bubbles, .child_count = BUBBLE_N,
};
#endif
/* --- local helpers & event callbacks --- */
static void on_leaf(ugui_widget_t* w, const ugui_radial_node_t* node, void* ctx)
{
(void)w; (void)ctx;
#ifdef UGUI_ENABLE_FONT
(void)snprintf(s_status_str, sizeof(s_status_str), "opened: %s", node->label);
ugui_widget_invalidate(&s_status);
#endif
printf("[radial.default] activated '%s' (id %u)\n",
node->label, (unsigned)node->id);
}
/* --- build it (in your screen's init) --- */
ugui_widget_radial_init(&s_root, &s_radial, &s_radial_ext);
ugui_widget_set_area(&s_radial, 0, (int16_t)(38 - 6),
(int16_t)UGUI_SCREEN_W, (int16_t)(UGUI_SCREEN_H - 38 - 24));
ugui_widget_set_id(&s_radial, WDGT_ID_DEMO);
ugui_widget_radial_set_font(&s_radial, &lexend_14pt_2bpp);
ugui_widget_radial_set_root(&s_radial, &k_root);
ugui_screen_add_widget(&s_screen, &s_radial);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_radial);
#endif
Two Levels: Submenus, Back, and a Direct Leaf¶
Two top-level bubbles are SUBMENUS (children != NULL) — selecting one descends into its own ring, pushing the current menu onto the widget's ancestor stack; a bubble flagged UGUI_RADIAL_BACK ascends one level. The third top-level bubble is a plain leaf, reachable without descending at all. No app code drives navigation — set_root() attaches the whole tree once and the widget owns descend/ascend from then on.

/* --- 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
};
/* --- 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_RADIAL)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_status;
#endif
#if defined(UGUI_WIDGET_ENABLE_RADIAL)
static char s_status_str[40];
#endif
#if defined(UGUI_WIDGET_ENABLE_RADIAL)
static ugui_widget_t s_radial;
static ugui_radial_ext_t s_radial_ext;
static void on_leaf(ugui_widget_t* w, const ugui_radial_node_t* node, void* ctx);
static const ugui_radial_node_t k_media[] = {
{ .label = "Play", .on_activate = on_leaf, .id = 11u },
{ .label = "Pause", .on_activate = on_leaf, .id = 12u },
{ .label = "Volume", .on_activate = on_leaf, .id = 13u },
{ .label = "Back", .flags = UGUI_RADIAL_BACK },
};
static const ugui_radial_node_t k_system[] = {
{ .label = "Wi-Fi", .on_activate = on_leaf, .id = 21u },
{ .label = "Bluetooth", .on_activate = on_leaf, .id = 22u },
{ .label = "Back", .flags = UGUI_RADIAL_BACK },
};
static const ugui_radial_node_t k_top[] = {
{ .label = "Media", .children = k_media,
.child_count = (uint8_t)(sizeof(k_media) / sizeof(k_media[0])) },
{ .label = "System", .children = k_system,
.child_count = (uint8_t)(sizeof(k_system) / sizeof(k_system[0])) },
{ .label = "Power", .on_activate = on_leaf, .id = 30u },
};
static const ugui_radial_node_t k_root = {
.children = k_top, .child_count = (uint8_t)(sizeof(k_top) / sizeof(k_top[0])),
};
#endif
/* --- local helpers & event callbacks --- */
static void on_leaf(ugui_widget_t* w, const ugui_radial_node_t* node, void* ctx)
{
(void)w; (void)ctx;
#ifdef UGUI_ENABLE_FONT
(void)snprintf(s_status_str, sizeof(s_status_str), "opened: %s", node->label);
ugui_widget_invalidate(&s_status);
#endif
printf("[radial.nested] activated '%s' (id %u)\n", node->label, (unsigned)node->id);
}
/* --- build it (in your screen's init) --- */
ugui_widget_radial_init(&s_root, &s_radial, &s_radial_ext);
ugui_widget_set_area(&s_radial, 0, (int16_t)(38 - 6),
(int16_t)UGUI_SCREEN_W, (int16_t)(UGUI_SCREEN_H - 38 - 24));
ugui_widget_set_id(&s_radial, WDGT_ID_DEMO);
ugui_widget_radial_set_font(&s_radial, &lexend_14pt_2bpp);
ugui_widget_radial_set_root(&s_radial, &k_root);
ugui_screen_add_widget(&s_screen, &s_radial);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_radial);
#endif
Styled Radio Presets: Theme, Hub Font & on_focus¶
A 5-station radio preset dial: each bubble carries a fixed value string (the hub shows the focused bubble's label AND value — a station name over its preset slot number), set_theme() replaces every colour with a fixed "industrial" palette, set_hub_font() gives the hub VALUE a larger digits-only font than the bubble labels (short enough to fit the auto-sized hub), and set_on_focus() fires on every cursor move — even browsing without confirming — so the status line always reflects the current selection.

/* --- 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
};
/* --- 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;
extern const ugui_font_t lexend_num_40pt_2bpp;
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_RADIAL)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_status;
#endif
#if defined(UGUI_WIDGET_ENABLE_RADIAL)
static char s_status_str[32];
#endif
#if defined(UGUI_WIDGET_ENABLE_RADIAL)
static ugui_widget_t s_radial;
static ugui_radial_ext_t s_radial_ext;
static const ugui_radial_node_t k_bubbles[] = {
{ .label = "Rock", .value = "1" },
{ .label = "Jazz", .value = "2" },
{ .label = "News", .value = "3" },
{ .label = "Talk", .value = "4" },
{ .label = "Pop", .value = "5" },
};
#define BUBBLE_N ((uint8_t)(sizeof(k_bubbles) / sizeof(k_bubbles[0])))
static const ugui_radial_node_t k_root = {
.children = k_bubbles, .child_count = BUBBLE_N,
};
#endif
/* --- local helpers & event callbacks --- */
static void on_focus(ugui_widget_t* w, const ugui_radial_node_t* node, void* ctx)
{
(void)w; (void)ctx;
#ifdef UGUI_ENABLE_FONT
(void)snprintf(s_status_str, sizeof(s_status_str), "preset %s: %s",
(node->value != NULL) ? node->value : "-", node->label);
ugui_widget_invalidate(&s_status);
#endif
printf("[radial.styled] focus -> %s\n", node->label);
}
/* --- build it (in your screen's init) --- */
ugui_widget_radial_init(&s_root, &s_radial, &s_radial_ext);
ugui_widget_set_area(&s_radial, 0, (int16_t)(38 - 6),
(int16_t)UGUI_SCREEN_W, (int16_t)(UGUI_SCREEN_H - 38 - 24));
ugui_widget_set_id(&s_radial, WDGT_ID_DEMO);
ugui_widget_radial_set_font(&s_radial, &lexend_14pt_2bpp);
#ifdef UGUI_ENABLE_FONT
ugui_widget_radial_set_hub_font(&s_radial, &lexend_num_40pt_2bpp);
#endif
ugui_widget_radial_set_theme(&s_radial,
ugui_color_hex(0x14140Fu), /* bg */
ugui_color_hex(0x3A3A28u), /* rim track */
ugui_color_hex(0x25251Au), /* bubble bg */
ugui_color_hex(0xE0D8B0u), /* bubble text */
ugui_color_hex(0xC8A000u), /* focus bg */
ugui_color_hex(0x14140Fu), /* focus text */
ugui_color_hex(0x1E1E15u), /* hub bg */
ugui_color_hex(0xE0D8B0u), /* hub text */
ugui_color_hex(0xC8A000u), /* accent */
ugui_color_hex(0x605840u)); /* disabled */
ugui_widget_radial_set_root(&s_radial, &k_root);
ugui_widget_radial_set_on_focus(&s_radial, on_focus, NULL);
ugui_screen_add_widget(&s_screen, &s_radial);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_radial);
#endif
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.