Menu

Source: src/menu.c · 453 lines · 21 functions.

Screen captures

Views of this part running on the Mini Arcade.

Main menu on the Mini Arcade
The main menu at its normal entry state.
Mini Arcade menu with Demo selected
Main menu with Demo selected.
Mini Arcade menu with Games selected
Main menu with Games selected.
Mini Arcade menu with Hall of Fame selected
Main menu with Hall of Fame selected.
Mini Arcade menu with Misc selected
Main menu with Misc selected.
Mini Arcade menu with Settings selected
Main menu with Settings selected.

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

active_items() · source line 111
static const menu_item_t *active_items(void)
  1. Decisioncurrent_screen == SCREEN_GAMES
    True
    1. Exit this functionreturn games_items;
    False

    Continue after this decision.

  2. Decisioncurrent_screen == SCREEN_SETTINGS
    True
    1. Exit this functionreturn settings_items;
    False

    Continue after this decision.

  3. Exit this functionreturn current_screen == SCREEN_MISC ? misc_items : main_items;
Read original function source
static const menu_item_t *active_items(void)
{
    if (current_screen == SCREEN_GAMES) return games_items;
    if (current_screen == SCREEN_SETTINGS) return settings_items;
    return current_screen == SCREEN_MISC ? misc_items : main_items;
}
active_selection() · source line 118
static size_t *active_selection(void)
  1. Decisioncurrent_screen == SCREEN_GAMES
    True
    1. Exit this functionreturn &games_selected;
    False

    Continue after this decision.

  2. Decisioncurrent_screen == SCREEN_SETTINGS
    True
    1. Exit this functionreturn &settings_selected;
    False

    Continue after this decision.

  3. Exit this functionreturn current_screen == SCREEN_MISC ? &misc_selected : &main_selected;
Read original function source
static size_t *active_selection(void)
{
    if (current_screen == SCREEN_GAMES) return &games_selected;
    if (current_screen == SCREEN_SETTINGS) return &settings_selected;
    return current_screen == SCREEN_MISC ? &misc_selected : &main_selected;
}
note_action() · source line 125
static void note_action(uint32_t now_ms)
  1. Executelast_action = now_ms;
  2. Executeredraw = true;
Read original function source
static void note_action(uint32_t now_ms)
{
    last_action = now_ms;
    redraw = true;
}
change_screen() · source line 131
static void change_screen(screen_id_t screen, uint32_t now_ms)

Calls into this documentation:

  1. Executecurrent_screen = screen;
  2. Decisionscreen == SCREEN_MENU
    True
    1. Executemain_selected = 0U;
    False
    1. Decisionscreen == SCREEN_GAMES
      True
      1. Executegames_selected = 0U;
      False
      1. Decisionscreen == SCREEN_SETTINGS
        True
        1. Executesettings_selected = 0U;
        False
        1. Decisionscreen == SCREEN_MISC
          True
          1. Executemisc_selected = 0U;
          False

          Continue after this decision.

  3. Executebutton_direction = 0;
  4. Executebutton_repeating = false;
  5. Executeheld_direction = MOVE_NONE;
  6. Executewaiting_for_centre = true;
  7. Executerepeating = false;
  8. Executenote_action(now_ms);
Read original function source
static void change_screen(screen_id_t screen, uint32_t now_ms)
{
    current_screen = screen;
    if (screen == SCREEN_MENU) main_selected = 0U;
    else if (screen == SCREEN_GAMES) games_selected = 0U;
    else if (screen == SCREEN_SETTINGS) settings_selected = 0U;
    else if (screen == SCREEN_MISC) misc_selected = 0U;
    button_direction = 0;
    button_repeating = false;
    held_direction = MOVE_NONE;
    waiting_for_centre = true;
    repeating = false;
    note_action(now_ms);
}
move_selection() · source line 299
static void move_selection(menu_direction_t direction, uint32_t now_ms)

