Add validation options and a full physical layout to the keyboard

uikeyboardopen_t grows: maxLength (auto-focuses confirm once full),
lineCount for multi-line entry with a NEWLINE key, trimmed/allowBlank
validation on confirm, and a second "are you sure" uiConfirm step.

Reworks uikeyboardqwerty from a packed flat list into a fixed 14x5 grid
shaped like a real keyboard - number row with -, =, ~ and backspace,
caps and shift (one-shot, XORed with caps for real-keyboard behavior),
and a bottom row with cancel/space/confirm in their normal corners.
Shorter rows are padded with invisible filler cells so every key lands
in its physical position; NEWLINE/CONFIRM/CANCEL are reserved slots
uikeyboard.c patches in conditionally, otherwise left as inert blanks.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 18:44:07 -05:00
parent 80f4348e21
commit 709bd7be52
4 changed files with 125 additions and 42 deletions
+45 -17
View File
@@ -26,11 +26,10 @@
uikeyboard_t UI_KEYBOARD;
void uiKeyboardFocusConfirm(void) {
uint8_t confirmIndex = UI_KEYBOARD.menu.itemCount - (UI_KEYBOARD.cancel ? 2 : 1);
uiMenuSetPosition(
&UI_KEYBOARD.menu,
confirmIndex % UI_KEYBOARD.menu.columns,
confirmIndex / UI_KEYBOARD.menu.columns
UI_KEYBOARD_QWERTY_CONFIRM_INDEX % UI_KEYBOARD_QWERTY_COLUMNS,
UI_KEYBOARD_QWERTY_CONFIRM_INDEX / UI_KEYBOARD_QWERTY_COLUMNS
);
}
@@ -111,8 +110,7 @@ void uiKeyboardMenuSelected(
const uint8_t index,
const uimenuitem_t *item
) {
uint8_t confirmIndex = menu->itemCount - (UI_KEYBOARD.cancel ? 2 : 1);
if(index == confirmIndex) {
if(index == UI_KEYBOARD_QWERTY_CONFIRM_INDEX) {
if(UI_KEYBOARD.trimmed) {
stringTrim(UI_KEYBOARD.text);
UI_KEYBOARD.textLabel.dirty = true;
@@ -132,18 +130,32 @@ void uiKeyboardMenuSelected(
return;
}
if(UI_KEYBOARD.cancel && index == menu->itemCount - 1) {
if(UI_KEYBOARD.cancel && index == UI_KEYBOARD_QWERTY_CANCEL_INDEX) {
UI_KEYBOARD.confirmed = false;
uiKeyboardClose();
return;
}
const char_t *key = item->button.label.text;
// Blank filler/reserved cell (includes an unused cancel/newline slot).
if(key[0] == '\0') return;
if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_BACKSPACE) == 0) {
uiKeyboardBackspace();
return;
}
if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_SHIFT) == 0) {
UI_KEYBOARD.shift = !UI_KEYBOARD.shift;
return;
}
if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_CAPS) == 0) {
UI_KEYBOARD.caps = !UI_KEYBOARD.caps;
return;
}
char_t c;
if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_SPACE) == 0) {
c = ' ';
@@ -152,6 +164,13 @@ void uiKeyboardMenuSelected(
stringCompare(key, UI_KEYBOARD_KEY_NEWLINE) == 0
) {
c = '\n';
} else if(key[0] >= 'A' && key[0] <= 'Z') {
// Letter keys are always drawn uppercase - shift/caps only decide
// the case of the character actually entered. Shift is a one-shot,
// consumed here; caps is a persistent toggle.
bool_t upper = UI_KEYBOARD.caps != UI_KEYBOARD.shift;
c = upper ? key[0] : (char_t)tolower(key[0]);
UI_KEYBOARD.shift = false;
} else {
c = key[0];
}
@@ -221,31 +240,40 @@ void uiKeyboardOpen(const uikeyboardopen_t *open) {
UI_KEYBOARD.allowBlank = open->allowBlank;
UI_KEYBOARD.user = open->user;
UI_KEYBOARD.confirmed = false;
UI_KEYBOARD.caps = false;
UI_KEYBOARD.shift = false;
UI_KEYBOARD.open = true;
uint8_t index = uiKeyboardQwertyBuildItems(UI_KEYBOARD.items);
// Builds the full fixed grid, including blank filler at the
// newline/confirm/cancel reserved slots (see uikeyboardqwerty.h) -
// patched with real buttons below where applicable.
uiKeyboardQwertyBuildItems(UI_KEYBOARD.items);
if(UI_KEYBOARD.lineCount > 1) {
UI_KEYBOARD.items[index].type = UI_MENU_WIDGET_TYPE_BUTTON;
uiButtonInit(&UI_KEYBOARD.items[index].button, UI_KEYBOARD_KEY_NEWLINE);
index++;
uiButtonInit(
&UI_KEYBOARD.items[UI_KEYBOARD_QWERTY_NEWLINE_INDEX].button,
UI_KEYBOARD_KEY_NEWLINE
);
}
UI_KEYBOARD.items[index].type = UI_MENU_WIDGET_TYPE_BUTTON;
uiButtonInit(&UI_KEYBOARD.items[index].button, UI_KEYBOARD_CONFIRM_LABEL);
index++;
uiButtonInit(
&UI_KEYBOARD.items[UI_KEYBOARD_QWERTY_CONFIRM_INDEX].button,
UI_KEYBOARD_CONFIRM_LABEL
);
if(UI_KEYBOARD.cancel) {
UI_KEYBOARD.items[index].type = UI_MENU_WIDGET_TYPE_BUTTON;
uiButtonInit(&UI_KEYBOARD.items[index].button, UI_KEYBOARD_CANCEL_LABEL);
index++;
uiButtonInit(
&UI_KEYBOARD.items[UI_KEYBOARD_QWERTY_CANCEL_INDEX].button,
UI_KEYBOARD_CANCEL_LABEL
);
}
uiMenuInit(
&UI_KEYBOARD.menu, uiKeyboardMenuSelected, uiKeyboardMenuClosed, NULL
);
uiMenuSetItems(
&UI_KEYBOARD.menu, UI_KEYBOARD.items, index, UI_KEYBOARD_QWERTY_COLUMNS
&UI_KEYBOARD.menu, UI_KEYBOARD.items, UI_KEYBOARD_QWERTY_KEY_COUNT,
UI_KEYBOARD_QWERTY_COLUMNS
);
// Back deletes a character rather than closing the dialog outright -
+20 -7
View File
@@ -15,18 +15,20 @@
#define UI_KEYBOARD_TITLE_SPRITES_MAX UI_KEYBOARD_TITLE_TEXT_MAX
#define UI_KEYBOARD_TEXT_MAX 64
#define UI_KEYBOARD_TEXT_SPRITES_MAX UI_KEYBOARD_TEXT_MAX
// Room for the active layout's keys plus the newline/confirm/cancel
// buttons uiKeyboard appends itself - only the QWERTY layout exists
// today.
#define UI_KEYBOARD_ITEMS_MAX (UI_KEYBOARD_QWERTY_KEY_COUNT + 3)
// The grid is a fixed size (see uikeyboardqwerty.h) - only the QWERTY
// layout exists today, so this is just its key count.
#define UI_KEYBOARD_ITEMS_MAX UI_KEYBOARD_QWERTY_KEY_COUNT
// Hardcoded for now - a uiModalOpen-style title/button text override is
// planned (see uikeyboardopen_t) but not built yet.
#define UI_KEYBOARD_TITLE_DEFAULT "ENTER YOUR TEXT"
// planned (see uikeyboardopen_t) but not built yet. Patched into the
// grid's reserved slots (UI_KEYBOARD_QWERTY_CONFIRM_INDEX etc.) rather
// than appended, since the grid is a fixed physical-keyboard shape.
#define UI_KEYBOARD_CONFIRM_LABEL "CONFIRM"
#define UI_KEYBOARD_CANCEL_LABEL "CANCEL"
// Only added to the grid when uikeyboardopen_t.lineCount > 1.
// Only patched into UI_KEYBOARD_QWERTY_NEWLINE_INDEX when
// uikeyboardopen_t.lineCount > 1 - otherwise that slot stays blank.
#define UI_KEYBOARD_KEY_NEWLINE "NEWLINE"
#define UI_KEYBOARD_TITLE_DEFAULT "ENTER YOUR TEXT"
#define UI_KEYBOARD_CONFIRM_QUESTION_MAX 64
// Used when uikeyboardopen_t.confirmLabel is NULL.
@@ -141,6 +143,15 @@ typedef struct {
bool_t allowBlank;
bool_t open;
bool_t confirmed;
// Persistent caps-lock toggle, and a one-shot shift consumed by the
// next letter typed - see uiKeyboardMenuSelected. A letter's effective
// case is caps != shift (matches physical keyboards: shift while caps
// is on gives lowercase). Key labels are always drawn uppercase
// regardless of this state - only the appended character's case
// changes.
bool_t caps;
bool_t shift;
} uikeyboard_t;
extern uikeyboard_t UI_KEYBOARD;
@@ -230,6 +241,8 @@ void uiKeyboardBackspace(void);
* onKeyPress before a character key is applied. When UI_KEYBOARD.confirm
* is set, picking confirm opens a second uiConfirm dialog (see
* uiKeyboardConfirmSecondaryResult) instead of closing immediately.
* SHIFT/CAPS toggle UI_KEYBOARD.shift/caps instead of entering anything.
* A blank ("") label is a filler/reserved grid cell and is a no-op.
*
* @param menu The keyboard's menu.
* @param index Index of the picked item.
+20 -4
View File
@@ -13,12 +13,28 @@ uikeyboardqwerty_t UI_KEYBOARD_QWERTY;
// String literals have static storage duration, so these stay valid for
// as long as the process runs - safe for uiButtonInit's non-copying
// label contract.
// label contract. "" cells are blank filler/reserved slots - see
// uikeyboardqwerty.h.
static const char_t *const UI_KEYBOARD_QWERTY_KEYS[UI_KEYBOARD_QWERTY_KEY_COUNT] = {
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
"A", "S", "D", "F", "G", "H", "J", "K", "L",
// ~ 1 2 3 4 5 6 7 8 9 0 - = DEL
"~", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "-", "=",
UI_KEYBOARD_QWERTY_KEY_BACKSPACE,
// Q W E R T Y U I O P
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", "", "", "", "",
// CAPS A S D F G H J K L (NEWLINE)
UI_KEYBOARD_QWERTY_KEY_CAPS,
"A", "S", "D", "F", "G", "H", "J", "K", "L", "", "", "", "",
// SHIFT Z X C V B N M SHIFT
UI_KEYBOARD_QWERTY_KEY_SHIFT,
"Z", "X", "C", "V", "B", "N", "M",
UI_KEYBOARD_QWERTY_KEY_SPACE, UI_KEYBOARD_QWERTY_KEY_BACKSPACE
UI_KEYBOARD_QWERTY_KEY_SHIFT,
"", "", "", "", "",
// (CANCEL) SPACE (CONFIRM)
"", "", "", "", "", "", UI_KEYBOARD_QWERTY_KEY_SPACE, "", "", "", "", "", "", ""
};
errorret_t uiKeyboardQwertyInit(void) {
+40 -14
View File
@@ -9,18 +9,43 @@
#include "error/error.h"
#include "ui/widget/uimenu.h"
// 26 letters plus SPACE and DEL, laid out as one flat columns-wide grid
// rather than the staggered rows a physical QWERTY keyboard uses - the
// menu/focus grid only supports uniform columns today, so exact row
// staggering is left for later.
#define UI_KEYBOARD_QWERTY_COLUMNS 10
#define UI_KEYBOARD_QWERTY_KEY_COUNT 28
// A fixed 14x5 grid shaped like a physical keyboard:
// ~ 1 2 3 4 5 6 7 8 9 0 - = DEL
// Q W E R T Y U I O P
// CAPS A S D F G H J K L (NEWLINE)
// SHIFT Z X C V B N M SHIFT
// (CANCEL) SPACE (CONFIRM)
// Rows shorter than 14 columns are padded with blank filler buttons
// (empty label) so every key lands in its real physical-keyboard
// position - the menu/focus grid only supports uniform columns, it has
// no concept of a key spanning multiple cells or a row being narrower
// than the grid, so this is the only way to get real positioning out of
// it. A filler cell is still a focusable/navigable-to grid slot, it just
// does nothing when selected - see uiKeyboardMenuSelected's blank-label
// check.
//
// NEWLINE/CANCEL/CONFIRM are reserved slots this builder always fills
// with blank filler - uiKeyboard patches them with real buttons
// depending on lineCount/cancel (CONFIRM is unconditional, just needed
// as a fixed slot the caller can address by constant).
#define UI_KEYBOARD_QWERTY_COLUMNS 14
#define UI_KEYBOARD_QWERTY_ROWS 5
#define UI_KEYBOARD_QWERTY_KEY_COUNT \
(UI_KEYBOARD_QWERTY_COLUMNS * UI_KEYBOARD_QWERTY_ROWS)
// Reserved slot indices uiKeyboard patches with real buttons - see
// uikeyboard.c's uiKeyboardOpen.
#define UI_KEYBOARD_QWERTY_NEWLINE_INDEX 38
#define UI_KEYBOARD_QWERTY_CANCEL_INDEX 56
#define UI_KEYBOARD_QWERTY_CONFIRM_INDEX 69
// Labels uiKeyboard checks for by pointer/string compare (see
// uiKeyboardQwertyBuildItems) to tell the space/backspace keys apart from
// an ordinary single-letter key.
// uiKeyboardMenuSelected) to tell special keys apart from an ordinary
// single-character key. A blank ("") label is a filler cell - see above.
#define UI_KEYBOARD_QWERTY_KEY_SPACE "SPACE"
#define UI_KEYBOARD_QWERTY_KEY_BACKSPACE "DEL"
#define UI_KEYBOARD_QWERTY_KEY_SHIFT "SHIFT"
#define UI_KEYBOARD_QWERTY_KEY_CAPS "CAPS"
typedef struct {
@@ -37,12 +62,13 @@ extern uikeyboardqwerty_t UI_KEYBOARD_QWERTY;
errorret_t uiKeyboardQwertyInit(void);
/**
* Builds the QWERTY layout's key buttons (26 letters, then SPACE and
* DEL) into items, starting at index 0, one UI_MENU_WIDGET_TYPE_BUTTON
* per key. Each button's label is either a single uppercase letter or the
* literal UI_KEYBOARD_QWERTY_KEY_SPACE/UI_KEYBOARD_QWERTY_KEY_BACKSPACE -
* uiKeyboard reads a selected item's own label back to know which key it
* was, rather than this function returning a separate mapping.
* Builds the QWERTY layout's key buttons into items, starting at index
* 0, one UI_MENU_WIDGET_TYPE_BUTTON per grid cell (UI_KEYBOARD_QWERTY_KEY_COUNT
* of them, in row-major order across UI_KEYBOARD_QWERTY_COLUMNS columns).
* Each button's label is a single character, one of the
* UI_KEYBOARD_QWERTY_KEY_* literals, or "" for a blank filler/reserved
* slot - uiKeyboard reads a selected item's own label back to know which
* key it was, rather than this function returning a separate mapping.
*
* @param items Destination array; must have room for at least
* UI_KEYBOARD_QWERTY_KEY_COUNT entries.