Implement on-screen keyboard dialog with QWERTY layout

Builds out uikeyboard/uikeyboardqwerty from placeholders into a working
controller-navigable text entry dialog: title/current-text labels, a flat
QWERTY key grid (letters, space, backspace), and confirm/cancel buttons in
one combined menu so d-pad navigation flows across all of it.

uikeyboardopen_t configures each dialog: onInput/onKeyPress callbacks,
optional cancel button, maxLength (auto-focuses confirm once full),
lineCount for multi-line entry (adds a NEWLINE key, reserves vertical
space up front), a second "are you sure" uiConfirm step, and
trimmed/allowBlank validation on confirm.

Back now deletes the last character first, falling through to close only
when cancel is allowed and the buffer is empty - this needed a general
cancel-intercept callback added to uifocusitem_t/uimenu_t
(uiMenuSetCancelCallback), since the focus system previously only
supported popping or fully swallowing back.

Wired a temporary "LOAD GAME (TEST)" entry into the main menu with
console-logging test callbacks for interactive testing; marked TODO for
removal once the keyboard is wired up somewhere real.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-26 15:26:57 -05:00
parent e630827b34
commit 80f4348e21
11 changed files with 733 additions and 23 deletions
+315 -2
View File
@@ -6,16 +6,329 @@
*/
#include "uikeyboard.h"
#include "ui/widget/uiframe.h"
#include "ui/widget/uibutton.h"
#include "ui/dialog/uiconfirm.h"
#include "assert/assert.h"
#include "util/memory.h"
#include "util/string.h"
#include "util/math.h"
#include "display/screen/screen.h"
#include "display/color.h"
#include "display/text/font.h"
#include "display/spritebatch/spritebatch.h"
#include "display/texture/texture.h"
#include "display/shader/shaderunlit.h"
#define UI_KEYBOARD_BACKDROP_COLOR color4b(0, 0, 0, 160)
#define UI_KEYBOARD_KEY_WIDTH 20.0f
uikeyboard_t UI_KEYBOARD;
void uiKeyboardFocusConfirm(void) {
uint8_t confirmIndex = UI_KEYBOARD.menu.itemCount - (UI_KEYBOARD.cancel ? 2 : 1);
uiMenuSetPosition(
&UI_KEYBOARD.menu,
confirmIndex % UI_KEYBOARD.menu.columns,
confirmIndex / UI_KEYBOARD.menu.columns
);
}
bool_t uiKeyboardTextIsBlank(void) {
for(const char_t *c = UI_KEYBOARD.text; *c != '\0'; c++) {
if(!stringIsWhitespace(*c)) return false;
}
return true;
}
void uiKeyboardAppendChar(const char_t c) {
size_t length = strlen(UI_KEYBOARD.text);
if(length >= UI_KEYBOARD.maxLength) return;
if(c == '\n') {
uint8_t lines = 1;
for(size_t i = 0; i < length; i++) {
if(UI_KEYBOARD.text[i] == '\n') lines++;
}
if(lines >= UI_KEYBOARD.lineCount) return;
}
UI_KEYBOARD.text[length] = c;
UI_KEYBOARD.text[length + 1] = '\0';
UI_KEYBOARD.textLabel.dirty = true;
uiLabelRebuffer(&UI_KEYBOARD.textLabel);
// Nothing more can be typed - jump focus to confirm so accepting is a
// single button press away.
if(length + 1 >= UI_KEYBOARD.maxLength) uiKeyboardFocusConfirm();
}
void uiKeyboardBackspace(void) {
size_t length = strlen(UI_KEYBOARD.text);
if(length == 0) return;
UI_KEYBOARD.text[length - 1] = '\0';
UI_KEYBOARD.textLabel.dirty = true;
uiLabelRebuffer(&UI_KEYBOARD.textLabel);
}
void uiKeyboardMenuClosed(const uimenu_t *menu) {
if(UI_KEYBOARD.onInput != NULL) {
UI_KEYBOARD.onInput(
UI_KEYBOARD.text, UI_KEYBOARD.confirmed, UI_KEYBOARD.user
);
}
}
bool_t uiKeyboardMenuCancel(const uimenu_t *menu) {
if(strlen(UI_KEYBOARD.text) > 0) {
uiKeyboardBackspace();
return true;
}
// Nothing left to delete - close, but only if this dialog is even
// allowed to be cancelled. Closed explicitly (rather than returning
// false to fall through to uiFocusUpdate's default pop) since that
// path only pops the focus stack - it never clears UI_KEYBOARD.open,
// which would leave uiKeyboardDraw drawing a focus-less dialog forever
// and the next uiKeyboardOpen() tripping its already-open assert.
if(!UI_KEYBOARD.cancel) return true;
UI_KEYBOARD.confirmed = false;
uiKeyboardClose();
return true;
}
void uiKeyboardConfirmSecondaryResult(const bool_t result, void *user) {
if(!result) return;
UI_KEYBOARD.confirmed = true;
uiKeyboardClose();
}
void uiKeyboardMenuSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
) {
uint8_t confirmIndex = menu->itemCount - (UI_KEYBOARD.cancel ? 2 : 1);
if(index == confirmIndex) {
if(UI_KEYBOARD.trimmed) {
stringTrim(UI_KEYBOARD.text);
UI_KEYBOARD.textLabel.dirty = true;
uiLabelRebuffer(&UI_KEYBOARD.textLabel);
}
if(!UI_KEYBOARD.allowBlank && uiKeyboardTextIsBlank()) return;
if(UI_KEYBOARD.confirm) {
uiConfirmOpen(
UI_KEYBOARD.confirmQuestion, uiKeyboardConfirmSecondaryResult, NULL
);
return;
}
UI_KEYBOARD.confirmed = true;
uiKeyboardClose();
return;
}
if(UI_KEYBOARD.cancel && index == menu->itemCount - 1) {
UI_KEYBOARD.confirmed = false;
uiKeyboardClose();
return;
}
const char_t *key = item->button.label.text;
if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_BACKSPACE) == 0) {
uiKeyboardBackspace();
return;
}
char_t c;
if(stringCompare(key, UI_KEYBOARD_QWERTY_KEY_SPACE) == 0) {
c = ' ';
} else if(
UI_KEYBOARD.lineCount > 1 &&
stringCompare(key, UI_KEYBOARD_KEY_NEWLINE) == 0
) {
c = '\n';
} else {
c = key[0];
}
if(UI_KEYBOARD.onKeyPress != NULL && !UI_KEYBOARD.onKeyPress(c, UI_KEYBOARD.user)) {
return;
}
uiKeyboardAppendChar(c);
}
errorret_t uiKeyboardInit(void) {
// Nothing to initialize yet -- uikeyboard_t is currently a
// placeholder with no fields.
memoryZero(&UI_KEYBOARD, sizeof(uikeyboard_t));
uiLabelInit(
&UI_KEYBOARD.titleLabel, UI_KEYBOARD.titleText,
UI_KEYBOARD.titleSprites, UI_KEYBOARD_TITLE_SPRITES_MAX
);
uiLabelInit(
&UI_KEYBOARD.textLabel, UI_KEYBOARD.text,
UI_KEYBOARD.textSprites, UI_KEYBOARD_TEXT_SPRITES_MAX
);
errorOk();
}
bool_t uiKeyboardIsOpen(void) {
return UI_KEYBOARD.open;
}
void uiKeyboardOpen(const uikeyboardopen_t *open) {
assertNotNull(open, "Open parameters cannot be NULL");
assertFalse(UI_KEYBOARD.open, "Keyboard is already open");
assertTrue(
open->maxLength <= UI_KEYBOARD_TEXT_MAX - 1,
"maxLength exceeds UI_KEYBOARD_TEXT_MAX"
);
assertTrue(
open->lineCount <= UI_KEYBOARD_LINE_COUNT_MAX,
"lineCount exceeds UI_KEYBOARD_LINE_COUNT_MAX"
);
stringCopy(
UI_KEYBOARD.titleText, UI_KEYBOARD_TITLE_DEFAULT,
UI_KEYBOARD_TITLE_TEXT_MAX - 1
);
UI_KEYBOARD.titleLabel.dirty = true;
uiLabelRebuffer(&UI_KEYBOARD.titleLabel);
UI_KEYBOARD.text[0] = '\0';
UI_KEYBOARD.textLabel.dirty = true;
uiLabelRebuffer(&UI_KEYBOARD.textLabel);
UI_KEYBOARD.onInput = open->onInput;
UI_KEYBOARD.onKeyPress = open->onKeyPress;
UI_KEYBOARD.cancel = open->cancel;
UI_KEYBOARD.maxLength = open->maxLength == 0 ?
UI_KEYBOARD_TEXT_MAX - 1 : open->maxLength;
UI_KEYBOARD.lineCount = open->lineCount == 0 ? 1 : open->lineCount;
UI_KEYBOARD.confirm = open->confirm;
stringCopy(
UI_KEYBOARD.confirmQuestion,
open->confirmLabel != NULL ?
open->confirmLabel : UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT,
UI_KEYBOARD_CONFIRM_QUESTION_MAX - 1
);
UI_KEYBOARD.trimmed = open->trimmed;
UI_KEYBOARD.allowBlank = open->allowBlank;
UI_KEYBOARD.user = open->user;
UI_KEYBOARD.confirmed = false;
UI_KEYBOARD.open = true;
uint8_t index = uiKeyboardQwertyBuildItems(UI_KEYBOARD.items);
if(UI_KEYBOARD.lineCount > 1) {
UI_KEYBOARD.items[index].type = UI_MENU_WIDGET_TYPE_BUTTON;
uiButtonInit(&UI_KEYBOARD.items[index].button, UI_KEYBOARD_KEY_NEWLINE);
index++;
}
UI_KEYBOARD.items[index].type = UI_MENU_WIDGET_TYPE_BUTTON;
uiButtonInit(&UI_KEYBOARD.items[index].button, UI_KEYBOARD_CONFIRM_LABEL);
index++;
if(UI_KEYBOARD.cancel) {
UI_KEYBOARD.items[index].type = UI_MENU_WIDGET_TYPE_BUTTON;
uiButtonInit(&UI_KEYBOARD.items[index].button, UI_KEYBOARD_CANCEL_LABEL);
index++;
}
uiMenuInit(
&UI_KEYBOARD.menu, uiKeyboardMenuSelected, uiKeyboardMenuClosed, NULL
);
uiMenuSetItems(
&UI_KEYBOARD.menu, UI_KEYBOARD.items, index, UI_KEYBOARD_QWERTY_COLUMNS
);
// Back deletes a character rather than closing the dialog outright -
// see uiKeyboardMenuCancel.
uiMenuSetCancelCallback(&UI_KEYBOARD.menu, uiKeyboardMenuCancel);
uiMenuOpen(&UI_KEYBOARD.menu);
}
void uiKeyboardClose(void) {
if(!UI_KEYBOARD.open) return;
UI_KEYBOARD.open = false;
if(uiMenuIsActive(&UI_KEYBOARD.menu)) {
uiMenuClose(&UI_KEYBOARD.menu);
} else {
uiKeyboardMenuClosed(NULL);
}
}
errorret_t uiKeyboardDraw(void) {
if(!UI_KEYBOARD.open) errorOk();
spritebatchsprite_t backdropSprite = {
.min = { 0.0f, 0.0f, 0.0f },
.max = { (float_t)SCREEN.width, (float_t)SCREEN.height, 0.0f },
.uvMin = { 0.0f, 0.0f },
.uvMax = { 1.0f, 1.0f }
};
shadermaterial_t backdropMaterial = {
.unlit = {
.color = UI_KEYBOARD_BACKDROP_COLOR,
.texture = &TEXTURE_WHITE
}
};
errorChain(
spriteBatchBuffer(&backdropSprite, 1, &SHADER_UNLIT, backdropMaterial)
);
errorChain(spriteBatchFlush());
float_t rowHeight = (float_t)FONT_DEFAULT.tileset->tileHeight;
// When multi-line, reserve the full lineCount rows up front so the
// dialog doesn't resize as newlines are typed - otherwise fall back to
// the label's own measured height (floored at one row for an empty
// string, which measures to zero).
float_t textHeight = UI_KEYBOARD.lineCount > 1 ?
(float_t)UI_KEYBOARD.lineCount * rowHeight :
mathMax(rowHeight, (float_t)UI_KEYBOARD.textLabel.height);
uint8_t rows = (UI_KEYBOARD.menu.itemCount + UI_KEYBOARD.menu.columns - 1) /
UI_KEYBOARD.menu.columns;
float_t contentWidth = mathMax(
mathMax((float_t)UI_KEYBOARD.titleLabel.width, (float_t)UI_KEYBOARD.textLabel.width),
(float_t)UI_KEYBOARD.menu.columns * UI_KEYBOARD_KEY_WIDTH
);
float_t width = contentWidth + (UI_FRAME_START_X * 2);
float_t height = (UI_FRAME_START_Y * 2)
+ rowHeight + UI_FRAME_PADDING_Y
+ textHeight + UI_FRAME_PADDING_Y
+ ((float_t)rows * rowHeight);
float_t x = (float_t)SCREEN.scanX +
((float_t)SCREEN.scanWidth - width) * 0.5f;
float_t y = (float_t)SCREEN.scanY +
((float_t)SCREEN.scanHeight - height) * 0.5f;
errorChain(uiFrameDraw(x, y, width, height));
float_t contentX = x + UI_FRAME_START_X;
float_t contentY = y + UI_FRAME_START_Y;
uiLabelSetX(&UI_KEYBOARD.titleLabel, contentX);
uiLabelSetY(&UI_KEYBOARD.titleLabel, contentY);
errorChain(uiLabelRender(&UI_KEYBOARD.titleLabel, COLOR_WHITE));
float_t textY = contentY + rowHeight + UI_FRAME_PADDING_Y;
uiLabelSetX(&UI_KEYBOARD.textLabel, contentX);
uiLabelSetY(&UI_KEYBOARD.textLabel, textY);
errorChain(uiLabelRender(&UI_KEYBOARD.textLabel, COLOR_WHITE));
float_t menuY = textY + textHeight + UI_FRAME_PADDING_Y;
errorChain(uiMenuDraw(&UI_KEYBOARD.menu, contentX, menuY, contentWidth, rowHeight));
errorChain(spriteBatchFlush());
errorOk();
}
+239 -3
View File
@@ -7,31 +7,267 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uilabel.h"
#include "ui/widget/uimenu.h"
#include "ui/dialog/keyboard/uikeyboardqwerty.h"
#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
// Room for the active layout's keys plus the newline/confirm/cancel
// buttons uiKeyboard appends itself - only the QWERTY layout exists
// today.
#define UI_KEYBOARD_ITEMS_MAX (UI_KEYBOARD_QWERTY_KEY_COUNT + 3)
// Hardcoded for now - a uiModalOpen-style title/button text override is
// planned (see uikeyboardopen_t) but not built yet.
#define UI_KEYBOARD_TITLE_DEFAULT "ENTER YOUR TEXT"
#define UI_KEYBOARD_CONFIRM_LABEL "CONFIRM"
#define UI_KEYBOARD_CANCEL_LABEL "CANCEL"
// Only added to the grid when uikeyboardopen_t.lineCount > 1.
#define UI_KEYBOARD_KEY_NEWLINE "NEWLINE"
#define UI_KEYBOARD_CONFIRM_QUESTION_MAX 64
// Used when uikeyboardopen_t.confirmLabel is NULL.
#define UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT "Is this correct?"
// Maximum number of lines uikeyboardopen_t.lineCount may request.
#define UI_KEYBOARD_LINE_COUNT_MAX 4
/**
* Callback invoked once the keyboard dialog is dismissed.
*
* @param text The entered text. Only meaningful when confirmed is true;
* points at UI_KEYBOARD's own buffer, so copy it out if it needs to
* outlive the next uiKeyboardOpen call.
* @param confirmed True if the confirm button was picked, false if
* cancelled.
* @param user Arbitrary pointer passed via uikeyboardopen_t.user.
*/
typedef void (*uikeyboardinputcallback_t)(
const char_t *text,
const bool_t confirmed,
void *user
);
/**
* Callback invoked before a key press is applied, giving the caller a
* chance to reject it - e.g. an email field rejecting "%".
*
* @param c The character about to be entered (SPACE included, DEL/
* confirm/cancel excluded - those aren't text being entered).
* @param user Arbitrary pointer passed via uikeyboardopen_t.user.
* @returns True to allow the character, false to reject it (ignored,
* nothing is entered).
*/
typedef bool_t (*uikeyboardkeypresscallback_t)(const char_t c, void *user);
/**
* Parameters for uiKeyboardOpen. Expect this to grow (e.g. title/button
* text overrides) as more dialogs start using the keyboard.
*/
typedef struct {
// Fired once the dialog is dismissed, confirmed or cancelled. May be
// NULL.
uikeyboardinputcallback_t onInput;
// Fired for each key press before it's applied. May be NULL to allow
// everything.
uikeyboardkeypresscallback_t onKeyPress;
// Whether the cancel button is shown, and whether pressing back with
// no text entered closes the dialog - see uiKeyboardMenuCancel.
bool_t cancel;
// Maximum length of the entered text, in characters excluding the
// null terminator. 0 defaults to the buffer's full capacity
// (UI_KEYBOARD_TEXT_MAX - 1). Must not exceed that.
size_t maxLength;
// When true, picking confirm opens a second uiConfirm "are you sure"
// dialog (see confirmLabel) on top of the keyboard before actually
// closing it - picking Cancel there returns to the keyboard with the
// text untouched.
bool_t confirm;
// Question text/locale message ID for the second confirm dialog when
// confirm is true; copied internally, safe to be transient. NULL uses
// UI_KEYBOARD_CONFIRM_QUESTION_DEFAULT. Ignored when confirm is false.
const char_t *confirmLabel;
// If true, leading/trailing whitespace is trimmed from the entered
// text when confirm is pressed, before it's checked against
// allowBlank or passed to onInput.
bool_t trimmed;
// If true, a blank result is accepted when confirm is pressed - blank
// means an empty string, or (when trimmed is false) a string of only
// spaces. When false, pressing confirm does nothing while the text is
// blank - see uiKeyboardTextIsBlank.
bool_t allowBlank;
// Maximum number of lines the entered text can span, from 1 to
// UI_KEYBOARD_LINE_COUNT_MAX. 0 defaults to 1. When > 1, a NEWLINE key
// is added to the grid, capped at inserting lineCount - 1 newlines.
uint8_t lineCount;
// Arbitrary pointer passed to onInput/onKeyPress.
void *user;
} uikeyboardopen_t;
typedef struct {
uilabel_t titleLabel;
char_t titleText[UI_KEYBOARD_TITLE_TEXT_MAX];
spritebatchsprite_t titleSprites[UI_KEYBOARD_TITLE_SPRITES_MAX];
uilabel_t textLabel;
char_t text[UI_KEYBOARD_TEXT_MAX];
spritebatchsprite_t textSprites[UI_KEYBOARD_TEXT_SPRITES_MAX];
uimenu_t menu;
uimenuitem_t items[UI_KEYBOARD_ITEMS_MAX];
char_t confirmQuestion[UI_KEYBOARD_CONFIRM_QUESTION_MAX];
uikeyboardinputcallback_t onInput;
uikeyboardkeypresscallback_t onKeyPress;
void *user;
size_t maxLength;
uint8_t lineCount;
bool_t cancel;
bool_t confirm;
bool_t trimmed;
bool_t allowBlank;
bool_t open;
bool_t confirmed;
} uikeyboard_t;
extern uikeyboard_t UI_KEYBOARD;
/**
* Initializes the on-screen keyboard. Currently a placeholder -- no
* behavior implemented yet.
* Initializes the on-screen keyboard.
*
* @return Any error that occurs.
*/
errorret_t uiKeyboardInit(void);
/**
* Draws the on-screen keyboard. Currently a placeholder -- no-op.
* 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.
*
* @return Any error that occurs.
*/
errorret_t uiKeyboardDraw(void);
/**
* Returns true when the keyboard dialog is currently open.
*
* @returns True if open.
*/
bool_t uiKeyboardIsOpen(void);
/**
* Opens the on-screen keyboard with an empty text buffer.
*
* @param open Parameters for this dialog; copied internally, safe to be
* transient. Cannot be NULL.
*/
void uiKeyboardOpen(const uikeyboardopen_t *open);
/**
* Closes the keyboard dialog, invoking the result callback set by
* uiKeyboardOpen as cancelled (confirmed=false) unless the confirm
* button was what triggered this close. No-op when already closed.
*/
void uiKeyboardClose(void);
/**
* Disposes of the on-screen keyboard.
*
* @return Any error that occurs.
*/
errorret_t uiKeyboardDispose(void);
/**
* Appends a character to the currently entered text, silently ignoring
* it once UI_KEYBOARD.maxLength is reached, or (for '\n') once the text
* already has UI_KEYBOARD.lineCount - 1 newlines. Does not consult
* onKeyPress - that happens in uiKeyboardMenuSelected before this is
* called. Moves focus to the confirm button once this append fills the
* text to maxLength - see uiKeyboardFocusConfirm.
*
* @param c The character to append.
*/
void uiKeyboardAppendChar(const char_t c);
/**
* Moves the menu's focus to the confirm button.
*/
void uiKeyboardFocusConfirm(void);
/**
* Returns whether the currently entered text counts as blank for
* UI_KEYBOARD.allowBlank purposes - an empty string, or (when
* UI_KEYBOARD.trimmed is false, so whitespace-only text was never
* reduced to empty) a string of only whitespace.
*
* @returns True if blank.
*/
bool_t uiKeyboardTextIsBlank(void);
/**
* Removes the last character of the currently entered text. No-op when
* already empty.
*/
void uiKeyboardBackspace(void);
/**
* Internal menu callback - routes a picked key/confirm/cancel item to
* uiKeyboardAppendChar/uiKeyboardBackspace/uiKeyboardClose, consulting
* onKeyPress before a character key is applied. When UI_KEYBOARD.confirm
* is set, picking confirm opens a second uiConfirm dialog (see
* uiKeyboardConfirmSecondaryResult) instead of closing immediately.
*
* @param menu The keyboard's menu.
* @param index Index of the picked item.
* @param item The picked item.
*/
void uiKeyboardMenuSelected(
const uimenu_t *menu,
const uint8_t index,
const uimenuitem_t *item
);
/**
* Result callback for the second "are you sure" uiConfirm dialog opened
* when UI_KEYBOARD.confirm is set - see uiKeyboardMenuSelected.
*
* @param result True if Confirm was picked there, false if Cancel.
* @param user Unused.
*/
void uiKeyboardConfirmSecondaryResult(const bool_t result, void *user);
/**
* Internal menu callback - fires the result callback set by
* uiKeyboardOpen once the menu finishes closing.
*
* @param menu Unused; matches uimenuclosedcallback_t.
*/
void uiKeyboardMenuClosed(const uimenu_t *menu);
/**
* Internal menu cancel callback (see uiMenuSetCancelCallback): back
* deletes the last entered character; with nothing left to delete, it
* closes the dialog as cancelled via uiKeyboardClose, but only when
* UI_KEYBOARD.cancel allows it - otherwise back is swallowed and does
* nothing.
*
* @param menu The keyboard's menu.
* @returns Always true - this always fully handles back itself
* (backspace, explicit close, or swallow), never falling through to the
* default pop.
*/
bool_t uiKeyboardMenuCancel(const uimenu_t *menu);
+21 -4
View File
@@ -6,17 +6,34 @@
*/
#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.
static const char_t *const UI_KEYBOARD_QWERTY_KEYS[UI_KEYBOARD_QWERTY_KEY_COUNT] = {
"Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P",
"A", "S", "D", "F", "G", "H", "J", "K", "L",
"Z", "X", "C", "V", "B", "N", "M",
UI_KEYBOARD_QWERTY_KEY_SPACE, UI_KEYBOARD_QWERTY_KEY_BACKSPACE
};
errorret_t uiKeyboardQwertyInit(void) {
// Nothing to initialize yet -- uikeyboardqwerty_t is currently a
// placeholder with no fields.
errorOk();
}
errorret_t uiKeyboardQwertyDraw(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) {
+27 -6
View File
@@ -7,6 +7,20 @@
#pragma once
#include "error/error.h"
#include "ui/widget/uimenu.h"
// 26 letters plus SPACE and DEL, laid out as one flat columns-wide grid
// rather than the staggered rows a physical QWERTY keyboard uses - the
// menu/focus grid only supports uniform columns today, so exact row
// staggering is left for later.
#define UI_KEYBOARD_QWERTY_COLUMNS 10
#define UI_KEYBOARD_QWERTY_KEY_COUNT 28
// Labels uiKeyboard checks for by pointer/string compare (see
// uiKeyboardQwertyBuildItems) to tell the space/backspace keys apart from
// an ordinary single-letter key.
#define UI_KEYBOARD_QWERTY_KEY_SPACE "SPACE"
#define UI_KEYBOARD_QWERTY_KEY_BACKSPACE "DEL"
typedef struct {
@@ -15,22 +29,29 @@ typedef struct {
extern uikeyboardqwerty_t UI_KEYBOARD_QWERTY;
/**
* Initializes the QWERTY keyboard layout. Currently a placeholder --
* no behavior implemented yet.
* 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);
/**
* Draws the QWERTY keyboard layout. Currently a placeholder -- no-op.
* Builds the QWERTY layout's key buttons (26 letters, then SPACE and
* DEL) into items, starting at index 0, one UI_MENU_WIDGET_TYPE_BUTTON
* per key. Each button's label is either a single uppercase letter or the
* literal UI_KEYBOARD_QWERTY_KEY_SPACE/UI_KEYBOARD_QWERTY_KEY_BACKSPACE -
* uiKeyboard reads a selected item's own label back to know which key it
* was, rather than this function returning a separate mapping.
*
* @return Any error that occurs.
* @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).
*/
errorret_t uiKeyboardQwertyDraw(void);
uint8_t uiKeyboardQwertyBuildItems(uimenuitem_t *items);
/**
* Disposes of the QWERTY keyboard layout.
* Disposes of the QWERTY keyboard layout. Currently a no-op.
*
* @return Any error that occurs.
*/
+1
View File
@@ -153,6 +153,7 @@ void uiFocusUpdate(void) {
}
if(inputPressed(INPUT_ACTION_CANCEL)) {
if(item->cancel != NULL && item->cancel(item)) return;
if(!item->disableBack) uiFocusPop();
return;
}
+10 -1
View File
@@ -53,10 +53,19 @@ struct uifocusitem_s {
uifocusitemcallback_t changed;
uifocusitemcallback_t closed;
uifocusitemdirectioncallback_t direction;
// Called when INPUT_ACTION_CANCEL is pressed, before disableBack/the
// default pop is applied; may be NULL. Returns true if fully handled
// (e.g. it did something else instead, or popped itself), which skips
// the default pop; false to fall through to the default disableBack/
// pop behavior below.
uifocusitemcallback_t cancel;
void *user;
// When true, INPUT_ACTION_CANCEL is ignored while this item is the
// topmost focus item - it does not pop. Does not affect a
// programmatic uiFocusPop()/uiFocusPopItem() call.
// programmatic uiFocusPop()/uiFocusPopItem() call. Only consulted
// when cancel is unset or returns false.
bool_t disableBack;
};
+36
View File
@@ -16,11 +16,15 @@
#include "locale/localemanager.h"
#include "asset/loader/locale/assetlocaleloader.h"
#include "ui/dialog/uiconfirm.h"
#include "ui/dialog/keyboard/uikeyboard.h"
#include "scene/mainmenu/scenemainmenu.h"
#include "console/console.h"
#define UI_MAIN_MENU_INDEX_START_GAME 0
#define UI_MAIN_MENU_INDEX_OPTIONS 1
#define UI_MAIN_MENU_INDEX_QUIT 2
// TODO: temporary - see UI_MAIN_MENU_ITEM_COUNT in uimainmenu.h.
#define UI_MAIN_MENU_INDEX_LOAD_GAME_TEST 3
uimainmenu_t UI_MAIN_MENU;
@@ -28,6 +32,22 @@ void uiMainMenuQuitConfirmed(const bool_t result, void *user) {
if(result) ENGINE.running = false;
}
bool_t uiMainMenuKeyboardTestKeyPress(const char_t c, void *user) {
consolePrint("uiKeyboard onKeyPress: '%c'", c);
return true;
}
void uiMainMenuKeyboardTestInput(
const char_t *text,
const bool_t confirmed,
void *user
) {
consolePrint(
"uiKeyboard onInput: confirmed=%s text='%s'",
confirmed ? "true" : "false", text
);
}
void uiMainMenuSelected(
const uimenu_t *menu,
const uint8_t index,
@@ -48,6 +68,20 @@ void uiMainMenuSelected(
uiConfirmOpen("main_menu.quit_confirm", uiMainMenuQuitConfirmed, NULL);
break;
case UI_MAIN_MENU_INDEX_LOAD_GAME_TEST: {
uikeyboardopen_t open = {
.onInput = uiMainMenuKeyboardTestInput,
.onKeyPress = uiMainMenuKeyboardTestKeyPress,
.cancel = false,
.confirm = true,
.user = NULL,
.maxLength = 6,
.lineCount = 2
};
uiKeyboardOpen(&open);
break;
}
default:
break;
}
@@ -84,6 +118,8 @@ errorret_t uiMainMenuInit(void) {
MENU_BUTTON(UI_MAIN_MENU.startGameLabel);
MENU_BUTTON(UI_MAIN_MENU.optionsLabel);
MENU_BUTTON(UI_MAIN_MENU.quitLabel);
// TODO: temporary - see UI_MAIN_MENU_ITEM_COUNT in uimainmenu.h.
MENU_BUTTON("LOAD GAME (TEST)");
MENU_END(UI_MAIN_MENU.items, 1);
// The main menu is the root screen once past the initial boot check -
+31 -2
View File
@@ -9,9 +9,12 @@
#include "error/error.h"
#include "ui/widget/uimenu.h"
#define UI_MAIN_MENU_ITEM_COUNT 3
// TODO: item count/height bumped for the temporary "LOAD GAME (TEST)"
// keyboard test entry in uiMainMenuSelected - drop back to 3/160 once
// the keyboard is wired up somewhere real and this is removed.
#define UI_MAIN_MENU_ITEM_COUNT 4
#define UI_MAIN_MENU_WIDTH 200.0f
#define UI_MAIN_MENU_HEIGHT 160.0f
#define UI_MAIN_MENU_HEIGHT 200.0f
#define UI_MAIN_MENU_LABEL_MAX 32
typedef struct {
@@ -49,3 +52,29 @@ errorret_t uiMainMenuDraw(void);
* @return Any error that occurs.
*/
errorret_t uiMainMenuDispose(void);
// TODO: temporary keyboard test hooks - see UI_MAIN_MENU_INDEX_LOAD_GAME_TEST
// in uimainmenu.c. Remove once the keyboard is wired up somewhere real.
/**
* Test onKeyPress callback - console-prints every key press and allows
* all of them.
*
* @param c The character about to be entered.
* @param user Unused.
* @returns Always true.
*/
bool_t uiMainMenuKeyboardTestKeyPress(const char_t c, void *user);
/**
* Test onInput callback - console-prints the final result.
*
* @param text The entered text.
* @param confirmed Whether confirm (vs cancel) was picked.
* @param user Unused.
*/
void uiMainMenuKeyboardTestInput(
const char_t *text,
const bool_t confirmed,
void *user
);
+5 -5
View File
@@ -101,6 +101,11 @@ uielement_t UI_ELEMENTS[] = {
.update = uiTextboxMiniListUpdate,
.draw = uiTextboxMiniListDraw
},
{
.init = uiKeyboardInit,
.draw = uiKeyboardDraw,
.dispose = uiKeyboardDispose
},
{
.init = uiModalInit,
.draw = uiModalDraw,
@@ -110,11 +115,6 @@ uielement_t UI_ELEMENTS[] = {
.init = uiConfirmInit,
.dispose = uiConfirmDispose
},
{
.init = uiKeyboardInit,
.draw = uiKeyboardDraw,
.dispose = uiKeyboardDispose
},
// ===========================================================================
// Overlayed components, can even outdraw all UI things.
+17
View File
@@ -68,6 +68,7 @@ void uiMenuOpen(uimenu_t *menu) {
menu
);
menu->focusItem->disableBack = menu->disableBack;
menu->focusItem->cancel = menu->cancel != NULL ? uiMenuFocusCancel : NULL;
}
void uiMenuClose(uimenu_t *menu) {
@@ -83,6 +84,14 @@ void uiMenuSetDisableBack(uimenu_t *menu, const bool_t disableBack) {
if(menu->focusItem != NULL) menu->focusItem->disableBack = disableBack;
}
void uiMenuSetCancelCallback(uimenu_t *menu, uimenucancelcallback_t cancel) {
assertNotNull(menu, "Menu cannot be NULL");
menu->cancel = cancel;
if(menu->focusItem != NULL) {
menu->focusItem->cancel = cancel != NULL ? uiMenuFocusCancel : NULL;
}
}
bool_t uiMenuIsActive(const uimenu_t *menu) {
assertNotNull(menu, "Menu cannot be NULL");
return menu->focusItem != NULL;
@@ -259,6 +268,14 @@ bool_t uiMenuFocusDirection(
return false;
}
bool_t uiMenuFocusCancel(const uifocusitem_t *focusItem) {
assertNotNull(focusItem, "Focus item cannot be NULL");
assertNotNull(focusItem->user, "Focus item user cannot be NULL");
uimenu_t *menu = (uimenu_t *)focusItem->user;
if(menu->cancel == NULL) return false;
return menu->cancel(menu);
}
uint8_t uiMenuFocusableCount(const uimenu_t *menu) {
assertNotNull(menu, "Menu cannot be NULL");
uint8_t count = 0;
+31
View File
@@ -55,6 +55,16 @@ typedef void (*uimenuchangedcallback_t)(
typedef void (*uimenuclosedcallback_t)(const uimenu_t *menu);
/**
* Callback invoked when INPUT_ACTION_CANCEL is pressed while the menu is
* focused - see uiMenuSetCancelCallback.
*
* @param menu The menu that was cancelled.
* @returns True if fully handled (skips the default disableBack/pop
* behavior); false to fall through to it.
*/
typedef bool_t (*uimenucancelcallback_t)(const uimenu_t *menu);
typedef struct uimenu_s {
uimenuitem_t *items;
uint8_t itemCount;
@@ -64,6 +74,7 @@ typedef struct uimenu_s {
uimenuselectedcallback_t selected;
uimenuclosedcallback_t closed;
uimenuchangedcallback_t changed;
uimenucancelcallback_t cancel;
void *user;
@@ -138,6 +149,17 @@ void uiMenuClose(uimenu_t *menu);
*/
void uiMenuSetDisableBack(uimenu_t *menu, const bool_t disableBack);
/**
* Sets a callback given first refusal of INPUT_ACTION_CANCEL while this
* menu is the topmost focus item, before disableBack/the default pop is
* applied. Safe to call before or after uiMenuOpen().
*
* @param menu The menu to update.
* @param cancel The callback to install, or NULL to remove it and
* restore default disableBack/pop behavior.
*/
void uiMenuSetCancelCallback(uimenu_t *menu, uimenucancelcallback_t cancel);
/**
* Returns whether the menu is currently active (on the UI focus stack).
*
@@ -230,6 +252,15 @@ bool_t uiMenuFocusDirection(
const uifocusdirection_t direction
);
/**
* Internal focus callback - forwards INPUT_ACTION_CANCEL to the menu's
* cancel handler, if one is set.
*
* @param focusItem The active focus item; user field must point to uimenu_t.
* @returns The cancel handler's result, or false if none is set.
*/
bool_t uiMenuFocusCancel(const uifocusitem_t *focusItem);
// Helper macros
#define MENU_BEGIN(menuPtr, itemsArray, selected, closed, changed) \
uimenu_t *menu = (menuPtr); \