diff --git a/src/dusk/ui/dialog/keyboard/uikeyboard.c b/src/dusk/ui/dialog/keyboard/uikeyboard.c index a6ddc40e..668d3502 100644 --- a/src/dusk/ui/dialog/keyboard/uikeyboard.c +++ b/src/dusk/ui/dialog/keyboard/uikeyboard.c @@ -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 - diff --git a/src/dusk/ui/dialog/keyboard/uikeyboard.h b/src/dusk/ui/dialog/keyboard/uikeyboard.h index e19c41be..582d351e 100644 --- a/src/dusk/ui/dialog/keyboard/uikeyboard.h +++ b/src/dusk/ui/dialog/keyboard/uikeyboard.h @@ -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. diff --git a/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.c b/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.c index 2ab17b86..fa96f499 100644 --- a/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.c +++ b/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.c @@ -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) { diff --git a/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.h b/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.h index 2cb50156..194f974c 100644 --- a/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.h +++ b/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.h @@ -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.