Skip to content

TextEdit Widget

The TextEdit widget is a single-line input field — a numeric entry, a username, a password. Init installs theme-tracked background/border/cursor colours, default border width, corner radius and padding, the CLICK/LEFT/ RIGHT/CONFIRM event mask, and (with UGUI_ENABLE_DEFAULTS) a visible default size; set_font() gives it a glyph set. The blinking cursor runs from the library's own tick — there is no on-screen keyboard built in, so an app drives input_char()/delete_char()/clear() from its own keys or a popup keyboard. set_mask() renders every glyph as a fixed character (the buffer itself is never touched — masking is display-only), and set_enabled(false) turns a field into a locked, read-only value display.

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 Field (numeric keypad)

A field created with nothing but ugui_widget_textedit_init() + set_font() — every colour, the border, radius, padding and size come from the library defaults. A numeric keypad drives the widget's own editing API directly: input_char() per digit, delete_char() for backspace, clear() for Clr.

Default Field (numeric keypad)

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

/* --- 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_TEXTEDIT)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t        s_edit;
static ugui_textedit_ext_t  s_edit_ext;
static char                 s_edit_buf[16] = "12.5";
#define KEY_N   12u
static ugui_widget_t     s_key[KEY_N];
static ugui_button_ext_t s_key_ext[KEY_N];
static ugui_widget_t     s_clr;
static ugui_button_ext_t s_clr_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_TEXTEDIT)
static ugui_text_cfg_t   s_key_text[KEY_N];
static ugui_text_cfg_t   s_clr_text;
#endif
#if defined(UGUI_WIDGET_ENABLE_TEXTEDIT)
static const char k_key_char[KEY_N] = {
    '7','8','9',
    '4','5','6',
    '1','2','3',
    '.','0','\b'
};
static const char* const k_key_label[KEY_N] = {
    "7","8","9", "4","5","6", "1","2","3", ".","0","<X"
};
#define FLD_W   300
#define FLD_H   36
#define FLD_X   ((int16_t)((UGUI_SCREEN_W - FLD_W) / 2))
#define FLD_Y   50
#define PAD_KW  70
#define PAD_KH  34
#define PAD_GAP 8
#define PAD_W   ((int16_t)(3 * PAD_KW + 2 * PAD_GAP))
#define PAD_X0  ((int16_t)((UGUI_SCREEN_W - PAD_W) / 2))
#define PAD_Y0  ((int16_t)(FLD_Y + FLD_H + 12))
#endif

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

    char c = k_key_char[idx];
    if (c == '\b') { ugui_textedit_delete_char(&s_edit); }
    else           { (void)ugui_textedit_input_char(&s_edit, c); }

#if UGUI_ENABLE_KEYPAD
    ugui_set_focus(&s_edit);   /* keep the field focused so the cursor stays */
#endif
}

static void on_clear(uint8_t widget_id, ugui_event_t event)
{
    (void)widget_id;
    if (event != UGUI_EVENT_CLICK) { return; }
    ugui_textedit_clear(&s_edit);
#if UGUI_ENABLE_KEYPAD
    ugui_set_focus(&s_edit);
#endif
}

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,
                       ugui_color_t fill, 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, fill, 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_textedit_init(&s_root, &s_edit, &s_edit_ext,
                          s_edit_buf, (uint8_t)sizeof(s_edit_buf));
ugui_widget_set_area(&s_edit, FLD_X, FLD_Y, FLD_W, FLD_H);
ugui_widget_set_id(&s_edit, WDGT_ID_DEMO);
#ifdef UGUI_ENABLE_FONT
ugui_textedit_set_font(&s_edit, &lexend_14pt_2bpp, UGUI_THEME_TEXT);
#endif
ugui_screen_add_widget(&s_screen, &s_edit);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_edit);
#endif

