Skip to content

Button Widget

The Button is functionally identical to the Box — same filled rounded rectangle, same optional border/icon/caption, even the same built-in press-feedback animation (ugui_widget_button_set_anim() is a thin wrapper over the box's ugui_widget_box_set_anim()). What it adds is its own UGUI_TYPE_BUTTON type tag, so application code can tell a pressable control from a decorative panel by type alone — at zero cost: the draw/event/tick vtable entries for UGUI_TYPE_BUTTON point at the exact same functions as UGUI_TYPE_BOX, and ugui_button_ext_t is a compile-time alias of ugui_box_ext_t, so there is no duplicate Flash and no extra RAM per widget.

One default does differ from Box: a bare box sizes itself as a percentage of the screen width (so it scales with the panel), while a bare button defaults to a fixed 120×44 px regardless of panel size — see the first example below.

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 Button

A button created with nothing but ugui_widget_button_init(). It inherits every default: centred, 120×44, accent (purple) fill, rounded corners and no border. This is the baseline every other configuration builds on.

Default Button

/* --- file-scope storage --- */
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t      s_demo_btn;
static ugui_button_ext_t  s_demo_btn_ext;

/* --- build it (in your screen's init) --- */
ugui_widget_button_init(&s_root, &s_demo_btn, &s_demo_btn_ext);   // Needed: the button
ugui_screen_add_widget(&s_screen, &s_demo_btn);  // Needed: register

Button with Text

The default button with a centred caption and a click handler. Each tap rewrites the label through ugui_widget_button_update_text() — the label lives in a static buffer because update_text stores the pointer without copying it.

Button with Text

/* --- 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 --- */
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t      s_demo_btn;
static ugui_button_ext_t  s_demo_btn_ext;
static ugui_text_cfg_t    s_demo_text;
static char     s_label[24];
static uint32_t s_taps;

/* --- local helpers & event callbacks --- */
static void on_button_click(uint8_t widget_id, ugui_event_t event)
{
    (void)widget_id;
    if (event != UGUI_EVENT_CLICK) { return; }
    s_taps++;
#ifdef UGUI_ENABLE_FONT
    (void)snprintf(s_label, sizeof(s_label), "Tapped %lu", (unsigned long)s_taps);
    ugui_widget_button_update_text(&s_demo_btn, s_label);   // swap the label text
#endif
    printf("[button.text] tap #%lu\n", (unsigned long)s_taps);
}

/* --- build it (in your screen's init) --- */
ugui_widget_button_init(&s_root, &s_demo_btn, &s_demo_btn_ext);   // Needed: the button
#ifdef UGUI_ENABLE_FONT
ugui_text_cfg_init(&s_demo_text, "Tap me", &lexend_14pt_2bpp,
                   UGUI_THEME_KNOB, UGUI_ALIGN_CENTER, (ugui_point_t){ 0, 0 },
                   /*use_widget_area=*/1u);                       // Needed: caption cfg
ugui_widget_button_set_text(&s_demo_btn, &s_demo_text);           // Needed: attach text
#endif
/* Needed: deliver taps to on_button_click. CLICK is the logical press; DOWN/UP
 * + KEY_CONFIRM let touch and keypad both drive it. */
ugui_widget_set_event_cb(&s_demo_btn, on_button_click,
                         UGUI_EMASK_CLICK | UGUI_EMASK_DOWN | UGUI_EMASK_UP
                         | UGUI_EMASK_KEY_CONFIRM);
ugui_screen_add_widget(&s_screen, &s_demo_btn);  // Needed: register
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_demo_btn);
#endif

Press Feedback

Three buttons, three press animations attached with ugui_widget_button_set_anim(): UGUI_ANIM_DARKEN, UGUI_ANIM_LIGHTEN and UGUI_ANIM_COLOR_INVERT. The effect fires on DOWN and restores on UP, so the event mask must include both. (Shown at rest — press to feel it.) This is the same animation engine a plain Box can attach with ugui_widget_box_set_anim() — Button just inherits it for free.

