Screen Keyboard

Source: src/screen_keyboard.c · 232 lines · 12 functions.

Module entry points

These functions are exposed to the rest of the project. Open one to follow its decisions and the helpers it calls.

How to read these diagrams

Each function has its own expandable diagram using the actual expressions and statements from this file. Decisions split into True and False; loop bodies are indented. Returns end the function immediately; break and continue follow their C meanings. Arrows between statements show their order when execution reaches them.

These are structural source diagrams before preprocessing, not runtime traces. Conditional compilation and macros remain source expressions. Ternary choices remain inside their statement. Calls are linked separately; callback execution and task scheduling are not inferred.

Functions in this file

nameCharacter() · source line 39
static bool nameCharacter(char character)
  1. Exit this functionreturn (character >= 'A' && character <= 'Z') || (character >= '0' && character <= '9') || character == '_' || character == ' ';
Read original function source
static bool nameCharacter(char character)
{
    return (character >= 'A' && character <= 'Z') ||
           (character >= '0' && character <= '9') || character == '_' || character == ' ';
}
textCharacter() · source line 45
static bool textCharacter(char character)
  1. Exit this functionreturn character >= 32 && character <= 126;
Read original function source
static bool textCharacter(char character)
{
    return character >= 32 && character <= 126;
}
currentCharacter() · source line 50
static char currentCharacter(size_t key)
  1. Decisionkey < 26U
    True
      1. Executechar character = letters[key];
      2. Exit this functionreturn lowerCase && !symbolPage ? (char)(character + ('a' - 'A')) : character;
    False

    Continue after this decision.

  2. Decisionkey < KEYBOARD_CHARACTER_KEYS
    True
    1. Exit this functionreturn letters[key];
    False

    Continue after this decision.

  3. Exit this functionreturn '\0';
Read original function source
static char currentCharacter(size_t key)
{
    if (key < 26U) {
        char character = letters[key];
        return lowerCase && !symbolPage ? (char)(character + ('a' - 'A')) : character;
    }
    if (key < KEYBOARD_CHARACTER_KEYS) return letters[key];
    return '\0';
}
appendCharacter() · source line 60
static void appendCharacter(char character)
  1. DecisiontextLength >= characterLimit || character == '\0'
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executetext[textLength++] = character;
  3. Executetext[textLength] = '\0';
Read original function source
static void appendCharacter(char character)
{
    if (textLength >= characterLimit || character == '\0') return;
    text[textLength++] = character;
    text[textLength] = '\0';
}
hasNonSpaceText() · source line 67
static bool hasNonSpaceText(void)
  1. FOR loopsize_t index = 0; index < textLength; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Decisiontext[index] != ' '
        True
        1. Exit this functionreturn true;
        False

        Continue after this decision.

  2. Exit this functionreturn false;
Read original function source
static bool hasNonSpaceText(void)
{
    for (size_t index = 0; index < textLength; ++index) {
        if (text[index] != ' ') return true;
    }
    return false;
}
copyHeading() · source line 75
static void copyHeading(const char *title)
  1. Executeconst char *source = title == NULL ? "ENTER TEXT" : title;
  2. Executesize_t length = 0;
  3. WHILE loopsource[length] != '\0' && length < SCREEN_KEYBOARD_TITLE_CHARS

    Repeat body while the loop condition permits; then continue below.

      1. Executeheading[length] = source[length];
      2. Execute++length;
  4. Executeheading[length] = '\0';
Read original function source
static void copyHeading(const char *title)
{
    const char *source = title == NULL ? "ENTER TEXT" : title;
    size_t length = 0;
    while (source[length] != '\0' && length < SCREEN_KEYBOARD_TITLE_CHARS) {
        heading[length] = source[length];
        ++length;
    }
    heading[length] = '\0';
}
startScreenKeyboard() · source line 86
void startScreenKeyboard(const char *initialText, size_t maxChars, KeyboardMode inputMode,
                         const char *title, uint32_t nowMs)

