Skip to content

Table Widget

The Table widget is a data grid — a results report, a live vitals panel, an alarm/event log. Cells are a flat, app-owned array of 8-byte tagged-union entries (text or icon), so a static report can live entirely in flash (zero content RAM). A pinned header row, per-cell alignment and semantic status/fill colours (IEC 60601-1-8 style: red+white / amber+black) are all part of the cell format. Value cells use guarded setters — they compare before writing and repaint only on an actual change, so re-asserting every cell every frame costs nothing between real data changes.

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

Static Results (flash-only)

A pre-use self-test report whose cell array is entirely static const — the whole table costs zero RAM. Shows a pinned header row (set_header_rows()), per-cell alignment, a fixed+flex column mix, and semantic statuses (PASS in nominal green, a FAIL row as an ALERT+FILL cell, a stale reading in disabled gray). Beside it, a NULL-cells table — grid rules only, an empty layout frame.

Static Results (flash-only)

/* --- 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_SLOT4 = 10, WDGT_ID_SLOT5
};

/* --- 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_TABLE)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t    s_cap_tbl;
static ugui_label_ext_t s_cap_tbl_ext;
static ugui_widget_t    s_cap_null;
static ugui_label_ext_t s_cap_null_ext;
static ugui_widget_t    s_tbl;              /* results table              */
static ugui_table_ext_t s_tbl_ext;
static ugui_widget_t    s_frame;            /* NULL-cells empty grid      */
static ugui_table_ext_t s_frame_ext;
#define C_L(s)      { { (s) }, UGUI_TABLE_CELL_ALIGN_LEFT,   { 0u, 0u, 0u } }
#define C_R(s)      { { (s) }, UGUI_TABLE_CELL_ALIGN_RIGHT,  { 0u, 0u, 0u } }
#define C_C(s, f)   { { (s) }, (uint8_t)(UGUI_TABLE_CELL_ALIGN_CENTER | (f)), { 0u, 0u, 0u } }
static const ugui_table_cell_t s_cells[5u * 3u] = {
    C_L("TEST"),      C_R("VALUE"),     C_C("RESULT", 0u),
    C_L("Leak"),      C_R("3 ml/min"),  C_C("PASS", UGUI_TABLE_ST_NOMINAL),
    C_L("Occlusion"), C_R("812 mmHg"),  C_C("PASS", UGUI_TABLE_ST_NOMINAL),
    C_L("Battery"),   C_R("6.4 V"),     C_C("PASS", UGUI_TABLE_ST_NOMINAL),
    { { "Sensor" }, (uint8_t)(UGUI_TABLE_CELL_ALIGN_LEFT | UGUI_TABLE_ST_DISABLED), { 0u, 0u, 0u } },
                      C_R("---"),       C_C("FAIL", UGUI_TABLE_ST_ALERT | UGUI_TABLE_CELL_FILL),
};
static const int16_t s_col_w[3] = { 0, 110, 90 };
#define TBL_X   20
#define TBL_Y   ((int16_t)(38 + 26))
#define TBL_RH  26
#define TBL_W   310
#define TBL_H   (5 * TBL_RH)
#define FRM_X   ((int16_t)(TBL_X + TBL_W + 24))
#define FRM_W   ((int16_t)(UGUI_SCREEN_W - FRM_X - 20))
#define FRM_H   (4 * TBL_RH)
#endif

/* --- local helpers & event callbacks --- */
static void add_label(ugui_widget_t* w, ugui_label_ext_t* ext, uint8_t id,
                      int16_t x, int16_t y, int16_t cw, int16_t ch,
                      const char* str, ugui_align_t align)
{
    ugui_widget_label_init(&s_root, w, ext);
    ugui_widget_set_area(w, x, y, cw, ch);
    ugui_widget_set_id  (w, id);
    ugui_widget_label_set_style(w, /*bg_fill=*/false, UGUI_THEME_BG, str,
                                &lexend_14pt_2bpp, UGUI_THEME_TEXT, align, 1u);
    ugui_screen_add_widget(&s_screen, w);
}

/* --- build it (in your screen's init) --- */
add_label(&s_cap_tbl, &s_cap_tbl_ext, WDGT_ID_SLOT4,
         TBL_X, 38, TBL_W, 22,
         "flash cells - header, align, status",
         (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER));

