Skip to content

Arc Widget

The Arc widget draws a ring, or a multi-ring stack, that fills to show a value — a read-only gauge, or (with a callback) a circular slider. Init installs a 135° start / 270° sweep, theme-accent fill and flat caps at a settable thickness; set_value() fills it (range defaults to 0..100). set_dot() adds a grab handle and set_callback() turns the ring into an interactive control — drag anywhere on it, or step it with focus + keys — so the same widget serves as a static ring, a live instrument, or a knob. set_rings() attaches a small static array of ugui_arc_ring_t so one widget rasterises several concentric rings (Apple-Watch-style activity rings) in a single pass, no per-ring widget and no heap.

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 Ring

Three calls make a presentable ring: ugui_widget_arc_init() installs the whole look (135° start, 270° sweep, 10 px track, theme-accent fill, flat caps), set_area() places it, set_value() fills it. Range defaults to 0..100, so a value of 68 reads as 68 %.

Default 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_DEMO = 10
};

/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_ARC)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t   s_arc;
static ugui_arc_ext_t  s_arc_ext;
#define ARC_S  (UGUI_SCREEN_H - 38 - 8)
#define ARC_X  ((UGUI_SCREEN_W - ARC_S) / 2)
#define ARC_Y  (38 + 2)
#endif

/* --- build it (in your screen's init) --- */
ugui_widget_arc_init(&s_root, &s_arc, &s_arc_ext);
ugui_widget_set_area(&s_arc, ARC_X, ARC_Y, ARC_S, ARC_S);
ugui_widget_arc_set_value(&s_arc, 68);
ugui_widget_set_id(&s_arc, WDGT_ID_DEMO);
ugui_screen_add_widget(&s_screen, &s_arc);

Value Gauge (live feed)

The same arc restyled into an instrument: set_thickness() fattens the track, set_cap(ROUND) rounds the fill ends, and a LABEL child parented to the arc sits in the ring hole showing the value as "NN%" — laid out in arc-relative coordinates, clipped to the ring, display-only so it never eats input.

Value Gauge (live feed)

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

/* --- 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_ARC)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t   s_arc;
static ugui_arc_ext_t  s_arc_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_ARC)
#if defined(UGUI_WIDGET_ENABLE_LABEL) && defined(UGUI_ENABLE_FONT)
static ugui_widget_t    s_val;          /* centred value readout (child of arc) */
static ugui_label_ext_t s_val_ext;
static char             s_val_buf[8];   /* "100%" + NUL; set_text keeps ptr    */
#endif
#endif
#if defined(UGUI_WIDGET_ENABLE_ARC)
#define ARC_S   (UGUI_SCREEN_H - 38 - 8)
#define ARC_X   ((UGUI_SCREEN_W - ARC_S) / 2)
#define ARC_Y   (38 + 2)
#define VAL_W   90
#define VAL_H   28
#define VAL_X   ((ARC_S - VAL_W) / 2)
#define VAL_Y   ((ARC_S - VAL_H) / 2)
#endif

/* --- build it (in your screen's init) --- */
ugui_widget_arc_init(&s_root, &s_arc, &s_arc_ext);
ugui_widget_set_area(&s_arc, ARC_X, ARC_Y, ARC_S, ARC_S);
ugui_widget_arc_set_thickness(&s_arc, 16u);
ugui_widget_arc_set_cap(&s_arc, UGUI_ARC_CAP_ROUND);
ugui_widget_arc_set_value(&s_arc, 50);
ugui_widget_set_id(&s_arc, WDGT_ID_DEMO);
ugui_screen_add_widget(&s_screen, &s_arc);

#if defined(UGUI_WIDGET_ENABLE_LABEL) && defined(UGUI_ENABLE_FONT)
/* Value readout — a CHILD of the arc, centred in the ring hole. Parented to
 * the arc so it uses arc-relative coords and is clipped to the ring; marked
 * display-only so it never intercepts touch. */
(void)snprintf(s_val_buf, sizeof(s_val_buf), "%d%%", 50);
ugui_widget_label_init(&s_arc, &s_val, &s_val_ext);
ugui_widget_set_area(&s_val, VAL_X, VAL_Y, VAL_W, VAL_H);
ugui_widget_set_id(&s_val, WDGT_ID_VALUE);
ugui_widget_label_set_style(&s_val, /*bg_fill=*/false, UGUI_THEME_BG, s_val_buf,
                            &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                            UGUI_ALIGN_CENTER, 1u);
ugui_widget_set_enabled(&s_val, false);   /* display-only — do not eat input */
ugui_screen_add_widget(&s_screen, &s_val);
#endif

Knob (interactive)