Calls into this documentation:

  1. Executemode = inputMode;
  2. ExecutecharacterLimit = maxChars > SCREEN_KEYBOARD_MAX_CHARS ? SCREEN_KEYBOARD_MAX_CHARS : maxChars;
  3. ExecutetextLength = 0;
  4. ExecutelowerCase = inputMode != KEYBOARD_NAME;
  5. ExecutesymbolPage = false;
  6. ExecutecopyHeading(title);
  7. DecisioninitialText != NULL
    True
      1. FOR loopsize_t index = 0; initialText[index] != '\0' && textLength < characterLimit; ++index

        Repeat body while the loop condition permits; then continue below.

          1. Executechar character = initialText[index];
          2. Decisionmode == KEYBOARD_NAME && character >= 'a' && character <= 'z'
            True
              1. Executecharacter = (char)(character - ('a' - 'A'));
            False

            Continue after this decision.

          3. Decision(mode == KEYBOARD_NAME && nameCharacter(character)) || (mode != KEYBOARD_NAME && textCharacter(character))
            True
              1. ExecuteappendCharacter(character);
            False

            Continue after this decision.

    False

    Continue after this decision.

  8. ExecuteselectedKey = 0;
  9. ExecutelastMove = nowMs;
  10. Executeresult = KEYBOARD_EDITING;
Read original function source
void startScreenKeyboard(const char *initialText, size_t maxChars, KeyboardMode inputMode,
                         const char *title, uint32_t nowMs)
{
    mode = inputMode;
    characterLimit = maxChars > SCREEN_KEYBOARD_MAX_CHARS ? SCREEN_KEYBOARD_MAX_CHARS : maxChars;
    textLength = 0;
    // Password and general text entry normally begin in lowercase. Name entry
    // remains uppercase-only for high-score compatibility.
    lowerCase = inputMode != KEYBOARD_NAME;
    symbolPage = false;
    copyHeading(title);

    if (initialText != NULL) {
        for (size_t index = 0; initialText[index] != '\0' && textLength < characterLimit; ++index) {
            char character = initialText[index];
            if (mode == KEYBOARD_NAME && character >= 'a' && character <= 'z') {
                character = (char)(character - ('a' - 'A'));
            }
            if ((mode == KEYBOARD_NAME && nameCharacter(character)) ||
                (mode != KEYBOARD_NAME && textCharacter(character))) {
                appendCharacter(character);
            }
        }
    }

    selectedKey = 0;
    lastMove = nowMs;
    result = KEYBOARD_EDITING;
}
updateScreenKeyboard() · source line 116
KeyboardResult updateScreenKeyboard(int xOffset, int yOffset, bool valid, bool aPressed,
                                    uint32_t nowMs)

