added buttons to main state

This commit is contained in:
2025-11-22 12:16:47 +01:00
parent 838b5b1836
commit 4e69ed9742
12 changed files with 644 additions and 60 deletions

View File

@ -7,6 +7,7 @@
#include <SDL3/SDL.h>
#include <cstdio>
#include <algorithm>
#include <array>
#include <cmath>
// Use dynamic logical dimensions from GlobalState instead of hardcoded values
@ -22,38 +23,115 @@ MenuState::MenuState(StateContext& ctx) : State(ctx) {}
void MenuState::onEnter() {
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "MenuState::onEnter called");
if (ctx.showExitConfirmPopup) {
*ctx.showExitConfirmPopup = false;
}
if (ctx.exitPopupSelectedButton) {
*ctx.exitPopupSelectedButton = 1;
}
}
void MenuState::onExit() {
if (ctx.showExitConfirmPopup) {
*ctx.showExitConfirmPopup = false;
}
}
void MenuState::handleEvent(const SDL_Event& e) {
// Keyboard navigation for menu buttons
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
auto setExitSelection = [&](int value) {
if (ctx.exitPopupSelectedButton) {
*ctx.exitPopupSelectedButton = value;
}
};
auto getExitSelection = [&]() -> int {
return ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1;
};
auto isExitPromptVisible = [&]() -> bool {
return ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup;
};
auto setExitPrompt = [&](bool visible) {
if (ctx.showExitConfirmPopup) {
*ctx.showExitConfirmPopup = visible;
}
};
if (isExitPromptVisible()) {
switch (e.key.scancode) {
case SDL_SCANCODE_LEFT:
case SDL_SCANCODE_UP:
setExitSelection(0);
return;
case SDL_SCANCODE_RIGHT:
case SDL_SCANCODE_DOWN:
setExitSelection(1);
return;
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
case SDL_SCANCODE_SPACE:
if (getExitSelection() == 0) {
setExitPrompt(false);
if (ctx.requestQuit) {
ctx.requestQuit();
} else {
SDL_Event quit{};
quit.type = SDL_EVENT_QUIT;
SDL_PushEvent(&quit);
}
} else {
setExitPrompt(false);
}
return;
case SDL_SCANCODE_ESCAPE:
setExitPrompt(false);
setExitSelection(1);
return;
default:
return;
}
}
switch (e.key.scancode) {
case SDL_SCANCODE_LEFT:
case SDL_SCANCODE_UP:
selectedButton = 0; // PLAY
{
const int total = 4;
selectedButton = (selectedButton + total - 1) % total;
break;
}
case SDL_SCANCODE_RIGHT:
case SDL_SCANCODE_DOWN:
selectedButton = 1; // LEVEL
{
const int total = 4;
selectedButton = (selectedButton + 1) % total;
break;
}
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
case SDL_SCANCODE_SPACE:
// Activate selected button
if (selectedButton == 0) {
// PLAY button - transition to Playing state
if (ctx.stateManager) {
ctx.stateManager->setState(AppState::Playing);
}
} else {
// LEVEL button - transition to LevelSelector state
if (ctx.stateManager) {
ctx.stateManager->setState(AppState::LevelSelector);
}
if (!ctx.stateManager) {
break;
}
switch (selectedButton) {
case 0:
ctx.stateManager->setState(AppState::Playing);
break;
case 1:
ctx.stateManager->setState(AppState::LevelSelector);
break;
case 2:
ctx.stateManager->setState(AppState::Options);
break;
case 3:
setExitPrompt(true);
setExitSelection(1);
break;
}
break;
case SDL_SCANCODE_ESCAPE:
setExitPrompt(true);
setExitSelection(1);
break;
default:
break;
@ -204,32 +282,88 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi
char levelBtnText[32];
int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0;
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
// Draw simple styled buttons (replicating menu_drawMenuButton)
auto drawMenuButtonLocal = [&](SDL_Renderer* r, FontAtlas& font, float cx, float cy, float w, float h, const std::string& label, SDL_Color bg, SDL_Color border, bool selected){
float x = cx - w/2; float y = cy - h/2;
// If selected, draw a glow effect
if (selected) {
SDL_SetRenderDrawColor(r, 255, 220, 0, 100);
SDL_SetRenderDrawColor(r, 255, 220, 0, 110);
SDL_FRect glow{ x-10, y-10, w+20, h+20 };
SDL_RenderFillRect(r, &glow);
}
SDL_SetRenderDrawColor(r, border.r, border.g, border.b, border.a);
SDL_FRect br{ x-6, y-6, w+12, h+12 }; SDL_RenderFillRect(r, &br);
SDL_SetRenderDrawColor(r, 255,255,255,255); SDL_FRect br2{ x-4, y-4, w+8, h+8 }; SDL_RenderFillRect(r, &br2);
SDL_SetRenderDrawColor(r, bg.r, bg.g, bg.b, bg.a); SDL_FRect br3{ x, y, w, h }; SDL_RenderFillRect(r, &br3);
float textScale = 1.6f; float approxCharW = 12.0f * textScale; float textW = label.length() * approxCharW; float tx = x + (w - textW) / 2.0f; float ty = y + (h - 20.0f * textScale) / 2.0f;
font.draw(r, tx+2, ty+2, label, textScale, SDL_Color{0,0,0,180});
float textScale = 1.5f; float approxCharW = 12.0f * textScale; float textW = label.length() * approxCharW; float tx = x + (w - textW) / 2.0f; float ty = y + (h - 20.0f * textScale) / 2.0f;
font.draw(r, tx+2, ty+2, label, textScale, SDL_Color{0,0,0,200});
font.draw(r, tx, ty, label, textScale, SDL_Color{255,255,255,255});
};
drawMenuButtonLocal(renderer, *ctx.pixelFont, btnX - btnW * 0.6f, btnY, btnW, btnH, std::string("PLAY"), SDL_Color{60,180,80,255}, SDL_Color{30,120,40,255}, selectedButton == 0);
{
struct MenuButtonDef {
SDL_Color bg;
SDL_Color border;
std::string label;
};
std::array<MenuButtonDef, 4> buttons = {
MenuButtonDef{ SDL_Color{60,180,80,255}, SDL_Color{30,120,40,255}, "PLAY" },
MenuButtonDef{ SDL_Color{40,140,240,255}, SDL_Color{20,100,200,255}, levelBtnText },
MenuButtonDef{ SDL_Color{130,80,210,255}, SDL_Color{90,40,170,255}, "OPTIONS" },
MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" }
};
float spacing = isSmall ? btnW * 1.15f : btnW * 1.05f;
for (size_t i = 0; i < buttons.size(); ++i) {
float offset = (static_cast<float>(i) - 1.5f) * spacing;
float cx = btnX + offset;
drawMenuButtonLocal(renderer, *ctx.pixelFont, cx, btnY, btnW, btnH, buttons[i].label, buttons[i].bg, buttons[i].border, selectedButton == static_cast<int>(i));
}
drawMenuButtonLocal(renderer, *ctx.pixelFont, btnX + btnW * 0.6f, btnY, btnW, btnH, std::string(levelBtnText), SDL_Color{40,140,240,255}, SDL_Color{20,100,200,255}, selectedButton == 1);
{
FILE* f = fopen("tetris_trace.log", "a"); if (f) { fprintf(f, "MenuState::render after draw LEVEL button\n"); fclose(f); }
}
if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) {
int selection = ctx.exitPopupSelectedButton ? *ctx.exitPopupSelectedButton : 1;
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 150);
SDL_FRect overlay{contentOffsetX, contentOffsetY, LOGICAL_W, LOGICAL_H};
SDL_RenderFillRect(renderer, &overlay);
float popupW = 420.0f;
float popupH = 230.0f;
float popupX = (LOGICAL_W - popupW) * 0.5f + contentOffsetX;
float popupY = (LOGICAL_H - popupH) * 0.5f + contentOffsetY;
SDL_SetRenderDrawColor(renderer, 20, 30, 50, 240);
SDL_FRect popup{popupX, popupY, popupW, popupH};
SDL_RenderFillRect(renderer, &popup);
SDL_SetRenderDrawColor(renderer, 90, 140, 220, 255);
SDL_RenderRect(renderer, &popup);
FontAtlas* titleFont = ctx.font ? ctx.font : ctx.pixelFont;
if (titleFont) {
titleFont->draw(renderer, popupX + 40.0f, popupY + 30.0f, "EXIT GAME?", 1.8f, SDL_Color{255, 230, 140, 255});
titleFont->draw(renderer, popupX + 40.0f, popupY + 80.0f, "Are you sure you want to quit?", 1.1f, SDL_Color{200, 210, 230, 255});
}
auto drawChoice = [&](const char* label, float cx, int idx) {
float btnW2 = 140.0f;
float btnH2 = 50.0f;
float x = cx - btnW2 / 2.0f;
float y = popupY + popupH - btnH2 - 30.0f;
bool selected = (selection == idx);
SDL_Color bg = selected ? SDL_Color{220, 180, 60, 255} : SDL_Color{80, 110, 160, 255};
SDL_Color border = selected ? SDL_Color{255, 220, 120, 255} : SDL_Color{40, 60, 100, 255};
SDL_SetRenderDrawColor(renderer, border.r, border.g, border.b, border.a);
SDL_FRect br{ x-4, y-4, btnW2+8, btnH2+8 };
SDL_RenderFillRect(renderer, &br);
SDL_SetRenderDrawColor(renderer, bg.r, bg.g, bg.b, bg.a);
SDL_FRect body{ x, y, btnW2, btnH2 };
SDL_RenderFillRect(renderer, &body);
if (titleFont) {
titleFont->draw(renderer, x + 20.0f, y + 10.0f, label, 1.2f, SDL_Color{15, 20, 35, 255});
}
};
drawChoice("YES", popupX + popupW * 0.3f, 0);
drawChoice("NO", popupX + popupW * 0.7f, 1);
}
// Popups (settings only - level popup is now a separate state)

View File

@ -12,5 +12,5 @@ public:
void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override;
private:
int selectedButton = 0; // 0 = PLAY, 1 = LEVEL
int selectedButton = 0; // 0 = PLAY, 1 = LEVEL, 2 = OPTIONS, 3 = EXIT
};

238
src/states/OptionsState.cpp Normal file
View File

@ -0,0 +1,238 @@
#include "OptionsState.h"
#include "../core/state/StateManager.h"
#include "../graphics/ui/Font.h"
#include <SDL3/SDL.h>
#include <cctype>
OptionsState::OptionsState(StateContext& ctx) : State(ctx) {}
void OptionsState::onEnter() {
m_selectedField = Field::PlayerName;
m_cursorTimer = 0.0;
m_cursorVisible = true;
if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) {
SDL_StartTextInput(focusWin);
}
}
void OptionsState::onExit() {
if (SDL_Window* focusWin = SDL_GetKeyboardFocus()) {
SDL_StopTextInput(focusWin);
}
}
void OptionsState::handleEvent(const SDL_Event& e) {
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
switch (e.key.scancode) {
case SDL_SCANCODE_ESCAPE:
exitToMenu();
return;
case SDL_SCANCODE_UP:
case SDL_SCANCODE_W:
moveSelection(-1);
return;
case SDL_SCANCODE_DOWN:
case SDL_SCANCODE_S:
moveSelection(1);
return;
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
case SDL_SCANCODE_SPACE:
activateSelection();
return;
case SDL_SCANCODE_LEFT:
case SDL_SCANCODE_RIGHT:
if (m_selectedField == Field::Fullscreen) {
toggleFullscreen();
return;
}
break;
default:
break;
}
if (m_selectedField == Field::PlayerName) {
handleNameInput(e);
}
} else if (e.type == SDL_EVENT_TEXT_INPUT && m_selectedField == Field::PlayerName) {
handleNameInput(e);
}
}
void OptionsState::update(double frameMs) {
m_cursorTimer += frameMs;
if (m_cursorTimer >= 450.0) {
m_cursorTimer = 0.0;
m_cursorVisible = !m_cursorVisible;
}
}
void OptionsState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) {
if (!renderer) return;
const float LOGICAL_W = 1200.0f;
const float LOGICAL_H = 1000.0f;
float winW = static_cast<float>(logicalVP.w);
float winH = static_cast<float>(logicalVP.h);
float contentW = LOGICAL_W * logicalScale;
float contentH = LOGICAL_H * logicalScale;
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
SDL_SetRenderDrawColor(renderer, 0, 0, 0, 140);
SDL_FRect dim{contentOffsetX, contentOffsetY, LOGICAL_W, LOGICAL_H};
SDL_RenderFillRect(renderer, &dim);
const float panelW = 560.0f;
const float panelH = 420.0f;
SDL_FRect panel{
(LOGICAL_W - panelW) * 0.5f + contentOffsetX,
(LOGICAL_H - panelH) * 0.5f + contentOffsetY,
panelW,
panelH
};
SDL_SetRenderDrawColor(renderer, 15, 20, 34, 230);
SDL_RenderFillRect(renderer, &panel);
SDL_SetRenderDrawColor(renderer, 70, 110, 190, 255);
SDL_RenderRect(renderer, &panel);
FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font;
FontAtlas* bodyFont = ctx.font ? ctx.font : ctx.pixelFont;
auto drawText = [&](FontAtlas* font, float x, float y, const std::string& text, float scale, SDL_Color color) {
if (!font) return;
font->draw(renderer, x, y, text, scale, color);
};
drawText(titleFont, panel.x + 24.0f, panel.y + 24.0f, "OPTIONS", 2.0f, {255, 230, 120, 255});
auto drawField = [&](Field field, float y, const std::string& label, const std::string& value) {
bool selected = (field == m_selectedField);
SDL_FRect row{panel.x + 20.0f, y - 10.0f, panel.w - 40.0f, 70.0f};
SDL_SetRenderDrawColor(renderer, selected ? 40 : 24, selected ? 80 : 36, selected ? 120 : 48, 220);
SDL_RenderFillRect(renderer, &row);
SDL_SetRenderDrawColor(renderer, 80, 120, 200, 255);
SDL_RenderRect(renderer, &row);
drawText(bodyFont, row.x + 18.0f, row.y + 12.0f, label, 1.4f, {200, 220, 255, 255});
drawText(bodyFont, row.x + 18.0f, row.y + 36.0f, value, 1.6f, {255, 255, 255, 255});
};
std::string nameDisplay = playerName();
if (nameDisplay.empty()) {
nameDisplay = "<ENTER NAME>";
}
if (m_selectedField == Field::PlayerName && m_cursorVisible) {
nameDisplay.push_back('_');
}
drawField(Field::PlayerName, panel.y + 90.0f, "PLAYER NAME", nameDisplay);
std::string fullscreenValue = isFullscreen() ? "ON" : "OFF";
drawField(Field::Fullscreen, panel.y + 180.0f, "FULLSCREEN", fullscreenValue);
drawField(Field::Back, panel.y + 270.0f, "BACK", "RETURN TO MENU");
drawText(bodyFont, panel.x + 24.0f, panel.y + panel.h - 50.0f,
"ARROWS = NAV ENTER = SELECT ESC = MENU", 1.1f, {190, 200, 215, 255});
drawText(bodyFont, panel.x + 24.0f, panel.y + panel.h - 26.0f,
"LETTERS/NUMBERS TYPE INTO NAME FIELD", 1.0f, {150, 160, 180, 255});
}
void OptionsState::moveSelection(int delta) {
int idx = static_cast<int>(m_selectedField);
int total = 3;
idx = (idx + delta + total) % total;
m_selectedField = static_cast<Field>(idx);
}
void OptionsState::activateSelection() {
switch (m_selectedField) {
case Field::PlayerName:
// Nothing to do; typing is always enabled
break;
case Field::Fullscreen:
toggleFullscreen();
break;
case Field::Back:
exitToMenu();
break;
}
}
void OptionsState::handleNameInput(const SDL_Event& e) {
if (!ctx.playerName) return;
if (e.type == SDL_EVENT_KEY_DOWN) {
if (e.key.scancode == SDL_SCANCODE_BACKSPACE) {
removeCharacter();
} else if (e.key.scancode == SDL_SCANCODE_SPACE) {
addCharacter(' ');
} else {
SDL_Keymod mods = SDL_GetModState();
SDL_Keycode keycode = SDL_GetKeyFromScancode(e.key.scancode, mods, true);
bool shift = (mods & SDL_KMOD_SHIFT) != 0;
char c = static_cast<char>(keycode);
if (keycode >= 'a' && keycode <= 'z') {
c = shift ? static_cast<char>(std::toupper(c)) : static_cast<char>(std::toupper(c));
addCharacter(c);
} else if (keycode >= '0' && keycode <= '9') {
addCharacter(static_cast<char>(keycode));
}
}
} else if (e.type == SDL_EVENT_TEXT_INPUT) {
const char* text = e.text.text;
while (*text) {
unsigned char c = static_cast<unsigned char>(*text);
if (std::isalnum(c) || c == ' ') {
addCharacter(static_cast<char>(std::toupper(c)));
}
++text;
}
}
}
void OptionsState::addCharacter(char c) {
if (!ctx.playerName) return;
if (c == '\0') return;
if (c == ' ' && ctx.playerName->empty()) return;
if (ctx.playerName->size() >= MAX_NAME_LENGTH) return;
ctx.playerName->push_back(c);
}
void OptionsState::removeCharacter() {
if (!ctx.playerName || ctx.playerName->empty()) return;
ctx.playerName->pop_back();
}
void OptionsState::toggleFullscreen() {
bool nextState = !isFullscreen();
if (ctx.applyFullscreen) {
ctx.applyFullscreen(nextState);
}
if (ctx.fullscreenFlag) {
*ctx.fullscreenFlag = nextState;
}
}
void OptionsState::exitToMenu() {
if (ctx.stateManager) {
ctx.stateManager->setState(AppState::Menu);
}
}
const std::string& OptionsState::playerName() const {
static std::string empty;
return ctx.playerName ? *ctx.playerName : empty;
}
bool OptionsState::isFullscreen() const {
if (ctx.queryFullscreen) {
return ctx.queryFullscreen();
}
return ctx.fullscreenFlag ? *ctx.fullscreenFlag : false;
}