Press Feedback

/* --- 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_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 --- */
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t      s_darken_btn;
static ugui_button_ext_t  s_darken_ext;
static ugui_text_cfg_t    s_darken_text;
static ugui_anim_cfg_t    s_darken_anim = {
    .type        = UGUI_ANIM_DARKEN,
    .param       = 35u,     /* darken the fill 35% on press */
    .duration_ms = 200u     /* fade back over 200 ms (animations build only) */
};
static ugui_widget_t      s_lighten_btn;
static ugui_button_ext_t  s_lighten_ext;
static ugui_text_cfg_t    s_lighten_text;
static ugui_anim_cfg_t    s_lighten_anim = {
    .type        = UGUI_ANIM_LIGHTEN,
    .param       = 45u,     /* lighten the fill 45% toward white on press */
    .duration_ms = 200u
};
static ugui_widget_t      s_invert_btn;
static ugui_button_ext_t  s_invert_ext;
static ugui_text_cfg_t    s_invert_text;
static ugui_anim_cfg_t    s_invert_anim = {
    .type        = UGUI_ANIM_COLOR_INVERT,
    .param       = 0u,      /* unused for INVERT */
    .duration_ms = 200u
};
#define BTN_X    60
#define BTN_W    (UGUI_SCREEN_W - 2 * BTN_X)
#define BTN_H    44
#define BTN0_Y   48
#define BTN_STEP (BTN_H + 12)
#define BTN1_Y   (BTN0_Y + 1 * BTN_STEP)
#define BTN2_Y   (BTN0_Y + 2 * BTN_STEP)