Calls into this documentation:

  1. Decisionresult != KEYBOARD_EDITING
    True
    1. Exit this functionreturn result;
    False

    Continue after this decision.

  2. Decisionvalid && nowMs - lastMove >= JOYSTICK_REPEAT_MS
    True
      1. DecisionxOffset > 600
        True
        1. ExecuteselectedKey = (selectedKey + 1U) % KEYBOARD_KEYS;
        False
        1. DecisionxOffset < -600
          True
          1. ExecuteselectedKey = (selectedKey + KEYBOARD_KEYS - 1U) % KEYBOARD_KEYS;
          False
          1. DecisionyOffset > 600
            True
            1. ExecuteselectedKey = (selectedKey + KEYBOARD_KEYS - KEYBOARD_COLUMNS) % KEYBOARD_KEYS;
            False
            1. DecisionyOffset < -600
              True
              1. ExecuteselectedKey = (selectedKey + KEYBOARD_COLUMNS) % KEYBOARD_KEYS;
              False
              1. Jump to labelgoto handle_press;
      2. ExecutelastMove = nowMs;
    False

    Continue after this decision.

  3. Executehandle_press: if (!aPressed) return result;
  4. DecisionselectedKey < KEYBOARD_CHARACTER_KEYS
    True
      1. ExecuteappendCharacter(symbolPage ? symbols[selectedKey % SYMBOL_COUNT] : currentCharacter(selectedKey));
    False
    1. DecisionselectedKey == KEYBOARD_SHIFT_KEY && mode != KEYBOARD_NAME
      True
        1. ExecutelowerCase = !lowerCase;
        2. ExecutesymbolPage = false;
      False
      1. DecisionselectedKey == KEYBOARD_SYMBOLS_KEY && mode != KEYBOARD_NAME
        True
          1. ExecutesymbolPage = !symbolPage;
        False
        1. DecisionselectedKey == KEYBOARD_SPACE_KEY
          True
            1. ExecuteappendCharacter(' ');
          False
          1. DecisionselectedKey == KEYBOARD_DELETE_KEY && textLength > 0U
            True
              1. Executetext[--textLength] = '\0';
            False
            1. DecisionselectedKey == KEYBOARD_DONE_KEY && textLength > 0U && (mode != KEYBOARD_NAME || hasNonSpaceText())
              True
                1. Executeresult = KEYBOARD_DONE;
              False
              1. DecisionselectedKey == KEYBOARD_CANCEL_KEY
                True
                  1. Executeresult = KEYBOARD_CANCELLED;
                False
                1. DecisionselectedKey == KEYBOARD_DASH_KEY && mode != KEYBOARD_NAME
                  True
                    1. ExecuteappendCharacter('-');
                  False
                  1. DecisionselectedKey == KEYBOARD_UNDERSCORE_KEY
                    True
                      1. ExecuteappendCharacter('_');
                    False
                    1. DecisionselectedKey == KEYBOARD_DOT_KEY && mode != KEYBOARD_NAME
                      True
                        1. ExecuteappendCharacter('.');
                      False

                      Continue after this decision.

  5. Exit this functionreturn result;
Read original function source
KeyboardResult updateScreenKeyboard(int xOffset, int yOffset, bool valid, bool aPressed,
                                    uint32_t nowMs)
{
    if (result != KEYBOARD_EDITING) return result;

    if (valid && nowMs - lastMove >= JOYSTICK_REPEAT_MS) {
        if (xOffset > 600) selectedKey = (selectedKey + 1U) % KEYBOARD_KEYS;
        else if (xOffset < -600) selectedKey = (selectedKey + KEYBOARD_KEYS - 1U) % KEYBOARD_KEYS;
        else if (yOffset > 600) selectedKey = (selectedKey + KEYBOARD_KEYS - KEYBOARD_COLUMNS) % KEYBOARD_KEYS;
        else if (yOffset < -600) selectedKey = (selectedKey + KEYBOARD_COLUMNS) % KEYBOARD_KEYS;
        else goto handle_press;
        lastMove = nowMs;
    }

handle_press:
    if (!aPressed) return result;

    if (selectedKey < KEYBOARD_CHARACTER_KEYS) {
        appendCharacter(symbolPage ? symbols[selectedKey % SYMBOL_COUNT] : currentCharacter(selectedKey));
    } else if (selectedKey == KEYBOARD_SHIFT_KEY && mode != KEYBOARD_NAME) {
        lowerCase = !lowerCase;
        symbolPage = false;
    } else if (selectedKey == KEYBOARD_SYMBOLS_KEY && mode != KEYBOARD_NAME) {
        symbolPage = !symbolPage;
    } else if (selectedKey == KEYBOARD_SPACE_KEY) {
        appendCharacter(' ');
    } else if (selectedKey == KEYBOARD_DELETE_KEY && textLength > 0U) {
        text[--textLength] = '\0';
    // High-score names must match the persistent score-table rules: no empty
    // or space-only entry may reach high_scores_submit().
    } else if (selectedKey == KEYBOARD_DONE_KEY && textLength > 0U &&
               (mode != KEYBOARD_NAME || hasNonSpaceText())) {
        result = KEYBOARD_DONE;
    } else if (selectedKey == KEYBOARD_CANCEL_KEY) {
        result = KEYBOARD_CANCELLED;
    } else if (selectedKey == KEYBOARD_DASH_KEY && mode != KEYBOARD_NAME) {
        appendCharacter('-');
    } else if (selectedKey == KEYBOARD_UNDERSCORE_KEY) {
        appendCharacter('_');
    } else if (selectedKey == KEYBOARD_DOT_KEY && mode != KEYBOARD_NAME) {
        appendCharacter('.');
    }
    return result;
}
cancelScreenKeyboard() · source line 161
void cancelScreenKeyboard(void)
  1. Decisionresult == KEYBOARD_EDITING
    True
    1. Executeresult = KEYBOARD_CANCELLED;
    False

    Continue after this decision.

