Updated bottom menu
This commit is contained in:
42
src/main.cpp
42
src/main.cpp
@ -45,6 +45,7 @@
|
||||
#include "core/Config.h"
|
||||
#include "core/Settings.h"
|
||||
#include "ui/MenuLayout.h"
|
||||
#include "ui/BottomMenu.h"
|
||||
|
||||
// Debug logging removed: no-op in this build (previously LOG_DEBUG)
|
||||
|
||||
@ -981,24 +982,30 @@ int main(int, char **)
|
||||
showSettingsPopup = false;
|
||||
} else {
|
||||
ui::MenuLayoutParams params{ LOGICAL_W, LOGICAL_H, winW, winH, logicalScale };
|
||||
auto buttonRects = ui::computeMenuButtonRects(params);
|
||||
|
||||
auto pointInRect = [&](const SDL_FRect& r) {
|
||||
return lx >= r.x && lx <= r.x + r.w && ly >= r.y && ly <= r.y + r.h;
|
||||
};
|
||||
auto menuInput = ui::handleBottomMenuInput(params, e, lx, ly, hoveredButton, true);
|
||||
hoveredButton = menuInput.hoveredIndex;
|
||||
|
||||
if (pointInRect(buttonRects[0])) {
|
||||
startMenuPlayTransition();
|
||||
} else if (pointInRect(buttonRects[1])) {
|
||||
requestStateFade(AppState::LevelSelector);
|
||||
} else if (pointInRect(buttonRects[2])) {
|
||||
requestStateFade(AppState::Options);
|
||||
} else if (pointInRect(buttonRects[3])) {
|
||||
// HELP - show inline help HUD in the MenuState
|
||||
if (menuState) menuState->showHelpPanel(true);
|
||||
} else if (pointInRect(buttonRects[4])) {
|
||||
showExitConfirmPopup = true;
|
||||
exitPopupSelectedButton = 1;
|
||||
if (menuInput.activated) {
|
||||
switch (*menuInput.activated) {
|
||||
case ui::BottomMenuItem::Play:
|
||||
startMenuPlayTransition();
|
||||
break;
|
||||
case ui::BottomMenuItem::Level:
|
||||
requestStateFade(AppState::LevelSelector);
|
||||
break;
|
||||
case ui::BottomMenuItem::Options:
|
||||
requestStateFade(AppState::Options);
|
||||
break;
|
||||
case ui::BottomMenuItem::Help:
|
||||
// HELP - show inline help HUD in the MenuState
|
||||
if (menuState) menuState->showHelpPanel(true);
|
||||
break;
|
||||
case ui::BottomMenuItem::Exit:
|
||||
showExitConfirmPopup = true;
|
||||
exitPopupSelectedButton = 1;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Settings button (gear icon area - top right)
|
||||
@ -1099,7 +1106,8 @@ int main(int, char **)
|
||||
float contentOffsetX = (winW - contentW) * 0.5f / logicalScale;
|
||||
float contentOffsetY = (winH - contentH) * 0.5f / logicalScale;
|
||||
ui::MenuLayoutParams params{ LOGICAL_W, LOGICAL_H, winW, winH, logicalScale };
|
||||
hoveredButton = ui::hitTestMenuButtons(params, lx, ly);
|
||||
auto menuInput = ui::handleBottomMenuInput(params, e, lx, ly, hoveredButton, true);
|
||||
hoveredButton = menuInput.hoveredIndex;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
#include "../graphics/renderers/UIRenderer.h"
|
||||
#include "../graphics/renderers/GameRenderer.h"
|
||||
#include "../ui/MenuLayout.h"
|
||||
#include "../ui/BottomMenu.h"
|
||||
#include <SDL3_image/SDL_image.h>
|
||||
|
||||
// Frosted tint helper: draw a safe, inexpensive frosted overlay for the panel area.
|
||||
@ -145,69 +146,16 @@ void MenuState::renderMainButtonTop(SDL_Renderer* renderer, float logicalScale,
|
||||
logicalVP.h,
|
||||
logicalScale
|
||||
};
|
||||
auto rects = ui::computeMenuButtonRects(params);
|
||||
|
||||
// Compose same button definition used in render()
|
||||
char levelBtnText[32];
|
||||
int startLevel = ctx.startLevelSelection ? *ctx.startLevelSelection : 0;
|
||||
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
||||
ui::BottomMenu menu = ui::buildBottomMenu(params, startLevel);
|
||||
|
||||
struct MenuButtonDef { SDL_Color bg; SDL_Color border; std::string label; };
|
||||
std::array<MenuButtonDef,5> 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,200,60,255}, SDL_Color{150,150,40,255}, "HELP" },
|
||||
MenuButtonDef{ SDL_Color{200,70,70,255}, SDL_Color{150,40,40,255}, "EXIT" }
|
||||
};
|
||||
|
||||
std::array<SDL_Texture*,5> icons = { playIcon, levelIcon, optionsIcon, helpIcon, exitIcon };
|
||||
|
||||
|
||||
// Draw PLAY as a real glowing button, and the four bottom items as HUD buttons.
|
||||
for (int i = 0; i < 5; ++i) {
|
||||
const SDL_FRect& r = rects[i];
|
||||
float cxCenter = r.x + r.w * 0.5f;
|
||||
float cyCenter = r.y + r.h * 0.5f;
|
||||
float btnW = r.w;
|
||||
float btnH = r.h;
|
||||
|
||||
const bool isHovered = (ctx.hoveredButton && *ctx.hoveredButton == i);
|
||||
const bool isSelected = (selectedButton == i);
|
||||
|
||||
double aMul = std::clamp(buttonGroupAlpha + buttonFlash * buttonFlashAmount, 0.0, 1.0);
|
||||
|
||||
if (i == 0) {
|
||||
SDL_Color bgCol{ 18, 22, 28, static_cast<Uint8>(std::round(180.0 * aMul)) };
|
||||
SDL_Color bdCol{ 255, 200, 70, static_cast<Uint8>(std::round(220.0 * aMul)) };
|
||||
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
|
||||
buttons[i].label, isHovered, isSelected,
|
||||
bgCol, bdCol, false, nullptr);
|
||||
} else {
|
||||
SDL_Color bgCol{ 20, 30, 42, static_cast<Uint8>(std::round(160.0 * aMul)) };
|
||||
SDL_Color bdCol{ 120, 220, 255, static_cast<Uint8>(std::round(200.0 * aMul)) };
|
||||
UIRenderer::drawButton(renderer, ctx.pixelFont, cxCenter, cyCenter, btnW, btnH,
|
||||
buttons[i].label, isHovered, isSelected,
|
||||
bgCol, bdCol, true, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// Draw small '+' separators between the bottom HUD buttons (matches the reference vibe).
|
||||
{
|
||||
SDL_BlendMode prevBlend = SDL_BLENDMODE_NONE;
|
||||
SDL_GetRenderDrawBlendMode(renderer, &prevBlend);
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(renderer, 120, 220, 255, 180);
|
||||
|
||||
float y = rects[1].y + rects[1].h * 0.5f;
|
||||
for (int i = 1; i < 4; ++i) {
|
||||
float x = (rects[i].x + rects[i].w + rects[i + 1].x) * 0.5f;
|
||||
SDL_RenderLine(renderer, x - 4.0f, y, x + 4.0f, y);
|
||||
SDL_RenderLine(renderer, x, y - 4.0f, x, y + 4.0f);
|
||||
}
|
||||
|
||||
SDL_SetRenderDrawBlendMode(renderer, prevBlend);
|
||||
}
|
||||
const int hovered = (ctx.hoveredButton ? *ctx.hoveredButton : -1);
|
||||
const double baseAlpha = 1.0;
|
||||
// Pulse is encoded as a signed delta so PLAY can dim/brighten while focused.
|
||||
const double pulseDelta = (buttonPulseAlpha - 1.0);
|
||||
const double flashDelta = buttonFlash * buttonFlashAmount;
|
||||
ui::renderBottomMenu(renderer, ctx.pixelFont, menu, hovered, selectedButton, baseAlpha, pulseDelta + flashDelta);
|
||||
}
|
||||
|
||||
void MenuState::onExit() {
|
||||
@ -226,6 +174,11 @@ void MenuState::onExit() {
|
||||
void MenuState::handleEvent(const SDL_Event& e) {
|
||||
// Keyboard navigation for menu buttons
|
||||
if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
|
||||
// When the player uses the keyboard, don't let an old mouse hover keep focus on a button.
|
||||
if (ctx.hoveredButton) {
|
||||
*ctx.hoveredButton = -1;
|
||||
}
|
||||
|
||||
auto triggerPlay = [&]() {
|
||||
if (ctx.startPlayTransition) {
|
||||
ctx.startPlayTransition();
|
||||
@ -622,7 +575,7 @@ void MenuState::update(double frameMs) {
|
||||
}
|
||||
}
|
||||
|
||||
// Update button group pulsing animation
|
||||
// Update pulsing animation (used for PLAY emphasis)
|
||||
if (buttonPulseEnabled) {
|
||||
buttonPulseTime += frameMs;
|
||||
double t = (buttonPulseTime * 0.001) * buttonPulseSpeed; // seconds * speed
|
||||
@ -652,11 +605,14 @@ void MenuState::update(double frameMs) {
|
||||
default:
|
||||
s = (std::sin(t * 2.0 * 3.14159265358979323846) * 0.5) + 0.5;
|
||||
}
|
||||
buttonGroupAlpha = buttonPulseMinAlpha + s * (buttonPulseMaxAlpha - buttonPulseMinAlpha);
|
||||
buttonPulseAlpha = buttonPulseMinAlpha + s * (buttonPulseMaxAlpha - buttonPulseMinAlpha);
|
||||
} else {
|
||||
buttonGroupAlpha = 1.0;
|
||||
buttonPulseAlpha = 1.0;
|
||||
}
|
||||
|
||||
// Keep the base group alpha stable; pulsing is applied selectively in the renderer.
|
||||
buttonGroupAlpha = 1.0;
|
||||
|
||||
// Update flash decay
|
||||
if (buttonFlash > 0.0) {
|
||||
buttonFlash -= frameMs * buttonFlashDecay;
|
||||
|
||||
@ -56,8 +56,9 @@ private:
|
||||
double levelHighlightGlowAlpha = 0.70; // 0..1 base glow alpha
|
||||
int levelHighlightThickness = 3; // inner outline thickness (px)
|
||||
SDL_Color levelHighlightColor = SDL_Color{80, 200, 255, 200};
|
||||
// Button group pulsing/fade parameters (applies to all four main buttons)
|
||||
double buttonGroupAlpha = 1.0; // current computed alpha (0..1)
|
||||
// Button pulsing/fade parameters (used for PLAY emphasis)
|
||||
double buttonGroupAlpha = 1.0; // base alpha for the whole group (kept stable)
|
||||
double buttonPulseAlpha = 1.0; // pulsing alpha (0..1), applied to PLAY only
|
||||
double buttonPulseTime = 0.0; // accumulator in ms
|
||||
bool buttonPulseEnabled = true; // enable/disable pulsing
|
||||
double buttonPulseSpeed = 1.0; // multiplier for pulse frequency
|
||||
|
||||
126
src/ui/BottomMenu.cpp
Normal file
126
src/ui/BottomMenu.cpp
Normal file
@ -0,0 +1,126 @@
|
||||
#include "ui/BottomMenu.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
#include "graphics/renderers/UIRenderer.h"
|
||||
#include "graphics/Font.h"
|
||||
|
||||
namespace ui {
|
||||
|
||||
static bool pointInRect(const SDL_FRect& r, float x, float y) {
|
||||
return x >= r.x && x <= (r.x + r.w) && y >= r.y && y <= (r.y + r.h);
|
||||
}
|
||||
|
||||
BottomMenu buildBottomMenu(const MenuLayoutParams& params, int startLevel) {
|
||||
BottomMenu menu{};
|
||||
|
||||
auto rects = computeMenuButtonRects(params);
|
||||
|
||||
char levelBtnText[32];
|
||||
std::snprintf(levelBtnText, sizeof(levelBtnText), "LEVEL %d", startLevel);
|
||||
|
||||
menu.buttons[0] = Button{ BottomMenuItem::Play, rects[0], "PLAY", false };
|
||||
menu.buttons[1] = Button{ BottomMenuItem::Level, rects[1], levelBtnText, true };
|
||||
menu.buttons[2] = Button{ BottomMenuItem::Options, rects[2], "OPTIONS", true };
|
||||
menu.buttons[3] = Button{ BottomMenuItem::Help, rects[3], "HELP", true };
|
||||
menu.buttons[4] = Button{ BottomMenuItem::Exit, rects[4], "EXIT", true };
|
||||
|
||||
return menu;
|
||||
}
|
||||
|
||||
void renderBottomMenu(SDL_Renderer* renderer,
|
||||
FontAtlas* font,
|
||||
const BottomMenu& menu,
|
||||
int hoveredIndex,
|
||||
int selectedIndex,
|
||||
double baseAlphaMul,
|
||||
double flashAddMul) {
|
||||
if (!renderer || !font) return;
|
||||
|
||||
const double baseMul = std::clamp(baseAlphaMul, 0.0, 1.0);
|
||||
const double flashMul = flashAddMul;
|
||||
|
||||
const int focusIndex = (hoveredIndex != -1) ? hoveredIndex : selectedIndex;
|
||||
|
||||
for (int i = 0; i < MENU_BTN_COUNT; ++i) {
|
||||
const Button& b = menu.buttons[i];
|
||||
const SDL_FRect& r = b.rect;
|
||||
|
||||
float cx = r.x + r.w * 0.5f;
|
||||
float cy = r.y + r.h * 0.5f;
|
||||
|
||||
bool isHovered = (hoveredIndex == i);
|
||||
bool isSelected = (selectedIndex == i);
|
||||
|
||||
// Requested behavior: flash only the PLAY button, and only when it's the active/focused item.
|
||||
const bool playIsActive = (i == 0) && (focusIndex == 0);
|
||||
const double aMul = std::clamp(baseMul + (playIsActive ? flashMul : 0.0), 0.0, 1.0);
|
||||
|
||||
if (!b.textOnly) {
|
||||
SDL_Color bgCol{ 18, 22, 28, static_cast<Uint8>(std::round(180.0 * aMul)) };
|
||||
SDL_Color bdCol{ 255, 200, 70, static_cast<Uint8>(std::round(220.0 * aMul)) };
|
||||
UIRenderer::drawButton(renderer, font, cx, cy, r.w, r.h,
|
||||
b.label, isHovered, isSelected,
|
||||
bgCol, bdCol, false, nullptr);
|
||||
} else {
|
||||
SDL_Color bgCol{ 20, 30, 42, static_cast<Uint8>(std::round(160.0 * aMul)) };
|
||||
SDL_Color bdCol{ 120, 220, 255, static_cast<Uint8>(std::round(200.0 * aMul)) };
|
||||
UIRenderer::drawButton(renderer, font, cx, cy, r.w, r.h,
|
||||
b.label, isHovered, isSelected,
|
||||
bgCol, bdCol, true, nullptr);
|
||||
}
|
||||
}
|
||||
|
||||
// '+' separators between the bottom HUD buttons (indices 1..4)
|
||||
{
|
||||
SDL_BlendMode prevBlend = SDL_BLENDMODE_NONE;
|
||||
SDL_GetRenderDrawBlendMode(renderer, &prevBlend);
|
||||
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND);
|
||||
SDL_SetRenderDrawColor(renderer, 120, 220, 255, static_cast<Uint8>(std::round(180.0 * baseMul)));
|
||||
|
||||
float y = menu.buttons[1].rect.y + menu.buttons[1].rect.h * 0.5f;
|
||||
for (int i = 1; i < 4; ++i) {
|
||||
float x = (menu.buttons[i].rect.x + menu.buttons[i].rect.w + menu.buttons[i + 1].rect.x) * 0.5f;
|
||||
SDL_RenderLine(renderer, x - 4.0f, y, x + 4.0f, y);
|
||||
SDL_RenderLine(renderer, x, y - 4.0f, x, y + 4.0f);
|
||||
}
|
||||
|
||||
SDL_SetRenderDrawBlendMode(renderer, prevBlend);
|
||||
}
|
||||
}
|
||||
|
||||
BottomMenuInputResult handleBottomMenuInput(const MenuLayoutParams& params,
|
||||
const SDL_Event& e,
|
||||
float x,
|
||||
float y,
|
||||
int prevHoveredIndex,
|
||||
bool inputEnabled) {
|
||||
BottomMenuInputResult result{};
|
||||
result.hoveredIndex = prevHoveredIndex;
|
||||
|
||||
if (!inputEnabled) {
|
||||
return result;
|
||||
}
|
||||
|
||||
if (e.type == SDL_EVENT_MOUSE_MOTION) {
|
||||
result.hoveredIndex = hitTestMenuButtons(params, x, y);
|
||||
return result;
|
||||
}
|
||||
|
||||
if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN && e.button.button == SDL_BUTTON_LEFT) {
|
||||
auto rects = computeMenuButtonRects(params);
|
||||
for (int i = 0; i < MENU_BTN_COUNT; ++i) {
|
||||
if (pointInRect(rects[i], x, y)) {
|
||||
result.activated = static_cast<BottomMenuItem>(i);
|
||||
result.hoveredIndex = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
63
src/ui/BottomMenu.h
Normal file
63
src/ui/BottomMenu.h
Normal file
@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include "ui/MenuLayout.h"
|
||||
#include "ui/UIConstants.h"
|
||||
|
||||
struct FontAtlas;
|
||||
|
||||
namespace ui {
|
||||
|
||||
enum class BottomMenuItem : int {
|
||||
Play = 0,
|
||||
Level = 1,
|
||||
Options = 2,
|
||||
Help = 3,
|
||||
Exit = 4,
|
||||
};
|
||||
|
||||
struct Button {
|
||||
BottomMenuItem item{};
|
||||
SDL_FRect rect{};
|
||||
std::string label;
|
||||
bool textOnly = true;
|
||||
};
|
||||
|
||||
struct BottomMenu {
|
||||
std::array<Button, MENU_BTN_COUNT> buttons{};
|
||||
};
|
||||
|
||||
BottomMenu buildBottomMenu(const MenuLayoutParams& params, int startLevel);
|
||||
|
||||
// Draws the cockpit HUD menu (PLAY + 4 bottom items) using existing UIRenderer primitives.
|
||||
// hoveredIndex: -1..4
|
||||
// selectedIndex: 0..4 (keyboard selection)
|
||||
// alphaMul: 0..1 (overall group alpha)
|
||||
void renderBottomMenu(SDL_Renderer* renderer,
|
||||
FontAtlas* font,
|
||||
const BottomMenu& menu,
|
||||
int hoveredIndex,
|
||||
int selectedIndex,
|
||||
double baseAlphaMul,
|
||||
double flashAddMul);
|
||||
|
||||
struct BottomMenuInputResult {
|
||||
int hoveredIndex = -1;
|
||||
std::optional<BottomMenuItem> activated;
|
||||
};
|
||||
|
||||
// Interprets mouse motion/button input for the bottom menu.
|
||||
// Expects x/y in the same logical coordinate space used by MenuLayout (the current main loop already provides this).
|
||||
BottomMenuInputResult handleBottomMenuInput(const MenuLayoutParams& params,
|
||||
const SDL_Event& e,
|
||||
float x,
|
||||
float y,
|
||||
int prevHoveredIndex,
|
||||
bool inputEnabled);
|
||||
|
||||
} // namespace ui
|
||||
@ -38,7 +38,9 @@ std::array<SDL_FRect, 5> computeMenuButtonRects(const MenuLayoutParams& p) {
|
||||
float centerX = LOGICAL_W * 0.5f + contentOffsetX;
|
||||
float bottomY = LOGICAL_H + contentOffsetY - marginBottom;
|
||||
float smallCY = bottomY - smallH * 0.5f;
|
||||
float playCY = smallCY - smallH * 0.5f - 16.0f - playH * 0.5f;
|
||||
// Extra breathing room between PLAY and the bottom row (requested).
|
||||
const float rowGap = 34.0f;
|
||||
float playCY = smallCY - smallH * 0.5f - rowGap - playH * 0.5f;
|
||||
|
||||
std::array<SDL_FRect, MENU_BTN_COUNT> rects{};
|
||||
rects[0] = SDL_FRect{ centerX - playW * 0.5f, playCY - playH * 0.5f, playW, playH };
|
||||
|
||||
Reference in New Issue
Block a user