diff --git a/src/dusk/ui/dialog/keyboard/uikeyboard.c b/src/dusk/ui/dialog/keyboard/uikeyboard.c index 3fda2199..a6ddc40e 100644 --- a/src/dusk/ui/dialog/keyboard/uikeyboard.c +++ b/src/dusk/ui/dialog/keyboard/uikeyboard.c @@ -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(); } diff --git a/src/dusk/ui/dialog/keyboard/uikeyboard.h b/src/dusk/ui/dialog/keyboard/uikeyboard.h index 780614b4..e19c41be 100644 --- a/src/dusk/ui/dialog/keyboard/uikeyboard.h +++ b/src/dusk/ui/dialog/keyboard/uikeyboard.h @@ -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); diff --git a/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.c b/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.c index 350e5179..2ab17b86 100644 --- a/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.c +++ b/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.c @@ -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) { diff --git a/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.h b/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.h index 5b0de032..2cb50156 100644 --- a/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.h +++ b/src/dusk/ui/dialog/keyboard/uikeyboardqwerty.h @@ -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. */ diff --git a/src/dusk/ui/focus/uifocus.c b/src/dusk/ui/focus/uifocus.c index 31e9f4c0..c5fb9020 100644 --- a/src/dusk/ui/focus/uifocus.c +++ b/src/dusk/ui/focus/uifocus.c @@ -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; } diff --git a/src/dusk/ui/focus/uifocusitem.h b/src/dusk/ui/focus/uifocusitem.h index 081de0df..32654321 100644 --- a/src/dusk/ui/focus/uifocusitem.h +++ b/src/dusk/ui/focus/uifocusitem.h @@ -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; }; diff --git a/src/dusk/ui/screen/mainmenu/uimainmenu.c b/src/dusk/ui/screen/mainmenu/uimainmenu.c index 4dd78f3d..d6e37bd9 100644 --- a/src/dusk/ui/screen/mainmenu/uimainmenu.c +++ b/src/dusk/ui/screen/mainmenu/uimainmenu.c @@ -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 - diff --git a/src/dusk/ui/screen/mainmenu/uimainmenu.h b/src/dusk/ui/screen/mainmenu/uimainmenu.h index 3a53c9ee..8c2178d7 100644 --- a/src/dusk/ui/screen/mainmenu/uimainmenu.h +++ b/src/dusk/ui/screen/mainmenu/uimainmenu.h @@ -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 +); diff --git a/src/dusk/ui/uielementlist.c b/src/dusk/ui/uielementlist.c index ac0e5651..68228361 100644 --- a/src/dusk/ui/uielementlist.c +++ b/src/dusk/ui/uielementlist.c @@ -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. diff --git a/src/dusk/ui/widget/uimenu.c b/src/dusk/ui/widget/uimenu.c index 8c736f2f..ca118f43 100644 --- a/src/dusk/ui/widget/uimenu.c +++ b/src/dusk/ui/widget/uimenu.c @@ -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; diff --git a/src/dusk/ui/widget/uimenu.h b/src/dusk/ui/widget/uimenu.h index 8fbb7ec3..c9a8c997 100644 --- a/src/dusk/ui/widget/uimenu.h +++ b/src/dusk/ui/widget/uimenu.h @@ -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); \