/* Numeric keypad — each key calls the widget's editing API directly. */
uint8_t i;
for (i = 0u; i < KEY_N; i++) {
    int16_t col = (int16_t)(i % 3u);
    int16_t row = (int16_t)(i / 3u);
    int16_t kx  = (int16_t)(PAD_X0 + col * (PAD_KW + PAD_GAP));
    int16_t ky  = (int16_t)(PAD_Y0 + row * (PAD_KH + PAD_GAP));
    add_button(&s_key[i], &s_key_ext[i], &s_key_text[i],
              (uint8_t)(WDGT_ID_SLOT0 + i), kx, ky, PAD_KW, PAD_KH,
              UGUI_THEME_SURFACE, k_key_label[i], on_key);
}

/* Full-width Clear under the grid. */
{
    int16_t clr_y = (int16_t)(PAD_Y0 + 4 * (PAD_KH + PAD_GAP));
    add_button(&s_clr, &s_clr_ext, &s_clr_text, WDGT_ID_CLEAR,
              PAD_X0, clr_y, PAD_W, 28, ugui_color_blend(UGUI_THEME_SURFACE, UGUI_THEME_BG, 96u), "Clear", on_clear);
}

Cursor Movement & Read-Only

A compact key row fires ugui_textedit_on_event(KEY_LEFT/RIGHT) to move the insertion cursor and input_char()/delete_char() to edit at it — so a typed character lands mid-string, not just at the end. Below it, a second field with set_enabled(false): the central dispatcher drops every event before the widget sees it, so it never focuses (no cursor) and ignores taps — a locked, read-only display.

Cursor Movement & Read-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_SLOT0 = 10, WDGT_ID_DEMO, WDGT_ID_CLEAR,
    WDGT_ID_READONLY
};

/* --- 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_TEXTEDIT)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t        s_edit;
static ugui_textedit_ext_t  s_edit_ext;
static char                 s_edit_buf[16] = "Hello";
static ugui_widget_t        s_ro;
static ugui_textedit_ext_t  s_ro_ext;
static char                 s_ro_buf[16] = "read-only";
#define KEY_N  7u
static ugui_widget_t     s_key[KEY_N];
static ugui_button_ext_t s_key_ext[KEY_N];
static ugui_widget_t     s_clr;
static ugui_button_ext_t s_clr_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_TEXTEDIT)
static ugui_text_cfg_t   s_key_text[KEY_N];
static ugui_text_cfg_t   s_clr_text;
#endif
#if defined(UGUI_WIDGET_ENABLE_TEXTEDIT)
#define ACT_LEFT   0x01
#define ACT_RIGHT  0x02
static const char k_key_act[KEY_N] = {
    ACT_LEFT, ACT_RIGHT, '\b', 'X', 'Y', 'Z', ' '
};
static const char* const k_key_label[KEY_N] = {
    "<", ">", "<X", "X", "Y", "Z", "sp"
};
#define FLD_W    ((int16_t)(UGUI_SCREEN_W - 2 * 10))
#define FLD_H    36
#define EDIT_Y   ((int16_t)(38 + 20))
#define KEYROW_Y ((int16_t)(EDIT_Y + FLD_H + 14))
#define KEY_H    36
#define CLR_Y    ((int16_t)(KEYROW_Y + KEY_H + 8))
#define CLR_H    26
#define RO_Y     ((int16_t)(CLR_Y + CLR_H + 30))
#endif

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

    char a = k_key_act[idx];
    switch (a) {
        case ACT_LEFT:  ugui_textedit_on_event(&s_edit, (ugui_event_t)UGUI_KEY_LEFT);  break;
        case ACT_RIGHT: ugui_textedit_on_event(&s_edit, (ugui_event_t)UGUI_KEY_RIGHT); break;
        case '\b':      ugui_textedit_delete_char(&s_edit); break;
        default:        (void)ugui_textedit_input_char(&s_edit, a); break;
    }
#if UGUI_ENABLE_KEYPAD
    ugui_set_focus(&s_edit);
#endif
}

static void on_clear(uint8_t widget_id, ugui_event_t event)
{
    (void)widget_id;
    if (event != UGUI_EVENT_CLICK) { return; }
    ugui_textedit_clear(&s_edit);
#if UGUI_ENABLE_KEYPAD
    ugui_set_focus(&s_edit);
#endif
}

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,
                       ugui_color_t fill, 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, fill, 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) --- */