Calls into this documentation:

  1. Executeconst size_t count = menu_count();
  2. Executesize_t *selection = active_selection();
  3. Executeconst size_t previous_selection = *selection;
  4. Decisioncount == 0
    True
      1. Exit this functionreturn;
    False

    Continue after this decision.

  5. Decisioncurrent_screen == SCREEN_MENU || current_screen == SCREEN_GAMES || current_screen == SCREEN_SETTINGS || current_screen == SCREEN_MISC
    True
      1. Executeconst size_t maximum_rows_per_column = 4U;
      2. Executeconst size_t items_per_page = maximum_rows_per_column * 2U;
      3. Executeconst size_t page_count = (count + items_per_page - 1U) / items_per_page;
      4. Executesize_t page = *selection / items_per_page;
      5. Executesize_t position = *selection % items_per_page;
      6. Executeconst size_t page_start = page * items_per_page;
      7. Executeconst size_t page_items = count - page_start < items_per_page ? count - page_start : items_per_page;
      8. Executesize_t rows_per_column = (page_items + 1U) / 2U;
      9. Decisionrows_per_column > maximum_rows_per_column
        True
        1. Executerows_per_column = maximum_rows_per_column;
        False

        Continue after this decision.

      10. Executesize_t column = position / rows_per_column;
      11. Executesize_t row = position % rows_per_column;
      12. Executesize_t rows = page_items > column * rows_per_column ? page_items - column * rows_per_column : 0U;
      13. Decisionrows > rows_per_column
        True
        1. Executerows = rows_per_column;
        False

        Continue after this decision.

      14. Decisiondirection == MOVE_LEFT || direction == MOVE_RIGHT
        True
          1. Executeconst size_t other_column = column == 0U ? 1U : 0U;
          2. Executesize_t other_rows = page_items > other_column * rows_per_column ? page_items - other_column * rows_per_column : 0U;
          3. Decisionother_rows > rows_per_column
            True
            1. Executeother_rows = rows_per_column;
            False

            Continue after this decision.

          4. Decisionother_rows > 0U
            True
              1. Executecolumn = other_column;
              2. Decisionrow >= other_rows
                True
                1. Executerow = other_rows - 1U;
                False

                Continue after this decision.

            False

            Continue after this decision.

        False
        1. Decisiondirection == MOVE_UP || direction == MOVE_DOWN
          True
            1. Decision(direction == MOVE_UP && row > 0U) || (direction == MOVE_DOWN && row + 1U < rows)
              True
                1. Executerow = direction == MOVE_UP ? row - 1U : row + 1U;
              False
                1. DO / WHILE looprows == 0U

                  Run body, then test condition; repeat while true.

                    1. Executepage = direction == MOVE_UP ? (page + page_count - 1U) % page_count : (page + 1U) % page_count;
                    2. Executeconst size_t next_start = page * items_per_page;
                    3. Executeconst size_t next_items = count - next_start < items_per_page ? count - next_start : items_per_page;
                    4. Executerows_per_column = (next_items + 1U) / 2U;
                    5. Decisionrows_per_column > maximum_rows_per_column
                      True
                      1. Executerows_per_column = maximum_rows_per_column;
                      False

                      Continue after this decision.

                    6. Executerows = next_items > column * rows_per_column ? next_items - column * rows_per_column : 0U;
                    7. Decisionrows > rows_per_column
                      True
                      1. Executerows = rows_per_column;
                      False

                      Continue after this decision.

                2. Executerow = direction == MOVE_UP ? rows - 1U : 0U;
          False
            1. Exit this functionreturn;
      15. Execute*selection = page * items_per_page + column * rows_per_column + row;
    False
      1. Exit this functionreturn;
  6. Decision*selection != previous_selection
    True
      1. ExecuteaudioPlay(AUDIO_SFX_MENU_MOVE);
    False

    Continue after this decision.

  7. Executenote_action(now_ms);
Read original function source
static void move_selection(menu_direction_t direction, uint32_t now_ms)
{
    const size_t count = menu_count();
    size_t *selection = active_selection();
    const size_t previous_selection = *selection;
    if (count == 0) {
        return;
    }

    if (current_screen == SCREEN_MENU || current_screen == SCREEN_GAMES ||
        current_screen == SCREEN_SETTINGS || current_screen == SCREEN_MISC) {
        const size_t maximum_rows_per_column = 4U;
        const size_t items_per_page = maximum_rows_per_column * 2U;
        const size_t page_count = (count + items_per_page - 1U) / items_per_page;
        size_t page = *selection / items_per_page;
        size_t position = *selection % items_per_page;
        const size_t page_start = page * items_per_page;
        const size_t page_items = count - page_start < items_per_page ?
            count - page_start : items_per_page;
        size_t rows_per_column = (page_items + 1U) / 2U;
        if (rows_per_column > maximum_rows_per_column) rows_per_column = maximum_rows_per_column;
        size_t column = position / rows_per_column;
        size_t row = position % rows_per_column;
        size_t rows = page_items > column * rows_per_column ?
            page_items - column * rows_per_column : 0U;
        if (rows > rows_per_column) rows = rows_per_column;

        if (direction == MOVE_LEFT || direction == MOVE_RIGHT) {
            const size_t other_column = column == 0U ? 1U : 0U;
            size_t other_rows = page_items > other_column * rows_per_column ?
                page_items - other_column * rows_per_column : 0U;
            if (other_rows > rows_per_column) other_rows = rows_per_column;
            if (other_rows > 0U) {
                column = other_column;
                if (row >= other_rows) row = other_rows - 1U;
            }
        } else if (direction == MOVE_UP || direction == MOVE_DOWN) {
            if ((direction == MOVE_UP && row > 0U) ||
                (direction == MOVE_DOWN && row + 1U < rows)) {
                row = direction == MOVE_UP ? row - 1U : row + 1U;
            } else {
                do {
                    page = direction == MOVE_UP ? (page + page_count - 1U) % page_count :
                                                  (page + 1U) % page_count;
                    const size_t next_start = page * items_per_page;
                    const size_t next_items = count - next_start < items_per_page ?
                        count - next_start : items_per_page;
                    rows_per_column = (next_items + 1U) / 2U;
                    if (rows_per_column > maximum_rows_per_column)
                        rows_per_column = maximum_rows_per_column;
                    rows = next_items > column * rows_per_column ?
                        next_items - column * rows_per_column : 0U;
                    if (rows > rows_per_column) rows = rows_per_column;
                } while (rows == 0U);
                row = direction == MOVE_UP ? rows - 1U : 0U;
            }
        } else {
            return;
        }
        *selection = page * items_per_page + column * rows_per_column + row;
    } else {
        return;
    }
    if (*selection != previous_selection) {
        audioPlay(AUDIO_SFX_MENU_MOVE);
    }
    note_action(now_ms);
}
Complete original file, including constants and data
/* Coded By JD(2026) */
#error "Reference listing only — not provided as build-ready source."
#include "menu.h"
#include "audio.h"
#include "hardware_features.h"