What sets the arc apart from a read-only gauge: it takes input. Drag anywhere on the ring (integer atan2, no float) or, when focused, step it with the keypad; set_dot() adds a grab handle. A pair of [-]/[+] buttons drives the same value directly, proving one widget, multiple input drivers, always in sync.

Knob (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_MINUS = 10, WDGT_ID_PLUS, WDGT_ID_DEMO,
    WDGT_ID_VALUE
};

/* --- 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_ARC)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t   s_arc;
static ugui_arc_ext_t  s_arc_ext;
static ugui_widget_t     s_minus;
static ugui_button_ext_t s_minus_ext;
static ugui_widget_t     s_plus;
static ugui_button_ext_t s_plus_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_ARC)
static ugui_text_cfg_t   s_minus_txt;
static ugui_text_cfg_t   s_plus_txt;
#endif
#if defined(UGUI_WIDGET_ENABLE_ARC)
#if defined(UGUI_WIDGET_ENABLE_LABEL) && defined(UGUI_ENABLE_FONT)
static ugui_widget_t    s_val;          /* value readout (child of arc) */
static ugui_label_ext_t s_val_ext;
static char             s_val_buf[8];
#endif
#endif
#if defined(UGUI_WIDGET_ENABLE_ARC)
#define KNOB_STEP  5
#define ARC_S   150
#define ARC_X   ((UGUI_SCREEN_W - ARC_S) / 2)
#define ARC_Y   (38 + 2)
#define VAL_W   80
#define VAL_H   28
#define VAL_X   ((ARC_S - VAL_W) / 2)
#define VAL_Y   ((ARC_S - VAL_H) / 2)
#define BTN_W   92
#define BTN_GAP 16
#define BTN_H   34
#define BTN_Y   (ARC_Y + ARC_S + 8)
#define BTN_MINUS_X  ((UGUI_SCREEN_W - (2 * BTN_W + BTN_GAP)) / 2)
#define BTN_PLUS_X   (BTN_MINUS_X + BTN_W + BTN_GAP)
#endif

/* --- local helpers & event callbacks --- */
static void knob_show(int16_t v)
{
#if defined(UGUI_WIDGET_ENABLE_LABEL) && defined(UGUI_ENABLE_FONT)
    (void)snprintf(s_val_buf, sizeof(s_val_buf), "%d", (int)v);
    ugui_widget_invalidate(&s_val);
#else
    (void)v;
#endif
}

static void on_knob_change(int16_t value, void* ctx)
{
    (void)ctx;
    knob_show(value);
}

static void on_step(uint8_t widget_id, ugui_event_t event)
{
    if (event != UGUI_EVENT_CLICK) { return; }

    int16_t v = ugui_widget_arc_get_value(&s_arc);
    if (widget_id == WDGT_ID_MINUS)      { v = (int16_t)(v - KNOB_STEP); }
    else if (widget_id == WDGT_ID_PLUS) { v = (int16_t)(v + KNOB_STEP); }

    ugui_widget_arc_set_value(&s_arc, v);                 /* clamps internally */
    knob_show(ugui_widget_arc_get_value(&s_arc));
}

static void add_button(ugui_widget_t* w, ugui_button_ext_t* ext, ugui_text_cfg_t* text,
                       uint8_t id, int16_t x, int16_t y, int16_t bw, int16_t bh,
                       const char* str, ugui_event_cb_t cb)
{
    ugui_widget_button_init(&s_root, w, ext);
    ugui_widget_set_area(w, x, y, bw, bh);
    ugui_widget_set_id(w, id);
    ugui_widget_button_set_style(w, true, 5u, 1u, UGUI_THEME_SURFACE, UGUI_THEME_ACCENT);
#ifdef UGUI_ENABLE_FONT
    ugui_text_cfg_init(text, str, &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                       UGUI_ALIGN_CENTER, (ugui_point_t){ 0, 0 }, 1u);
    ugui_widget_button_set_text(w, text);
#else
    (void)str;
#endif
    ugui_widget_set_event_cb(w, cb,
                             UGUI_EMASK_CLICK | UGUI_EMASK_DOWN | UGUI_EMASK_UP
                             | UGUI_EMASK_KEY_CONFIRM);
    ugui_screen_add_widget(&s_screen, w);
#if UGUI_ENABLE_KEYPAD
    ugui_screen_add_focus(&s_screen, w);
#endif
}