/* --- local helpers & event callbacks --- */
static void make_anim_button(ugui_widget_t* w, ugui_button_ext_t* ext,
                             ugui_text_cfg_t* text, ugui_anim_cfg_t* anim,
                             uint8_t id, int16_t y, const char* caption)
{
    ugui_widget_button_init(&s_root, w, ext);
    ugui_widget_set_area(w, BTN_X, y, BTN_W, BTN_H);
    ugui_widget_set_id  (w, id);
    ugui_widget_button_set_style(w, /*filled=*/true, 10u, 0u, UGUI_THEME_ACCENT, UGUI_THEME_ACCENT);
#ifdef UGUI_ENABLE_FONT
    ugui_text_cfg_init(text, caption, &lexend_14pt_2bpp, UGUI_THEME_KNOB,
                       UGUI_ALIGN_CENTER, (ugui_point_t){ 0, 0 }, /*use_widget_area=*/1u);
    ugui_widget_button_set_text(w, text);
#else
    (void)caption;
#endif
    /* Needed: attach the press animation + wire DOWN/UP so it actually fires.
     * No on_event callback — the effect is driven by the event mask alone. */
    ugui_widget_button_set_anim(w, anim);
    ugui_widget_set_event_cb(w, NULL,
                             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) --- */
make_anim_button(&s_darken_btn, &s_darken_ext, &s_darken_text, &s_darken_anim,
                 WDGT_ID_SLOT0, BTN0_Y, "Darken");
make_anim_button(&s_lighten_btn, &s_lighten_ext, &s_lighten_text, &s_lighten_anim,
                 WDGT_ID_SLOT1, BTN1_Y, "Lighten");
make_anim_button(&s_invert_btn, &s_invert_ext, &s_invert_text, &s_invert_anim,
                 WDGT_ID_SLOT2, BTN2_Y, "Invert");

Icon + Text

An icon button — a glyph on the left, a verb on the right. Icons attach with the Box helper ugui_widget_box_set_icon(): the button ext is the same type at the same union slot, so reusing it adds zero Flash.

Icon + Text

/* --- image assets (inc/icons/ + inc/images/, shipped with the library;
 *     each header DEFINES its image — include it in ONE .c only) --- */
#ifdef UGUI_WIDGET_ENABLE_IMAGE
#include "icons/search_icon_1_grayscale_4.h"   /* defines img_search_icon_1 */
#endif

/* --- 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 --- */
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t      s_demo_btn;
static ugui_button_ext_t  s_demo_btn_ext;
static ugui_text_cfg_t    s_demo_text;
static ugui_anim_cfg_t    s_demo_anim = {
    .type        = UGUI_ANIM_DARKEN,
    .param       = 30u,
    .duration_ms = 180u
};
#define BTN_W   210
#define BTN_H   56
#define BTN_X   ((UGUI_SCREEN_W - BTN_W) / 2)
#define BTN_Y   ((UGUI_SCREEN_H - BTN_H) / 2)
#define BTN_FILL    UGUI_THEME_ACCENT
#define ICON_W  24
#define ICON_X  22
#define ICON_Y  ((BTN_H - ICON_W) / 2)
#define TEXT_X  (ICON_X + ICON_W + 14)          /* just past the icon */

/* --- local helpers & event callbacks --- */
static void on_button_click(uint8_t widget_id, ugui_event_t event)
{
    (void)widget_id;
    if (event == UGUI_EVENT_CLICK) { printf("[button.icon] Search pressed\n"); }
}

/* --- build it (in your screen's init) --- */
ugui_widget_button_init(&s_root, &s_demo_btn, &s_demo_btn_ext);    // Needed: the button
ugui_widget_set_area(&s_demo_btn, BTN_X, BTN_Y, BTN_W, BTN_H);     // Needed: room for icon+text
ugui_widget_button_set_style(&s_demo_btn, true, 10u, 0u, BTN_FILL, UGUI_THEME_ACCENT);
#ifdef UGUI_WIDGET_ENABLE_IMAGE
/* Icon via the BOX helper — button ext IS box ext (same slot). */
ugui_widget_box_set_icon(&s_demo_btn, &img_search_icon_1,
                         (ugui_point_t){ ICON_X, ICON_Y },
                         UGUI_COLOR_WHITE, BTN_FILL);              // Needed: the icon
#endif
#ifdef UGUI_ENABLE_FONT
/* Label sits in the region right of the icon (left-aligned, no overlap). */
ugui_text_cfg_init(&s_demo_text, "Search", &lexend_14pt_2bpp, UGUI_THEME_KNOB,
                   (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER),
                   (ugui_point_t){ 0, 0 }, /*use_widget_area=*/0u);
s_demo_text.area = (ugui_rect_t){ TEXT_X, 0, BTN_W - TEXT_X - 10, BTN_H };
ugui_widget_button_set_text(&s_demo_btn, &s_demo_text);           // Needed: the label
#endif
/* Needed: press animation + click handler so it behaves like a button. */
ugui_widget_button_set_anim(&s_demo_btn, &s_demo_anim);
ugui_widget_set_event_cb(&s_demo_btn, on_button_click,
                         UGUI_EMASK_CLICK | UGUI_EMASK_DOWN | UGUI_EMASK_UP
                         | UGUI_EMASK_KEY_CONFIRM);
ugui_screen_add_widget(&s_screen, &s_demo_btn);  // Needed: register
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_demo_btn);
#endif

Action Button (toggle)

The everyday primary-action pattern: one button whose label and fill both flip on each tap (START ↔ STOP), with a status line echoing the state. Driven from the click handler with update_text + set_fill_color.

Action Button (toggle)