#define MENU_TIMEOUT_MS 120000U
#define JOYSTICK_ENTER 600
#define JOYSTICK_RELEASE 350
#define JOYSTICK_REPEAT_START_MS 400U
#define JOYSTICK_REPEAT_MS 150U

typedef struct {
    const char *text;
    screen_id_t target;
    bool submenu;
} menu_item_t;

typedef enum {
    MOVE_NONE,
    MOVE_UP,
    MOVE_DOWN,
    MOVE_LEFT,
    MOVE_RIGHT,
} menu_direction_t;

static const menu_item_t main_items[] = {
    {"DEMO", SCREEN_ABOUT, false},
    {"GAMES", SCREEN_GAMES, true},
    {"HALL OF FAME", SCREEN_HALL_OF_FAME, false},
    {"MISC", SCREEN_MISC, true},
    {"SETTINGS", SCREEN_SETTINGS, true},
};

// Add each game here when its module is ported and linked.
static const menu_item_t games_items[] = {
    {"ASTEROIDS", SCREEN_ASTEROIDS, false},
    {"BATTLE ZONE", SCREEN_BATTLE_ZONE, false},
    {"BATTLESHIPS", SCREEN_BATTLESHIPS, false},
    {"BLACKJACK", SCREEN_BLACKJACK, false},
    {"BOULDER DASH", SCREEN_BOULDER_DASH, false},
    {"BREAKOUT", SCREEN_BREAKOUT, false},
    {"CHECKERS", SCREEN_CHECKERS, false},
    {"CONNECT 4", SCREEN_CONNECT_FOUR, false},
    {"CRAWLER", SCREEN_CRAWLER, false},
    {"DEFENDER", SCREEN_DEFENDER, false},
    {"DUNGEON", SCREEN_DUNGEON, false},
    {"GALAGA", SCREEN_GALAGA, false},
    {"INVADERS", SCREEN_INVADERS, false},
    {"MAZE CHASE", SCREEN_MAZE_CHASE, false},
    {"MISSILE COMMAND", SCREEN_MISSILE_COMMAND, false},
    {"OTHELLO", SCREEN_OTHELLO, false},
    {"PIG", SCREEN_PIG, false},
    {"POKER", SCREEN_POKER, false},
    {"POKER DICE", SCREEN_POKER_DICE, false},
    {"SCRAMBLE", SCREEN_SCRAMBLE, false},
    {"SIMON", SCREEN_SIMON, false},
    {"TEMPEST", SCREEN_TEMPEST, false},
    {"YAHTZEE", SCREEN_YAHTZEE, false},
    {"BACK", SCREEN_MENU, false},
};

static const menu_item_t settings_items[] = {
    {"AI MOVE", SCREEN_AI_MOVE, false},
    {"AI RESTART", SCREEN_AI_RESTART, false},
    {"BATTERY", SCREEN_BATTERY, false},
    {"BRIGHTNESS", SCREEN_BRIGHTNESS, false},
    {"VOLUME", SCREEN_VOLUME, false},
    {"CONTROLLER TEST", SCREEN_CONTROLLER_TEST, false},
    {"ESP INFO", SCREEN_ESP_INFO, false},
    {"FILE BROWSER", SCREEN_SD_CARD_TEST, false},
    {"FONT TEST", SCREEN_FONT_TEST, false},
    {"GAME CONFIG", SCREEN_GAME_CONFIG, false},
    {"GRAPHICS TEST", SCREEN_GRAPHICS_TEST, false},
    {"POWER OFF", SCREEN_POWER_OFF, false},
#if RAM_MONITOR_ENABLED
    {"RAM LOG", SCREEN_RAM_LOG, false},
#endif
    {"RESTART", SCREEN_RESTART, false},
    {"SPIFFS TEST", SCREEN_SPIFFS_TEST, false},
    {"SOUND TEST", SCREEN_SOUND_TEST, false},
    {"WIFI", SCREEN_WIFI, false},
    {"BACK", SCREEN_MENU, false},
};

