#include "core/rendering/menu.hpp" #include #include #include #include #include #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 getValue; std::function change; // dir: -1=left, +1=right }; // --- Estat --- static bool open_ = false; static int cursor_ = 0; static std::vector items_; static std::unique_ptr 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 dir) { if (dir < 0) Screen::get()->prevShaderType(); else Screen::get()->nextShaderType(); }}); // PRESET items_.push_back({"PRESET", ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) { if (dir < 0) Screen::get()->prevPreset(); else 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 dir) { Overlay::cycleRenderInfo(dir); }}); } // --- 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(b) << 16) | (static_cast(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("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(items_.size())) % static_cast(items_.size()); break; case SDL_SCANCODE_DOWN: cursor_ = (cursor_ + 1) % static_cast(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(i) * ITEM_SPACING; bool selected = (static_cast(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