Fix cutscene/focus bugs found on PSP hardware, rework keyboard layout

Cutscene loader: a MODAL item's optionCount byte was read directly
inside an assertTrue() condition. On the PSP release build
(DUSK_ASSERTIONS_FAKED), assertTrue expands to a no-op and never
evaluates its argument, so that read - and the offset advance it was
responsible for - silently never happened, desyncing every item after
it in the stream. Moved the read out to its own statement and replaced
the assert with a real error throw, since this is untrusted file
content.

uifocus: fixed a wraparound bug in uiFocusMoveDirection - moving left/up
from position 0 truncated to uint8_t before the modulo wrap (0-1 => 255,
then 255 % cols), landing on the wrong column instead of the row's last
one. Only ever visible on a grid wider than a handful of columns, which
nothing but the new keyboard has.

Keyboard: merged uikeyboardqwerty into uikeyboard (only one layout
exists, so the split no longer earned its keep) and deleted the unused
numbers/symbols placeholder files. Reshaped the grid to a tighter 11x5
layout with per-mode key tables (unshifted/caps/shift) instead of
computing case transforms, and fixed the reserved NEWLINE/CONFIRM/CANCEL
slot indices to match. Added shift-symbols for digits and -/=, and
uimenu's directional navigation now skips blank filler cells (wrapping
around a row/column as if they weren't there) instead of landing on
them - a generic fix in uimenu.c, not keyboard-specific, since nothing
else uses blank cells.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-27 19:26:07 -05:00
parent 709bd7be52
commit 00bfaf6360
13 changed files with 508 additions and 351 deletions
@@ -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
);
}
}
@@ -6,7 +6,4 @@
target_sources(${DUSK_LIBRARY_TARGET_NAME}
PUBLIC
uikeyboard.c
uikeyboardqwerty.c
uikeyboardnumbers.c
uikeyboardsymbols.c
)
+190 -30
View File
@@ -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 -
+160 -16
View File
@@ -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
);
@@ -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();
}
@@ -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);
@@ -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();
}
@@ -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);
@@ -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();
}
@@ -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);
+11 -3
View File
@@ -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;
}
}
+61 -5
View File
@@ -230,15 +230,13 @@ 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;
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(index == 0xFF) return false;
if(index != 0xFF) {
uimenuitem_t *item = &menu->items[index];
bool_t right = direction == UI_FOCUS_DIRECTION_RIGHT;
@@ -264,7 +262,65 @@ bool_t uiMenuFocusDirection(
}
return true;
}
}
}
return uiMenuFocusSkipBlanks(menu, focusItem, direction);
}
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;
}
}
// Every cell along this line is blank/missing - nothing to land on,
// fall through to the default single-step move.
return false;
}
+40 -3
View File
@@ -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.