Files
aee/source/core/rendering/menu.cpp
2026-04-04 23:34:35 +02:00

266 lines
9.4 KiB
C++

#include "core/rendering/menu.hpp"
#include <cstdio>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "core/rendering/overlay.hpp"
#include "core/rendering/screen.hpp"
#include "core/rendering/text.hpp"
#include "game/options.hpp"
namespace Menu {
// --- Constants visuals ---
static constexpr int SCREEN_W = 320;
static constexpr int SCREEN_H = 200;
static constexpr int BOX_W = 220;
static constexpr int BOX_H = 150;
static constexpr int BOX_X = (SCREEN_W - BOX_W) / 2; // 50
static constexpr int BOX_Y = (SCREEN_H - BOX_H) / 2; // 25
static constexpr Uint32 BG_COLOR = 0xFF1A0E0E; // fons marró fosc (ABGR)
static constexpr Uint8 BG_ALPHA = 220; // semi-transparent
static constexpr Uint32 BORDER_COLOR = 0xFFFFFF00; // cyan
static constexpr Uint32 TITLE_COLOR = 0xFFFFFFFF; // blanc
static constexpr Uint32 LABEL_COLOR = 0xFFCCCCCC; // gris clar
static constexpr Uint32 VALUE_COLOR = 0xFFFFFF00; // cyan
static constexpr Uint32 CURSOR_COLOR = 0xFF00FFFF; // groc
static constexpr Uint32 FOOTER_COLOR = 0xFF888888; // gris
static constexpr int TITLE_PAD_Y = 4;
static constexpr int ITEM_PAD_X = 10;
static constexpr int ITEM_SPACING = 11; // 8 px glifo + 3 pad
static constexpr int FOOTER_PAD_Y = 4;
// --- Items ---
enum class ItemKind { Toggle,
Cycle,
IntRange };
struct Item {
const char* label;
ItemKind kind;
std::function<std::string()> getValue;
std::function<void(int dir)> change; // dir: -1=left, +1=right
};
// --- Estat ---
static bool open_ = false;
static int cursor_ = 0;
static std::vector<Item> items_;
static std::unique_ptr<Text> font_;
// --- Helpers ---
static std::string yesNo(bool b) { return b ? "SI" : "NO"; }
static std::string onOff(bool b) { return b ? "ON" : "OFF"; }
// Construeix la llista d'items (Video)
static void buildItems() {
items_.clear();
// ZOOM
items_.push_back({"ZOOM", ItemKind::IntRange,
[] {
char buf[16];
std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
return std::string(buf);
},
[](int dir) {
if (dir < 0) Screen::get()->decZoom();
else if (dir > 0) Screen::get()->incZoom();
}});
// PANTALLA (fullscreen)
items_.push_back({"PANTALLA", ItemKind::Toggle,
[] { return std::string(Screen::get()->isFullscreen() ? "COMPLETA" : "FINESTRA"); },
[](int) { Screen::get()->toggleFullscreen(); }});
// SHADER
items_.push_back({"SHADER", ItemKind::Toggle,
[] { return onOff(Options::video.shader_enabled); },
[](int) { Screen::get()->toggleShaders(); }});
// ASPECTE 4:3
items_.push_back({"ASPECTE 4:3", ItemKind::Toggle,
[] { return yesNo(Options::video.aspect_ratio_4_3); },
[](int) { Screen::get()->toggleAspectRatio(); }});
// SUPERSAMPLING
items_.push_back({"SUPERSAMPLING", ItemKind::Toggle,
[] { return onOff(Options::video.supersampling); },
[](int) { Screen::get()->toggleSupersampling(); }});
// TIPUS SHADER
items_.push_back({"TIPUS SHADER", ItemKind::Cycle,
[] { return std::string(Screen::get()->getActiveShaderName()); },
[](int) { Screen::get()->nextShaderType(); }});
// PRESET
items_.push_back({"PRESET", ItemKind::Cycle,
[] { return std::string(Screen::get()->getCurrentPresetName()); },
[](int) { Screen::get()->nextPreset(); }});
// FILTRE 4:3
items_.push_back({"FILTRE 4:3", ItemKind::Toggle,
[] { return std::string(Options::video.stretch_filter_linear ? "LINEAR" : "NEAREST"); },
[](int) { Screen::get()->toggleStretchFilter(); }});
// RENDER INFO
items_.push_back({"RENDER INFO", ItemKind::Cycle,
[] {
switch (Options::render_info.position) {
case Options::RenderInfoPosition::OFF: return std::string("OFF");
case Options::RenderInfoPosition::TOP: return std::string("TOP");
case Options::RenderInfoPosition::BOTTOM: return std::string("BOTTOM");
}
return std::string("OFF");
},
[](int) { Overlay::toggleRenderInfo(); }});
}
// --- Dibuix ---
// Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria)
// src_argb és el color desitjat (canal alpha ignorat, s'usa src_alpha)
static void blendRect(Uint32* buf, int x, int y, int w, int h, Uint32 src_argb, Uint8 src_alpha) {
const Uint8 sa = src_alpha;
const Uint8 sr = src_argb & 0xFF;
const Uint8 sg = (src_argb >> 8) & 0xFF;
const Uint8 sb = (src_argb >> 16) & 0xFF;
const Uint8 inv = 255 - sa;
for (int row = y; row < y + h; row++) {
if (row < 0 || row >= SCREEN_H) continue;
for (int col = x; col < x + w; col++) {
if (col < 0 || col >= SCREEN_W) continue;
Uint32* p = &buf[col + row * SCREEN_W];
Uint32 dst = *p;
Uint8 dr = dst & 0xFF;
Uint8 dg = (dst >> 8) & 0xFF;
Uint8 db = (dst >> 16) & 0xFF;
Uint8 r = (sr * sa + dr * inv) / 255;
Uint8 g = (sg * sa + dg * inv) / 255;
Uint8 b = (sb * sa + db * inv) / 255;
*p = 0xFF000000u | (static_cast<Uint32>(b) << 16) | (static_cast<Uint32>(g) << 8) | r;
}
}
}
static void fillRect(Uint32* buf, int x, int y, int w, int h, Uint32 color) {
for (int row = y; row < y + h; row++) {
if (row < 0 || row >= SCREEN_H) continue;
for (int col = x; col < x + w; col++) {
if (col < 0 || col >= SCREEN_W) continue;
buf[col + row * SCREEN_W] = color;
}
}
}
static void drawBorder(Uint32* buf, int x, int y, int w, int h, Uint32 color) {
fillRect(buf, x, y, w, 1, color); // top
fillRect(buf, x, y + h - 1, w, 1, color); // bottom
fillRect(buf, x, y, 1, h, color); // left
fillRect(buf, x + w - 1, y, 1, h, color); // right
}
// --- API pública ---
void init() {
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
buildItems();
cursor_ = 0;
open_ = false;
}
void destroy() {
font_.reset();
items_.clear();
}
auto isOpen() -> bool {
return open_;
}
void toggle() {
open_ = !open_;
}
void close() {
open_ = false;
}
void handleKey(SDL_Scancode sc) {
if (!open_ || items_.empty()) return;
switch (sc) {
case SDL_SCANCODE_UP:
cursor_ = (cursor_ - 1 + static_cast<int>(items_.size())) % static_cast<int>(items_.size());
break;
case SDL_SCANCODE_DOWN:
cursor_ = (cursor_ + 1) % static_cast<int>(items_.size());
break;
case SDL_SCANCODE_LEFT:
items_[cursor_].change(-1);
break;
case SDL_SCANCODE_RIGHT:
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
items_[cursor_].change(+1);
break;
default:
break;
}
}
void render(Uint32* pixel_data) {
if (!open_ || !font_ || !pixel_data) return;
// Fons semi-transparent
blendRect(pixel_data, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR, BG_ALPHA);
// Vora
drawBorder(pixel_data, BOX_X, BOX_Y, BOX_W, BOX_H, BORDER_COLOR);
// Títol
const char* title = "OPCIONS DE VIDEO";
int title_w = font_->width(title);
font_->draw(pixel_data, BOX_X + (BOX_W - title_w) / 2, BOX_Y + TITLE_PAD_Y, title, TITLE_COLOR);
// Línia sota el títol
int title_line_y = BOX_Y + TITLE_PAD_Y + font_->charHeight() + 2;
fillRect(pixel_data, BOX_X + 4, title_line_y, BOX_W - 8, 1, BORDER_COLOR);
// Items
int items_y = title_line_y + 4;
for (size_t i = 0; i < items_.size(); i++) {
int y = items_y + static_cast<int>(i) * ITEM_SPACING;
bool selected = (static_cast<int>(i) == cursor_);
// Cursor
if (selected) {
font_->draw(pixel_data, BOX_X + 4, y, ">", CURSOR_COLOR);
}
// Label
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
font_->draw(pixel_data, BOX_X + ITEM_PAD_X, y, items_[i].label, label_color);
// Valor (dreta)
std::string value = items_[i].getValue();
int value_w = font_->width(value.c_str());
Uint32 value_color = selected ? CURSOR_COLOR : VALUE_COLOR;
font_->draw(pixel_data, BOX_X + BOX_W - ITEM_PAD_X - value_w, y, value.c_str(), value_color);
}
// Peu
const char* footer = "^v:MOU <>:CANVIA ESC:IX";
int footer_w = font_->width(footer);
int footer_y = BOX_Y + BOX_H - font_->charHeight() - FOOTER_PAD_Y;
font_->draw(pixel_data, BOX_X + (BOX_W - footer_w) / 2, footer_y, footer, FOOTER_COLOR);
}
} // namespace Menu