/* Editable field. */
ugui_widget_textedit_init(&s_root, &s_edit, &s_edit_ext,
                          s_edit_buf, (uint8_t)sizeof(s_edit_buf));
ugui_widget_set_area(&s_edit, 10, EDIT_Y, FLD_W, FLD_H);
ugui_widget_set_id(&s_edit, WDGT_ID_DEMO);
#ifdef UGUI_ENABLE_FONT
ugui_textedit_set_font(&s_edit, &lexend_14pt_2bpp, UGUI_THEME_TEXT);
#endif
/* Cursor starts at the end of "Hello". */
ugui_screen_add_widget(&s_screen, &s_edit);
#if UGUI_ENABLE_KEYPAD
ugui_screen_add_focus(&s_screen, &s_edit);
#endif

/* Key row. */
{
    int16_t avail = (int16_t)(UGUI_SCREEN_W - 2 * 10);
    int16_t kw    = (int16_t)((avail - (int16_t)(KEY_N) * 6 + 6) / (int16_t)KEY_N);
    uint8_t i;
    for (i = 0u; i < KEY_N; i++) {
        int16_t kx = (int16_t)(10 + (int16_t)i * (kw + 6));
        add_button(&s_key[i], &s_key_ext[i], &s_key_text[i],
                  (uint8_t)(WDGT_ID_SLOT0 + i), kx, KEYROW_Y, kw, KEY_H,
                  UGUI_THEME_SURFACE, k_key_label[i], on_key);
    }
}
add_button(&s_clr, &s_clr_ext, &s_clr_text, WDGT_ID_CLEAR,
          10, CLR_Y, FLD_W, CLR_H, ugui_color_blend(UGUI_THEME_SURFACE, UGUI_THEME_BG, 96u), "Clear", on_clear);

ugui_widget_textedit_init(&s_root, &s_ro, &s_ro_ext,
                          s_ro_buf, (uint8_t)sizeof(s_ro_buf));
ugui_widget_set_area(&s_ro, 10, RO_Y, FLD_W, FLD_H);
ugui_widget_set_id(&s_ro, WDGT_ID_READONLY);
#ifdef UGUI_ENABLE_FONT
ugui_textedit_set_font(&s_ro, &lexend_14pt_2bpp, ugui_color_blend(UGUI_THEME_TEXT, UGUI_THEME_BG, 110u));
#endif
ugui_textedit_set_colors(&s_ro, UGUI_THEME_BG, UGUI_THEME_BORDER, UGUI_THEME_BORDER);
ugui_widget_set_enabled(&s_ro, false);   /* locked: never focuses, no cursor */
ugui_screen_add_widget(&s_screen, &s_ro);

Colours, Sizes & Password Masking

A colour column (three set_colors() presets), a size column (the same field at three border/radius/padding/height combos — plain ext fields, no dedicated setter), and a password field with set_mask('*') — the buffer still holds the real text (get_text() returns it), but every glyph renders as *.

Colours, Sizes & Password Masking

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