35
src/states/OptionsState.h Normal file
View File

@ -0,0 +1,35 @@
#pragma once
#include "State.h"
class OptionsState : public State {
public:
explicit OptionsState(StateContext& ctx);
void onEnter() override;
void onExit() override;
void handleEvent(const SDL_Event& e) override;
void update(double frameMs) override;
void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override;
private:
enum class Field : int {
PlayerName = 0,
Fullscreen = 1,
Back = 2
};
static constexpr int MAX_NAME_LENGTH = 12;
Field m_selectedField = Field::PlayerName;
double m_cursorTimer = 0.0;
bool m_cursorVisible = true;
void moveSelection(int delta);
void activateSelection();
void handleNameInput(const SDL_Event& e);
void addCharacter(char c);
void removeCharacter();
void toggleFullscreen();
void exitToMenu();
const std::string& playerName() const;
bool isFullscreen() const;
};

View File

@ -3,6 +3,8 @@
#include <SDL3/SDL.h>
#include <memory>
#include <vector>
#include <functional>
#include <string>
// Forward declarations for frequently used types
class Game;
@ -48,6 +50,11 @@ struct StateContext {
bool* showSettingsPopup = nullptr;
bool* showExitConfirmPopup = nullptr; // If true, show "Exit game?" confirmation while playing
int* exitPopupSelectedButton = nullptr; // 0 = YES, 1 = NO (default)
std::string* playerName = nullptr; // Shared player name buffer for highscores/options
bool* fullscreenFlag = nullptr; // Tracks current fullscreen state when available
std::function<void(bool)> applyFullscreen; // Allows states to request fullscreen changes
std::function<bool()> queryFullscreen; // Optional callback if fullscreenFlag is not reliable
std::function<void()> requestQuit; // Allows menu/option states to close the app gracefully
// Pointer to the application's StateManager so states can request transitions
StateManager* stateManager = nullptr;
};