Tile Widget¶
The Tile is a dashboard readout card: a themed panel with a big
fixed-point value, optional label/limit text, an optional two-frame animated
icon, and an app-driven alarm mode (VALUE colour, BORDER highlight, or a
steady FILL) that a threshold check can raise and clear at runtime.
set_value() / set_value_invalid() drive the readout (the latter renders a
"---" placeholder for a disconnected sensor); set_alarm() / clear_alarm()
recolour the tile; add_text() / add_icon() attach the caller-owned
text/sprite slots that make one tile read as a complete vital or gauge —
from a bedside monitor to an automotive cluster.
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¶
Patient Monitor (alarm ladder + probe-off)¶
Six vital tiles in a 2×3 grid, each ~10 lines: big readout, label, alarm-limit text and (where it fits) an animated sprite. Tapping PULSE walks the IEC 60601-1-8 alarm ladder — off, red 2 Hz crisis, amber 0.5 Hz caution, steady cyan advisory — via set_alarm() / clear_alarm(); tapping SpO2 toggles set_value_invalid().

/* --- 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_SLOT0, WDGT_ID_SLOT1,
WDGT_ID_SLOT2, WDGT_ID_SLOT3, WDGT_ID_SLOT4
};
/* --- 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/celsius_icon_1_grayscale_4.h" /* defines img_celsius_icon_1 */
#include "icons/heart_icon_1_fill_grayscale_4.h" /* defines img_heart_icon_1_fill */
#include "icons/heart_icon_2_pulse_grayscale_4.h" /* defines img_heart_icon_2_pulse */
#include "icons/lungs_icon_1_grayscale_4.h" /* defines img_lungs_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;
extern const ugui_font_t lexend_num_40pt_2bpp;
extern const ugui_font_t lexend_num_64pt_2bpp;
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_TILE)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
static ugui_widget_t s_status; /* interaction hint / echo */
static ugui_box_ext_t s_status_ext;
static ugui_text_cfg_t s_status_text;
static char s_status_str[64];
typedef struct {
ugui_widget_t w;
ugui_tile_ext_t ext;
ugui_text_cfg_t label; /* "PULSE bpm" — top centre */
ugui_text_cfg_t limits; /* "50 - 120" — bottom right, muted */
} vt_tile_t;
static vt_tile_t s_hr; /* beating heart + alarm cycling on tap */
static vt_tile_t s_spo2; /* probe-off toggle on tap */
static vt_tile_t s_nibp;
static vt_tile_t s_resp; /* breathing lungs sprite */
static vt_tile_t s_temp; /* decimals = 1 */
static vt_tile_t s_pr;
static uint8_t s_alarm_state = 0u; /* 0 off, 1 crisis, 2 caution, 3 advisory */
static bool s_probe_off = false;
#define VT_BG ugui_color_hex(0x06080Au) /* page */
#define VT_CARD ugui_color_hex(0x11151Bu) /* tile fill */
#define VT_FRAME ugui_color_hex(0x2A3442u) /* tile outline */
#define VT_HR_COL ugui_color_hex(0x00E676u) /* ECG green */
#define VT_SPO2_COL ugui_color_hex(0x00C8FFu) /* pleth cyan */
#define VT_NIBP_COL ugui_color_hex(0xFF6A9Eu) /* NIBP pink */
#define VT_RESP_COL ugui_color_hex(0xFFB000u) /* resp amber */
#define VT_TEMP_COL ugui_color_hex(0xE8ECF1u) /* temp white */
#define VT_PR_COL ugui_color_hex(0xC792EAu) /* pulse-rate violet */
#define VT_MUTED ugui_color_hex(0x6B7787u) /* limit text */
#define VT_MARGIN 10
#define VT_GAP 8
#define VT_STATUS_H 22
#define VT_GRID_Y 38
#define VT_GRID_H (UGUI_SCREEN_H - VT_GRID_Y - VT_STATUS_H - (2 * VT_MARGIN))
#define VT_TILE_W ((UGUI_SCREEN_W - (2 * VT_MARGIN) - (2 * VT_GAP)) / 3)
#define VT_TILE_H ((VT_GRID_H - VT_GAP) / 2)
#define VT_X(col) (VT_MARGIN + (col) * (VT_TILE_W + VT_GAP))
#define VT_Y(row) (VT_GRID_Y + (row) * (VT_TILE_H + VT_GAP))
#endif
/* --- local helpers & event callbacks --- */
static void set_status(const char* txt)
{
(void)snprintf(s_status_str, sizeof(s_status_str), "%s", txt);
ugui_widget_invalidate(&s_status);
}
static void on_hr_tap(uint8_t widget_id, ugui_event_t event)
{
(void)widget_id;
if (event != UGUI_EVENT_CLICK) { return; }
s_alarm_state = (uint8_t)((s_alarm_state + 1u) % 4u);
switch (s_alarm_state) {
case 1u: /* crisis: red, 2 Hz, value + border */
ugui_widget_tile_set_alarm(&s_hr.w, UGUI_COLOR_RED, 250u,
UGUI_TILE_ALARM_VALUE | UGUI_TILE_ALARM_BORDER);
set_status("PULSE alarm: CRISIS (red, 2 Hz)");
break;
case 2u: /* caution: yellow, 0.5 Hz, border */
ugui_widget_tile_set_alarm(&s_hr.w, UGUI_COLOR_YELLOW, 1000u,
UGUI_TILE_ALARM_BORDER);
set_status("PULSE alarm: CAUTION (yellow, 0.5 Hz)");
break;
case 3u: /* advisory: cyan, steady, border */
ugui_widget_tile_set_alarm(&s_hr.w, UGUI_COLOR_CYAN, 0u,
UGUI_TILE_ALARM_BORDER);
set_status("PULSE alarm: ADVISORY (cyan, steady)");
break;
default:
ugui_widget_tile_clear_alarm(&s_hr.w);
set_status("PULSE alarm: off | tap SpO2 = probe off");
break;
}
}
static void on_spo2_tap(uint8_t widget_id, ugui_event_t event)
{
(void)widget_id;
if (event != UGUI_EVENT_CLICK) { return; }
s_probe_off = !s_probe_off;
if (s_probe_off) {
ugui_widget_tile_set_value_invalid(&s_spo2.w);
set_status("SpO2 probe off: readout shows ---");
} else {
ugui_widget_tile_set_value(&s_spo2.w, 98);
set_status("SpO2 probe reconnected");
}
}
static void make_vital(vt_tile_t* t, uint8_t id, int16_t x, int16_t y,
const char* label, const char* limits,
ugui_color_t color, const ugui_font_t* value_font,
uint8_t decimals, int32_t value)
{
ugui_widget_tile_init(&s_root, &t->w, &t->ext);
ugui_widget_set_area(&t->w, x, y, (int16_t)VT_TILE_W, (int16_t)VT_TILE_H);
ugui_widget_set_id(&t->w, id);
ugui_widget_tile_set_style(&t->w, VT_CARD, VT_FRAME, VT_BG, 1u, 8u);
ugui_widget_tile_set_value_format(&t->w, value_font, color, decimals);
ugui_text_cfg_init(&t->label, label, &lexend_14pt_2bpp, color,
UGUI_ALIGN_TOP | UGUI_ALIGN_H_CENTER,
(ugui_point_t){ 0, 4 }, 1u);
(void)ugui_widget_tile_add_text(&t->w, &t->label);
if (limits != NULL) {
ugui_text_cfg_init(&t->limits, limits, &lexend_14pt_2bpp, VT_MUTED,
UGUI_ALIGN_BOTTOM | UGUI_ALIGN_RIGHT,
(ugui_point_t){ -4, -2 }, 1u);
(void)ugui_widget_tile_add_text(&t->w, &t->limits);
}
ugui_widget_tile_set_value(&t->w, value);
ugui_screen_add_widget(&s_screen, &t->w);
}
/* --- build it (in your screen's init) --- */
make_vital(&s_hr, WDGT_ID_DEMO, VT_X(0), VT_Y(0),
"PULSE bpm", "50 - 120", VT_HR_COL, &lexend_num_64pt_2bpp, 0u, 72);
make_vital(&s_spo2, WDGT_ID_SLOT0, VT_X(1), VT_Y(0),
"SpO2 %", "90 - 100", VT_SPO2_COL, &lexend_num_64pt_2bpp, 0u, 98);
make_vital(&s_nibp, WDGT_ID_SLOT1, VT_X(2), VT_Y(0),
"NIBP MAP mmHg", "60 - 110", VT_NIBP_COL, &lexend_num_40pt_2bpp, 0u, 93);
make_vital(&s_resp, WDGT_ID_SLOT2, VT_X(0), VT_Y(1),
"RESP /min", "8 - 30", VT_RESP_COL, &lexend_num_40pt_2bpp, 0u, 16);
make_vital(&s_temp, WDGT_ID_SLOT3, VT_X(1), VT_Y(1),
"TEMP C", "35.5 - 38.5", VT_TEMP_COL, &lexend_num_40pt_2bpp, 1u, 368);
make_vital(&s_pr, WDGT_ID_SLOT4, VT_X(2), VT_Y(1),
"PR bpm", "50 - 120", VT_PR_COL, &lexend_num_40pt_2bpp, 0u, 71);
#ifdef UGUI_WIDGET_ENABLE_IMAGE
/* Animated sprites: the heart beats at the displayed rate
* (60000 / 72 bpm ≈ 830 ms per beat → ~415 ms per half-flip),
* the lungs breathe at the RESP rate, the °C badge is static. */
(void)ugui_widget_tile_add_icon(&s_hr.w, &img_heart_icon_1_fill,
&img_heart_icon_2_pulse, 6, 4,
VT_HR_COL, 415u);
(void)ugui_widget_tile_add_icon(&s_resp.w, &img_lungs_icon_1, NULL, 6, 4,
VT_RESP_COL, 1875u);
(void)ugui_widget_tile_add_icon(&s_temp.w, &img_celsius_icon_1, NULL, 6, 4,
VT_TEMP_COL, 0u);
#endif
/* Interactions: alarm ladder on PULSE, probe-off on SpO2. */
ugui_widget_set_event_cb(&s_hr.w, on_hr_tap,
UGUI_EMASK_CLICK | UGUI_EMASK_KEY_CONFIRM);
ugui_widget_set_event_cb(&s_spo2.w, on_spo2_tap,
UGUI_EMASK_CLICK | UGUI_EMASK_KEY_CONFIRM);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_hr.w);
ugui_screen_add_focus(&s_screen, &s_spo2.w);
#endif
/* Interaction hint / echo line. */
(void)snprintf(s_status_str, sizeof(s_status_str),
"tap PULSE = alarm cycle | tap SpO2 = probe off");
ugui_widget_box_init(&s_root, &s_status, &s_status_ext);
ugui_widget_set_area(&s_status, VT_MARGIN,
(int16_t)(UGUI_SCREEN_H - VT_STATUS_H - 4),
(int16_t)(UGUI_SCREEN_W - (2 * VT_MARGIN)),
(int16_t)VT_STATUS_H);
ugui_widget_box_set_style(&s_status, true, 0u, 0u, VT_BG, VT_BG);
ugui_widget_set_enabled(&s_status, false);
ugui_text_cfg_init(&s_status_text, s_status_str, &lexend_14pt_2bpp,
VT_MUTED, UGUI_ALIGN_CENTER, (ugui_point_t){ 0, 0 }, 1u);
ugui_widget_box_set_text(&s_status, &s_status_text);
ugui_screen_add_widget(&s_screen, &s_status);
Automotive Cluster (overspeed alarm)¶
The SAME tile widget in a different composition: a 64 pt speed readout, gear/range/trip tiles, and an outside-temperature tile with a NEGATIVE fixed-point value. Tapping SPEED accelerates by 20 km/h and, above 130, raises an overspeed warning with the FILL restyle mode — a steady amber card, the third alarm mode this family demonstrates.