Read original function source
void cancelScreenKeyboard(void)
{
    if (result == KEYBOARD_EDITING) result = KEYBOARD_CANCELLED;
}
getScreenKeyboardText() · source line 166
bool getScreenKeyboardText(char *destination, size_t capacity)
  1. Decisionresult != KEYBOARD_DONE || destination == NULL || capacity <= textLength
    True
    1. Exit this functionreturn false;
    False

    Continue after this decision.

  2. Executememcpy(destination, text, textLength + 1U);
  3. Exit this functionreturn true;
Read original function source
bool getScreenKeyboardText(char *destination, size_t capacity)
{
    if (result != KEYBOARD_DONE || destination == NULL || capacity <= textLength) return false;
    memcpy(destination, text, textLength + 1U);
    return true;
}
keyLabel() · source line 173
static const char *keyLabel(size_t key, char character[2])

Calls into this documentation:

  1. Decisionkey < KEYBOARD_CHARACTER_KEYS
    True
      1. Executecharacter[0] = symbolPage ? symbols[key % SYMBOL_COUNT] : currentCharacter(key);
      2. Executecharacter[1] = '\0';
      3. Exit this functionreturn character;
    False

    Continue after this decision.

  2. Decisionkey == KEYBOARD_SHIFT_KEY
    True
    1. Exit this functionreturn mode == KEYBOARD_NAME ? "" : (lowerCase ? "abc" : "ABC");
    False

    Continue after this decision.

  3. Decisionkey == KEYBOARD_SYMBOLS_KEY
    True
    1. Exit this functionreturn mode == KEYBOARD_NAME ? "" : "SYM";
    False

    Continue after this decision.

  4. Decisionkey == KEYBOARD_SPACE_KEY
    True
    1. Exit this functionreturn "SPACE";
    False

    Continue after this decision.

  5. Decisionkey == KEYBOARD_DELETE_KEY
    True
    1. Exit this functionreturn "DEL";
    False

    Continue after this decision.

  6. Decisionkey == KEYBOARD_DONE_KEY
    True
    1. Exit this functionreturn "DONE";
    False

    Continue after this decision.

  7. Decisionkey == KEYBOARD_CANCEL_KEY
    True
    1. Exit this functionreturn "CANCEL";
    False

    Continue after this decision.

  8. Decisionkey == KEYBOARD_DASH_KEY
    True
    1. Exit this functionreturn mode == KEYBOARD_NAME ? "" : "-";
    False

    Continue after this decision.

  9. Decisionkey == KEYBOARD_UNDERSCORE_KEY
    True
    1. Exit this functionreturn "_";
    False

    Continue after this decision.

  10. Exit this functionreturn mode == KEYBOARD_NAME ? "" : ".";
Read original function source
static const char *keyLabel(size_t key, char character[2])
{
    if (key < KEYBOARD_CHARACTER_KEYS) {
        character[0] = symbolPage ? symbols[key % SYMBOL_COUNT] : currentCharacter(key);
        character[1] = '\0';
        return character;
    }
    if (key == KEYBOARD_SHIFT_KEY) return mode == KEYBOARD_NAME ? "" : (lowerCase ? "abc" : "ABC");
    if (key == KEYBOARD_SYMBOLS_KEY) return mode == KEYBOARD_NAME ? "" : "SYM";
    if (key == KEYBOARD_SPACE_KEY) return "SPACE";
    if (key == KEYBOARD_DELETE_KEY) return "DEL";
    if (key == KEYBOARD_DONE_KEY) return "DONE";
    if (key == KEYBOARD_CANCEL_KEY) return "CANCEL";
    if (key == KEYBOARD_DASH_KEY) return mode == KEYBOARD_NAME ? "" : "-";
    if (key == KEYBOARD_UNDERSCORE_KEY) return "_";
    return mode == KEYBOARD_NAME ? "" : ".";
}
drawScreenKeyboard() · source line 191
void drawScreenKeyboard(uint16_t *frame)

