Skip to content

Label Widget

The Label widget draws text — a caption, a value readout, a status banner. Unlike most widgets it has no default geometry (init() only zeroes the ext, so set_area() is mandatory), and set_style() sets the string, font, colour, alignment and an optional filled background in one call. bg_fill = false (the default) draws only glyphs, blending over whatever is already on screen — ideal for a caption over an image or coloured panel; bg_fill = true clears the widget rectangle first, so the label reads as a self-contained chip on any background. ugui_label_set_text() swaps the string pointer and invalidates the widget for live values, and — because hit tests don't care about widget type — a label becomes fully clickable with the same set_event_cb() call a button uses.

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 Label

The three calls that turn nothing into visible text: init() links the ext, set_area() gives it a rectangle (labels have no default geometry), and set_style() sets the string/font/colour with a transparent background so only the glyphs are drawn.

Default Label

/* --- 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_LABEL) && defined(UGUI_ENABLE_FONT)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t    s_lbl;
static ugui_label_ext_t s_lbl_ext;
#endif

/* --- build it (in your screen's init) --- */
ugui_widget_label_init(&s_root, &s_lbl, &s_lbl_ext);                  // Needed
ugui_widget_set_area(&s_lbl, 10, 100,
                     (int16_t)(UGUI_SCREEN_W - (2 * 10)), 28); // Needed
ugui_widget_set_id(&s_lbl, WDGT_ID_DEMO);
ugui_widget_label_set_style(&s_lbl,
                            /*bg_fill=*/false, UGUI_THEME_BG,
                            "Hello uGui", &lexend_14pt_2bpp,
                            UGUI_THEME_TEXT, UGUI_ALIGN_CENTER,
                            /*use_widget_area=*/1u);                  // Needed
ugui_screen_add_widget(&s_screen, &s_lbl);           // Needed

Alignment

Three full-width labels that differ ONLY in their ugui_align_t: LEFT|V_CENTER, CENTER, RIGHT|V_CENTER. Each gets a faint filled background so its rectangle is visible — that is what makes the alignment obvious: the same-width box, the text pinned to a different edge.

Alignment

