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:
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 -
|
||||
|
||||
@@ -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);
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user