Calls into this documentation:

  1. Decisionframe == NULL || result != KEYBOARD_EDITING
    True
    1. Exit this functionreturn;
    False

    Continue after this decision.

  2. Executeclear(frame, COLOR_BLACK);
  3. Executeconst int heading_x = mode == KEYBOARD_NAME ? 160 : 170;
  4. ExecutedrawPet2001TextCentredTransparent(frame, heading_x, 4, heading, COLOR_CYAN, 2);
  5. Executechar displayed[24];
  6. Executeconst size_t visible_limit = sizeof(displayed) - 1U;
  7. Executeconst size_t start = textLength > visible_limit ? textLength - visible_limit : 0U;
  8. Executeconst size_t visible_length = textLength - start;
  9. FOR loopsize_t index = 0; index < visible_length; ++index

    Repeat body while the loop condition permits; then continue below.

      1. Executedisplayed[index] = mode == KEYBOARD_PASSWORD ? '*' : text[start + index];
  10. Executedisplayed[visible_length] = '\0';
  11. Executechar count[16];
  12. Executesnprintf(count, sizeof(count), "%u/%u", (unsigned)textLength, (unsigned)characterLimit);
  13. Executeconst char *page = symbolPage ? "SYMBOLS" : (lowerCase ? "LOWER CASE" : "UPPER CASE");
  14. Executeconst uint16_t page_color = symbolPage ? COLOR_YELLOW : (lowerCase ? COLOR_GREEN : COLOR_CYAN);
  15. ExecutedrawPet2001TextTransparent(frame, 8, 16, page, page_color, 1);
  16. ExecutedrawPet2001TextTransparent(frame, 8, 27, displayed, COLOR_ORANGE, 2);
  17. ExecutedrawPet2001TextTransparent(frame, 270, 8, count, COLOR_LIGHTGREY, 1);
  18. ExecutedrawPet2001TextTransparent(frame, 8 + (int)visible_length * PET2001_FONT_GLYPH_WIDTH * 2, 27, "_", COLOR_CYAN, 2);
  19. FOR loopsize_t key = 0; key < KEYBOARD_KEYS; ++key

    Repeat body while the loop condition permits; then continue below.

      1. Executeconst int x = 1 + (int)(key % KEYBOARD_COLUMNS) * 35;
      2. Executeconst int y = 50 + (int)(key / KEYBOARD_COLUMNS) * 21;
      3. Executeconst bool selected = key == selectedKey;
      4. Executechar character[2] = {0};
      5. Executeconst char *label = keyLabel(key, character);
      6. Executeconst int scale = key < KEYBOARD_CHARACTER_KEYS ? 2 : 1;
      7. Executeconst int text_y = scale == 2 ? y + 2 : y + 5;
      8. ExecutedrawPet2001TextCentredTransparent(frame, x + 16, text_y, label, selected ? COLOR_ORANGE : COLOR_LIGHTGREY, scale);
