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);
|
item->battleForceAction.targetIndex = assetCutsceneReadU8(data, &offset);
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_MODAL:
|
case CUTSCENE_ITEM_TYPE_MODAL: {
|
||||||
assetCutsceneReadEmbeddedString(
|
assetCutsceneReadEmbeddedString(
|
||||||
data, &offset, item->modal.title, CUTSCENE_MODAL_TITLE_MAX_CHARS
|
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
|
// v1 only supports the message-only form: MODAL and MODAL_OPTIONS
|
||||||
// share this same tag with no separate discriminator, and there is
|
// share this same tag with no separate discriminator, and there is
|
||||||
// no native-callback registry yet to resolve an options callback.
|
// no native-callback registry yet to resolve an options callback.
|
||||||
assertTrue(
|
// Read into a local first (not inline in the check below) - on a
|
||||||
assetCutsceneReadU8(data, &offset) == 0,
|
// release build with DUSK_ASSERTIONS_FAKED, an assert's condition
|
||||||
"Cutscene MODAL item with options is not supported in file-based "
|
// is never evaluated at all, so a byte-consuming call inside one
|
||||||
"cutscenes yet - use MODAL_OPTIONS_MARKERS instead"
|
// 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;
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
case CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS: {
|
case CUTSCENE_ITEM_TYPE_MODAL_OPTIONS_MARKERS: {
|
||||||
assetCutsceneReadEmbeddedString(
|
assetCutsceneReadEmbeddedString(
|
||||||
@@ -410,8 +426,10 @@ errorret_t assetCutsceneLoaderSync(assetloading_t *loading) {
|
|||||||
}
|
}
|
||||||
assetLoaderErrorThrow(
|
assetLoaderErrorThrow(
|
||||||
loading,
|
loading,
|
||||||
"Cutscene item type %u is not supported in file-based cutscenes",
|
"Cutscene item type %u is not supported in file-based cutscenes "
|
||||||
(uint32_t)item->type
|
"(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}
|
target_sources(${DUSK_LIBRARY_TARGET_NAME}
|
||||||
PUBLIC
|
PUBLIC
|
||||||
uikeyboard.c
|
uikeyboard.c
|
||||||
uikeyboardqwerty.c
|
|
||||||
uikeyboardnumbers.c
|
|
||||||
uikeyboardsymbols.c
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -25,11 +25,153 @@
|
|||||||
|
|
||||||
uikeyboard_t UI_KEYBOARD;
|
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) {
|
void uiKeyboardFocusConfirm(void) {
|
||||||
uiMenuSetPosition(
|
uiMenuSetPosition(
|
||||||
&UI_KEYBOARD.menu,
|
&UI_KEYBOARD.menu,
|
||||||
UI_KEYBOARD_QWERTY_CONFIRM_INDEX % UI_KEYBOARD_QWERTY_COLUMNS,
|
UI_KEYBOARD_CONFIRM_INDEX % UI_KEYBOARD_COLUMNS,
|
||||||
UI_KEYBOARD_QWERTY_CONFIRM_INDEX / UI_KEYBOARD_QWERTY_COLUMNS
|
UI_KEYBOARD_CONFIRM_INDEX / UI_KEYBOARD_COLUMNS
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -110,7 +252,7 @@ void uiKeyboardMenuSelected(
|
|||||||
const uint8_t index,
|
const uint8_t index,
|
||||||
const uimenuitem_t *item
|
const uimenuitem_t *item
|
||||||
) {
|
) {
|
||||||
if(index == UI_KEYBOARD_QWERTY_CONFIRM_INDEX) {
|
if(index == UI_KEYBOARD_CONFIRM_INDEX) {
|
||||||
if(UI_KEYBOARD.trimmed) {
|
if(UI_KEYBOARD.trimmed) {
|
||||||
stringTrim(UI_KEYBOARD.text);
|
stringTrim(UI_KEYBOARD.text);
|
||||||
UI_KEYBOARD.textLabel.dirty = true;
|
UI_KEYBOARD.textLabel.dirty = true;
|
||||||
@@ -130,49 +272,66 @@ void uiKeyboardMenuSelected(
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(UI_KEYBOARD.cancel && index == UI_KEYBOARD_QWERTY_CANCEL_INDEX) {
|
if(UI_KEYBOARD.cancel && index == UI_KEYBOARD_CANCEL_INDEX) {
|
||||||
UI_KEYBOARD.confirmed = false;
|
UI_KEYBOARD.confirmed = false;
|
||||||
uiKeyboardClose();
|
uiKeyboardClose();
|
||||||
return;
|
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).
|
// Blank filler/reserved cell (includes an unused cancel/newline slot).
|
||||||
if(key[0] == '\0') return;
|
if(key[0] == '\0') return;
|
||||||
|
|
||||||
if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_BACKSPACE) == 0) {
|
if(stringCompare(key, UI_KEYBOARD_KEY_BACKSPACE) == 0) {
|
||||||
uiKeyboardBackspace();
|
uiKeyboardBackspace();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_SHIFT) == 0) {
|
if(stringCompare(key, UI_KEYBOARD_KEY_SHIFT) == 0) {
|
||||||
UI_KEYBOARD.shift = !UI_KEYBOARD.shift;
|
UI_KEYBOARD.shift = !UI_KEYBOARD.shift;
|
||||||
|
uiKeyboardUpdateDisplay(
|
||||||
|
UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_CAPS) == 0) {
|
if(stringCompare(key, UI_KEYBOARD_KEY_CAPS) == 0) {
|
||||||
UI_KEYBOARD.caps = !UI_KEYBOARD.caps;
|
UI_KEYBOARD.caps = !UI_KEYBOARD.caps;
|
||||||
|
uiKeyboardUpdateDisplay(
|
||||||
|
UI_KEYBOARD.items, UI_KEYBOARD.shift, UI_KEYBOARD.caps
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
char_t c;
|
char_t c;
|
||||||
if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_SPACE) == 0) {
|
if(stringCompare(key, UI_KEYBOARD_KEY_SPACE) == 0) {
|
||||||
c = ' ';
|
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 {
|
} 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)) {
|
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;
|
UI_KEYBOARD.open = true;
|
||||||
|
|
||||||
// Builds the full fixed grid, including blank filler at the
|
// Builds the full fixed grid, including blank filler at the
|
||||||
// newline/confirm/cancel reserved slots (see uikeyboardqwerty.h) -
|
// newline/confirm/cancel reserved slots - patched with real buttons
|
||||||
// patched with real buttons below where applicable.
|
// below where applicable. Letters start lowercase and digits
|
||||||
uiKeyboardQwertyBuildItems(UI_KEYBOARD.items);
|
// unshifted, matching caps/shift both being reset to false above.
|
||||||
|
uiKeyboardBuildItems(UI_KEYBOARD.items, false, false);
|
||||||
|
|
||||||
if(UI_KEYBOARD.lineCount > 1) {
|
if(UI_KEYBOARD.lineCount > 1) {
|
||||||
uiButtonInit(
|
uiButtonInit(
|
||||||
&UI_KEYBOARD.items[UI_KEYBOARD_QWERTY_NEWLINE_INDEX].button,
|
&UI_KEYBOARD.items[UI_KEYBOARD_NEWLINE_INDEX].button,
|
||||||
UI_KEYBOARD_KEY_NEWLINE
|
UI_KEYBOARD_KEY_NEWLINE
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
uiButtonInit(
|
uiButtonInit(
|
||||||
&UI_KEYBOARD.items[UI_KEYBOARD_QWERTY_CONFIRM_INDEX].button,
|
&UI_KEYBOARD.items[UI_KEYBOARD_CONFIRM_INDEX].button,
|
||||||
UI_KEYBOARD_CONFIRM_LABEL
|
UI_KEYBOARD_CONFIRM_LABEL
|
||||||
);
|
);
|
||||||
|
|
||||||
if(UI_KEYBOARD.cancel) {
|
if(UI_KEYBOARD.cancel) {
|
||||||
uiButtonInit(
|
uiButtonInit(
|
||||||
&UI_KEYBOARD.items[UI_KEYBOARD_QWERTY_CANCEL_INDEX].button,
|
&UI_KEYBOARD.items[UI_KEYBOARD_CANCEL_INDEX].button,
|
||||||
UI_KEYBOARD_CANCEL_LABEL
|
UI_KEYBOARD_CANCEL_LABEL
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -272,8 +432,8 @@ void uiKeyboardOpen(const uikeyboardopen_t *open) {
|
|||||||
&UI_KEYBOARD.menu, uiKeyboardMenuSelected, uiKeyboardMenuClosed, NULL
|
&UI_KEYBOARD.menu, uiKeyboardMenuSelected, uiKeyboardMenuClosed, NULL
|
||||||
);
|
);
|
||||||
uiMenuSetItems(
|
uiMenuSetItems(
|
||||||
&UI_KEYBOARD.menu, UI_KEYBOARD.items, UI_KEYBOARD_QWERTY_KEY_COUNT,
|
&UI_KEYBOARD.menu, UI_KEYBOARD.items, UI_KEYBOARD_KEY_COUNT,
|
||||||
UI_KEYBOARD_QWERTY_COLUMNS
|
UI_KEYBOARD_COLUMNS
|
||||||
);
|
);
|
||||||
|
|
||||||
// Back deletes a character rather than closing the dialog outright -
|
// Back deletes a character rather than closing the dialog outright -
|
||||||
|
|||||||
@@ -9,25 +9,65 @@
|
|||||||
#include "error/error.h"
|
#include "error/error.h"
|
||||||
#include "ui/widget/uilabel.h"
|
#include "ui/widget/uilabel.h"
|
||||||
#include "ui/widget/uimenu.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_TEXT_MAX 64
|
||||||
#define UI_KEYBOARD_TITLE_SPRITES_MAX UI_KEYBOARD_TITLE_TEXT_MAX
|
#define UI_KEYBOARD_TITLE_SPRITES_MAX UI_KEYBOARD_TITLE_TEXT_MAX
|
||||||
#define UI_KEYBOARD_TEXT_MAX 64
|
#define UI_KEYBOARD_TEXT_MAX 64
|
||||||
#define UI_KEYBOARD_TEXT_SPRITES_MAX UI_KEYBOARD_TEXT_MAX
|
#define UI_KEYBOARD_TEXT_SPRITES_MAX UI_KEYBOARD_TEXT_MAX
|
||||||
// The grid is a fixed size (see uikeyboardqwerty.h) - only the QWERTY
|
// The grid is a fixed size - only one layout exists today, so this is
|
||||||
// layout exists today, so this is just its key count.
|
// just its key count.
|
||||||
#define UI_KEYBOARD_ITEMS_MAX UI_KEYBOARD_QWERTY_KEY_COUNT
|
#define UI_KEYBOARD_ITEMS_MAX UI_KEYBOARD_KEY_COUNT
|
||||||
|
|
||||||
// Hardcoded for now - a uiModalOpen-style title/button text override is
|
// Hardcoded for now - a uiModalOpen-style title/button text override is
|
||||||
// planned (see uikeyboardopen_t) but not built yet. Patched into the
|
// planned (see uikeyboardopen_t) but not built yet. Patched into the
|
||||||
// grid's reserved slots (UI_KEYBOARD_QWERTY_CONFIRM_INDEX etc.) rather
|
// grid's reserved slots (UI_KEYBOARD_CONFIRM_INDEX etc.) rather than
|
||||||
// than appended, since the grid is a fixed physical-keyboard shape.
|
// appended, since the grid is a fixed shape.
|
||||||
#define UI_KEYBOARD_CONFIRM_LABEL "CONFIRM"
|
#define UI_KEYBOARD_CONFIRM_LABEL "CONFIRM"
|
||||||
#define UI_KEYBOARD_CANCEL_LABEL "CANCEL"
|
#define UI_KEYBOARD_CANCEL_LABEL "CANCEL"
|
||||||
// Only 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_TITLE_DEFAULT "ENTER YOUR TEXT"
|
||||||
|
|
||||||
#define UI_KEYBOARD_CONFIRM_QUESTION_MAX 64
|
#define UI_KEYBOARD_CONFIRM_QUESTION_MAX 64
|
||||||
@@ -145,11 +185,9 @@ typedef struct {
|
|||||||
bool_t confirmed;
|
bool_t confirmed;
|
||||||
|
|
||||||
// Persistent caps-lock toggle, and a one-shot shift consumed by the
|
// Persistent caps-lock toggle, and a one-shot shift consumed by the
|
||||||
// next letter typed - see uiKeyboardMenuSelected. A letter's effective
|
// next letter/digit/-/= typed - see uiKeyboardMenuSelected. Key labels
|
||||||
// case is caps != shift (matches physical keyboards: shift while caps
|
// are redrawn to match (see uiKeyboardUpdateDisplay) rather than
|
||||||
// is on gives lowercase). Key labels are always drawn uppercase
|
// staying static.
|
||||||
// regardless of this state - only the appended character's case
|
|
||||||
// changes.
|
|
||||||
bool_t caps;
|
bool_t caps;
|
||||||
bool_t shift;
|
bool_t shift;
|
||||||
} uikeyboard_t;
|
} uikeyboard_t;
|
||||||
@@ -166,8 +204,7 @@ errorret_t uiKeyboardInit(void);
|
|||||||
/**
|
/**
|
||||||
* Draws the on-screen keyboard: a semi-transparent black backdrop
|
* Draws the on-screen keyboard: a semi-transparent black backdrop
|
||||||
* covering the whole screen, then its own centered frame with the title,
|
* 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
|
* currently entered text, and the key grid. No-op when not open.
|
||||||
* not open.
|
|
||||||
*
|
*
|
||||||
* @return Any error that occurs.
|
* @return Any error that occurs.
|
||||||
*/
|
*/
|
||||||
@@ -284,3 +321,110 @@ void uiKeyboardMenuClosed(const uimenu_t *menu);
|
|||||||
* default pop.
|
* default pop.
|
||||||
*/
|
*/
|
||||||
bool_t uiKeyboardMenuCancel(const uimenu_t *menu);
|
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;
|
if(m->direction != dir) continue;
|
||||||
|
|
||||||
uint8_t x = (uint8_t)(item->x + m->dx);
|
// Widened to int16_t so a move off the left/top edge (e.g. x=0,
|
||||||
uint8_t y = (uint8_t)(item->y + m->dy);
|
// dx=-1) stays negative here instead of wrapping straight to 255 via
|
||||||
uiFocusSetPosition(item, x, y);
|
// 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;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+81
-25
@@ -230,41 +230,97 @@ bool_t uiMenuFocusDirection(
|
|||||||
) {
|
) {
|
||||||
assertNotNull(focusItem, "Focus item cannot be NULL");
|
assertNotNull(focusItem, "Focus item cannot be NULL");
|
||||||
assertNotNull(focusItem->user, "Focus item user 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;
|
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];
|
if(direction == UI_FOCUS_DIRECTION_LEFT || direction == UI_FOCUS_DIRECTION_RIGHT) {
|
||||||
bool_t right = 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(index != 0xFF) {
|
||||||
if(right) {
|
uimenuitem_t *item = &menu->items[index];
|
||||||
uiSliderStepUp(&item->slider);
|
bool_t right = direction == UI_FOCUS_DIRECTION_RIGHT;
|
||||||
} else {
|
|
||||||
uiSliderStepDown(&item->slider);
|
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) {
|
return uiMenuFocusSkipBlanks(menu, focusItem, direction);
|
||||||
uiCheckboxToggle(&item->checkbox);
|
}
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(item->type == UI_MENU_WIDGET_TYPE_DROPDOWN) {
|
bool_t uiMenuItemIsBlank(const uimenuitem_t *item) {
|
||||||
if(right) {
|
return
|
||||||
uiDropdownStepNext(&item->dropdown);
|
item->type == UI_MENU_WIDGET_TYPE_BUTTON &&
|
||||||
} else {
|
item->button.label.text[0] == '\0';
|
||||||
uiDropdownStepPrev(&item->dropdown);
|
}
|
||||||
|
|
||||||
|
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;
|
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
|
* Internal focus callback - gives the focused item's widget a chance to
|
||||||
* handle LEFT/RIGHT itself (e.g. a slider adjusting its value) before
|
* handle LEFT/RIGHT itself (e.g. a slider adjusting its value); failing
|
||||||
* falling back to the default cell-to-cell movement.
|
* 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 focusItem The active focus item; user field must point to uimenu_t.
|
||||||
* @param direction The direction that was pressed or held.
|
* @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(
|
bool_t uiMenuFocusDirection(
|
||||||
const uifocusitem_t *focusItem,
|
const uifocusitem_t *focusItem,
|
||||||
const uifocusdirection_t direction
|
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
|
* Internal focus callback - forwards INPUT_ACTION_CANCEL to the menu's
|
||||||
* cancel handler, if one is set.
|
* cancel handler, if one is set.
|
||||||
|
|||||||
Reference in New Issue
Block a user