/* --- 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 --- */
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t      s_demo_btn;
static ugui_button_ext_t  s_demo_btn_ext;
static ugui_text_cfg_t    s_demo_text;
static ugui_anim_cfg_t    s_demo_anim = {
    .type        = UGUI_ANIM_DARKEN,
    .param       = 25u,
    .duration_ms = 160u
};
static ugui_widget_t   s_status_w;
static ugui_box_ext_t  s_status_ext;
static ugui_text_cfg_t s_status_text;
static bool         s_running;
static ugui_color_t s_col_start;   /* green */
static ugui_color_t s_col_stop;    /* red   */
#define BTN_W     200
#define BTN_H     52
#define STATUS_W  240
#define STATUS_H  30
#define BLOCK_GAP 18
#define BLOCK_H   (BTN_H + BLOCK_GAP + STATUS_H)
#define BTN_X     ((UGUI_SCREEN_W - BTN_W) / 2)
#define BTN_Y     ((UGUI_SCREEN_H - BLOCK_H) / 2)
#define STATUS_X  ((UGUI_SCREEN_W - STATUS_W) / 2)
#define STATUS_Y  (BTN_Y + BTN_H + BLOCK_GAP)

/* --- local helpers & event callbacks --- */
static void apply_state(void)
{
    const char* label  = s_running ? "STOP"      : "START";
    const char* status = s_running ? "Running..." : "Idle";
    ugui_widget_button_set_fill_color(&s_demo_btn, s_running ? s_col_stop : s_col_start);
#ifdef UGUI_ENABLE_FONT
    ugui_widget_button_update_text(&s_demo_btn, label);
    ugui_widget_box_update_text_str(&s_status_w, status);
#else
    (void)label; (void)status;
#endif
}

static void on_button_click(uint8_t widget_id, ugui_event_t event)
{
    (void)widget_id;
    if (event != UGUI_EVENT_CLICK) { return; }
    s_running = !s_running;
    apply_state();
    printf("[button.toggle] %s\n", s_running ? "RUNNING" : "IDLE");
}

/* --- build it (in your screen's init) --- */
s_running   = false;               // Needed: start idle (START shown)
s_col_start = UGUI_COLOR_GREEN;    // Needed: the two fills the click handler
s_col_stop  = UGUI_COLOR_RED;      //         flips between (runtime-seeded)
ugui_widget_button_init(&s_root, &s_demo_btn, &s_demo_btn_ext);   // Needed: the button
ugui_widget_set_area(&s_demo_btn, BTN_X, BTN_Y, BTN_W, BTN_H);    // Needed
ugui_widget_button_set_style(&s_demo_btn, true, 12u, 0u, s_col_start, UGUI_THEME_ACCENT);
#ifdef UGUI_ENABLE_FONT
ugui_text_cfg_init(&s_demo_text, "START", &lexend_14pt_2bpp, UGUI_COLOR_WHITE,
                   UGUI_ALIGN_CENTER, (ugui_point_t){ 0, 0 }, /*use_widget_area=*/1u);
ugui_widget_button_set_text(&s_demo_btn, &s_demo_text);
#endif
ugui_widget_button_set_anim(&s_demo_btn, &s_demo_anim);
ugui_widget_set_event_cb(&s_demo_btn, on_button_click,
                         UGUI_EMASK_CLICK | UGUI_EMASK_DOWN | UGUI_EMASK_UP
                         | UGUI_EMASK_KEY_CONFIRM);
ugui_screen_add_widget(&s_screen, &s_demo_btn);   // Needed: register
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_demo_btn);
#endif

/* Status box: a plain box (NOT a button) that reflects the last action. */
ugui_widget_box_init(&s_root, &s_status_w, &s_status_ext);
ugui_widget_set_area(&s_status_w, STATUS_X, STATUS_Y, STATUS_W, STATUS_H);
ugui_widget_box_set_style(&s_status_w, true, 6u, 1u, UGUI_THEME_SURFACE, UGUI_THEME_ACCENT);
#ifdef UGUI_ENABLE_FONT
ugui_text_cfg_init(&s_status_text, "Idle", &lexend_14pt_2bpp, UGUI_THEME_TEXT,
                   UGUI_ALIGN_CENTER, (ugui_point_t){ 0, 0 }, 1u);
ugui_widget_box_set_text(&s_status_w, &s_status_text);
#endif
ugui_screen_add_widget(&s_screen, &s_status_w);