static const menu_item_t misc_items[] = {
    {"BOING BALL", SCREEN_BOING_BALL, false},
    {"CALCULATOR", SCREEN_CALCULATOR, false},
    {"CLOCK", SCREEN_CLOCK, false},
    {"KALEIDOSCOPE", SCREEN_KALEIDOSCOPE, false},
    {"MANDELBROT", SCREEN_MANDELBROT, false},
    {"MATRIX", SCREEN_MATRIX, false},
    {"SLIDESHOW", SCREEN_SLIDESHOW, false},
    {"WARP TUNNEL", SCREEN_WARP_TUNNEL, false},
    {"BACK", SCREEN_MENU, false},
};

static screen_id_t current_screen = SCREEN_MENU;
static size_t main_selected = 0;
static size_t games_selected = 0;
static size_t settings_selected = 0;
static size_t misc_selected = 0;
static bool redraw = true;
static bool waiting_for_centre = true;
static bool repeating = false;
static menu_direction_t held_direction = MOVE_NONE;
static uint32_t last_action = 0;
static uint32_t last_move = 0;
static int button_direction = 0;
static bool button_repeating = false;
static uint32_t last_button_move = 0;

static const menu_item_t *active_items(void)
{
    if (current_screen == SCREEN_GAMES) return games_items;
    if (current_screen == SCREEN_SETTINGS) return settings_items;
    return current_screen == SCREEN_MISC ? misc_items : main_items;
}

static size_t *active_selection(void)
{
    if (current_screen == SCREEN_GAMES) return &games_selected;
    if (current_screen == SCREEN_SETTINGS) return &settings_selected;
    return current_screen == SCREEN_MISC ? &misc_selected : &main_selected;
}

static void note_action(uint32_t now_ms)
{
    last_action = now_ms;
    redraw = true;
}

static void change_screen(screen_id_t screen, uint32_t now_ms)
{
    current_screen = screen;
    if (screen == SCREEN_MENU) main_selected = 0U;
    else if (screen == SCREEN_GAMES) games_selected = 0U;
    else if (screen == SCREEN_SETTINGS) settings_selected = 0U;
    else if (screen == SCREEN_MISC) misc_selected = 0U;
    button_direction = 0;
    button_repeating = false;
    held_direction = MOVE_NONE;
    waiting_for_centre = true;
    repeating = false;
    note_action(now_ms);
}

void menu_initialize(uint32_t now_ms)
{
    main_selected = 0;
    games_selected = 0;
    settings_selected = 0;
    misc_selected = 0;
    change_screen(SCREEN_MENU, now_ms);
}

screen_id_t menu_current_screen(void)
{
    return current_screen;
}

const char *menuScreenshotLabel(void)
{
    switch (current_screen) {
        case SCREEN_MENU: return "main-menu";
        case SCREEN_GAMES: return "games-menu";
        case SCREEN_SETTINGS: return "settings-menu";
        case SCREEN_BRIGHTNESS: return "brightness";
        case SCREEN_VOLUME: return "volume";
        case SCREEN_AI_MOVE: return "ai-move";
        case SCREEN_AI_RESTART: return "ai-restart";
        case SCREEN_WIFI: return "wifi";
        case SCREEN_POWER_OFF: return "power-off";
        case SCREEN_RESTART: return "restart";
        case SCREEN_ESP_INFO: return "esp-info";
        case SCREEN_RAM_LOG: return "ram-log";
        case SCREEN_GAME_CONFIG: return "game-config";
        case SCREEN_MISC: return "misc-menu";
        case SCREEN_ABOUT: return "about";
        case SCREEN_MATRIX: return "matrix";
        case SCREEN_BOING_BALL: return "boing-ball";
        case SCREEN_DEMO: return "demo";
        case SCREEN_SIMON: return "simon";
        case SCREEN_CALCULATOR: return "calculator";
        case SCREEN_KALEIDOSCOPE: return "kaleidoscope";
        case SCREEN_MANDELBROT: return "mandelbrot";
        case SCREEN_SLIDESHOW: return "slideshow";
        case SCREEN_WARP_TUNNEL: return "warp-tunnel";
        case SCREEN_CONTROLLER_TEST: return "controller-test";
        case SCREEN_GRAPHICS_TEST: return "graphics-test";
        case SCREEN_FONT_TEST: return "font-test";
        case SCREEN_CHECKERS: return "checkers";
        case SCREEN_OTHELLO: return "othello";
        case SCREEN_CONNECT_FOUR: return "connect-four";
        case SCREEN_BATTLESHIPS: return "battleships";
        case SCREEN_BATTLE_ZONE: return "battle-zone";
        case SCREEN_BOULDER_DASH: return "boulder-dash";
        case SCREEN_BREAKOUT: return "breakout";
        case SCREEN_CRAWLER: return "crawler";
        case SCREEN_MAZE_CHASE: return "maze-chase";
        case SCREEN_BATTERY: return "battery";
        case SCREEN_SD_CARD_TEST: return "file-browser";
        case SCREEN_SPIFFS_TEST: return "spiffs-test";
        case SCREEN_SOUND_TEST: return "sound-test";
        case SCREEN_ASTEROIDS: return "asteroids";
        case SCREEN_TEMPEST: return "tempest";
        case SCREEN_INVADERS: return "invaders";
        case SCREEN_YAHTZEE: return "yahtzee";
        case SCREEN_BLACKJACK: return "blackjack";
        case SCREEN_POKER: return "poker";
        case SCREEN_POKER_DICE: return "poker-dice";
        case SCREEN_PIG: return "pig";
        case SCREEN_MISSILE_COMMAND: return "missile-command";
        case SCREEN_SCRAMBLE: return "scramble";
        case SCREEN_DEFENDER: return "defender";
        case SCREEN_GALAGA: return "galaga";
        case SCREEN_DUNGEON: return "dungeon";
        case SCREEN_HALL_OF_FAME: return "hall-of-fame";
        case SCREEN_CLOCK: return "clock";
        default: return "unknown";
    }
}

