diff --git a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c index 23aea0aa..9defbe91 100644 --- a/src/dusk/asset/loader/cutscene/assetcutsceneloader.c +++ b/src/dusk/asset/loader/cutscene/assetcutsceneloader.c @@ -327,7 +327,7 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) { item->battleForceAction.targetIndex = assetCutsceneReadU8(data, &offset); break; - case CUTSCENE_ITEM_TYPE_MODAL: + case CUTSCENE_ITEM_TYPE_MODAL: { assetCutsceneReadEmbeddedString( data, &offset, item->modal.title, CUTSCENE_MODAL_TITLE_MAX_CHARS ); @@ -337,12 +337,28 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) { // v1 only supports the message-only form: MODAL and MODAL_OPTIONS // share this same tag with no separate discriminator, and there is // no native-callback registry yet to resolve an options callback. - assertTrue( - assetCutsceneReadU8(data, &offset) == 0, - "Cutscene MODAL item with options is not supported in file-based " - "cutscenes yet - use MODAL_OPTIONS_MARKERS instead" - ); + // Read into a local first (not inline in the check below) - on a + // release build with DUSK_ASSERTIONS_FAKED, an assert's condition + // is never evaluated at all, so a byte-consuming call inside one + // would silently desync every item after it. This is untrusted + // file content anyway, so it gets a real error, not an assert. + uint8_t optionCount = assetCutsceneReadU8(data, &offset); + if(optionCount != 0) { + memoryFree(data); + memoryFree(out->items); + out->items = NULL; + if(out->pool != NULL) { + memoryFree(out->pool); + out->pool = NULL; + } + assetLoaderErrorThrow( + loading, + "Cutscene MODAL item with options is not supported in " + "file-based cutscenes yet - use MODAL_OPTIONS_MARKERS instead" + ); + } break; + } case CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS: { assetCutsceneReadEmbeddedString( @@ -410,8 +426,10 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) { } assetLoaderErrorThrow( loading, - "Cutscene item type %u is not supported in file-based cutscenes", - (uint32_t)item->type + "Cutscene item type %u is not supported in file-based cutscenes " + "(item %u/%u, offset %u/%u, poolSize %u)", + (uint32_t)item->type, (uint32_t)i, (uint32_t)itemCount, + (uint32_t)offset, (uint32_t)fileSize, (uint32_t)poolSize ); } } diff --git a/src/dusk/ui/dialog/keyboard/CMakeLists.txt b/src/dusk/ui/dialog/keyboard/CMakeLists.txt index 26276b29..9e5f2567 100644 --- a/src/dusk/ui/dialog/keyboard/CMakeLists.txt +++ b/src/dusk/ui/dialog/keyboard/CMakeLists.txt @@ -6,7 +6,4 @@ target_sources(${DUSK_LIBRARY_TARGET_NAME} PUBLIC uikeyboard.c - uikeyboardqwerty.c - uikeyboardnumbers.c - uikeyboardsymbols.c ) diff --git a/src/dusk/ui/dialog/keyboard/uikeyboard.c b/src/dusk/ui/dialog/keyboard/uikeyboard.c index 668d3502..8a149827 100644 --- a/src/dusk/ui/dialog/keyboard/uikeyboard.c +++ b/src/dusk/ui/dialog/keyboard/uikeyboard.c @@ -25,11 +25,153 @@ uikeyboard_t UI_KEYBOARD; +// 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. "" cells are blank filler/reserved slots, NULL is +// shorthand for the same thing where there's no key at all - see +// uikeyboard.h and uiKeyboardNormalizeKey. +// +// Unshifted, caps off - also the canonical table (see +// uiKeyboardGetCanonicalKey). +static const char_t *const UI_KEYBOARD_KEYS_NONE[UI_KEYBOARD_KEY_COUNT] = { + // 1 2 3 4 5 6 7 8 9 0 DEL + "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", + UI_KEYBOARD_KEY_BACKSPACE, + + // q w e r t y u i o p + "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", NULL, + + // CAPS a s d f g h j k l (NEWLINE) + UI_KEYBOARD_KEY_CAPS, + "a", "s", "d", "f", "g", "h", "j", "k", "l", NULL, + + // SHIFT z x c v b n m SHIFT + UI_KEYBOARD_KEY_SHIFT, + "z", "x", "c", "v", "b", "n", "m", + UI_KEYBOARD_KEY_SHIFT, + NULL, NULL, + + // (CANCEL) SPACE (CONFIRM) + NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL, + NULL +}; + +// Caps on - letters uppercase; caps doesn't affect digits/symbols on a +// physical keyboard, so this table is otherwise identical to _NONE. +static const char_t *const UI_KEYBOARD_KEYS_CAPS[UI_KEYBOARD_KEY_COUNT] = { + "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", + UI_KEYBOARD_KEY_BACKSPACE, + + "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", NULL, + + UI_KEYBOARD_KEY_CAPS, + "A", "S", "D", "F", "G", "H", "J", "K", "L", NULL, + + UI_KEYBOARD_KEY_SHIFT, + "Z", "X", "C", "V", "B", "N", "M", + UI_KEYBOARD_KEY_SHIFT, + NULL, NULL, + + NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL, + NULL +}; + +// Shift held - letters uppercase, digits become their US-layout shift +// symbols, '-'/'=' become '_'/'+'. Takes priority over caps (see +// uiKeyboardGetChar). +static const char_t *const UI_KEYBOARD_KEYS_SHIFT[UI_KEYBOARD_KEY_COUNT] = { + "!", "@", "#", "$", "%", "^", "&", "*", "(", ")", + UI_KEYBOARD_KEY_BACKSPACE, + + "Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P", NULL, + + UI_KEYBOARD_KEY_CAPS, + "A", "S", "D", "F", "G", "H", "J", "K", "L", NULL, + + UI_KEYBOARD_KEY_SHIFT, + "Z", "X", "C", "V", "B", "N", "M", + UI_KEYBOARD_KEY_SHIFT, + NULL, NULL, + + NULL, NULL, NULL, NULL, NULL, UI_KEYBOARD_KEY_SPACE, NULL, NULL, NULL, NULL, + NULL +}; + +// Collapses a table entry's NULL ("no key here") down to "" (blank +// filler), the only form of "nothing" callers need to handle. +const char_t * uiKeyboardNormalizeKey(const char_t *key) { + return key != NULL ? key : ""; +} + +// Shift takes priority over caps - there's no fourth table for "shift +// while caps is on", unlike a physical keyboard. +const char_t *const * uiKeyboardSelectTable( + const bool_t shift, + const bool_t caps +) { + if(shift) return UI_KEYBOARD_KEYS_SHIFT; + if(caps) return UI_KEYBOARD_KEYS_CAPS; + return UI_KEYBOARD_KEYS_NONE; +} + +uint8_t uiKeyboardBuildItems( + uimenuitem_t *items, + const bool_t shift, + const bool_t caps +) { + assertNotNull(items, "Items cannot be NULL"); + const char_t *const *table = uiKeyboardSelectTable(shift, caps); + + for(uint8_t i = 0; i < UI_KEYBOARD_KEY_COUNT; i++) { + items[i].type = UI_MENU_WIDGET_TYPE_BUTTON; + uiButtonInit(&items[i].button, uiKeyboardNormalizeKey(table[i])); + } + + return UI_KEYBOARD_KEY_COUNT; +} + +void uiKeyboardUpdateDisplay( + uimenuitem_t *items, + const bool_t shift, + const bool_t caps +) { + assertNotNull(items, "Items cannot be NULL"); + const char_t *const *table = uiKeyboardSelectTable(shift, caps); + + for(uint8_t i = 0; i < UI_KEYBOARD_KEY_COUNT; i++) { + items[i].button.label.text = uiKeyboardNormalizeKey(table[i]); + items[i].button.label.dirty = true; + } +} + +const char_t * uiKeyboardGetCanonicalKey(const uint8_t index) { + assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range"); + return uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_NONE[index]); +} + +bool_t uiKeyboardIsShiftSensitive(const uint8_t index) { + assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range"); + return stringCompare( + uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_NONE[index]), + uiKeyboardNormalizeKey(UI_KEYBOARD_KEYS_SHIFT[index]) + ) != 0; +} + +char_t uiKeyboardGetChar( + const uint8_t index, + const bool_t shift, + const bool_t caps +) { + assertTrue(index < UI_KEYBOARD_KEY_COUNT, "Index out of range"); + const char_t *const *table = uiKeyboardSelectTable(shift, caps); + return uiKeyboardNormalizeKey(table[index])[0]; +} + void uiKeyboardFocusConfirm(void) { uiMenuSetPosition( &UI_KEYBOARD.menu, - UI_KEYBOARD_QWERTY_CONFIRM_INDEX % UI_KEYBOARD_QWERTY_COLUMNS, - UI_KEYBOARD_QWERTY_CONFIRM_INDEX / UI_KEYBOARD_QWERTY_COLUMNS + UI_KEYBOARD_CONFIRM_INDEX % UI_KEYBOARD_COLUMNS, + UI_KEYBOARD_CONFIRM_INDEX / UI_KEYBOARD_COLUMNS ); } @@ -110,7 +252,7 @@ void uiKeyboardMenuSelected( const uint8_t index, const uimenuitem_t *item ) { - if(index == UI_KEYBOARD_QWERTY_CONFIRM_INDEX) { + if(index == UI_KEYBOARD_CONFIRM_INDEX) { if(UI_KEYBOARD.trimmed) { stringTrim(UI_KEYBOARD.text); UI_KEYBOARD.textLabel.dirty = true; @@ -130,49 +272,66 @@ void uiKeyboardMenuSelected( return; } - if(UI_KEYBOARD.cancel && index == UI_KEYBOARD_QWERTY_CANCEL_INDEX) { + if(UI_KEYBOARD.cancel && index == UI_KEYBOARD_CANCEL_INDEX) { UI_KEYBOARD.confirmed = false; uiKeyboardClose(); return; } - const char_t *key = item->button.label.text; + if(index == UI_KEYBOARD_NEWLINE_INDEX && UI_KEYBOARD.lineCount > 1) { + if( + UI_KEYBOARD.onKeyPress != NULL && + !UI_KEYBOARD.onKeyPress('\n', UI_KEYBOARD.user) + ) return; + uiKeyboardAppendChar('\n'); + return; + } + + // The canonical (unshifted) key, independent of whatever character + // it's currently drawn as - see uiKeyboardGetCanonicalKey. Used to + // identify the key, not item->button.label.text, which changes with + // shift/caps. + const char_t *key = uiKeyboardGetCanonicalKey(index); // 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_KEY_BACKSPACE) == 0) { uiKeyboardBackspace(); return; } - if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_SHIFT) == 0) { + if(stringCompare(key, UI_KEYBOARD_KEY_SHIFT) == 0) { UI_KEYBOARD.shift = !UI_KEYBOARD.shift; + uiKeyboardUpdateDisplay( + UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps + ); return; } - if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_CAPS) == 0) { + if(stringCompare(key, UI_KEYBOARD_KEY_CAPS) == 0) { UI_KEYBOARD.caps = !UI_KEYBOARD.caps; + uiKeyboardUpdateDisplay( + UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps + ); return; } char_t c; - if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_SPACE) == 0) { + if(stringCompare(key, UI_KEYBOARD_KEY_SPACE) == 0) { c = ' '; - } else if( - UI_KEYBOARD.lineCount > 1 && - 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]; + c = uiKeyboardGetChar(index, UI_KEYBOARD.shift, UI_KEYBOARD.caps); + + // Shift is a one-shot, consumed by any letter/digit/-/= it actually + // affected (and the display refreshed to drop back to whatever caps + // alone says); caps is a persistent toggle. + if(UI_KEYBOARD.shift && uiKeyboardIsShiftSensitive(index)) { + UI_KEYBOARD.shift = false; + uiKeyboardUpdateDisplay( + UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps + ); + } } if(UI_KEYBOARD.onKeyPress != NULL && !UI_KEYBOARD.onKeyPress(c, UI_KEYBOARD.user)) { @@ -245,25 +404,26 @@ void uiKeyboardOpen(const uikeyboardopen_t *open) { UI_KEYBOARD.open = true; // 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); + // newline/confirm/cancel reserved slots - patched with real buttons + // below where applicable. Letters start lowercase and digits + // unshifted, matching caps/shift both being reset to false above. + uiKeyboardBuildItems(UI_KEYBOARD.items, false, false); if(UI_KEYBOARD.lineCount > 1) { uiButtonInit( - &UI_KEYBOARD.items[UI_KEYBOARD_QWERTY_NEWLINE_INDEX].button, + &UI_KEYBOARD.items[UI_KEYBOARD_NEWLINE_INDEX].button, UI_KEYBOARD_KEY_NEWLINE ); } uiButtonInit( - &UI_KEYBOARD.items[UI_KEYBOARD_QWERTY_CONFIRM_INDEX].button, + &UI_KEYBOARD.items[UI_KEYBOARD_CONFIRM_INDEX].button, UI_KEYBOARD_CONFIRM_LABEL ); if(UI_KEYBOARD.cancel) { uiButtonInit( - &UI_KEYBOARD.items[UI_KEYBOARD_QWERTY_CANCEL_INDEX].button, + &UI_KEYBOARD.items[UI_KEYBOARD_CANCEL_INDEX].button, UI_KEYBOARD_CANCEL_LABEL ); } @@ -272,8 +432,8 @@ void uiKeyboardOpen(const uikeyboardopen_t *open) { &UI_KEYBOARD.menu, uiKeyboardMenuSelected, uiKeyboardMenuClosed, NULL ); uiMenuSetItems( - &UI_KEYBOARD.menu, UI_KEYBOARD.items, UI_KEYBOARD_QWERTY_KEY_COUNT, - UI_KEYBOARD_QWERTY_COLUMNS + &UI_KEYBOARD.menu, UI_KEYBOARD.items, UI_KEYBOARD_KEY_COUNT, + UI_KEYBOARD_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 582d351e..7509318e 100644 --- a/src/dusk/ui/dialog/keyboard/uikeyboard.h +++ b/src/dusk/ui/dialog/keyboard/uikeyboard.h @@ -9,25 +9,65 @@ #include "error/error.h" #include "ui/widget/uilabel.h" #include "ui/widget/uimenu.h" -#include "ui/dialog/keyboard/uikeyboardqwerty.h" + +// A fixed 11x5 QWERTY grid - the only layout that exists (numbers/ +// symbols layouts were removed as separate concepts; digits already +// live on this one): +// 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 11 columns are padded with blank filler buttons +// (empty label) so every key lands in its intended 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. A +// UI_KEYBOARD_KEYS_* table may use NULL for a slot with no key at all - +// see uiKeyboardGetCanonicalKey, which normalizes it to the same blank +// filler behavior. +// +// NEWLINE/CANCEL/CONFIRM are reserved slots the tables always leave +// blank - uiKeyboardOpen patches them with real buttons depending on +// lineCount/cancel (CONFIRM is unconditional, just needed as a fixed +// slot addressable by constant). +#define UI_KEYBOARD_COLUMNS 11 +#define UI_KEYBOARD_ROWS 5 +#define UI_KEYBOARD_KEY_COUNT (UI_KEYBOARD_COLUMNS * UI_KEYBOARD_ROWS) + +// Reserved slot indices uiKeyboardOpen patches with real buttons. +#define UI_KEYBOARD_NEWLINE_INDEX 32 +#define UI_KEYBOARD_CANCEL_INDEX 44 +#define UI_KEYBOARD_CONFIRM_INDEX 54 + +// Labels uiKeyboardMenuSelected checks for by pointer/string compare to +// tell special keys apart from an ordinary single-character key. A blank +// ("") label is a filler cell - see above. Identical across every +// UI_KEYBOARD_KEYS_* table, since these don't change with shift/caps. +#define UI_KEYBOARD_KEY_SPACE "SPACE" +#define UI_KEYBOARD_KEY_BACKSPACE "DEL" +#define UI_KEYBOARD_KEY_SHIFT "SHIFT" +#define UI_KEYBOARD_KEY_CAPS "CAPS" +// Only patched into UI_KEYBOARD_NEWLINE_INDEX when +// uikeyboardopen_t.lineCount > 1 - otherwise that slot stays blank. +#define UI_KEYBOARD_KEY_NEWLINE "NEWLINE" #define UI_KEYBOARD_TITLE_TEXT_MAX 64 #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 -// 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 +// The grid is a fixed size - only one layout exists today, so this is +// just its key count. +#define UI_KEYBOARD_ITEMS_MAX UI_KEYBOARD_KEY_COUNT // Hardcoded for now - a uiModalOpen-style title/button text override is // 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. +// grid's reserved slots (UI_KEYBOARD_CONFIRM_INDEX etc.) rather than +// appended, since the grid is a fixed shape. #define UI_KEYBOARD_CONFIRM_LABEL "CONFIRM" #define UI_KEYBOARD_CANCEL_LABEL "CANCEL" -// 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 @@ -145,11 +185,9 @@ typedef struct { 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. + // next letter/digit/-/= typed - see uiKeyboardMenuSelected. Key labels + // are redrawn to match (see uiKeyboardUpdateDisplay) rather than + // staying static. bool_t caps; bool_t shift; } uikeyboard_t; @@ -166,8 +204,7 @@ errorret_t uiKeyboardInit(void); /** * Draws the on-screen keyboard: a semi-transparent black backdrop * covering the whole screen, then its own centered frame with the title, - * currently entered text, and the active layout's key grid. No-op when - * not open. + * currently entered text, and the key grid. No-op when not open. * * @return Any error that occurs. */ @@ -284,3 +321,110 @@ void uiKeyboardMenuClosed(const uimenu_t *menu); * default pop. */ bool_t uiKeyboardMenuCancel(const uimenu_t *menu); + +/** + * Collapses a UI_KEYBOARD_KEYS_* table entry's NULL ("no key here") down + * to "" (blank filler), the only form of "nothing" callers need to + * handle. + * + * @param key A table entry, possibly NULL. + * @returns key, or "" if key was NULL. + */ +const char_t * uiKeyboardNormalizeKey(const char_t *key); + +/** + * Picks which of UI_KEYBOARD_KEYS_NONE/_CAPS/_SHIFT is active for the + * given state - shift takes priority over caps when both are set. + * + * @param shift Current shift state. + * @param caps Current caps state. + * @returns The selected table. + */ +const char_t *const * uiKeyboardSelectTable( + const bool_t shift, + const bool_t caps +); + +/** + * Builds the key grid's buttons into items, starting at index 0, one + * UI_MENU_WIDGET_TYPE_BUTTON per grid cell (UI_KEYBOARD_KEY_COUNT of + * them, in row-major order across UI_KEYBOARD_COLUMNS columns). Each + * button's label is a single character, one of the UI_KEYBOARD_KEY_* + * literals, or "" for a blank filler/reserved slot, read straight from + * whichever of the three UI_KEYBOARD_KEYS_* tables shift/caps selects + * (see uiKeyboardGetChar) - can be refreshed later with + * uiKeyboardUpdateDisplay. See uiKeyboardGetCanonicalKey for identifying + * a picked key independent of its displayed character. + * + * @param items Destination array; must have room for at least + * UI_KEYBOARD_KEY_COUNT entries. + * @param shift Initial shift state - see uiKeyboardGetChar. + * @param caps Initial caps state - see uiKeyboardGetChar. + * @returns The number of items written (UI_KEYBOARD_KEY_COUNT). + */ +uint8_t uiKeyboardBuildItems( + uimenuitem_t *items, + const bool_t shift, + const bool_t caps +); + +/** + * Repoints every key already built into items by uiKeyboardBuildItems at + * whichever UI_KEYBOARD_KEYS_* table the new shift/caps state selects, + * marking all of them dirty so they redraw. Unconditional (not just the + * letter/digit/-/= keys that actually differ between tables) - simpler + * than tracking which changed, and cheap since this only runs on an + * infrequent SHIFT/CAPS press. + * + * @param items The same array passed to uiKeyboardBuildItems. + * @param shift New shift state - see uiKeyboardGetChar. + * @param caps New caps state - see uiKeyboardGetChar. + */ +void uiKeyboardUpdateDisplay( + uimenuitem_t *items, + const bool_t shift, + const bool_t caps +); + +/** + * Returns a grid slot's canonical (unshifted, no-caps) key label, + * independent of whatever character it's currently drawn as - use this + * to identify which key was picked instead of reading a selected item's + * own (possibly shifted) label.text. + * + * @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1. + * @returns The slot's canonical label. + */ +const char_t * uiKeyboardGetCanonicalKey(const uint8_t index); + +/** + * True if a grid slot's character actually differs between the + * unshifted and shifted tables - i.e. a letter, digit, '-', or '='. False + * for a slot whose UI_KEYBOARD_KEYS_* entry is the same in every table + * (SPACE, DEL, SHIFT, CAPS, or blank filler). + * + * @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1. + * @returns True if shift affects this slot's character. + */ +bool_t uiKeyboardIsShiftSensitive(const uint8_t index); + +/** + * Looks up the character a grid slot represents under the given shift/ + * caps state, straight from whichever UI_KEYBOARD_KEYS_* table they + * select - shift takes priority over caps when both are set (so, unlike + * a physical keyboard, shift while caps is on still gives uppercase, not + * lowercase - there is no fourth table for that combination). For a slot + * whose label isn't a single character (a blank filler cell, or a + * multi-character special key like "SPACE"), returns that label's first + * byte ('\0' for blank). + * + * @param index Grid slot index, from 0 to UI_KEYBOARD_KEY_COUNT - 1. + * @param shift Current shift state (one-shot, see UI_KEYBOARD.shift). + * @param caps Current caps state (persistent toggle, see UI_KEYBOARD.caps). + * @returns The character to display/type. + */ +char_t uiKeyboardGetChar( + const uint8_t index, + const bool_t shift, + const bool_t caps +); diff --git a/src/dusk/ui/dialog/keyboard/uikeyboardnumbers.c b/src/dusk/ui/dialog/keyboard/uikeyboardnumbers.c deleted file mode 100644 index a885f95e..00000000 --- a/src/dusk/ui/dialog/keyboard/uikeyboardnumbers.c +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uikeyboardnumbers.h" - -uikeyboardnumbers_t UI_KEYBOARD_NUMBERS; - -errorret_t uiKeyboardNumbersInit(void) { - // Nothing to initialize yet -- uikeyboardnumbers_t is currently a - // placeholder with no fields. - errorOk(); -} - -errorret_t uiKeyboardNumbersDraw(void) { - errorOk(); -} - -errorret_t uiKeyboardNumbersDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/dialog/keyboard/uikeyboardnumbers.h b/src/dusk/ui/dialog/keyboard/uikeyboardnumbers.h deleted file mode 100644 index c39291a4..00000000 --- a/src/dusk/ui/dialog/keyboard/uikeyboardnumbers.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -typedef struct { - -} uikeyboardnumbers_t; - -extern uikeyboardnumbers_t UI_KEYBOARD_NUMBERS; - -/** - * Initializes the numbers keyboard layout. Currently a placeholder -- - * no behavior implemented yet. - * - * @return Any error that occurs. - */ -errorret_t uiKeyboardNumbersInit(void); - -/** - * Draws the numbers keyboard layout. Currently a placeholder -- no-op. - * - * @return Any error that occurs. - */ -errorret_t uiKeyboardNumbersDraw(void); - -/** - * Disposes of the numbers keyboard layout. - * - * @return Any error that occurs. - */ -errorret_t uiKeyboardNumbersDispose(void); diff --git a/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.c b/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.c deleted file mode 100644 index fa96f499..00000000 --- a/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.c +++ /dev/null @@ -1,57 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uikeyboardqwerty.h" -#include "assert/assert.h" -#include "ui/widget/uibutton.h" - -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. "" cells are blank filler/reserved slots - see -// uikeyboardqwerty.h. -static const char_t *const UI_KEYBOARD_QWERTY_KEYS[UI_KEYBOARD_QWERTY_KEY_COUNT] = { - // ~ 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_SHIFT, - "", "", "", "", "", - - // (CANCEL) SPACE (CONFIRM) - "", "", "", "", "", "", UI_KEYBOARD_QWERTY_KEY_SPACE, "", "", "", "", "", "", "" -}; - -errorret_t uiKeyboardQwertyInit(void) { - errorOk(); -} - -uint8_t uiKeyboardQwertyBuildItems(uimenuitem_t *items) { - assertNotNull(items, "Items cannot be NULL"); - - for(uint8_t i = 0; i < UI_KEYBOARD_QWERTY_KEY_COUNT; i++) { - items[i].type = UI_MENU_WIDGET_TYPE_BUTTON; - uiButtonInit(&items[i].button, UI_KEYBOARD_QWERTY_KEYS[i]); - } - - return UI_KEYBOARD_QWERTY_KEY_COUNT; -} - -errorret_t uiKeyboardQwertyDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.h b/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.h deleted file mode 100644 index 194f974c..00000000 --- a/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.h +++ /dev/null @@ -1,84 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" -#include "ui/widget/uimenu.h" - -// 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 -// 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 { - -} uikeyboardqwerty_t; - -extern uikeyboardqwerty_t UI_KEYBOARD_QWERTY; - -/** - * Initializes the QWERTY keyboard layout. Currently a no-op - the key - * labels are static string literals with no per-instance state. - * - * @return Any error that occurs. - */ -errorret_t uiKeyboardQwertyInit(void); - -/** - * 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. - * @returns The number of items written (UI_KEYBOARD_QWERTY_KEY_COUNT). - */ -uint8_t uiKeyboardQwertyBuildItems(uimenuitem_t *items); - -/** - * Disposes of the QWERTY keyboard layout. Currently a no-op. - * - * @return Any error that occurs. - */ -errorret_t uiKeyboardQwertyDispose(void); diff --git a/src/dusk/ui/dialog/keyboard/uikeyboardsymbols.c b/src/dusk/ui/dialog/keyboard/uikeyboardsymbols.c deleted file mode 100644 index 1a85476f..00000000 --- a/src/dusk/ui/dialog/keyboard/uikeyboardsymbols.c +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#include "uikeyboardsymbols.h" - -uikeyboardsymbols_t UI_KEYBOARD_SYMBOLS; - -errorret_t uiKeyboardSymbolsInit(void) { - // Nothing to initialize yet -- uikeyboardsymbols_t is currently a - // placeholder with no fields. - errorOk(); -} - -errorret_t uiKeyboardSymbolsDraw(void) { - errorOk(); -} - -errorret_t uiKeyboardSymbolsDispose(void) { - errorOk(); -} diff --git a/src/dusk/ui/dialog/keyboard/uikeyboardsymbols.h b/src/dusk/ui/dialog/keyboard/uikeyboardsymbols.h deleted file mode 100644 index 224b876b..00000000 --- a/src/dusk/ui/dialog/keyboard/uikeyboardsymbols.h +++ /dev/null @@ -1,37 +0,0 @@ -/** - * Copyright (c) 2026 Dominic Masters - * - * This software is released under the MIT License. - * https://opensource.org/licenses/MIT - */ - -#pragma once -#include "error/error.h" - -typedef struct { - -} uikeyboardsymbols_t; - -extern uikeyboardsymbols_t UI_KEYBOARD_SYMBOLS; - -/** - * Initializes the symbols keyboard layout. Currently a placeholder -- - * no behavior implemented yet. - * - * @return Any error that occurs. - */ -errorret_t uiKeyboardSymbolsInit(void); - -/** - * Draws the symbols keyboard layout. Currently a placeholder -- no-op. - * - * @return Any error that occurs. - */ -errorret_t uiKeyboardSymbolsDraw(void); - -/** - * Disposes of the symbols keyboard layout. - * - * @return Any error that occurs. - */ -errorret_t uiKeyboardSymbolsDispose(void); diff --git a/src/dusk/ui/focus/uifocus.c b/src/dusk/ui/focus/uifocus.c index c5fb9020..443ed035 100644 --- a/src/dusk/ui/focus/uifocus.c +++ b/src/dusk/ui/focus/uifocus.c @@ -118,9 +118,17 @@ void uiFocusMoveDirection( ) { if(m->direction != dir) continue; - uint8_t x = (uint8_t)(item->x + m->dx); - uint8_t y = (uint8_t)(item->y + m->dy); - uiFocusSetPosition(item, x, y); + // Widened to int16_t so a move off the left/top edge (e.g. x=0, + // dx=-1) stays negative here instead of wrapping straight to 255 via + // a uint8_t truncation - that would then land on `255 % cols` + // (column 3 of a 14-wide grid, not the intended last column) rather + // than wrapping to the row/column's actual last cell. + int16_t x = (int16_t)item->x + m->dx; + int16_t y = (int16_t)item->y + m->dy; + if(x < 0) x += item->cols; + if(y < 0) y += item->rows; + + uiFocusSetPosition(item, (uint8_t)x, (uint8_t)y); break; } } diff --git a/src/dusk/ui/widget/uimenu.c b/src/dusk/ui/widget/uimenu.c index ca118f43..4728295a 100644 --- a/src/dusk/ui/widget/uimenu.c +++ b/src/dusk/ui/widget/uimenu.c @@ -230,41 +230,97 @@ bool_t uiMenuFocusDirection( ) { assertNotNull(focusItem, "Focus item cannot be NULL"); assertNotNull(focusItem->user, "Focus item user cannot be NULL"); - if(direction != UI_FOCUS_DIRECTION_LEFT && direction != UI_FOCUS_DIRECTION_RIGHT) { - return false; - } - uimenu_t *menu = (uimenu_t *)focusItem->user; - uint8_t slot = focusItem->y * menu->columns + focusItem->x; - uint8_t index = uiMenuFocusSlotToIndex(menu, slot); - if(index == 0xFF) return false; - uimenuitem_t *item = &menu->items[index]; - bool_t right = direction == UI_FOCUS_DIRECTION_RIGHT; + if(direction == UI_FOCUS_DIRECTION_LEFT || direction == UI_FOCUS_DIRECTION_RIGHT) { + uint8_t slot = focusItem->y * menu->columns + focusItem->x; + uint8_t index = uiMenuFocusSlotToIndex(menu, slot); - if(item->type == UI_MENU_WIDGET_TYPE_SLIDER) { - if(right) { - uiSliderStepUp(&item->slider); - } else { - uiSliderStepDown(&item->slider); + if(index != 0xFF) { + uimenuitem_t *item = &menu->items[index]; + bool_t right = direction == UI_FOCUS_DIRECTION_RIGHT; + + if(item->type == UI_MENU_WIDGET_TYPE_SLIDER) { + if(right) { + uiSliderStepUp(&item->slider); + } else { + uiSliderStepDown(&item->slider); + } + return true; + } + + if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) { + uiCheckboxToggle(&item->checkbox); + return true; + } + + if(item->type == UI_MENU_WIDGET_TYPE_DROPDOWN) { + if(right) { + uiDropdownStepNext(&item->dropdown); + } else { + uiDropdownStepPrev(&item->dropdown); + } + return true; + } } - return true; } - if(item->type == UI_MENU_WIDGET_TYPE_CHECKBOX) { - uiCheckboxToggle(&item->checkbox); - return true; - } + return uiMenuFocusSkipBlanks(menu, focusItem, direction); +} - if(item->type == UI_MENU_WIDGET_TYPE_DROPDOWN) { - if(right) { - uiDropdownStepNext(&item->dropdown); - } else { - uiDropdownStepPrev(&item->dropdown); +bool_t uiMenuItemIsBlank(const uimenuitem_t *item) { + return + item->type == UI_MENU_WIDGET_TYPE_BUTTON && + item->button.label.text[0] == '\0'; +} + +bool_t uiMenuFocusSkipBlanks( + uimenu_t *menu, + const uifocusitem_t *focusItem, + const uifocusdirection_t direction +) { + int8_t dx = 0; + int8_t dy = 0; + for( + const uifocusdirmap_t *m = UI_FOCUS_DIR_MAP; + m->action != INPUT_ACTION_NULL; + m++ + ) { + if(m->direction != direction) continue; + dx = m->dx; + dy = m->dy; + break; + } + if(dx == 0 && dy == 0) return false; + + // Only one of dx/dy is ever non-zero (a direction moves along a single + // axis), so the number of distinct cells reachable before this would + // cycle back to the start is exactly that axis's length. + uint8_t attempts = dx != 0 ? menu->columns : focusItem->rows; + + int16_t x = focusItem->x; + int16_t y = focusItem->y; + + for(uint8_t i = 0; i < attempts; i++) { + x += dx; + y += dy; + if(x < 0) x += menu->columns; + if(y < 0) y += focusItem->rows; + x %= menu->columns; + y %= focusItem->rows; + + uint8_t slot = (uint8_t)y * menu->columns + (uint8_t)x; + uint8_t index = uiMenuFocusSlotToIndex(menu, slot); + // Treat a missing item (label/spacer slot) the same as a blank + // button - both get skipped over transparently. + if(index != 0xFF && !uiMenuItemIsBlank(&menu->items[index])) { + uiMenuSetPosition(menu, (uint8_t)x, (uint8_t)y); + return true; } - return true; } + // Every cell along this line is blank/missing - nothing to land on, + // fall through to the default single-step move. return false; } diff --git a/src/dusk/ui/widget/uimenu.h b/src/dusk/ui/widget/uimenu.h index c9a8c997..74a8defd 100644 --- a/src/dusk/ui/widget/uimenu.h +++ b/src/dusk/ui/widget/uimenu.h @@ -240,18 +240,55 @@ bool_t uiMenuFocusClosed(const uifocusitem_t *focusItem); /** * Internal focus callback - gives the focused item's widget a chance to - * handle LEFT/RIGHT itself (e.g. a slider adjusting its value) before - * falling back to the default cell-to-cell movement. + * handle LEFT/RIGHT itself (e.g. a slider adjusting its value); failing + * that, moves to the next non-blank cell in the given direction (see + * uiMenuFocusSkipBlanks) rather than the default single-step move, which + * would happily land on a deliberately-blank layout cell (e.g. a + * UI_MENU_WIDGET_TYPE_BUTTON with an empty label, used as filler). * * @param focusItem The active focus item; user field must point to uimenu_t. * @param direction The direction that was pressed or held. - * @returns True if the focused widget handled the direction. + * @returns True if handled (a widget adjusted itself, or focus moved to + * a non-blank cell); false to fall through to the default single-step + * move (only when every cell along this line is blank). */ bool_t uiMenuFocusDirection( const uifocusitem_t *focusItem, const uifocusdirection_t direction ); +/** + * True for a menu item that should be transparently skipped during + * d-pad navigation - a UI_MENU_WIDGET_TYPE_BUTTON with an empty label, + * used purely to hold a grid cell's position (the menu/focus grid has no + * concept of a row narrower than its column count, so short rows are + * padded with these instead). Labels/spacers are unaffected by this - + * they're already excluded from the focus grid entirely. + * + * @param item The item to check. + * @returns True if this item is blank filler. + */ +bool_t uiMenuItemIsBlank(const uimenuitem_t *item); + +/** + * Moves focusItem to the next cell in the given direction that isn't + * blank (see uiMenuItemIsBlank), wrapping within that row/column as many + * times as needed - so, from the menu's perspective, a blank cell simply + * isn't there. Gives up (returning false, so the default single-step + * move applies instead) only if every cell along that entire row/column + * is blank. + * + * @param menu The menu to move within. + * @param focusItem The active focus item; must belong to menu. + * @param direction The direction that was pressed or held. + * @returns True if a non-blank cell was found and focus moved there. + */ +bool_t uiMenuFocusSkipBlanks( + uimenu_t *menu, + const uifocusitem_t *focusItem, + const uifocusdirection_t direction +); + /** * Internal focus callback - forwards INPUT_ACTION_CANCEL to the menu's * cancel handler, if one is set.