/* --- build it (in your screen's init) --- */
ugui_widget_arc_init(&s_root, &s_arc, &s_arc_ext);
ugui_widget_set_area(&s_arc, ARC_X, ARC_Y, ARC_S, ARC_S);
ugui_widget_arc_set_range(&s_arc, 0, 100);
ugui_widget_arc_set_value(&s_arc, 40);
ugui_widget_arc_set_thickness(&s_arc, 16u);
ugui_widget_arc_set_cap(&s_arc, UGUI_ARC_CAP_ROUND);
ugui_widget_arc_set_dot(&s_arc, 6u, UGUI_THEME_KNOB);   /* grab handle (grows on press) */
ugui_widget_arc_set_step(&s_arc, KNOB_STEP);
ugui_widget_arc_set_callback(&s_arc, on_knob_change, NULL);
ugui_widget_set_id(&s_arc, WDGT_ID_DEMO);
ugui_screen_add_widget(&s_screen, &s_arc);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_arc);
#endif

#if defined(UGUI_WIDGET_ENABLE_LABEL) && defined(UGUI_ENABLE_FONT)
(void)snprintf(s_val_buf, sizeof(s_val_buf), "%d", 40);
ugui_widget_label_init(&s_arc, &s_val, &s_val_ext);
ugui_widget_set_area(&s_val, VAL_X, VAL_Y, VAL_W, VAL_H);
ugui_widget_set_id(&s_val, WDGT_ID_VALUE);
ugui_widget_label_set_style(&s_val, /*bg_fill=*/false, UGUI_THEME_BG, s_val_buf,
                            &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                            UGUI_ALIGN_CENTER, 1u);
ugui_widget_set_enabled(&s_val, false);   /* knob drag needs unblocked touch */
ugui_screen_add_widget(&s_screen, &s_val);
#endif

/* Step buttons — a second, independent driver for the same value. */
add_button(&s_minus, &s_minus_ext, &s_minus_txt, WDGT_ID_MINUS,
          BTN_MINUS_X, BTN_Y, BTN_W, BTN_H, "-", on_step);
add_button(&s_plus, &s_plus_ext, &s_plus_txt, WDGT_ID_PLUS,
          BTN_PLUS_X, BTN_Y, BTN_W, BTN_H, "+", on_step);

Concentric Rings (live feed)

set_rings() attaches a static array of three ugui_arc_ring_t — one widget rasterises every ring in a single scan-line pass, no per-ring widget, no heap. All rings share the widget's start/sweep/thickness/cap; each ring carries its own radius, value and colours.

Concentric Rings (live feed)

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

/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_ARC) && UGUI_ARC_ENABLE_MULTI_RING
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t   s_arc;
static ugui_arc_ext_t  s_arc_ext;
static ugui_arc_ring_t s_rings[3];      /* mutated by the feed (non-const)   */
#define COL_MOVE      ugui_color_hex(0xFA3C4Cu)   /* red    — outer */
#define COL_EXERCISE  ugui_color_hex(0x2BD576u)   /* green  — mid   */
#define COL_STAND     ugui_color_hex(0x22C3E6u)   /* cyan   — inner */
#define COL_MOVE_TR   ugui_color_hex(0x3A0E12u)
#define COL_EX_TR     ugui_color_hex(0x0E2A18u)
#define COL_STAND_TR  ugui_color_hex(0x0C2630u)
#define ARC_S   160
#define ARC_X   10
#define ARC_Y   (38 + 2)
#endif

/* --- local helpers & event callbacks --- */
static void set_ring(uint8_t i, uint8_t radius, ugui_color_t fill, ugui_color_t track)
{
    s_rings[i].radius      = radius;
    s_rings[i].value       = 50u;
    s_rings[i].color_fill  = fill;
    s_rings[i].color_track = track;
    s_rings[i].color_bg    = UGUI_THEME_BG;
    s_rings[i].dot_radius  = 0u;
    s_rings[i].dot_color   = UGUI_THEME_BG;
}

/* --- build it (in your screen's init) --- */
set_ring(0u, 76u, COL_MOVE,     COL_MOVE_TR);   /* outer */
set_ring(1u, 54u, COL_EXERCISE, COL_EX_TR);     /* mid   */
set_ring(2u, 32u, COL_STAND,    COL_STAND_TR);  /* inner */

ugui_widget_arc_init(&s_root, &s_arc, &s_arc_ext);
ugui_widget_set_area(&s_arc, ARC_X, ARC_Y, ARC_S, ARC_S);
ugui_widget_arc_set_angles(&s_arc, 0, 360);
ugui_widget_arc_set_thickness(&s_arc, 12u);
ugui_widget_arc_set_cap(&s_arc, UGUI_ARC_CAP_ROUND);
/* In multi-ring mode fill/track come from each ring; only bg is used here. */
ugui_widget_arc_set_colors(&s_arc, UGUI_THEME_BG, UGUI_THEME_BG, UGUI_THEME_BG);
ugui_widget_arc_set_rings(&s_arc, s_rings, 3u);
ugui_widget_set_id(&s_arc, WDGT_ID_DEMO);
ugui_screen_add_widget(&s_screen, &s_arc);


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.