ugui_widget_table_init(&s_root, &s_tbl, &s_tbl_ext);
ugui_widget_set_area(&s_tbl, TBL_X, TBL_Y, TBL_W, TBL_H);
ugui_widget_table_set_font(&s_tbl, &lexend_14pt_2bpp);
ugui_widget_table_set_geometry(&s_tbl, TBL_RH, 8u, 1u);
ugui_widget_table_set_grid(&s_tbl, s_cells, 5u, 3u);
ugui_widget_table_set_col_widths(&s_tbl, s_col_w);
ugui_widget_table_set_header_rows(&s_tbl, 1u);
ugui_screen_add_widget(&s_screen, &s_tbl);

/* NULL-cells table: surface + grid rules only — an empty layout frame
 * (zero content RAM), the line/polygon/LED NULL-ext convention. */
add_label(&s_cap_null, &s_cap_null_ext, WDGT_ID_SLOT5,
         FRM_X, 38, FRM_W, 22,
         "NULL cells",
         (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER));

ugui_widget_table_init(&s_root, &s_frame, &s_frame_ext);
ugui_widget_set_area(&s_frame, FRM_X, TBL_Y, FRM_W, FRM_H);
ugui_widget_table_set_geometry(&s_frame, TBL_RH, 8u, 1u);
ugui_widget_table_set_grid(&s_frame, NULL, 4u, 3u);
ugui_screen_add_widget(&s_screen, &s_frame);

Live Vitals (guarded setters)

A patient-vitals grid whose VALUE/STAT cells are re-asserted every frame through the guarded set_cell_text()/set_cell_status() setters — they invalidate only on a real change, so the table repaints at the data rate, not the frame rate. SpO2 sweeps nominal green → caution amber → ALERT+FILL and back; Temp alternates between a live reading and the stale-data pattern.

Live Vitals (guarded setters)

/* --- 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_TABLE)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t    s_tbl;
static ugui_table_ext_t s_tbl_ext;
#define ROWS 5u
#define COLS 4u
static ugui_table_cell_t s_cells[ROWS * COLS] = {
    { { "PARAM" },     UGUI_TABLE_CELL_ALIGN_LEFT,   { 0u, 0u, 0u } },
    { { "VALUE" },     UGUI_TABLE_CELL_ALIGN_RIGHT,  { 0u, 0u, 0u } },
    { { "UNIT" },      UGUI_TABLE_CELL_ALIGN_LEFT,   { 0u, 0u, 0u } },
    { { "STAT" },      UGUI_TABLE_CELL_ALIGN_CENTER, { 0u, 0u, 0u } },

    { { "Heart rate" }, UGUI_TABLE_CELL_ALIGN_LEFT,  { 0u, 0u, 0u } },
    { { "--" },         UGUI_TABLE_CELL_ALIGN_RIGHT, { 0u, 0u, 0u } },
    { { "bpm" },        UGUI_TABLE_CELL_ALIGN_LEFT,  { 0u, 0u, 0u } },
    { { "OK" },         UGUI_TABLE_CELL_ALIGN_CENTER,{ 0u, 0u, 0u } },

    { { "SpO2" },       UGUI_TABLE_CELL_ALIGN_LEFT,  { 0u, 0u, 0u } },
    { { "--" },         UGUI_TABLE_CELL_ALIGN_RIGHT, { 0u, 0u, 0u } },
    { { "%" },          UGUI_TABLE_CELL_ALIGN_LEFT,  { 0u, 0u, 0u } },
    { { "OK" },         UGUI_TABLE_CELL_ALIGN_CENTER,{ 0u, 0u, 0u } },

    { { "NIBP" },       UGUI_TABLE_CELL_ALIGN_LEFT,  { 0u, 0u, 0u } },
    { { "120/80" },     UGUI_TABLE_CELL_ALIGN_RIGHT, { 0u, 0u, 0u } },
    { { "mmHg" },       UGUI_TABLE_CELL_ALIGN_LEFT,  { 0u, 0u, 0u } },
    { { "OK" },         UGUI_TABLE_CELL_ALIGN_CENTER,{ 0u, 0u, 0u } },

    { { "Temp" },       UGUI_TABLE_CELL_ALIGN_LEFT,  { 0u, 0u, 0u } },
    { { "--" },         UGUI_TABLE_CELL_ALIGN_RIGHT, { 0u, 0u, 0u } },
    { { "C" },          UGUI_TABLE_CELL_ALIGN_LEFT,  { 0u, 0u, 0u } },
    { { "OK" },         UGUI_TABLE_CELL_ALIGN_CENTER,{ 0u, 0u, 0u } },
};
static const int16_t s_col_w[COLS] = { 0, 90, 70, 80 };
#define TBL_X   20
#define TBL_Y   ((int16_t)(38 + 4))
#define TBL_RH  30
#define TBL_W   330
#define TBL_H   (5 * TBL_RH)
#endif

/* --- build it (in your screen's init) --- */
ugui_widget_table_init(&s_root, &s_tbl, &s_tbl_ext);
ugui_widget_set_area(&s_tbl, TBL_X, TBL_Y, TBL_W, TBL_H);
ugui_widget_table_set_font(&s_tbl, &lexend_14pt_2bpp);
ugui_widget_table_set_geometry(&s_tbl, TBL_RH, 8u, 1u);
ugui_widget_table_set_grid(&s_tbl, s_cells, (uint8_t)ROWS, (uint8_t)COLS);
ugui_widget_table_set_col_widths(&s_tbl, s_col_w);
ugui_widget_table_set_header_rows(&s_tbl, 1u);
ugui_screen_add_widget(&s_screen, &s_tbl);

