diff --git a/src/graphics/renderers/GameRenderer.cpp b/src/graphics/renderers/GameRenderer.cpp index 0c5703a..137c88c 100644 --- a/src/graphics/renderers/GameRenderer.cpp +++ b/src/graphics/renderers/GameRenderer.cpp @@ -122,7 +122,8 @@ void GameRenderer::renderPlayingState( float logicalScale, float winW, float winH, - bool showExitConfirmPopup + bool showExitConfirmPopup, + int exitPopupSelectedButton ) { if (!game || !pixelFont) return; @@ -460,6 +461,10 @@ void GameRenderer::renderPlayingState( float btnY = popupY + popupH - 60.0f; // YES button + if (exitPopupSelectedButton == 0) { + // Draw glow for selected YES button + drawRectWithOffset(yesX - 6.0f, btnY - 6.0f, btnW + 12.0f, btnH + 12.0f, {255, 220, 0, 100}); + } drawRectWithOffset(yesX - 2.0f, btnY - 2.0f, btnW + 4.0f, btnH + 4.0f, {100, 120, 140, 255}); drawRectWithOffset(yesX, btnY, btnW, btnH, {200, 60, 60, 255}); const std::string yes = "YES"; @@ -469,6 +474,10 @@ void GameRenderer::renderPlayingState( yes, 1.0f, {255, 255, 255, 255}); // NO button + if (exitPopupSelectedButton == 1) { + // Draw glow for selected NO button + drawRectWithOffset(noX - 6.0f, btnY - 6.0f, btnW + 12.0f, btnH + 12.0f, {255, 220, 0, 100}); + } drawRectWithOffset(noX - 2.0f, btnY - 2.0f, btnW + 4.0f, btnH + 4.0f, {100, 120, 140, 255}); drawRectWithOffset(noX, btnY, btnW, btnH, {80, 140, 80, 255}); const std::string no = "NO"; diff --git a/src/graphics/renderers/GameRenderer.h b/src/graphics/renderers/GameRenderer.h index e9bd40e..6b72bb3 100644 --- a/src/graphics/renderers/GameRenderer.h +++ b/src/graphics/renderers/GameRenderer.h @@ -26,7 +26,8 @@ public: float logicalScale, float winW, float winH, - bool showExitConfirmPopup + bool showExitConfirmPopup, + int exitPopupSelectedButton = 1 // 0=YES, 1=NO ); private: diff --git a/src/states/LevelSelectorState.cpp b/src/states/LevelSelectorState.cpp index 6ee45e9..7c5e918 100644 --- a/src/states/LevelSelectorState.cpp +++ b/src/states/LevelSelectorState.cpp @@ -193,6 +193,8 @@ void LevelSelectorState::handleEvent(const SDL_Event& e) { // convert mouse to logical coords (viewport is already centered) float lx = (float(e.button.x) - float(lastLogicalVP.x)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f); float ly = (float(e.button.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f); + + // Use same panel calculation as render (centered) SDL_FRect panel = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f); Grid g = MakeGrid(panel); int hit = HitTest(g, int(lx), int(ly)); @@ -210,6 +212,8 @@ void LevelSelectorState::handleEvent(const SDL_Event& e) { // convert mouse to logical coords (viewport is already centered) float lx = (float(e.motion.x) - float(lastLogicalVP.x)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f); float ly = (float(e.motion.y) - float(lastLogicalVP.y)) / (lastLogicalScale > 0.f ? lastLogicalScale : 1.f); + + // Use same panel calculation as render (centered) SDL_FRect panel = DrawPanel(nullptr, LOGICAL_W, LOGICAL_H, /*draw=*/false, 0.f, 0.f); Grid g = MakeGrid(panel); hoveredLevel = HitTest(g, int(lx), int(ly)); @@ -225,28 +229,30 @@ void LevelSelectorState::render(SDL_Renderer* renderer, float logicalScale, SDL_ // Cache for input conversion lastLogicalScale = logicalScale; lastLogicalVP = logicalVP; - drawLevelSelectionPopup(renderer); + drawLevelSelectionPopup(renderer, logicalScale, logicalVP); } -void LevelSelectorState::drawLevelSelectionPopup(SDL_Renderer* renderer) { +void LevelSelectorState::drawLevelSelectionPopup(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) { if (!renderer) return; - // Get dynamic logical dimensions - const int LOGICAL_W = GlobalState::instance().getLogicalWidth(); - const int LOGICAL_H = GlobalState::instance().getLogicalHeight(); + // Use fixed logical dimensions to match main.cpp and ensure consistent layout + const float LOGICAL_W = 1200.f; + const float LOGICAL_H = 1000.f; + + // Compute content offsets (same approach as MenuState for proper centering) + float winW = (float)logicalVP.w; + float winH = (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; - // Since ApplicationManager sets up a centered viewport, we draw directly in logical coordinates - // The viewport (LOGICAL_W x LOGICAL_H) is already centered within the window - float vw = float(LOGICAL_W); - float vh = float(LOGICAL_H); - float offX = 0.f; // No offset needed since viewport is centered - - // Panel and title strip (in logical space) - SDL_FRect panel = DrawPanel(renderer, vw, vh-140.0f, /*draw=*/true, offX, 0.f); + // Panel and title strip (in logical space) - centered properly with offsets + SDL_FRect panel = DrawPanel(renderer, LOGICAL_W, LOGICAL_H, /*draw=*/true, contentOffsetX, contentOffsetY); // Title text - prefer pixelFont for a blocky title if available, fallback to regular font FontAtlas* titleFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; - DrawText(renderer, titleFont, "SELECT STARTING LEVEL", vw / 2.f + offX, panel.y + 20.f, 1.2f, COL_TITLE, true, true); + DrawText(renderer, titleFont, "SELECT STARTING LEVEL", LOGICAL_W / 2.f + contentOffsetX, panel.y + 20.f, 1.2f, COL_TITLE, true, true); // Grid of levels Grid g = MakeGrid(panel); @@ -256,10 +262,10 @@ void LevelSelectorState::drawLevelSelectionPopup(SDL_Renderer* renderer) { DrawCell(renderer, rc, i, hoveredLevel == i, selectedLevel == i); } - // Footer/instructions - use regular TTF font for readability + // Footer/instructions - use regular TTF font for readability, centered and higher FontAtlas* footerFont = ctx.pixelFont ? ctx.pixelFont : ctx.font; - DrawText(renderer, footerFont, "CLICK A LEVEL TO SELECT \u2022 ESC = CANCEL", - vw / 2.f + offX, vh - 56.f, 1.0f, COL_FOOTER, true, true); + DrawText(renderer, footerFont, "CLICK A LEVEL TO SELECT • ESC = CANCEL", + LOGICAL_W / 2.f + contentOffsetX, panel.y + panel.h + 30.f, 1.0f, COL_FOOTER, true, true); } bool LevelSelectorState::isMouseInPopup(float mouseX, float mouseY, float& popupX, float& popupY, float& popupW, float& popupH) { diff --git a/src/states/LevelSelectorState.h b/src/states/LevelSelectorState.h index 270ef68..e35d5d0 100644 --- a/src/states/LevelSelectorState.h +++ b/src/states/LevelSelectorState.h @@ -19,7 +19,7 @@ private: SDL_Rect lastLogicalVP{0,0,0,0}; // Helper functions - void drawLevelSelectionPopup(SDL_Renderer* renderer); + void drawLevelSelectionPopup(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP); bool isMouseInPopup(float mouseX, float mouseY, float& popupX, float& popupY, float& popupW, float& popupH); int getLevelFromMouse(float mouseX, float mouseY, float popupX, float popupY, float popupW, float popupH); void updateHoverFromMouse(float mouseX, float mouseY); diff --git a/src/states/MenuState.cpp b/src/states/MenuState.cpp index ec5a522..39c1364 100644 --- a/src/states/MenuState.cpp +++ b/src/states/MenuState.cpp @@ -2,6 +2,7 @@ #include "persistence/Scores.h" #include "graphics/Font.h" #include "../core/GlobalState.h" +#include "../core/state/StateManager.h" #include "../audio/Audio.h" #include #include @@ -27,8 +28,37 @@ void MenuState::onExit() { } void MenuState::handleEvent(const SDL_Event& e) { - // Key-specific handling (allow main to handle global keys) - (void)e; + // Keyboard navigation for menu buttons + if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) { + switch (e.key.scancode) { + case SDL_SCANCODE_LEFT: + case SDL_SCANCODE_UP: + selectedButton = 0; // PLAY + break; + case SDL_SCANCODE_RIGHT: + case SDL_SCANCODE_DOWN: + selectedButton = 1; // LEVEL + 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); + } + } + break; + default: + break; + } + } } void MenuState::update(double frameMs) { @@ -175,8 +205,16 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi 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){ + 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_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); @@ -185,10 +223,10 @@ void MenuState::render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logi font.draw(r, tx+2, ty+2, label, textScale, SDL_Color{0,0,0,180}); 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}); + 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); { } - 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}); + 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); } } diff --git a/src/states/MenuState.h b/src/states/MenuState.h index 7b6f332..88c500d 100644 --- a/src/states/MenuState.h +++ b/src/states/MenuState.h @@ -10,4 +10,7 @@ public: void handleEvent(const SDL_Event& e) override; void update(double frameMs) override; void render(SDL_Renderer* renderer, float logicalScale, SDL_Rect logicalVP) override; + +private: + int selectedButton = 0; // 0 = PLAY, 1 = LEVEL }; diff --git a/src/states/PlayingState.cpp b/src/states/PlayingState.cpp index f3aaf31..c7c6680 100644 --- a/src/states/PlayingState.cpp +++ b/src/states/PlayingState.cpp @@ -33,15 +33,30 @@ void PlayingState::handleEvent(const SDL_Event& e) { // If exit-confirm popup is visible, handle shortcuts here if (ctx.showExitConfirmPopup && *ctx.showExitConfirmPopup) { - // Confirm with Enter (main or keypad) - if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER) { - *ctx.showExitConfirmPopup = false; - // Reset game and return to menu - ctx.game->reset(false); - if (ctx.stateManager) ctx.stateManager->setState(AppState::Menu); + // Navigate between YES (0) and NO (1) buttons + if (e.key.scancode == SDL_SCANCODE_LEFT || e.key.scancode == SDL_SCANCODE_UP) { + exitPopupSelectedButton = 0; // YES return; } - // Cancel with Esc + if (e.key.scancode == SDL_SCANCODE_RIGHT || e.key.scancode == SDL_SCANCODE_DOWN) { + exitPopupSelectedButton = 1; // NO + return; + } + + // Activate selected button with Enter or Space + if (e.key.scancode == SDL_SCANCODE_RETURN || e.key.scancode == SDL_SCANCODE_KP_ENTER || e.key.scancode == SDL_SCANCODE_SPACE) { + *ctx.showExitConfirmPopup = false; + if (exitPopupSelectedButton == 0) { + // YES - Reset game and return to menu + ctx.game->reset(false); + if (ctx.stateManager) ctx.stateManager->setState(AppState::Menu); + } else { + // NO - Just close popup and resume + ctx.game->setPaused(false); + } + return; + } + // Cancel with Esc (same as NO) if (e.key.scancode == SDL_SCANCODE_ESCAPE) { *ctx.showExitConfirmPopup = false; ctx.game->setPaused(false); diff --git a/src/states/PlayingState.h b/src/states/PlayingState.h index f8b177e..341611f 100644 --- a/src/states/PlayingState.h +++ b/src/states/PlayingState.h @@ -14,4 +14,5 @@ public: private: // Local per-state variables if needed bool localPaused = false; + int exitPopupSelectedButton = 1; // 0 = YES, 1 = NO (default to NO for safety) };