/* --- 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_SLOT0, WDGT_ID_SLOT1,
WDGT_ID_SLOT2, WDGT_ID_SLOT3
};
/* --- 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/celsius_icon_1_grayscale_4.h" /* defines img_celsius_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;
extern const ugui_font_t lexend_num_40pt_2bpp;
extern const ugui_font_t lexend_num_64pt_2bpp;
/* --- file-scope storage --- */
#if defined(UGUI_WIDGET_ENABLE_TILE)
static ugui_screen_t s_screen;
static ugui_widget_t s_root;
typedef struct {
ugui_widget_t w;
ugui_tile_ext_t ext;
#ifdef UGUI_ENABLE_FONT
ugui_text_cfg_t label;
ugui_text_cfg_t hint;
#endif
} cl_tile_t;
static cl_tile_t s_speed; /* 64 pt readout + overspeed FILL alarm */
static cl_tile_t s_gear;
static cl_tile_t s_range;
static cl_tile_t s_trip; /* decimals = 1 */
static cl_tile_t s_temp; /* negative value + °C sprite */
static int32_t s_kmh = 88;
#define CL_BG ugui_color_hex(0x000000u)
#define CL_CARD ugui_color_hex(0x0E1116u)
#define CL_FRAME ugui_color_hex(0x232A33u)
#define CL_SPEED_COL ugui_color_hex(0xF2F5F8u)
#define CL_GEAR_COL ugui_color_hex(0xFFB000u)
#define CL_RANGE_COL ugui_color_hex(0x7CE38Bu)
#define CL_TRIP_COL ugui_color_hex(0xB9C2CEu)
#define CL_TEMP_COL ugui_color_hex(0x00C8FFu)
#define CL_MUTED ugui_color_hex(0x5C6673u)
#define CL_Y0 38
#define CL_SPEED_W 240
#define CL_SPEED_H 210
#define CL_COL1_X 258
#define CL_COL2_X 366
#define CL_SIDE_W 104
#define CL_SIDE_H 100
#define CL_ROW1_Y (CL_Y0 + CL_SIDE_H + 8) /* 146 */
#define CL_ROW2_Y (CL_Y0 + CL_SPEED_H + 8) /* 256 */
#define CL_BOT_H (UGUI_SCREEN_H - CL_ROW2_Y - 8)
#endif
/* --- local helpers & event callbacks --- */
static void on_speed_tap(uint8_t widget_id, ugui_event_t event)
{
(void)widget_id;
if (event != UGUI_EVENT_CLICK) { return; }
s_kmh += 20;
if (s_kmh > 170) { s_kmh = 90; }
ugui_widget_tile_set_value(&s_speed.w, s_kmh);
if (s_kmh > 130) {
ugui_widget_tile_set_alarm(&s_speed.w, UGUI_COLOR_AMBER, 0u,
UGUI_TILE_ALARM_FILL);
} else {
ugui_widget_tile_clear_alarm(&s_speed.w);
}
printf("[tile.cluster] speed -> %ld km/h\n", (long)s_kmh);
}
static void make_cl_tile(cl_tile_t* t, uint8_t id, int16_t x, int16_t y,
int16_t wd, int16_t ht, const char* label,
ugui_color_t color, const ugui_font_t* value_font,
uint8_t decimals, int32_t value)
{
ugui_widget_tile_init(&s_root, &t->w, &t->ext);
ugui_widget_set_area(&t->w, x, y, wd, ht);
ugui_widget_set_id(&t->w, id);
ugui_widget_tile_set_style(&t->w, CL_CARD, CL_FRAME, CL_BG, 1u, 10u);
#ifdef UGUI_ENABLE_FONT
ugui_widget_tile_set_value_format(&t->w, value_font, color, decimals);
ugui_text_cfg_init(&t->label, label, &lexend_14pt_2bpp, CL_MUTED,
UGUI_ALIGN_TOP | UGUI_ALIGN_H_CENTER,
(ugui_point_t){ 0, 4 }, 1u);
(void)ugui_widget_tile_add_text(&t->w, &t->label);
#else
(void)label; (void)color; (void)value_font; (void)decimals;
#endif
ugui_widget_tile_set_value(&t->w, value);
ugui_screen_add_widget(&s_screen, &t->w);
}
/* --- build it (in your screen's init) --- */
make_cl_tile(&s_speed, WDGT_ID_DEMO, 10, CL_Y0,
(int16_t)CL_SPEED_W, (int16_t)CL_SPEED_H,
"km/h", CL_SPEED_COL, &lexend_num_64pt_2bpp, 0u, s_kmh);
#ifdef UGUI_ENABLE_FONT
/* Third text slot: interaction hint inside the card, bottom centre. */
ugui_text_cfg_init(&s_speed.hint, "tap = accelerate", &lexend_14pt_2bpp,
CL_MUTED, UGUI_ALIGN_BOTTOM | UGUI_ALIGN_H_CENTER,
(ugui_point_t){ 0, -4 }, 1u);
(void)ugui_widget_tile_add_text(&s_speed.w, &s_speed.hint);
#endif
ugui_widget_set_event_cb(&s_speed.w, on_speed_tap,
UGUI_EMASK_CLICK | UGUI_EMASK_KEY_CONFIRM);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_speed.w);
#endif
make_cl_tile(&s_gear, WDGT_ID_SLOT0, (int16_t)CL_COL1_X, CL_Y0,
(int16_t)CL_SIDE_W, (int16_t)CL_SIDE_H,
"GEAR", CL_GEAR_COL, &lexend_num_40pt_2bpp, 0u, 4);
make_cl_tile(&s_range, WDGT_ID_SLOT1, (int16_t)CL_COL2_X, CL_Y0,
(int16_t)CL_SIDE_W, (int16_t)CL_SIDE_H,
"RANGE km", CL_RANGE_COL, &lexend_num_40pt_2bpp, 0u, 320);
make_cl_tile(&s_trip, WDGT_ID_SLOT2, (int16_t)CL_COL1_X, (int16_t)CL_ROW1_Y,
(int16_t)(CL_SIDE_W + CL_SIDE_W + 8), (int16_t)CL_SIDE_H,
"TRIP km", CL_TRIP_COL, &lexend_num_40pt_2bpp, 1u, 3842);
/* Negative fixed-point value: -35 tenths → "-3.5" ('-' glyph).
* The strip is shorter than the 40 pt line height, so the value gets
* an EXPLICIT area (full card, right-centred) instead of the default
* radius-inset one, and the label moves beside the °C sprite. */
make_cl_tile(&s_temp, WDGT_ID_SLOT3, 10, (int16_t)CL_ROW2_Y,
(int16_t)(UGUI_SCREEN_W - 20), (int16_t)CL_BOT_H,
"OUTSIDE", CL_TEMP_COL, &lexend_num_40pt_2bpp, 1u, -35);
#ifdef UGUI_ENABLE_FONT
ugui_widget_tile_set_value_area(&s_temp.w, 0, 0,
(int16_t)(UGUI_SCREEN_W - 20 - 16),
(int16_t)CL_BOT_H,
UGUI_ALIGN_RIGHT | UGUI_ALIGN_V_CENTER);
s_temp.label.align = UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER;
s_temp.label.align_offset = (ugui_point_t){ 44, 0 };
#endif
#ifdef UGUI_WIDGET_ENABLE_IMAGE
(void)ugui_widget_tile_add_icon(&s_temp.w, &img_celsius_icon_1, NULL,
10, (int16_t)((CL_BOT_H - 24) / 2),
CL_TEMP_COL, 0u);
#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.