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; uikeyboard_t UI_KEYBOARD;
void uiKeyboardFocusConfirm(void) { void uiKeyboardFocusConfirm(void) {
uint8_t confirmIndex = UI_KEYBOARD.menu.itemCount - (UI_KEYBOARD.cancel ? 2 : 1);
uiMenuSetPosition( uiMenuSetPosition(
&UI_KEYBOARD.menu, &UI_KEYBOARD.menu,
confirmIndex % UI_KEYBOARD.menu.columns, UI_KEYBOARD_QWERTY_CONFIRM_INDEX % UI_KEYBOARD_QWERTY_COLUMNS,
confirmIndex / UI_KEYBOARD.menu.columns UI_KEYBOARD_QWERTY_CONFIRM_INDEX / UI_KEYBOARD_QWERTY_COLUMNS
); );
} }
@@ -111,8 +110,7 @@ void uiKeyboardMenuSelected(
const uint8_t index, const uint8_t index,
const uimenuitem_t *item const uimenuitem_t *item
) { ) {
uint8_t confirmIndex = menu->itemCount - (UI_KEYBOARD.cancel ? 2 : 1); if(index == UI_KEYBOARD_QWERTY_CONFIRM_INDEX) {
if(index == confirmIndex) {
if(UI_KEYBOARD.trimmed) { if(UI_KEYBOARD.trimmed) {
stringTrim(UI_KEYBOARD.text); stringTrim(UI_KEYBOARD.text);
UI_KEYBOARD.textLabel.dirty = true; UI_KEYBOARD.textLabel.dirty = true;
@@ -132,18 +130,32 @@ void uiKeyboardMenuSelected(
return; return;
} }
if(UI_KEYBOARD.cancel && index == menu->itemCount - 1) { if(UI_KEYBOARD.cancel && index == UI_KEYBOARD_QWERTY_CANCEL_INDEX) {
UI_KEYBOARD.confirmed = false; UI_KEYBOARD.confirmed = false;
uiKeyboardClose(); uiKeyboardClose();
return; return;
} }
const char_t *key = item->button.label.text; 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) { if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_BACKSPACE) == 0) {
uiKeyboardBackspace(); uiKeyboardBackspace();
return; 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; char_t c;
if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_SPACE) == 0) { if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_SPACE) == 0) {
c = ' '; c = ' ';
@@ -152,6 +164,13 @@ void uiKeyboardMenuSelected(
stringCompare(key, UI_KEYBOARD_KEY_NEWLINE) == 0 stringCompare(key, UI_KEYBOARD_KEY_NEWLINE) == 0
) { ) {
c = '\n'; 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 { } else {
c = key[0]; c = key[0];
} }
@@ -221,31 +240,40 @@ void uiKeyboardOpen(const uikeyboardopen_t *open) {
UI_KEYBOARD.allowBlank = open->allowBlank; UI_KEYBOARD.allowBlank = open->allowBlank;
UI_KEYBOARD.user = open->user; UI_KEYBOARD.user = open->user;
UI_KEYBOARD.confirmed = false; UI_KEYBOARD.confirmed = false;
UI_KEYBOARD.caps = false;
UI_KEYBOARD.shift = false;
UI_KEYBOARD.open = true; 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) { if(UI_KEYBOARD.lineCount > 1) {
UI_KEYBOARD.items[index].type = UI_MENU_WIDGET_TYPE_BUTTON; uiButtonInit(
uiButtonInit(&UI_KEYBOARD.items[index].button, UI_KEYBOARD_KEY_NEWLINE); &UI_KEYBOARD.items[UI_KEYBOARD_QWERTY_NEWLINE_INDEX].button,
index++; UI_KEYBOARD_KEY_NEWLINE
);
} }
UI_KEYBOARD.items[index].type = UI_MENU_WIDGET_TYPE_BUTTON; uiButtonInit(
uiButtonInit(&UI_KEYBOARD.items[index].button, UI_KEYBOARD_CONFIRM_LABEL); &UI_KEYBOARD.items[UI_KEYBOARD_QWERTY_CONFIRM_INDEX].button,
index++; UI_KEYBOARD_CONFIRM_LABEL
);
if(UI_KEYBOARD.cancel) { if(UI_KEYBOARD.cancel) {
UI_KEYBOARD.items[index].type = UI_MENU_WIDGET_TYPE_BUTTON; uiButtonInit(
uiButtonInit(&UI_KEYBOARD.items[index].button, UI_KEYBOARD_CANCEL_LABEL); &UI_KEYBOARD.items[UI_KEYBOARD_QWERTY_CANCEL_INDEX].button,
index++; UI_KEYBOARD_CANCEL_LABEL
);
} }
uiMenuInit( uiMenuInit(
&UI_KEYBOARD.menu, uiKeyboardMenuSelected, uiKeyboardMenuClosed, NULL &UI_KEYBOARD.menu, uiKeyboardMenuSelected, uiKeyboardMenuClosed, NULL
); );
uiMenuSetItems( 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 - // 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_TITLE_SPRITES_MAX UI_KEYBOARD_TITLE_TEXT_MAX
#define UI_KEYBOARD_TEXT_MAX 64 #define UI_KEYBOARD_TEXT_MAX 64
#define UI_KEYBOARD_TEXT_SPRITES_MAX UI_KEYBOARD_TEXT_MAX #define UI_KEYBOARD_TEXT_SPRITES_MAX UI_KEYBOARD_TEXT_MAX
// Room for the active layout's keys plus the newline/confirm/cancel // The grid is a fixed size (see uikeyboardqwerty.h) - only the QWERTY
// buttons uiKeyboard appends itself - only the QWERTY layout exists // layout exists today, so this is just its key count.
// today. #define UI_KEYBOARD_ITEMS_MAX UI_KEYBOARD_QWERTY_KEY_COUNT
#define UI_KEYBOARD_ITEMS_MAX (UI_KEYBOARD_QWERTY_KEY_COUNT + 3)
// Hardcoded for now - a uiModalOpen-style title/button text override is // Hardcoded for now - a uiModalOpen-style title/button text override is
// planned (see uikeyboardopen_t) but not built yet. // planned (see uikeyboardopen_t) but not built yet. Patched into the
#define UI_KEYBOARD_TITLE_DEFAULT "ENTER YOUR TEXT" // 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_CONFIRM_LABEL "CONFIRM"
#define UI_KEYBOARD_CANCEL_LABEL "CANCEL" #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_KEY_NEWLINE "NEWLINE"
#define UI_KEYBOARD_TITLE_DEFAULT "ENTER YOUR TEXT"
#define UI_KEYBOARD_CONFIRM_QUESTION_MAX 64 #define UI_KEYBOARD_CONFIRM_QUESTION_MAX 64
// Used when uikeyboardopen_t.confirmLabel is NULL. // Used when uikeyboardopen_t.confirmLabel is NULL.
@@ -141,6 +143,15 @@ typedef struct {
bool_t allowBlank; bool_t allowBlank;
bool_t open; bool_t open;
bool_t confirmed; 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; } uikeyboard_t;
extern uikeyboard_t UI_KEYBOARD; extern uikeyboard_t UI_KEYBOARD;
@@ -230,6 +241,8 @@ void uiKeyboardBackspace(void);
* onKeyPress before a character key is applied. When UI_KEYBOARD.confirm * onKeyPress before a character key is applied. When UI_KEYBOARD.confirm
* is set, picking confirm opens a second uiConfirm dialog (see * is set, picking confirm opens a second uiConfirm dialog (see
* uiKeyboardConfirmSecondaryResult) instead of closing immediately. * 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 menu The keyboard's menu.
* @param index Index of the picked item. * @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 // String literals have static storage duration, so these stay valid for
// as long as the process runs - safe for uiButtonInit's non-copying // 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] = { static const char_t *const UI_KEYBOARD_QWERTY_KEYS[UI_KEYBOARD_QWERTY_KEY_COUNT] = {
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", // ~ 1 2 3 4 5 6 7 8 9 0 - = DEL
"A", "S", "D", "F", "G", "H", "J", "K", "L", "~", "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", "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) { errorret_t uiKeyboardQwertyInit(void) {
+40 -14
View File
@@ -9,18 +9,43 @@
#include "error/error.h" #include "error/error.h"
#include "ui/widget/uimenu.h" #include "ui/widget/uimenu.h"
// 26 letters plus SPACE and DEL, laid out as one flat columns-wide grid // A fixed 14x5 grid shaped like a physical keyboard:
// rather than the staggered rows a physical QWERTY keyboard uses - the // ~ 1 2 3 4 5 6 7 8 9 0 - = DEL
// menu/focus grid only supports uniform columns today, so exact row // Q W E R T Y U I O P
// staggering is left for later. // CAPS A S D F G H J K L (NEWLINE)
#define UI_KEYBOARD_QWERTY_COLUMNS 10 // SHIFT Z X C V B N M SHIFT
#define UI_KEYBOARD_QWERTY_KEY_COUNT 28 // (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 // Labels uiKeyboard checks for by pointer/string compare (see
// uiKeyboardQwertyBuildItems) to tell the space/backspace keys apart from // uiKeyboardMenuSelected) to tell special keys apart from an ordinary
// an ordinary single-letter key. // single-character key. A blank ("") label is a filler cell - see above.
#define UI_KEYBOARD_QWERTY_KEY_SPACE "SPACE" #define UI_KEYBOARD_QWERTY_KEY_SPACE "SPACE"
#define UI_KEYBOARD_QWERTY_KEY_BACKSPACE "DEL" #define UI_KEYBOARD_QWERTY_KEY_BACKSPACE "DEL"
#define UI_KEYBOARD_QWERTY_KEY_SHIFT "SHIFT"
#define UI_KEYBOARD_QWERTY_KEY_CAPS "CAPS"
typedef struct { typedef struct {
@@ -37,12 +62,13 @@ extern uikeyboardqwerty_t UI_KEYBOARD_QWERTY;
errorret_t uiKeyboardQwertyInit(void); errorret_t uiKeyboardQwertyInit(void);
/** /**
* Builds the QWERTY layout's key buttons (26 letters, then SPACE and * Builds the QWERTY layout's key buttons into items, starting at index
* DEL) into items, starting at index 0, one UI_MENU_WIDGET_TYPE_BUTTON * 0, one UI_MENU_WIDGET_TYPE_BUTTON per grid cell (UI_KEYBOARD_QWERTY_KEY_COUNT
* per key. Each button's label is either a single uppercase letter or the * of them, in row-major order across UI_KEYBOARD_QWERTY_COLUMNS columns).
* literal UI_KEYBOARD_QWERTY_KEY_SPACE/UI_KEYBOARD_QWERTY_KEY_BACKSPACE - * Each button's label is a single character, one of the
* uiKeyboard reads a selected item's own label back to know which key it * UI_KEYBOARD_QWERTY_KEY_* literals, or "" for a blank filler/reserved
* was, rather than this function returning a separate mapping. * 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 * @param items Destination array; must have room for at least
* UI_KEYBOARD_QWERTY_KEY_COUNT entries. * UI_KEYBOARD_QWERTY_KEY_COUNT entries.