Alarm Log (scroll, icons, selection)

A 15-entry alarm history in a 7-row viewport: pinned header, zebra striping, a right-edge scrollbar, ICON cells tinted by the cell's semantic status, row selection via set_on_select(), and scroll_to() for jumping the viewport (log tail-follow).

Alarm Log (scroll, icons, 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_SLOT0 = 10, WDGT_ID_SLOT1, 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/alert_icon_1_grayscale_4.h"   /* defines img_alert_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 --- */
#if defined(UGUI_WIDGET_ENABLE_TABLE)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t     s_top_btn;
static ugui_button_ext_t s_top_btn_ext;
static ugui_text_cfg_t   s_top_btn_text;
static ugui_widget_t     s_tail_btn;
static ugui_button_ext_t s_tail_btn_ext;
static ugui_text_cfg_t   s_tail_btn_text;
static ugui_widget_t    s_status;
static ugui_label_ext_t s_status_ext;
static char     s_sel_buf[28];
static ugui_widget_t    s_tbl;
static ugui_table_ext_t s_tbl_ext;
#define ROWS 16u        /* 1 header + 15 log entries */
#define COLS 4u
#define C_TXT(s, f)  { { (s) }, (uint8_t)(f), { 0u, 0u, 0u } }
#define C_ICO(st)    { { .icon = &img_alert_icon_1 }, \
                       (uint8_t)(UGUI_TABLE_CELL_ICON | (st) | UGUI_TABLE_CELL_ALIGN_CENTER), \
                       { 0u, 0u, 0u } }
#define C_NONE       { { NULL }, 0u, { 0u, 0u, 0u } }
#define ROW_HI(t, src) \
    C_ICO(UGUI_TABLE_ST_ALERT),   C_TXT(t, 0u), C_TXT(src, 0u), \
    C_TXT("HIGH", UGUI_TABLE_ST_ALERT | UGUI_TABLE_CELL_FILL | UGUI_TABLE_CELL_ALIGN_CENTER)
#define ROW_MED(t, src) \
    C_ICO(UGUI_TABLE_ST_CAUTION), C_TXT(t, 0u), C_TXT(src, 0u), \
    C_TXT("MED", UGUI_TABLE_ST_CAUTION | UGUI_TABLE_CELL_ALIGN_CENTER)
#define ROW_LOW(t, src) \
    C_NONE,                       C_TXT(t, 0u), C_TXT(src, 0u), \
    C_TXT("LOW", UGUI_TABLE_ST_INFO | UGUI_TABLE_CELL_ALIGN_CENTER)