Style Variations

The same ugui_widget_button_set_style() call with four argument sets: filled, outline (border only), pill (radius ≥ height/2) and sharp (radius 0 — square corners).

Style Variations

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

/* --- 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 --- */
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
#define BTN_COUNT 4u
static ugui_widget_t      s_btn[BTN_COUNT];
static ugui_button_ext_t  s_btn_ext[BTN_COUNT];
static ugui_text_cfg_t    s_btn_text[BTN_COUNT];
static ugui_anim_cfg_t    s_anim = {
    .type        = UGUI_ANIM_DARKEN,
    .param       = 30u,
    .duration_ms = 180u
};
#define BTN_W    132
#define BTN_H    52
#define COL0_X   24
#define COL1_X   (UGUI_SCREEN_W - COL0_X - BTN_W)
#define ROW0_Y   62
#define ROW1_Y   (ROW0_Y + BTN_H + 22)

/* --- local helpers & event callbacks --- */
static void on_button_click(uint8_t widget_id, ugui_event_t event)
{
    if (event == UGUI_EVENT_CLICK) {
        printf("[button.styles] slot %u pressed\n", (unsigned)widget_id);
    }
}

static void make_button(uint8_t idx, uint8_t id, int16_t x, int16_t y,
                        bool filled, uint8_t radius, uint8_t border_w,
                        const char* caption, ugui_color_t text_color)
{
    ugui_widget_button_init(&s_root, &s_btn[idx], &s_btn_ext[idx]);
    ugui_widget_set_area(&s_btn[idx], x, y, BTN_W, BTN_H);
    ugui_widget_set_id  (&s_btn[idx], id);
    ugui_widget_button_set_style(&s_btn[idx], filled, radius, border_w,
                                 UGUI_THEME_ACCENT, UGUI_THEME_ACCENT);
#ifdef UGUI_ENABLE_FONT
    ugui_text_cfg_init(&s_btn_text[idx], caption, &lexend_14pt_2bpp, text_color,
                       UGUI_ALIGN_CENTER, (ugui_point_t){ 0, 0 }, /*use_widget_area=*/1u);
    ugui_widget_button_set_text(&s_btn[idx], &s_btn_text[idx]);
#else
    (void)caption; (void)text_color;
#endif
    ugui_widget_button_set_anim(&s_btn[idx], &s_anim);
    ugui_widget_set_event_cb(&s_btn[idx], on_button_click,
                             UGUI_EMASK_CLICK | UGUI_EMASK_DOWN | UGUI_EMASK_UP
                             | UGUI_EMASK_KEY_CONFIRM);
    ugui_screen_add_widget(&s_screen, &s_btn[idx]);
#if UGUI_ENABLE_KEYPAD
    ugui_screen_add_focus(&s_screen, &s_btn[idx]);
#endif
}

/* --- build it (in your screen's init) --- */
/* Filled — solid rounded (white label on accent). */
make_button(0u, WDGT_ID_SLOT0, COL0_X, ROW0_Y, /*filled=*/true,  10u, 0u,
            "Filled", UGUI_THEME_KNOB);
/* Outline — border only; the page shows through, so use accent-coloured text. */
make_button(1u, WDGT_ID_SLOT1, COL1_X, ROW0_Y, /*filled=*/false, 10u, 2u,
            "Outline", UGUI_THEME_ACCENT);
/* Pill — radius >= height/2 rounds the ends into a capsule. */
make_button(2u, WDGT_ID_SLOT2, COL0_X, ROW1_Y, /*filled=*/true,  (uint8_t)(BTN_H / 2), 0u,
            "Pill", UGUI_THEME_KNOB);
/* Sharp — radius 0 keeps square corners. */
make_button(3u, WDGT_ID_SLOT3, COL1_X, ROW1_Y, /*filled=*/true,  0u, 0u,
            "Sharp", UGUI_THEME_KNOB);


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.