/* --- 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_LEFT = 10, WDGT_ID_CENTER, WDGT_ID_RIGHT
};

/* --- 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_LABEL) && defined(UGUI_ENABLE_FONT)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t    s_left;
static ugui_label_ext_t s_left_ext;
static ugui_widget_t    s_center;
static ugui_label_ext_t s_center_ext;
static ugui_widget_t    s_right;
static ugui_label_ext_t s_right_ext;
#define AL_X       10
#define AL_W       ((int16_t)(UGUI_SCREEN_W - (2 * 10)))
#define AL_H       34
#define AL_ROW0_Y  54
#define AL_ROW_STEP 52
#endif

/* --- build it (in your screen's init) --- */
ugui_widget_label_init(&s_root, &s_left, &s_left_ext);
ugui_widget_set_area(&s_left, AL_X, AL_ROW0_Y + 0 * AL_ROW_STEP, AL_W, AL_H);
ugui_widget_set_id(&s_left, WDGT_ID_LEFT);
ugui_widget_label_set_style(&s_left, /*bg_fill=*/true, UGUI_THEME_SURFACE,
                            "Left aligned", &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                            (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
                            1u);
ugui_screen_add_widget(&s_screen, &s_left);

ugui_widget_label_init(&s_root, &s_center, &s_center_ext);
ugui_widget_set_area(&s_center, AL_X, AL_ROW0_Y + 1 * AL_ROW_STEP, AL_W, AL_H);
ugui_widget_set_id(&s_center, WDGT_ID_CENTER);
ugui_widget_label_set_style(&s_center, /*bg_fill=*/true, UGUI_THEME_SURFACE,
                            "Center aligned", &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                            UGUI_ALIGN_CENTER, 1u);
ugui_screen_add_widget(&s_screen, &s_center);

ugui_widget_label_init(&s_root, &s_right, &s_right_ext);
ugui_widget_set_area(&s_right, AL_X, AL_ROW0_Y + 2 * AL_ROW_STEP, AL_W, AL_H);
ugui_widget_set_id(&s_right, WDGT_ID_RIGHT);
ugui_widget_label_set_style(&s_right, /*bg_fill=*/true, UGUI_THEME_SURFACE,
                            "Right aligned", &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                            (ugui_align_t)(UGUI_ALIGN_RIGHT | UGUI_ALIGN_V_CENTER),
                            1u);
ugui_screen_add_widget(&s_screen, &s_right);

Transparent vs Opaque

The one property that separates a text overlay from a text box: bg_fill = false draws only glyphs over whatever is already there; bg_fill = true clears the rectangle to bg_color first. Both labels sit over the same busy accent panel so the difference is obvious.

Transparent vs Opaque

/* --- 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_TRANSPARENT = 10, WDGT_ID_OPAQUE
};

/* --- 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_LABEL) && defined(UGUI_ENABLE_FONT)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t    s_transp;
static ugui_label_ext_t s_transp_ext;
static ugui_widget_t    s_opaque;
static ugui_label_ext_t s_opaque_ext;
#endif

/* --- build it (in your screen's init) --- */
ugui_widget_label_init(&s_root, &s_transp, &s_transp_ext);
ugui_widget_set_area(&s_transp, 10 + 12, 70,
                     (int16_t)(UGUI_SCREEN_W - (2 * 10) - 24), 30);
ugui_widget_set_id(&s_transp, WDGT_ID_TRANSPARENT);
ugui_widget_label_set_style(&s_transp, /*bg_fill=*/false, UGUI_THEME_BG,
                            "Transparent (no fill)", &lexend_14pt_2bpp,
                            UGUI_THEME_KNOB, UGUI_ALIGN_CENTER, 1u);
ugui_screen_add_widget(&s_screen, &s_transp);

/* Opaque label: bg_fill = true — a solid bg_color chip over the panel. */
ugui_widget_label_init(&s_root, &s_opaque, &s_opaque_ext);
ugui_widget_set_area(&s_opaque, 10 + 12, 128,
                     (int16_t)(UGUI_SCREEN_W - (2 * 10) - 24), 34);
ugui_widget_set_id(&s_opaque, WDGT_ID_OPAQUE);
ugui_widget_label_set_style(&s_opaque, /*bg_fill=*/true, UGUI_THEME_BG,
                            "Opaque bg_fill", &lexend_14pt_2bpp,
                            UGUI_THEME_TEXT, UGUI_ALIGN_CENTER, 1u);
ugui_screen_add_widget(&s_screen, &s_opaque);

Semantic Colours

Six transparent labels, each a different colour drawn from the library's semantic roles: the theme accent, then OK / caution / alert / info / disabled. On an embedded status screen these are how a glance conveys state without any icon.

Semantic Colours

/* --- 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_LABEL) && defined(UGUI_ENABLE_FONT)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
#define COL_ROWS  6u
static ugui_widget_t    s_lbl[COL_ROWS];
static ugui_label_ext_t s_lbl_ext[COL_ROWS];
#define COL_X       24
#define COL_W       ((int16_t)(UGUI_SCREEN_W - 48))
#define COL_H       26
#define COL_ROW0_Y  46
#define COL_ROW_STEP 30
#endif

/* --- build it (in your screen's init) --- */
const struct { const char* text; ugui_color_t color; } rows[COL_ROWS] = {
    { "Accent  (theme)",   UGUI_THEME_ACCENT     },
    { "OK / nominal",      UGUI_COLOR_GREEN  },
    { "Caution",           UGUI_COLOR_AMBER  },
    { "Alert / fault",     UGUI_COLOR_RED    },
    { "Info",              UGUI_COLOR_CYAN   },
    { "Disabled",          UGUI_COLOR_GRAY   },
};

for (uint8_t i = 0u; i < (uint8_t)COL_ROWS; i++) {
    ugui_widget_label_init(&s_root, &s_lbl[i], &s_lbl_ext[i]);
    ugui_widget_set_area(&s_lbl[i], COL_X,
                         (int16_t)(COL_ROW0_Y + (i * COL_ROW_STEP)),
                         COL_W, COL_H);
    ugui_widget_set_id(&s_lbl[i], (uint8_t)(WDGT_ID_SLOT0 + i));
    ugui_widget_label_set_style(&s_lbl[i], /*bg_fill=*/false, UGUI_THEME_BG,
                                rows[i].text, &lexend_14pt_2bpp, rows[i].color,
                                (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
                                1u);
    ugui_screen_add_widget(&s_screen, &s_lbl[i]);
}

Live Update (tap to change text)

A label is display-only, but its text is not frozen: ugui_label_set_text() swaps the string pointer and invalidates the widget, so the next frame redraws it. IMPORTANT: the call stores the POINTER, not a copy — the strings it is handed must outlive the widget, so a live value needs a static buffer, never a stack local.

Live Update (tap to change text)

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

/* --- 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_LABEL) && defined(UGUI_ENABLE_FONT)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t    s_msg;
static ugui_label_ext_t s_msg_ext;
static ugui_widget_t    s_count;
static ugui_label_ext_t s_count_ext;
static ugui_widget_t     s_next_btn;
static ugui_button_ext_t s_next_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_LABEL) && defined(UGUI_ENABLE_FONT)
static ugui_text_cfg_t   s_next_text;
#endif
#if defined(UGUI_WIDGET_ENABLE_LABEL) && defined(UGUI_ENABLE_FONT)
static char     s_count_buf[24];
static uint32_t s_updates;
static const char* const s_msgs[] = {
    "Tap to update  ->",
    "Text updated!  [1]",
    "New string     [2]",
    "Label is live  [3]",
};
#define MSG_COUNT ((uint8_t)(sizeof(s_msgs) / sizeof(s_msgs[0])))
#endif

/* --- local helpers & event callbacks --- */
static void on_next(uint8_t widget_id, ugui_event_t event)
{
    (void)widget_id;
    if (event != UGUI_EVENT_CLICK) { return; }

    s_updates++;
    /* Swap the message label to the next string (pointer swap, no copy). */
    ugui_label_set_text(&s_msg, s_msgs[s_updates % (uint32_t)MSG_COUNT]);
    /* Rewrite the counter into its static buffer, then point the label at it. */
    (void)snprintf(s_count_buf, sizeof(s_count_buf), "Updates: %lu",
                   (unsigned long)s_updates);
    ugui_label_set_text(&s_count, s_count_buf);
    printf("[label.dynamic] update #%lu\n", (unsigned long)s_updates);
}

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);
#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_label_init(&s_root, &s_msg, &s_msg_ext);
ugui_widget_set_area(&s_msg, 20, 84, (int16_t)(UGUI_SCREEN_W - 40), 34);
ugui_widget_set_id(&s_msg, WDGT_ID_DEMO);
ugui_widget_label_set_style(&s_msg, /*bg_fill=*/true, UGUI_THEME_SURFACE,
                            s_msgs[0], &lexend_14pt_2bpp, UGUI_THEME_ACCENT,
                            UGUI_ALIGN_CENTER, 1u);
ugui_screen_add_widget(&s_screen, &s_msg);

/* The running-count label. */
ugui_widget_label_init(&s_root, &s_count, &s_count_ext);
ugui_widget_set_area(&s_count, 20, 128, (int16_t)(UGUI_SCREEN_W - 40), 24);
ugui_widget_set_id(&s_count, WDGT_ID_COUNT);
ugui_widget_label_set_style(&s_count, false, UGUI_THEME_BG,
                            s_count_buf, &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                            UGUI_ALIGN_CENTER, 1u);
ugui_screen_add_widget(&s_screen, &s_count);

/* The driver button (a plain button widget, not the label under test). */
add_button(&s_next_btn, &s_next_ext, &s_next_text, WDGT_ID_NEXT_BTN,
          40, 170, (int16_t)(UGUI_SCREEN_W - 80), 34,
          "Next text", on_next);

Device Status Panel

Nothing new about a single label — this shows how the alignment/background/colour primitives combine into a genuine UI: a card holds a filled heading banner, five key/value rows (keys neutral + left-aligned, values right-aligned and coloured by their semantic state), and a filled footer banner.

Device Status Panel

/* --- 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_SLOT1, WDGT_ID_DEMO,
    WDGT_ID_SLOT2
};

/* --- 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_LABEL) && defined(UGUI_ENABLE_FONT)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t   s_card;
static ugui_box_ext_t  s_card_ext;
static ugui_widget_t    s_head;
static ugui_label_ext_t s_head_ext;
static ugui_widget_t    s_foot;
static ugui_label_ext_t s_foot_ext;
#define PANEL_ROWS  5u
static ugui_widget_t    s_key[PANEL_ROWS];
static ugui_label_ext_t s_key_ext[PANEL_ROWS];
static ugui_widget_t    s_val[PANEL_ROWS];
static ugui_label_ext_t s_val_ext[PANEL_ROWS];
#define CARD_X    10
#define CARD_Y    42
#define CARD_W    ((int16_t)(UGUI_SCREEN_W - (2 * 10)))
#define CARD_H    188
#define ROW0_Y    86
#define ROW_STEP  23
#define ROW_H     20
#endif

/* --- build it (in your screen's init) --- */
/* Row data — value colours resolve at runtime, so the table is local. */
const struct { const char* key; const char* val; ugui_color_t color; }
rows[PANEL_ROWS] = {
    { "CPU load",  "42 %", UGUI_COLOR_GREEN },
    { "Memory",    "78 %", UGUI_COLOR_AMBER },
    { "Core temp", "58 C", UGUI_COLOR_GREEN },
    { "Uplink",    "OK",   UGUI_COLOR_GREEN },
    { "Battery",   "17 %", UGUI_COLOR_RED   },
};

/* Card background — the surface all the labels sit on. */
ugui_widget_box_init(&s_root, &s_card, &s_card_ext);
ugui_widget_set_area(&s_card, CARD_X, CARD_Y, CARD_W, CARD_H);
ugui_widget_set_id(&s_card, WDGT_ID_SLOT0);
ugui_widget_box_set_style(&s_card, true, 6u, 1u, UGUI_THEME_SURFACE, UGUI_THEME_ACCENT);
ugui_screen_add_widget(&s_screen, &s_card);

/* Heading banner — opaque accent bar with white text. */
ugui_widget_label_init(&s_root, &s_head, &s_head_ext);
ugui_widget_set_area(&s_head, CARD_X + 8, CARD_Y + 8, (int16_t)(CARD_W - 16), 26);
ugui_widget_set_id(&s_head, WDGT_ID_SLOT1);
ugui_widget_label_set_style(&s_head, /*bg_fill=*/true, UGUI_THEME_ACCENT,
                            "System Status", &lexend_14pt_2bpp, UGUI_THEME_KNOB,
                            UGUI_ALIGN_CENTER, 1u);
ugui_screen_add_widget(&s_screen, &s_head);

/* Key/value rows — transparent labels over the card. */
for (uint8_t i = 0u; i < (uint8_t)PANEL_ROWS; i++) {
    const int16_t y = (int16_t)(ROW0_Y + (i * ROW_STEP));

    ugui_widget_label_init(&s_root, &s_key[i], &s_key_ext[i]);
    ugui_widget_set_area(&s_key[i], CARD_X + 16, y, 150, ROW_H);
    ugui_widget_set_id(&s_key[i], WDGT_ID_DEMO);
    ugui_widget_label_set_style(&s_key[i], false, UGUI_THEME_SURFACE,
                                rows[i].key, &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                                (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
                                1u);
    ugui_screen_add_widget(&s_screen, &s_key[i]);

    ugui_widget_label_init(&s_root, &s_val[i], &s_val_ext[i]);
    ugui_widget_set_area(&s_val[i], (int16_t)(CARD_X + CARD_W - 16 - 126), y,
                         126, ROW_H);
    ugui_widget_set_id(&s_val[i], WDGT_ID_DEMO);
    ugui_widget_label_set_style(&s_val[i], false, UGUI_THEME_SURFACE,
                                rows[i].val, &lexend_14pt_2bpp, rows[i].color,
                                (ugui_align_t)(UGUI_ALIGN_RIGHT | UGUI_ALIGN_V_CENTER),
                                1u);
    ugui_screen_add_widget(&s_screen, &s_val[i]);
}

/* Footer banner — opaque caution bar summarising the rows. */
ugui_widget_label_init(&s_root, &s_foot, &s_foot_ext);
ugui_widget_set_area(&s_foot, CARD_X + 8, (int16_t)(CARD_Y + CARD_H - 30),
                     (int16_t)(CARD_W - 16), 22);
ugui_widget_set_id(&s_foot, WDGT_ID_SLOT2);
ugui_widget_label_set_style(&s_foot, /*bg_fill=*/true, UGUI_COLOR_AMBER,
                            "1 alert   |   1 caution", &lexend_14pt_2bpp,
                            UGUI_COLOR_BLACK, UGUI_ALIGN_CENTER, 1u);
ugui_screen_add_widget(&s_screen, &s_foot);

Clickable Label

A label is display-only by default, but hit-testing doesn't care about widget type — the SAME set_event_cb() call a button uses makes the whole label rectangle (not just the glyphs) a hit target. What it does NOT get for free is a button's press animation, so this screen supplies its own feedback: each tap swaps the fill/text/string via set_style().

Clickable Label

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

/* --- 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_LABEL) && defined(UGUI_ENABLE_FONT)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t    s_click;
static ugui_label_ext_t s_click_ext;
static ugui_widget_t    s_count;
static ugui_label_ext_t s_count_ext;
static char     s_count_buf[24];
static uint32_t s_taps;
static bool     s_on;
#endif

/* --- local helpers & event callbacks --- */
static void apply_state(void)
{
    if (s_on) {
        ugui_widget_label_set_style(&s_click, /*bg_fill=*/true, UGUI_COLOR_GREEN,
                                    "Tapped!  (ON)", &lexend_14pt_2bpp,
                                    UGUI_COLOR_BLACK, UGUI_ALIGN_CENTER, 1u);
    } else {
        ugui_widget_label_set_style(&s_click, /*bg_fill=*/true, UGUI_THEME_SURFACE,
                                    "Tap me  (OFF)", &lexend_14pt_2bpp,
                                    UGUI_THEME_TEXT, UGUI_ALIGN_CENTER, 1u);
    }
}

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

    s_on = !s_on;
    s_taps++;
    apply_state();
    (void)snprintf(s_count_buf, sizeof(s_count_buf), "Taps: %lu",
                   (unsigned long)s_taps);
    ugui_label_set_text(&s_count, s_count_buf);
    printf("[label.clickable] tap #%lu -> %s\n",
           (unsigned long)s_taps, s_on ? "ON" : "OFF");
}

/* --- build it (in your screen's init) --- */
ugui_widget_label_init(&s_root, &s_click, &s_click_ext);
ugui_widget_set_area(&s_click, 40, 90, (int16_t)(UGUI_SCREEN_W - 80), 40);
ugui_widget_set_id(&s_click, WDGT_ID_DEMO);
apply_state();                                    // seed the OFF style
ugui_widget_set_event_cb(&s_click, on_label_click,
                         UGUI_EMASK_CLICK | UGUI_EMASK_DOWN | UGUI_EMASK_UP
                         | UGUI_EMASK_KEY_CONFIRM);   // <-- makes it clickable
ugui_screen_add_widget(&s_screen, &s_click);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_click);
#endif

/* Tap counter — updated by on_label_click() above on every tap. */
ugui_widget_label_init(&s_root, &s_count, &s_count_ext);
ugui_widget_set_area(&s_count, 10, 148,
                     (int16_t)(UGUI_SCREEN_W - (2 * 10)), 24);
ugui_widget_set_id(&s_count, WDGT_ID_COUNT);
ugui_widget_label_set_style(&s_count, false, UGUI_THEME_BG,
                            s_count_buf, &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                            UGUI_ALIGN_CENTER, 1u);
ugui_screen_add_widget(&s_screen, &s_count);


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.