static const ugui_table_cell_t s_cells[ROWS * COLS] = {
    C_NONE, C_TXT("TIME", 0u), C_TXT("SOURCE", 0u),
    C_TXT("PRI", UGUI_TABLE_CELL_ALIGN_CENTER),

    ROW_LOW("07:58", "Self test pass"),
    ROW_LOW("08:02", "Rec started"),
    ROW_MED("08:14", "Signal weak"),
    ROW_LOW("08:20", "On mains"),
    ROW_HI ("08:31", "SpO2 low"),
    ROW_MED("08:32", "Lead off"),
    ROW_LOW("08:40", "Rec resumed"),
    ROW_MED("08:57", "NIBP timeout"),
    ROW_LOW("09:05", "Operator login"),
    ROW_HI ("09:12", "Occlusion"),
    ROW_MED("09:18", "Battery low"),
    ROW_LOW("09:26", "Limits changed"),
    ROW_HI ("09:33", "Apnea"),
    ROW_LOW("09:41", "Alarm silenced"),
    ROW_LOW("09:44", "Alarm re-armed"),
};
static const int16_t s_col_w[COLS] = { 34, 70, 0, 70 };
#define TBL_X   20
#define TBL_Y   ((int16_t)(38 + 4))
#define TBL_RH  26
#define TBL_W   ((int16_t)(UGUI_SCREEN_W - 160))
#define TBL_H   (7 * TBL_RH)
#define BTN_X   ((int16_t)(UGUI_SCREEN_W - 128))
#define BTN_W   108
#endif

/* --- local helpers & event callbacks --- */
static void on_top(uint8_t widget_id, ugui_event_t event)
{
    (void)widget_id;
    if (event == UGUI_EVENT_CLICK) { ugui_widget_table_scroll_to(&s_tbl, 1u); }
}

static void on_tail(uint8_t widget_id, ugui_event_t event)
{
    (void)widget_id;
    if (event == UGUI_EVENT_CLICK) {
        ugui_widget_table_scroll_to(&s_tbl, (uint16_t)(ROWS - 1u));
    }
}

static void on_select(ugui_widget_t* w, uint16_t row, void* ctx)
{
    (void)w; (void)ctx;
    /* SOURCE column of the selected entry (col 2 is always a text cell). */
    const char* src = s_cells[((uint16_t)row * COLS) + 2u].u.text;
    (void)snprintf(s_sel_buf, sizeof(s_sel_buf), "#%u %s", (unsigned)row,
                   (src != NULL) ? src : "-");
    ugui_widget_label_update_text_str(&s_status, s_sel_buf);
}

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
}

static void add_label(ugui_widget_t* w, ugui_label_ext_t* ext, uint8_t id,
                      int16_t x, int16_t y, int16_t cw, int16_t ch,
                      const char* str, ugui_align_t align)
{
    ugui_widget_label_init(&s_root, w, ext);
    ugui_widget_set_area(w, x, y, cw, ch);
    ugui_widget_set_id  (w, id);
    ugui_widget_label_set_style(w, /*bg_fill=*/false, UGUI_THEME_BG, str,
                                &lexend_14pt_2bpp, UGUI_THEME_TEXT, align, 1u);
    ugui_screen_add_widget(&s_screen, w);
}

/* --- build it (in your screen's init) --- */
ugui_widget_table_init(&s_root, &s_tbl, &s_tbl_ext);
ugui_widget_set_area(&s_tbl, TBL_X, TBL_Y, TBL_W, TBL_H);
ugui_widget_table_set_font(&s_tbl, &lexend_14pt_2bpp);
ugui_widget_table_set_geometry(&s_tbl, TBL_RH, 6u, 1u);
ugui_widget_table_set_grid(&s_tbl, s_cells, (uint8_t)ROWS, (uint8_t)COLS);
ugui_widget_table_set_col_widths(&s_tbl, s_col_w);
ugui_widget_table_set_header_rows(&s_tbl, 1u);
ugui_widget_table_set_zebra(&s_tbl, true);
ugui_widget_table_set_on_select(&s_tbl, on_select, NULL);
ugui_screen_add_widget(&s_screen, &s_tbl);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_tbl);   /* keypad */
#endif

/* scroll_to(): Top/Tail buttons jump the viewport (log tail-follow). */
add_button(&s_top_btn, &s_top_btn_ext, &s_top_btn_text, WDGT_ID_SLOT0,
          BTN_X, TBL_Y, BTN_W, 28, "Top", on_top);
add_button(&s_tail_btn, &s_tail_btn_ext, &s_tail_btn_text, WDGT_ID_SLOT1,
          BTN_X, (int16_t)(TBL_Y + 34), BTN_W, 28, "Tail", on_tail);

/* Selection readout, updated by on_select() above. */
(void)snprintf(s_sel_buf, sizeof(s_sel_buf), "tap a row");
add_label(&s_status, &s_status_ext, WDGT_ID_SLOT4,
         BTN_X, (int16_t)(TBL_Y + 74), BTN_W, 44,
         s_sel_buf,
         (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER));


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.