/* --- 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_TEXTEDIT)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
#define COL_N  3u
static ugui_widget_t        s_col_w  [COL_N];
static ugui_textedit_ext_t  s_col_ext[COL_N];
static char                 s_col_buf[COL_N][12] = { "Accent", "Success", "Alert" };
#define SIZE_N  3u
static ugui_widget_t        s_size_w  [SIZE_N];
static ugui_textedit_ext_t  s_size_ext[SIZE_N];
static char                 s_size_buf[SIZE_N][12] = { "Small", "Medium", "Large" };
static const uint8_t k_size_border[SIZE_N] = { 1u, 2u, 3u };
static const uint8_t k_size_radius[SIZE_N] = { 2u, 6u, 12u };
static const uint8_t k_size_pad   [SIZE_N] = { 3u, 6u, 10u };
static const int16_t k_size_h     [SIZE_N] = { 26, 34, 44 };
static ugui_widget_t        s_pw_w;
static ugui_textedit_ext_t  s_pw_ext;
static char                 s_pw_buf[16] = "secret";
#define HDR_Y      38
#define COL_X      10
#define COL_W      200
#define COL_ROW0   ((int16_t)(HDR_Y + 22))
#define COL_STEP   44
#define SIZE_X     ((int16_t)(COL_X + COL_W + 24))
#define SIZE_W     190
#define PW_Y       ((int16_t)(UGUI_SCREEN_H - 54))
#define PW_H       36
#endif

/* --- build it (in your screen's init) --- */
ugui_color_t border_col[COL_N];
border_col[0] = UGUI_THEME_ACCENT;
border_col[1] = UGUI_THEME_OK;
border_col[2] = UGUI_THEME_CANCEL;

uint8_t i;
for (i = 0u; i < COL_N; i++) {
    int16_t y = (int16_t)(COL_ROW0 + (int16_t)i * COL_STEP);
    ugui_widget_textedit_init(&s_root, &s_col_w[i], &s_col_ext[i],
                              s_col_buf[i], (uint8_t)sizeof(s_col_buf[i]));
    ugui_widget_set_area(&s_col_w[i], COL_X, y, COL_W, 34);
    ugui_widget_set_id(&s_col_w[i], (uint8_t)(WDGT_ID_SLOT0 + i));
#ifdef UGUI_ENABLE_FONT
    ugui_textedit_set_font(&s_col_w[i], &lexend_14pt_2bpp, UGUI_THEME_TEXT);
#endif
    ugui_textedit_set_colors(&s_col_w[i], UGUI_THEME_SURFACE, border_col[i], border_col[i]);
    ugui_widget_set_enabled(&s_col_w[i], false);   /* display-only */
    ugui_screen_add_widget(&s_screen, &s_col_w[i]);
}

int16_t sy = COL_ROW0;
for (i = 0u; i < SIZE_N; i++) {
    int16_t h = k_size_h[i];
    ugui_widget_textedit_init(&s_root, &s_size_w[i], &s_size_ext[i],
                              s_size_buf[i], (uint8_t)sizeof(s_size_buf[i]));
    ugui_widget_set_area(&s_size_w[i], SIZE_X, sy, SIZE_W, h);
    ugui_widget_set_id(&s_size_w[i], (uint8_t)(WDGT_ID_SLOT3 + i));
#ifdef UGUI_ENABLE_FONT
    ugui_textedit_set_font(&s_size_w[i], &lexend_14pt_2bpp, UGUI_THEME_TEXT);
#endif
    s_size_ext[i].border_width  = k_size_border[i];
    s_size_ext[i].corner_radius = k_size_radius[i];
    s_size_ext[i].padding_x     = k_size_pad[i];
    ugui_widget_set_enabled(&s_size_w[i], false);
    ugui_screen_add_widget(&s_screen, &s_size_w[i]);
    sy = (int16_t)(sy + h + 8);
}

ugui_widget_textedit_init(&s_root, &s_pw_w, &s_pw_ext,
                          s_pw_buf, (uint8_t)sizeof(s_pw_buf));
ugui_widget_set_area(&s_pw_w, COL_X, PW_Y,
                     (int16_t)(UGUI_SCREEN_W - 2 * 10), PW_H);
ugui_widget_set_id(&s_pw_w, WDGT_ID_DEMO);
#ifdef UGUI_ENABLE_FONT
ugui_textedit_set_font(&s_pw_w, &lexend_14pt_2bpp, UGUI_THEME_TEXT);
#endif
ugui_textedit_set_mask(&s_pw_w, '*');   /* render every glyph as '*' */
ugui_widget_set_enabled(&s_pw_w, false);
ugui_screen_add_widget(&s_screen, &s_pw_w);