size_t menu_count(void)
{
    if (current_screen == SCREEN_GAMES) {
        return sizeof(games_items) / sizeof(games_items[0]);
    }
    if (current_screen == SCREEN_BOULDER_DASH) return 0;
    if (current_screen == SCREEN_BRIGHTNESS || current_screen == SCREEN_VOLUME || current_screen == SCREEN_AI_MOVE || current_screen == SCREEN_AI_RESTART || current_screen == SCREEN_WIFI || current_screen == SCREEN_POWER_OFF || current_screen == SCREEN_RESTART || current_screen == SCREEN_ESP_INFO || current_screen == SCREEN_RAM_LOG || current_screen == SCREEN_GAME_CONFIG || current_screen == SCREEN_CONTROLLER_TEST || current_screen == SCREEN_GRAPHICS_TEST || current_screen == SCREEN_FONT_TEST || current_screen == SCREEN_BATTERY || current_screen == SCREEN_SD_CARD_TEST || current_screen == SCREEN_SPIFFS_TEST || current_screen == SCREEN_SOUND_TEST || current_screen == SCREEN_ABOUT || current_screen == SCREEN_MATRIX || current_screen == SCREEN_BOING_BALL || current_screen == SCREEN_DEMO || current_screen == SCREEN_SIMON || current_screen == SCREEN_CALCULATOR || current_screen == SCREEN_KALEIDOSCOPE || current_screen == SCREEN_MANDELBROT || current_screen == SCREEN_SLIDESHOW || current_screen == SCREEN_WARP_TUNNEL || current_screen == SCREEN_HALL_OF_FAME || current_screen == SCREEN_CLOCK || current_screen == SCREEN_CHECKERS || current_screen == SCREEN_OTHELLO || current_screen == SCREEN_CONNECT_FOUR || current_screen == SCREEN_BATTLESHIPS || current_screen == SCREEN_BATTLE_ZONE || current_screen == SCREEN_BREAKOUT || current_screen == SCREEN_CRAWLER || current_screen == SCREEN_MAZE_CHASE || current_screen == SCREEN_ASTEROIDS || current_screen == SCREEN_TEMPEST || current_screen == SCREEN_INVADERS || current_screen == SCREEN_YAHTZEE || current_screen == SCREEN_BLACKJACK || current_screen == SCREEN_POKER || current_screen == SCREEN_POKER_DICE || current_screen == SCREEN_PIG || current_screen == SCREEN_MISSILE_COMMAND || current_screen == SCREEN_SCRAMBLE || current_screen == SCREEN_DEFENDER || current_screen == SCREEN_GALAGA || current_screen == SCREEN_DUNGEON) {
        return 0;
    }
    if (current_screen == SCREEN_SETTINGS) return sizeof(settings_items) / sizeof(settings_items[0]);
    return current_screen == SCREEN_MISC ? sizeof(misc_items) / sizeof(misc_items[0])
                                       : sizeof(main_items) / sizeof(main_items[0]);
}

size_t menu_selected_item(void)
{
    return *active_selection();
}

const char *menu_title(void)
{
    if (current_screen == SCREEN_GAMES) {
        return "GAMES";
    }
    if (current_screen == SCREEN_SETTINGS) return "SETTINGS";
    return current_screen == SCREEN_MISC ? "MISC" : "MENU";
}

const char *menu_item_text(size_t index)
{
    return index < menu_count() ? active_items()[index].text : "";
}

bool menu_item_is_submenu(size_t index)
{
    return index < menu_count() && active_items()[index].submenu;
}

bool menu_selection_visible(void)
{
    return true;
}

bool menu_take_redraw(void)
{
    const bool needed = redraw;
    redraw = false;
    return needed;
}