Read original function source
void drawScreenKeyboard(uint16_t *frame)
{
    if (frame == NULL || result != KEYBOARD_EDITING) return;

    clear(frame, COLOR_BLACK);
    // High-score names are centred; other keyboard headings retain their tuned offset.
    const int heading_x = mode == KEYBOARD_NAME ? 160 : 170;
    drawPet2001TextCentredTransparent(frame, heading_x, 4, heading, COLOR_CYAN, 2);

    // Keep password characters masked, but show a clear count and a visible
    // cursor so every accepted keypress has immediate on-screen feedback.
    char displayed[24];
    const size_t visible_limit = sizeof(displayed) - 1U;
    const size_t start = textLength > visible_limit ? textLength - visible_limit : 0U;
    const size_t visible_length = textLength - start;
    for (size_t index = 0; index < visible_length; ++index) {
        displayed[index] = mode == KEYBOARD_PASSWORD ? '*' : text[start + index];
    }
    displayed[visible_length] = '\0';
    char count[16];
    snprintf(count, sizeof(count), "%u/%u", (unsigned)textLength, (unsigned)characterLimit);
    const char *page = symbolPage ? "SYMBOLS" : (lowerCase ? "LOWER CASE" : "UPPER CASE");
    const uint16_t page_color = symbolPage ? COLOR_YELLOW : (lowerCase ? COLOR_GREEN : COLOR_CYAN);
    // Keep the page indicator clear of the entered-text row; tune this y offset here.
    drawPet2001TextTransparent(frame, 8, 16, page, page_color, 1);
    drawPet2001TextTransparent(frame, 8, 27, displayed, COLOR_ORANGE, 2);
    drawPet2001TextTransparent(frame, 270, 8, count, COLOR_LIGHTGREY, 1);
    drawPet2001TextTransparent(frame, 8 + (int)visible_length * PET2001_FONT_GLYPH_WIDTH * 2,
                               27, "_", COLOR_CYAN, 2);

    for (size_t key = 0; key < KEYBOARD_KEYS; ++key) {
        const int x = 1 + (int)(key % KEYBOARD_COLUMNS) * 35;
        const int y = 50 + (int)(key / KEYBOARD_COLUMNS) * 21;
        const bool selected = key == selectedKey;
        char character[2] = {0};
        const char *label = keyLabel(key, character);
        const int scale = key < KEYBOARD_CHARACTER_KEYS ? 2 : 1;
        const int text_y = scale == 2 ? y + 2 : y + 5;
        drawPet2001TextCentredTransparent(frame, x + 16, text_y, label,
                                          selected ? COLOR_ORANGE : COLOR_LIGHTGREY, scale);
    }
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "screen_keyboard.h"

#include "game_graphics.h"
#include "PET2001_font.h"

#include <stdio.h>
#include <string.h>

#define KEYBOARD_COLUMNS 9U
#define KEYBOARD_ROWS 5U
#define KEYBOARD_KEYS (KEYBOARD_COLUMNS * KEYBOARD_ROWS)
#define KEYBOARD_CHARACTER_KEYS 36U
#define SYMBOL_COUNT (sizeof(symbols) - 1U)
#define KEYBOARD_SHIFT_KEY 36U
#define KEYBOARD_SYMBOLS_KEY 37U
#define KEYBOARD_SPACE_KEY 38U
#define KEYBOARD_DELETE_KEY 39U
#define KEYBOARD_DONE_KEY 40U
#define KEYBOARD_CANCEL_KEY 41U
#define KEYBOARD_DASH_KEY 42U
#define KEYBOARD_UNDERSCORE_KEY 43U
#define KEYBOARD_DOT_KEY 44U
#define JOYSTICK_REPEAT_MS 180U

static const char letters[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
static const char symbols[] = "!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";

static char text[SCREEN_KEYBOARD_MAX_CHARS + 1U];
static char heading[SCREEN_KEYBOARD_TITLE_CHARS + 1U];
static size_t textLength;
static size_t characterLimit;
static size_t selectedKey;
static uint32_t lastMove;
static KeyboardMode mode;
static bool lowerCase;
static bool symbolPage;
static KeyboardResult result = KEYBOARD_CANCELLED;

static bool nameCharacter(char character)
{
    return (character >= 'A' && character <= 'Z') ||
           (character >= '0' && character <= '9') || character == '_' || character == ' ';
}

static bool textCharacter(char character)
{
    return character >= 32 && character <= 126;
}

static char currentCharacter(size_t key)
{
    if (key < 26U) {
        char character = letters[key];
        return lowerCase && !symbolPage ? (char)(character + ('a' - 'A')) : character;
    }
    if (key < KEYBOARD_CHARACTER_KEYS) return letters[key];
    return '\0';
}

static void appendCharacter(char character)
{
    if (textLength >= characterLimit || character == '\0') return;
    text[textLength++] = character;
    text[textLength] = '\0';
}

static bool hasNonSpaceText(void)
{
    for (size_t index = 0; index < textLength; ++index) {
        if (text[index] != ' ') return true;
    }
    return false;
}

static void copyHeading(const char *title)
{
    const char *source = title == NULL ? "ENTER TEXT" : title;
    size_t length = 0;
    while (source[length] != '\0' && length < SCREEN_KEYBOARD_TITLE_CHARS) {
        heading[length] = source[length];
        ++length;
    }
    heading[length] = '\0';
}

void startScreenKeyboard(const char *initialText, size_t maxChars, KeyboardMode inputMode,
                         const char *title, uint32_t nowMs)
{
    mode = inputMode;
    characterLimit = maxChars > SCREEN_KEYBOARD_MAX_CHARS ? SCREEN_KEYBOARD_MAX_CHARS : maxChars;
    textLength = 0;
    // Password and general text entry normally begin in lowercase. Name entry
    // remains uppercase-only for high-score compatibility.
    lowerCase = inputMode != KEYBOARD_NAME;
    symbolPage = false;
    copyHeading(title);

    if (initialText != NULL) {
        for (size_t index = 0; initialText[index] != '\0' && textLength < characterLimit; ++index) {
            char character = initialText[index];
            if (mode == KEYBOARD_NAME && character >= 'a' && character <= 'z') {
                character = (char)(character - ('a' - 'A'));
            }
            if ((mode == KEYBOARD_NAME && nameCharacter(character)) ||
                (mode != KEYBOARD_NAME && textCharacter(character))) {
                appendCharacter(character);
            }
        }
    }

    selectedKey = 0;
    lastMove = nowMs;
    result = KEYBOARD_EDITING;
}

KeyboardResult updateScreenKeyboard(int xOffset, int yOffset, bool valid, bool aPressed,
                                    uint32_t nowMs)
{
    if (result != KEYBOARD_EDITING) return result;

    if (valid && nowMs - lastMove >= JOYSTICK_REPEAT_MS) {
        if (xOffset > 600) selectedKey = (selectedKey + 1U) % KEYBOARD_KEYS;
        else if (xOffset < -600) selectedKey = (selectedKey + KEYBOARD_KEYS - 1U) % KEYBOARD_KEYS;
        else if (yOffset > 600) selectedKey = (selectedKey + KEYBOARD_KEYS - KEYBOARD_COLUMNS) % KEYBOARD_KEYS;
        else if (yOffset < -600) selectedKey = (selectedKey + KEYBOARD_COLUMNS) % KEYBOARD_KEYS;
        else goto handle_press;
        lastMove = nowMs;
    }

handle_press:
    if (!aPressed) return result;

    if (selectedKey < KEYBOARD_CHARACTER_KEYS) {
        appendCharacter(symbolPage ? symbols[selectedKey % SYMBOL_COUNT] : currentCharacter(selectedKey));
    } else if (selectedKey == KEYBOARD_SHIFT_KEY && mode != KEYBOARD_NAME) {
        lowerCase = !lowerCase;
        symbolPage = false;
    } else if (selectedKey == KEYBOARD_SYMBOLS_KEY && mode != KEYBOARD_NAME) {
        symbolPage = !symbolPage;
    } else if (selectedKey == KEYBOARD_SPACE_KEY) {
        appendCharacter(' ');
    } else if (selectedKey == KEYBOARD_DELETE_KEY && textLength > 0U) {
        text[--textLength] = '\0';
    // High-score names must match the persistent score-table rules: no empty
    // or space-only entry may reach high_scores_submit().
    } else if (selectedKey == KEYBOARD_DONE_KEY && textLength > 0U &&
               (mode != KEYBOARD_NAME || hasNonSpaceText())) {
        result = KEYBOARD_DONE;
    } else if (selectedKey == KEYBOARD_CANCEL_KEY) {
        result = KEYBOARD_CANCELLED;
    } else if (selectedKey == KEYBOARD_DASH_KEY && mode != KEYBOARD_NAME) {
        appendCharacter('-');
    } else if (selectedKey == KEYBOARD_UNDERSCORE_KEY) {
        appendCharacter('_');
    } else if (selectedKey == KEYBOARD_DOT_KEY && mode != KEYBOARD_NAME) {
        appendCharacter('.');
    }
    return result;
}

void cancelScreenKeyboard(void)
{
    if (result == KEYBOARD_EDITING) result = KEYBOARD_CANCELLED;
}

bool getScreenKeyboardText(char *destination, size_t capacity)
{
    if (result != KEYBOARD_DONE || destination == NULL || capacity <= textLength) return false;
    memcpy(destination, text, textLength + 1U);
    return true;
}

static const char *keyLabel(size_t key, char character[2])
{
    if (key < KEYBOARD_CHARACTER_KEYS) {
        character[0] = symbolPage ? symbols[key % SYMBOL_COUNT] : currentCharacter(key);
        character[1] = '\0';
        return character;
    }
    if (key == KEYBOARD_SHIFT_KEY) return mode == KEYBOARD_NAME ? "" : (lowerCase ? "abc" : "ABC");
    if (key == KEYBOARD_SYMBOLS_KEY) return mode == KEYBOARD_NAME ? "" : "SYM";
    if (key == KEYBOARD_SPACE_KEY) return "SPACE";
    if (key == KEYBOARD_DELETE_KEY) return "DEL";
    if (key == KEYBOARD_DONE_KEY) return "DONE";
    if (key == KEYBOARD_CANCEL_KEY) return "CANCEL";
    if (key == KEYBOARD_DASH_KEY) return mode == KEYBOARD_NAME ? "" : "-";
    if (key == KEYBOARD_UNDERSCORE_KEY) return "_";
    return mode == KEYBOARD_NAME ? "" : ".";
}

void drawScreenKeyboard(uint16_t *frame)
{
    if (frame == NULL || result != KEYBOARD_EDITING) return;

    clear(frame, COLOR_BLACK);
    // High-score names are centred; other keyboard headings retain their tuned offset.
    const int heading_x = mode == KEYBOARD_NAME ? 160 : 170;
    drawPet2001TextCentredTransparent(frame, heading_x, 4, heading, COLOR_CYAN, 2);

    // Keep password characters masked, but show a clear count and a visible
    // cursor so every accepted keypress has immediate on-screen feedback.
    char displayed[24];
    const size_t visible_limit = sizeof(displayed) - 1U;
    const size_t start = textLength > visible_limit ? textLength - visible_limit : 0U;
    const size_t visible_length = textLength - start;
    for (size_t index = 0; index < visible_length; ++index) {
        displayed[index] = mode == KEYBOARD_PASSWORD ? '*' : text[start + index];
    }
    displayed[visible_length] = '\0';
    char count[16];
    snprintf(count, sizeof(count), "%u/%u", (unsigned)textLength, (unsigned)characterLimit);
    const char *page = symbolPage ? "SYMBOLS" : (lowerCase ? "LOWER CASE" : "UPPER CASE");
    const uint16_t page_color = symbolPage ? COLOR_YELLOW : (lowerCase ? COLOR_GREEN : COLOR_CYAN);
    // Keep the page indicator clear of the entered-text row; tune this y offset here.
    drawPet2001TextTransparent(frame, 8, 16, page, page_color, 1);
    drawPet2001TextTransparent(frame, 8, 27, displayed, COLOR_ORANGE, 2);
    drawPet2001TextTransparent(frame, 270, 8, count, COLOR_LIGHTGREY, 1);
    drawPet2001TextTransparent(frame, 8 + (int)visible_length * PET2001_FONT_GLYPH_WIDTH * 2,
                               27, "_", COLOR_CYAN, 2);

    for (size_t key = 0; key < KEYBOARD_KEYS; ++key) {
        const int x = 1 + (int)(key % KEYBOARD_COLUMNS) * 35;
        const int y = 50 + (int)(key / KEYBOARD_COLUMNS) * 21;
        const bool selected = key == selectedKey;
        char character[2] = {0};
        const char *label = keyLabel(key, character);
        const int scale = key < KEYBOARD_CHARACTER_KEYS ? 2 : 1;
        const int text_y = scale == 2 ? y + 2 : y + 5;
        drawPet2001TextCentredTransparent(frame, x + 16, text_y, label,
                                          selected ? COLOR_ORANGE : COLOR_LIGHTGREY, scale);
    }
}