Login Form (keyboard popup)

Two fields — Username (plain) and Password (masked) — plus a docked alphanum keyboard in popup mode. Tapping a field opens the keyboard targeting that field's own buffer; on OK the built-in popup handlers copy the edited text back and repaint the field (the password field re-renders masked). Submit reads both values with get_text() — the real password included, proving masking is display-only.

Login Form (keyboard popup)

/* --- 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_SLOT8,
    WDGT_ID_SUBMIT, WDGT_ID_STATUS, WDGT_ID_KEYBOARD
};

/* --- 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_TEXTEDIT) && defined(UGUI_WIDGET_ENABLE_ALPHANUM_KEYBOARD)
static ugui_screen_t  s_screen;
static ugui_widget_t   s_root;
static ugui_widget_t        s_user_lbl;
static ugui_label_ext_t     s_user_lbl_ext;
static ugui_widget_t        s_user_w;
static ugui_textedit_ext_t  s_user_ext;
static char                 s_user_buf[24] = "admin";
static ugui_widget_t        s_pass_lbl;
static ugui_label_ext_t     s_pass_lbl_ext;
static ugui_widget_t        s_pass_w;
static ugui_textedit_ext_t  s_pass_ext;
static char                 s_pass_buf[24] = "pw123";
static ugui_widget_t        s_submit;
static ugui_button_ext_t    s_submit_ext;
static ugui_widget_t        s_status;
static ugui_label_ext_t     s_status_ext;
static ugui_widget_t                s_kbd;
static ugui_alphanum_keyboard_ext_t s_kbd_ext;
#endif
#if defined(UGUI_WIDGET_ENABLE_TEXTEDIT) && defined(UGUI_WIDGET_ENABLE_ALPHANUM_KEYBOARD)
static ugui_text_cfg_t      s_submit_text;
static char s_status_buf[40];
#endif
#if defined(UGUI_WIDGET_ENABLE_TEXTEDIT) && defined(UGUI_WIDGET_ENABLE_ALPHANUM_KEYBOARD)
#define LBL_X   10
#define LBL_W   96
#define FLD_X   ((int16_t)(LBL_X + LBL_W + 8))
#define FLD_W   ((int16_t)(UGUI_SCREEN_W - FLD_X - 10))
#define FLD_H   34
#define ROW0_Y  ((int16_t)(38 + 16))
#define ROW_STEP 46
#define SUBMIT_Y ((int16_t)(ROW0_Y + 2 * ROW_STEP + 6))
#define SUBMIT_H 34
#define STATUS_Y ((int16_t)(SUBMIT_Y + SUBMIT_H + 12))
#define POP_X   4
#define POP_Y   16
#define POP_W   (UGUI_SCREEN_W - 8)
#define POP_H   (UGUI_SCREEN_H - 28)
#endif

/* --- local helpers & event callbacks --- */
static void on_field_click(uint8_t widget_id, ugui_event_t event)
{
    if (event != UGUI_EVENT_CLICK) { return; }
    switch (widget_id) {
        case WDGT_ID_DEMO:    /* Username */
            ugui_alphanum_popup_open(&s_kbd, &s_user_w, s_user_buf,
                                     (uint8_t)sizeof(s_user_buf));
            break;
        case WDGT_ID_SLOT0:   /* Password */
            ugui_alphanum_popup_open(&s_kbd, &s_pass_w, s_pass_buf,
                                     (uint8_t)sizeof(s_pass_buf));
            break;
        default:
            break;
    }
}