void menu_select(uint32_t now_ms)
{
    if (current_screen == SCREEN_BOULDER_DASH) return;
    if (current_screen == SCREEN_BRIGHTNESS || current_screen == SCREEN_VOLUME || current_screen == SCREEN_AI_MOVE || current_screen == SCREEN_AI_RESTART || current_screen == SCREEN_WIFI || current_screen == SCREEN_POWER_OFF || current_screen == SCREEN_RESTART || current_screen == SCREEN_ESP_INFO || current_screen == SCREEN_RAM_LOG || current_screen == SCREEN_GAME_CONFIG || current_screen == SCREEN_CONTROLLER_TEST || current_screen == SCREEN_GRAPHICS_TEST || current_screen == SCREEN_FONT_TEST || current_screen == SCREEN_BATTERY || current_screen == SCREEN_SD_CARD_TEST || current_screen == SCREEN_SPIFFS_TEST || current_screen == SCREEN_ABOUT || current_screen == SCREEN_MATRIX || current_screen == SCREEN_BOING_BALL || current_screen == SCREEN_SIMON || current_screen == SCREEN_CALCULATOR || current_screen == SCREEN_KALEIDOSCOPE || current_screen == SCREEN_MANDELBROT || current_screen == SCREEN_SLIDESHOW || current_screen == SCREEN_HALL_OF_FAME || current_screen == SCREEN_CLOCK || current_screen == SCREEN_CHECKERS || current_screen == SCREEN_OTHELLO || current_screen == SCREEN_CONNECT_FOUR || current_screen == SCREEN_BATTLESHIPS || current_screen == SCREEN_BATTLE_ZONE || current_screen == SCREEN_BREAKOUT || current_screen == SCREEN_CRAWLER || current_screen == SCREEN_ASTEROIDS || current_screen == SCREEN_TEMPEST || current_screen == SCREEN_INVADERS || current_screen == SCREEN_YAHTZEE || current_screen == SCREEN_BLACKJACK || current_screen == SCREEN_POKER || current_screen == SCREEN_POKER_DICE || current_screen == SCREEN_PIG || current_screen == SCREEN_MISSILE_COMMAND || current_screen == SCREEN_SCRAMBLE || current_screen == SCREEN_DEFENDER || current_screen == SCREEN_GALAGA || current_screen == SCREEN_DUNGEON) {
        return;
    }
    audioPlay(AUDIO_SFX_MENU_SELECT);
    change_screen(active_items()[*active_selection()].target, now_ms);
}

void menu_back(uint32_t now_ms)
{
    if (current_screen == SCREEN_BOULDER_DASH) {
        change_screen(SCREEN_GAMES, now_ms);
        return;
    }
    change_screen((current_screen == SCREEN_CHECKERS || current_screen == SCREEN_OTHELLO || current_screen == SCREEN_CONNECT_FOUR || current_screen == SCREEN_BATTLESHIPS || current_screen == SCREEN_BATTLE_ZONE || current_screen == SCREEN_BREAKOUT || current_screen == SCREEN_CRAWLER || current_screen == SCREEN_MAZE_CHASE || current_screen == SCREEN_ASTEROIDS || current_screen == SCREEN_TEMPEST || current_screen == SCREEN_INVADERS || current_screen == SCREEN_MISSILE_COMMAND || current_screen == SCREEN_SCRAMBLE || current_screen == SCREEN_SIMON || current_screen == SCREEN_DEFENDER || current_screen == SCREEN_GALAGA || current_screen == SCREEN_YAHTZEE || current_screen == SCREEN_BLACKJACK || current_screen == SCREEN_POKER || current_screen == SCREEN_POKER_DICE || current_screen == SCREEN_PIG || current_screen == SCREEN_DUNGEON) ? SCREEN_GAMES :
                  (current_screen == SCREEN_CLOCK || current_screen == SCREEN_MATRIX || current_screen == SCREEN_BOING_BALL || current_screen == SCREEN_DEMO || current_screen == SCREEN_CALCULATOR || current_screen == SCREEN_KALEIDOSCOPE || current_screen == SCREEN_MANDELBROT || current_screen == SCREEN_SLIDESHOW || current_screen == SCREEN_WARP_TUNNEL) ? SCREEN_MISC :
                  (current_screen == SCREEN_BRIGHTNESS || current_screen == SCREEN_VOLUME || current_screen == SCREEN_AI_MOVE || current_screen == SCREEN_AI_RESTART || current_screen == SCREEN_WIFI || current_screen == SCREEN_POWER_OFF || current_screen == SCREEN_RESTART || current_screen == SCREEN_ESP_INFO || current_screen == SCREEN_RAM_LOG || current_screen == SCREEN_GAME_CONFIG || current_screen == SCREEN_CONTROLLER_TEST || current_screen == SCREEN_GRAPHICS_TEST || current_screen == SCREEN_FONT_TEST || current_screen == SCREEN_BATTERY || current_screen == SCREEN_SD_CARD_TEST || current_screen == SCREEN_SPIFFS_TEST || current_screen == SCREEN_SOUND_TEST) ? SCREEN_SETTINGS : SCREEN_MENU,
                  now_ms);
}

void menu_home(uint32_t now_ms)
{
    change_screen(SCREEN_MENU, now_ms);
}

static void move_selection(menu_direction_t direction, uint32_t now_ms)
{
    const size_t count = menu_count();
    size_t *selection = active_selection();
    const size_t previous_selection = *selection;
    if (count == 0) {
        return;
    }

    if (current_screen == SCREEN_MENU || current_screen == SCREEN_GAMES ||
        current_screen == SCREEN_SETTINGS || current_screen == SCREEN_MISC) {
        const size_t maximum_rows_per_column = 4U;
        const size_t items_per_page = maximum_rows_per_column * 2U;
        const size_t page_count = (count + items_per_page - 1U) / items_per_page;
        size_t page = *selection / items_per_page;
        size_t position = *selection % items_per_page;
        const size_t page_start = page * items_per_page;
        const size_t page_items = count - page_start < items_per_page ?
            count - page_start : items_per_page;
        size_t rows_per_column = (page_items + 1U) / 2U;
        if (rows_per_column > maximum_rows_per_column) rows_per_column = maximum_rows_per_column;
        size_t column = position / rows_per_column;
        size_t row = position % rows_per_column;
        size_t rows = page_items > column * rows_per_column ?
            page_items - column * rows_per_column : 0U;
        if (rows > rows_per_column) rows = rows_per_column;

        if (direction == MOVE_LEFT || direction == MOVE_RIGHT) {
            const size_t other_column = column == 0U ? 1U : 0U;
            size_t other_rows = page_items > other_column * rows_per_column ?
                page_items - other_column * rows_per_column : 0U;
            if (other_rows > rows_per_column) other_rows = rows_per_column;
            if (other_rows > 0U) {
                column = other_column;
                if (row >= other_rows) row = other_rows - 1U;
            }
        } else if (direction == MOVE_UP || direction == MOVE_DOWN) {
            if ((direction == MOVE_UP && row > 0U) ||
                (direction == MOVE_DOWN && row + 1U < rows)) {
                row = direction == MOVE_UP ? row - 1U : row + 1U;
            } else {
                do {
                    page = direction == MOVE_UP ? (page + page_count - 1U) % page_count :
                                                  (page + 1U) % page_count;
                    const size_t next_start = page * items_per_page;
                    const size_t next_items = count - next_start < items_per_page ?
                        count - next_start : items_per_page;
                    rows_per_column = (next_items + 1U) / 2U;
                    if (rows_per_column > maximum_rows_per_column)
                        rows_per_column = maximum_rows_per_column;
                    rows = next_items > column * rows_per_column ?
                        next_items - column * rows_per_column : 0U;
                    if (rows > rows_per_column) rows = rows_per_column;
                } while (rows == 0U);
                row = direction == MOVE_UP ? rows - 1U : 0U;
            }
        } else {
            return;
        }
        *selection = page * items_per_page + column * rows_per_column + row;
    } else {
        return;
    }
    if (*selection != previous_selection) {
        audioPlay(AUDIO_SFX_MENU_MOVE);
    }
    note_action(now_ms);
}

bool menu_update_buttons(bool left, bool right, uint32_t now_ms)
{
    if (!left && !right) {
        button_direction = 0;
        button_repeating = false;
        return false;
    }
    // Give buttons their own repeat timing without requiring joystick centring.
    held_direction = MOVE_NONE;
    repeating = false;
    const int direction = (int)right - (int)left;
    if (direction == 0 || menu_count() == 0) {
        button_direction = 0;
        button_repeating = false;
        return true;
    }
    if (direction == button_direction && now_ms - last_button_move <
        (button_repeating ? JOYSTICK_REPEAT_MS : JOYSTICK_REPEAT_START_MS)) {
        return true;
    }
    button_repeating = direction == button_direction;
    button_direction = direction;
    last_button_move = now_ms;
    move_selection(direction < 0 ? MOVE_UP : MOVE_DOWN, now_ms);
    return true;
}