static void on_submit(uint8_t widget_id, ugui_event_t event)
{
    (void)widget_id;
    if (event != UGUI_EVENT_CLICK) { return; }
    const char* user = ugui_textedit_get_text(&s_user_w);
    const char* pass = ugui_textedit_get_text(&s_pass_w);
    /* Password is masked on screen but readable here — masking is display-only. */
    printf("[textedit.form] submit: user='%s' pass='%s'\n",
           (user != NULL) ? user : "", (pass != NULL) ? pass : "");
#ifdef UGUI_ENABLE_FONT
    (void)snprintf(s_status_buf, sizeof(s_status_buf), "Signed in as %s",
                   (user != NULL) ? user : "");
    ugui_widget_label_update_text_str(&s_status, s_status_buf);
#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);
}

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_ACCENT, 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 make_field(ugui_widget_t* lbl, ugui_label_ext_t* lbl_ext,
                       ugui_widget_t* fw, ugui_textedit_ext_t* fe,
                       char* buf, uint8_t cap, int16_t y,
                       const char* caption, uint8_t id, char mask)
{
    add_label(lbl, lbl_ext, (uint8_t)(WDGT_ID_SLOT8 + id),
             LBL_X, y, LBL_W, FLD_H, caption,
             (ugui_align_t)(UGUI_ALIGN_LEFT | UGUI_ALIGN_V_CENTER));

    ugui_widget_textedit_init(&s_root, fw, fe, buf, cap);
    ugui_widget_set_area(fw, FLD_X, y, FLD_W, FLD_H);
    ugui_widget_set_id(fw, id);
#ifdef UGUI_ENABLE_FONT
    ugui_textedit_set_font(fw, &lexend_14pt_2bpp, UGUI_THEME_TEXT);
#endif
    if (mask != 0) { ugui_textedit_set_mask(fw, mask); }
    /* Tap (or ENTER on a focused field) opens the keyboard popup for this field. */
    ugui_widget_set_event_cb(fw, on_field_click, UGUI_EMASK_CLICK | UGUI_EMASK_KEY_CONFIRM);
    ugui_screen_add_widget(&s_screen, fw);
#if UGUI_ENABLE_KEYPAD
    ugui_screen_add_focus(&s_screen, fw);
#endif
}

/* --- build it (in your screen's init) --- */
make_field(&s_user_lbl, &s_user_lbl_ext, &s_user_w, &s_user_ext,
           s_user_buf, (uint8_t)sizeof(s_user_buf),
           ROW0_Y + 0 * ROW_STEP, "Username", WDGT_ID_DEMO, 0);
make_field(&s_pass_lbl, &s_pass_lbl_ext, &s_pass_w, &s_pass_ext,
           s_pass_buf, (uint8_t)sizeof(s_pass_buf),
           ROW0_Y + 1 * ROW_STEP, "Password", WDGT_ID_SLOT0, '*');

/* Submit button. */
add_button(&s_submit, &s_submit_ext, &s_submit_text, WDGT_ID_SUBMIT,
          FLD_X, SUBMIT_Y, FLD_W, SUBMIT_H, "Sign in", on_submit);

/* Status line. */
add_label(&s_status, &s_status_ext, WDGT_ID_STATUS,
         LBL_X, STATUS_Y, (int16_t)(UGUI_SCREEN_W - 2 * 10), 22,
         "Tap a field to edit  |  Sign in reads both values",
         UGUI_ALIGN_CENTER);

/* Popup keyboard — added LAST so it draws on top. */
ugui_widget_alphanum_init(NULL, &s_kbd, &s_kbd_ext);
ugui_widget_set_area(&s_kbd, POP_X, POP_Y, (int16_t)POP_W, (int16_t)POP_H);
ugui_widget_set_id(&s_kbd, WDGT_ID_KEYBOARD);
#ifdef UGUI_ENABLE_FONT
ugui_widget_alphanum_set_font(&s_kbd, &lexend_14pt_2bpp);
#endif
ugui_widget_alphanum_enable_header_preview(&s_kbd, 20u, 26u, "Edit field");
ugui_widget_alphanum_enable_ok_cancel(&s_kbd, 30u, &s_root);
ugui_screen_add_widget(&s_screen, &s_kbd);


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.