void menu_update_joystick(int x_offset, int y_offset, bool valid, uint32_t now_ms)
{
    if (current_screen == SCREEN_BOULDER_DASH) return;
    if (current_screen == SCREEN_POWER_OFF || current_screen == SCREEN_GAME_CONFIG || current_screen == SCREEN_CONTROLLER_TEST || current_screen == SCREEN_GRAPHICS_TEST || current_screen == SCREEN_FONT_TEST || current_screen == SCREEN_BATTERY || current_screen == SCREEN_SD_CARD_TEST || current_screen == SCREEN_SPIFFS_TEST || current_screen == SCREEN_SOUND_TEST || current_screen == SCREEN_ABOUT || current_screen == SCREEN_HALL_OF_FAME || current_screen == SCREEN_CLOCK || current_screen == SCREEN_MATRIX || current_screen == SCREEN_BOING_BALL || current_screen == SCREEN_DEMO || current_screen == SCREEN_SIMON || current_screen == SCREEN_CALCULATOR || current_screen == SCREEN_KALEIDOSCOPE || current_screen == SCREEN_MANDELBROT || current_screen == SCREEN_SLIDESHOW || current_screen == SCREEN_WARP_TUNNEL || current_screen == SCREEN_CHECKERS || current_screen == SCREEN_OTHELLO || current_screen == SCREEN_CONNECT_FOUR || current_screen == SCREEN_BATTLESHIPS || current_screen == SCREEN_BATTLE_ZONE || current_screen == SCREEN_BREAKOUT || current_screen == SCREEN_CRAWLER || current_screen == SCREEN_MAZE_CHASE || current_screen == SCREEN_ASTEROIDS || current_screen == SCREEN_TEMPEST || current_screen == SCREEN_INVADERS || current_screen == SCREEN_YAHTZEE || current_screen == SCREEN_BLACKJACK || current_screen == SCREEN_POKER || current_screen == SCREEN_POKER_DICE || current_screen == SCREEN_PIG || current_screen == SCREEN_MISSILE_COMMAND || current_screen == SCREEN_SCRAMBLE || current_screen == SCREEN_DEFENDER || current_screen == SCREEN_GALAGA || current_screen == SCREEN_DUNGEON) {
        return;
    }
    if (!valid) {
        waiting_for_centre = true;
        held_direction = MOVE_NONE;
        return;
    }

    const int x_magnitude = x_offset < 0 ? -x_offset : x_offset;
    const int y_magnitude = y_offset < 0 ? -y_offset : y_offset;
    if (x_magnitude <= JOYSTICK_RELEASE && y_magnitude <= JOYSTICK_RELEASE) {
        waiting_for_centre = false;
        held_direction = MOVE_NONE;
        repeating = false;
        return;
    }
    if (waiting_for_centre) {
        return;
    }

    menu_direction_t direction = MOVE_NONE;
    if (x_magnitude >= y_magnitude && x_magnitude >= JOYSTICK_ENTER) {
        direction = x_offset > 0 ? MOVE_RIGHT : MOVE_LEFT;
    } else if (y_magnitude >= JOYSTICK_ENTER) {
        direction = y_offset > 0 ? MOVE_UP : MOVE_DOWN;
    }
    if (direction == MOVE_NONE) {
        return;
    }

    if (direction != held_direction) {
        held_direction = direction;
        repeating = false;
        last_move = now_ms;
        move_selection(direction, now_ms);
    } else if (now_ms - last_move >=
               (repeating ? JOYSTICK_REPEAT_MS : JOYSTICK_REPEAT_START_MS)) {
        repeating = true;
        last_move = now_ms;
        move_selection(direction, now_ms);
    }
}

void menu_update(uint32_t now_ms)
{
    if (current_screen == SCREEN_BOULDER_DASH) return;
    if (current_screen == SCREEN_POWER_OFF || current_screen == SCREEN_GAME_CONFIG || current_screen == SCREEN_CONTROLLER_TEST || current_screen == SCREEN_GRAPHICS_TEST || current_screen == SCREEN_FONT_TEST || current_screen == SCREEN_BATTERY || current_screen == SCREEN_SD_CARD_TEST || current_screen == SCREEN_SPIFFS_TEST || current_screen == SCREEN_SOUND_TEST || current_screen == SCREEN_ABOUT || current_screen == SCREEN_HALL_OF_FAME || current_screen == SCREEN_CLOCK || current_screen == SCREEN_MATRIX || current_screen == SCREEN_BOING_BALL || current_screen == SCREEN_DEMO || current_screen == SCREEN_SIMON || current_screen == SCREEN_CALCULATOR || current_screen == SCREEN_KALEIDOSCOPE || current_screen == SCREEN_MANDELBROT || current_screen == SCREEN_SLIDESHOW || current_screen == SCREEN_WARP_TUNNEL || current_screen == SCREEN_CHECKERS || current_screen == SCREEN_OTHELLO || current_screen == SCREEN_CONNECT_FOUR || current_screen == SCREEN_BATTLESHIPS || current_screen == SCREEN_BATTLE_ZONE || current_screen == SCREEN_BREAKOUT || current_screen == SCREEN_CRAWLER || current_screen == SCREEN_MAZE_CHASE || current_screen == SCREEN_ASTEROIDS || current_screen == SCREEN_TEMPEST || current_screen == SCREEN_INVADERS || current_screen == SCREEN_YAHTZEE || current_screen == SCREEN_BLACKJACK || current_screen == SCREEN_POKER || current_screen == SCREEN_POKER_DICE || current_screen == SCREEN_PIG || current_screen == SCREEN_MISSILE_COMMAND || current_screen == SCREEN_SCRAMBLE || current_screen == SCREEN_DEFENDER || current_screen == SCREEN_GALAGA || current_screen == SCREEN_DUNGEON) {
        return;
    }
    if (now_ms - last_action >= MENU_TIMEOUT_MS) {
        // Timeout opens ABOUT because it is the established route into the Demo.
        change_screen(SCREEN_ABOUT, now_ms);
        return;
    }
}