diff --git a/source/core/rendering/menu.cpp b/source/core/rendering/menu.cpp index c302423..ebddb33 100644 --- a/source/core/rendering/menu.cpp +++ b/source/core/rendering/menu.cpp @@ -49,20 +49,20 @@ namespace Menu { static constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada (~150 ms al 90%) // --- Items --- - enum class ItemKind : std::uint8_t { Toggle, - Cycle, - IntRange, - Submenu, - KeyBind, - Action }; + enum class ItemKind : std::uint8_t { TOGGLE, + CYCLE, + INT_RANGE, + SUBMENU, + KEY_BIND, + ACTION }; struct Item { const char* label; ItemKind kind; - std::function getValue; // opcional - std::function change; // per Toggle/Cycle/IntRange - std::function enter; // per Submenu i Action - SDL_Scancode* scancode{nullptr}; // per KeyBind + std::function get_value; // opcional + std::function change; // per TOGGLE/CYCLE/INT_RANGE + std::function enter; // per SUBMENU i ACTION + SDL_Scancode* scancode{nullptr}; // per KEY_BIND std::function visible; // nullptr ⇒ sempre visible }; @@ -78,12 +78,12 @@ namespace Menu { // Troba el pròxim ítem visible en direcció `dir` (±1) a partir de `from`. // Si cap és visible retorna `from`. static auto nextVisibleCursor(const Page& p, int from, int dir) -> int { - const int n = static_cast(p.items.size()); - if (n <= 0) { + const int N = static_cast(p.items.size()); + if (N <= 0) { return from; } - for (int i = 1; i <= n; ++i) { - int idx = ((from + dir * i) % n + n) % n; + for (int i = 1; i <= N; ++i) { + int idx = ((from + dir * i) % N + N) % N; if (isVisible(p.items[idx])) { return idx; } @@ -92,35 +92,35 @@ namespace Menu { } // --- Estat --- - static std::vector stack_; - static std::unique_ptr font_; - static float open_anim_{0.0F}; // 0 = tancat, 1 = obert - static float animated_h_{0.0F}; // alçada actual animada (smoothing cap al target visible) - static Uint32 last_ticks_{0}; - static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar - static bool closing_{false}; // true mentre l'animació de tancament és en curs + static std::vector stack; + static std::unique_ptr font; + static float open_anim{0.0F}; // 0 = tancat, 1 = obert + static float animated_h{0.0F}; // alçada actual animada (smoothing cap al target visible) + static Uint32 last_ticks{0}; + static SDL_Scancode* capturing{nullptr}; // != null → esperant tecla per assignar + static bool closing{false}; // true mentre l'animació de tancament és en curs // --- Transició entre pàgines --- static constexpr float TRANSITION_SPEED = 5.5F; // ~180 ms - static Page transition_outgoing_{.title = "", .items = {}, .cursor = 0}; - static bool transition_active_{false}; - static float transition_progress_{1.0F}; - static int transition_dir_{+1}; // +1 endavant, -1 enrere + static Page transition_outgoing{.title = "", .items = {}, .cursor = 0}; + static bool transition_active{false}; + static float transition_progress{1.0F}; + static int transition_dir{+1}; // +1 endavant, -1 enrere // Helpers per triggerar transicions - static void pushPage(Page newPage) { - transition_outgoing_ = stack_.back(); - stack_.push_back(std::move(newPage)); - transition_active_ = true; - transition_progress_ = 0.0F; - transition_dir_ = +1; + static void pushPage(Page new_page) { + transition_outgoing = stack.back(); + stack.push_back(std::move(new_page)); + transition_active = true; + transition_progress = 0.0F; + transition_dir = +1; } static void popPage() { - transition_outgoing_ = stack_.back(); - stack_.pop_back(); - transition_active_ = true; - transition_progress_ = 0.0F; - transition_dir_ = -1; + transition_outgoing = stack.back(); + stack.pop_back(); + transition_active = true; + transition_progress = 0.0F; + transition_dir = -1; } // --- Helpers --- @@ -138,11 +138,11 @@ namespace Menu { static auto buildRoot() -> Page { Page p{.title = Locale::get("menu.titles.root"), .items = {}, .cursor = 0}; - p.items.push_back({Locale::get("menu.items.video"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr}); - p.items.push_back({Locale::get("menu.items.audio"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr}); - p.items.push_back({Locale::get("menu.items.controls"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr}); - p.items.push_back({Locale::get("menu.items.game"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr}); - p.items.push_back({Locale::get("menu.items.system"), ItemKind::Submenu, nullptr, nullptr, [] { pushPage(buildSystem()); }, nullptr}); + p.items.push_back({Locale::get("menu.items.video"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildVideo()); }, nullptr}); + p.items.push_back({Locale::get("menu.items.audio"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildAudio()); }, nullptr}); + p.items.push_back({Locale::get("menu.items.controls"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildControls()); }, nullptr}); + p.items.push_back({Locale::get("menu.items.game"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildGame()); }, nullptr}); + p.items.push_back({Locale::get("menu.items.system"), ItemKind::SUBMENU, nullptr, nullptr, [] { pushPage(buildSystem()); }, nullptr}); return p; } @@ -151,7 +151,7 @@ namespace Menu { // Zoom i fullscreen: sense sentit a WASM (el navegador posseix el canvas) #ifndef __EMSCRIPTEN__ - p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::IntRange, [] { + p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::INT_RANGE, [] { char buf[16]; std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom()); return std::string(buf); }, [](int dir) { @@ -159,15 +159,15 @@ namespace Menu { } else if (dir > 0) { Screen::get()->incZoom(); } }, nullptr, nullptr}); - p.items.push_back({Locale::get("menu.items.screen"), ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr, nullptr, nullptr}); + p.items.push_back({Locale::get("menu.items.screen"), ItemKind::TOGGLE, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr, nullptr, nullptr}); #endif // Opcions visuals generals (sempre visibles) - p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr, nullptr, nullptr}); + p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::TOGGLE, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr, nullptr, nullptr}); - p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::Toggle, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr, nullptr, nullptr}); + p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::TOGGLE, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr, nullptr, nullptr}); - p.items.push_back({Locale::get("menu.items.scaling_mode"), ItemKind::Cycle, [] { + p.items.push_back({Locale::get("menu.items.scaling_mode"), ItemKind::CYCLE, [] { switch (Options::video.scaling_mode) { case Options::ScalingMode::DISABLED: return std::string(Locale::get("menu.values.scaling_disabled")); case Options::ScalingMode::STRETCH: return std::string(Locale::get("menu.values.scaling_stretch")); @@ -177,30 +177,30 @@ namespace Menu { } return std::string(Locale::get("menu.values.scaling_integer")); }, [](int dir) { Screen::get()->cycleScalingMode(dir); }, nullptr, nullptr, nullptr}); - p.items.push_back({Locale::get("menu.items.texture_filter"), ItemKind::Cycle, [] { return std::string(Options::video.texture_filter == Options::TextureFilter::LINEAR + p.items.push_back({Locale::get("menu.items.texture_filter"), ItemKind::CYCLE, [] { return std::string(Options::video.texture_filter == Options::TextureFilter::LINEAR ? Locale::get("menu.values.linear") : Locale::get("menu.values.nearest")); }, [](int dir) { Screen::get()->cycleTextureFilter(dir); }, nullptr, nullptr, nullptr}); - p.items.push_back({Locale::get("menu.items.internal_resolution"), ItemKind::IntRange, [] { + p.items.push_back({Locale::get("menu.items.internal_resolution"), ItemKind::INT_RANGE, [] { char buf[16]; std::snprintf(buf, sizeof(buf), "%dX", Options::video.internal_resolution); return std::string(buf); }, [](int dir) { Screen::get()->changeInternalResolution(dir); }, nullptr, nullptr, nullptr}); // Bloc shaders: no disponible a WASM (NO_SHADERS, sense SDL3 GPU a WebGL2) #ifndef __EMSCRIPTEN__ - p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr}); + p.items.push_back({Locale::get("menu.items.shader"), ItemKind::TOGGLE, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr}); - p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) { + p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::CYCLE, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) { if (dir < 0) { Screen::get()->prevShaderType(); } else { Screen::get()->nextShaderType(); } }, nullptr, nullptr, [] { return Options::video.shader_enabled; }}); - p.items.push_back({Locale::get("menu.items.preset"), ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) { + p.items.push_back({Locale::get("menu.items.preset"), ItemKind::CYCLE, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) { if (dir < 0) { Screen::get()->prevPreset(); } else { Screen::get()->nextPreset(); } }, nullptr, nullptr, [] { return Options::video.shader_enabled; }}); - p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr, nullptr, [] { + p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::TOGGLE, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr, nullptr, [] { if (!Options::video.shader_enabled) { return false; } const char* name = Screen::get()->getActiveShaderName(); @@ -208,7 +208,7 @@ namespace Menu { #endif // Informació de render - p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::Cycle, [] { + p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::CYCLE, [] { switch (Options::render_info.position) { case Options::RenderInfoPosition::OFF: return std::string(Locale::get("menu.values.off")); case Options::RenderInfoPosition::TOP: return std::string(Locale::get("menu.values.top")); @@ -216,7 +216,7 @@ namespace Menu { } return std::string(Locale::get("menu.values.off")); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr, nullptr, nullptr}); - p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::Toggle, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr, nullptr, [] { return Options::render_info.position != Options::RenderInfoPosition::OFF; }}); + p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::TOGGLE, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr, nullptr, [] { return Options::render_info.position != Options::RenderInfoPosition::OFF; }}); return p; } @@ -241,34 +241,34 @@ namespace Menu { static auto buildControls() -> Page { Page p{.title = Locale::get("menu.titles.controls"), .items = {}, .cursor = 0}; - p.items.push_back({Locale::get("menu.items.move_up"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.up}); - p.items.push_back({Locale::get("menu.items.move_down"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.down}); - p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.left}); - p.items.push_back({Locale::get("menu.items.move_right"), ItemKind::KeyBind, nullptr, nullptr, nullptr, &Options::keys_game.right}); - p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KeyBind, nullptr, nullptr, nullptr, KeyConfig::scancodePtr("menu_toggle")}); + p.items.push_back({Locale::get("menu.items.move_up"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.up}); + p.items.push_back({Locale::get("menu.items.move_down"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.down}); + p.items.push_back({Locale::get("menu.items.move_left"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.left}); + p.items.push_back({Locale::get("menu.items.move_right"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, &Options::keys_game.right}); + p.items.push_back({Locale::get("menu.items.menu_key"), ItemKind::KEY_BIND, nullptr, nullptr, nullptr, KeyConfig::scancodePtr("menu_toggle")}); return p; } static auto buildAudio() -> Page { Page p{.title = Locale::get("menu.titles.audio"), .items = {}, .cursor = 0}; - p.items.push_back({Locale::get("menu.items.master_enable"), ItemKind::Toggle, [] { return onOff(Options::audio.enabled); }, [](int) { + p.items.push_back({Locale::get("menu.items.master_enable"), ItemKind::TOGGLE, [] { return onOff(Options::audio.enabled); }, [](int) { Options::audio.enabled = !Options::audio.enabled; Options::applyAudio(); }, nullptr}); - p.items.push_back({Locale::get("menu.items.master_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr}); + p.items.push_back({Locale::get("menu.items.master_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr}); - p.items.push_back({Locale::get("menu.items.music"), ItemKind::Toggle, [] { return onOff(Options::audio.music.enabled); }, [](int) { + p.items.push_back({Locale::get("menu.items.music"), ItemKind::TOGGLE, [] { return onOff(Options::audio.music.enabled); }, [](int) { Options::audio.music.enabled = !Options::audio.music.enabled; Options::applyAudio(); }, nullptr}); - p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.music.volume); }, [](int dir) { stepVolume(Options::audio.music.volume, dir); }, nullptr}); + p.items.push_back({Locale::get("menu.items.music_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.music.volume); }, [](int dir) { stepVolume(Options::audio.music.volume, dir); }, nullptr}); - p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::Toggle, [] { return onOff(Options::audio.sound.enabled); }, [](int) { + p.items.push_back({Locale::get("menu.items.sounds"), ItemKind::TOGGLE, [] { return onOff(Options::audio.sound.enabled); }, [](int) { Options::audio.sound.enabled = !Options::audio.sound.enabled; Options::applyAudio(); }, nullptr}); - p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::IntRange, [] { return volPct(Options::audio.sound.volume); }, [](int dir) { stepVolume(Options::audio.sound.volume, dir); }, nullptr}); + p.items.push_back({Locale::get("menu.items.sounds_volume"), ItemKind::INT_RANGE, [] { return volPct(Options::audio.sound.volume); }, [](int dir) { stepVolume(Options::audio.sound.volume, dir); }, nullptr}); return p; } @@ -276,11 +276,11 @@ namespace Menu { static auto buildGame() -> Page { Page p{.title = Locale::get("menu.titles.game"), .items = {}, .cursor = 0}; - p.items.push_back({Locale::get("menu.items.use_new_logo"), ItemKind::Toggle, [] { return yesNo(Options::game.use_new_logo); }, [](int) { Options::game.use_new_logo = !Options::game.use_new_logo; }, nullptr}); + p.items.push_back({Locale::get("menu.items.use_new_logo"), ItemKind::TOGGLE, [] { return yesNo(Options::game.use_new_logo); }, [](int) { Options::game.use_new_logo = !Options::game.use_new_logo; }, nullptr}); - p.items.push_back({Locale::get("menu.items.show_title_credits"), ItemKind::Toggle, [] { return yesNo(Options::game.show_title_credits); }, [](int) { Options::game.show_title_credits = !Options::game.show_title_credits; }, nullptr}); + p.items.push_back({Locale::get("menu.items.show_title_credits"), ItemKind::TOGGLE, [] { return yesNo(Options::game.show_title_credits); }, [](int) { Options::game.show_title_credits = !Options::game.show_title_credits; }, nullptr}); - p.items.push_back({Locale::get("menu.items.show_preload"), ItemKind::Toggle, [] { return yesNo(Options::game.show_preload); }, [](int) { Options::game.show_preload = !Options::game.show_preload; }, nullptr}); + p.items.push_back({Locale::get("menu.items.show_preload"), ItemKind::TOGGLE, [] { return yesNo(Options::game.show_preload); }, [](int) { Options::game.show_preload = !Options::game.show_preload; }, nullptr}); return p; } @@ -289,7 +289,7 @@ namespace Menu { Page p{.title = Locale::get("menu.titles.system"), .items = {}, .cursor = 0}; p.subtitle = std::string("v") + Texts::VERSION + " (" + Version::GIT_HASH + ")"; - p.items.push_back({Locale::get("menu.items.restart"), ItemKind::Action, nullptr, nullptr, [] { + p.items.push_back({Locale::get("menu.items.restart"), ItemKind::ACTION, nullptr, nullptr, [] { if (Director::get()) { Director::get()->requestRestart(); } @@ -298,7 +298,7 @@ namespace Menu { nullptr}); #ifndef __EMSCRIPTEN__ - p.items.push_back({Locale::get("menu.items.exit_game"), ItemKind::Action, nullptr, nullptr, [] { + p.items.push_back({Locale::get("menu.items.exit_game"), ItemKind::ACTION, nullptr, nullptr, [] { if (Director::get()) { Director::get()->requestQuit(); } @@ -314,11 +314,11 @@ namespace Menu { // Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria) 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; + 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; @@ -332,9 +332,9 @@ namespace Menu { 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; + 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; } } @@ -365,8 +365,8 @@ namespace Menu { // body = (N-1) * ITEM_SPACING + charH — així BOTTOM_PAD és el buit real // sota el text del darrer ítem, no un buit extra per sobre d'un "slot" buit. static auto boxHeight(const Page& page) -> int { - const int n = static_cast(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); })); - int body = (n == 0) ? 8 : ((n - 1) * ITEM_SPACING) + 8; + const int N = static_cast(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); })); + int body = (N == 0) ? 8 : ((N - 1) * ITEM_SPACING) + 8; int header = HEADER_H + (page.subtitle.empty() ? 0 : SUBTITLE_H); return header + body + BOTTOM_PAD; } @@ -374,89 +374,89 @@ namespace Menu { // --- API pública --- void init() { - font_ = std::make_unique("fonts/8bithud.fnt", "fonts/8bithud.gif"); - stack_.clear(); - open_anim_ = 0.0F; - closing_ = false; - last_ticks_ = SDL_GetTicks(); + font = std::make_unique("fonts/8bithud.fnt", "fonts/8bithud.gif"); + stack.clear(); + open_anim = 0.0F; + closing = false; + last_ticks = SDL_GetTicks(); } void destroy() { - font_.reset(); - stack_.clear(); - closing_ = false; + font.reset(); + stack.clear(); + closing = false; } // "Actiu": accepta input. Durant l'animació de tancament la pila encara // té pàgines però ja no ha de processar tecles. auto isOpen() -> bool { - return !stack_.empty() && !closing_; + return !stack.empty() && !closing; } // "Visible": encara hi ha caixa per pintar (incloent close animation). auto isVisible() -> bool { - return !stack_.empty(); + return !stack.empty(); } void toggle() { - if (closing_ && !stack_.empty()) { + if (closing && !stack.empty()) { // Cancel·la el tancament en curs — continua l'animació cap a "obert" - // des del valor actual d'open_anim_. - closing_ = false; - last_ticks_ = SDL_GetTicks(); + // des del valor actual d'open_anim. + closing = false; + last_ticks = SDL_GetTicks(); return; } if (isOpen()) { close(); } else { - stack_.push_back(buildRoot()); - open_anim_ = 0.0F; - closing_ = false; - animated_h_ = static_cast(boxHeight(stack_.back())); - last_ticks_ = SDL_GetTicks(); + stack.push_back(buildRoot()); + open_anim = 0.0F; + closing = false; + animated_h = static_cast(boxHeight(stack.back())); + last_ticks = SDL_GetTicks(); } } - // close() no buida la pila immediatament: marca closing_ i deixa que - // render() faça decréixer open_anim_ fins a 0. En aquell moment es neteja + // close() no buida la pila immediatament: marca closing i deixa que + // render() faça decréixer open_anim fins a 0. En aquell moment es neteja // l'estat. Si es crida estant ja tancat o tancant-se, no-op. void close() { - if (stack_.empty() || closing_) { + if (stack.empty() || closing) { return; } - closing_ = true; - capturing_ = nullptr; - transition_active_ = false; - transition_progress_ = 1.0F; - last_ticks_ = SDL_GetTicks(); + closing = true; + capturing = nullptr; + transition_active = false; + transition_progress = 1.0F; + last_ticks = SDL_GetTicks(); } auto isCapturing() -> bool { - return capturing_ != nullptr; + return capturing != nullptr; } void captureKey(SDL_Scancode sc) { - if (capturing_ == nullptr) { + if (capturing == nullptr) { return; } if (sc == SDL_SCANCODE_ESCAPE) { // Cancel·la - capturing_ = nullptr; + capturing = nullptr; return; } - *capturing_ = sc; - capturing_ = nullptr; + *capturing = sc; + capturing = nullptr; } void handleKey(SDL_Scancode sc) { if (!isOpen()) { return; } - Page& page = stack_.back(); + Page& page = stack.back(); if (page.items.empty()) { // Pàgina buida — només backspace surt if (sc == SDL_SCANCODE_BACKSPACE) { - if (stack_.size() > 1) { + if (stack.size() > 1) { popPage(); } else { close(); @@ -477,32 +477,32 @@ namespace Menu { page.cursor = nextVisibleCursor(page, page.cursor, +1); break; case SDL_SCANCODE_LEFT: - if (page.items[page.cursor].kind != ItemKind::Submenu && + if (page.items[page.cursor].kind != ItemKind::SUBMENU && page.items[page.cursor].change) { page.items[page.cursor].change(-1); } break; case SDL_SCANCODE_RIGHT: - if (page.items[page.cursor].kind != ItemKind::Submenu && + if (page.items[page.cursor].kind != ItemKind::SUBMENU && page.items[page.cursor].change) { page.items[page.cursor].change(+1); } break; case SDL_SCANCODE_RETURN: case SDL_SCANCODE_KP_ENTER: - if (page.items[page.cursor].kind == ItemKind::Submenu || - page.items[page.cursor].kind == ItemKind::Action) { + if (page.items[page.cursor].kind == ItemKind::SUBMENU || + page.items[page.cursor].kind == ItemKind::ACTION) { if (page.items[page.cursor].enter) { page.items[page.cursor].enter(); } - } else if (page.items[page.cursor].kind == ItemKind::KeyBind) { - capturing_ = page.items[page.cursor].scancode; + } else if (page.items[page.cursor].kind == ItemKind::KEY_BIND) { + capturing = page.items[page.cursor].scancode; } else if (page.items[page.cursor].change) { page.items[page.cursor].change(+1); } break; case SDL_SCANCODE_BACKSPACE: - if (stack_.size() > 1) { + if (stack.size() > 1) { popPage(); } else { close(); @@ -514,8 +514,8 @@ namespace Menu { // Després de qualsevol acció, si el cursor quedara sobre un ítem ocult // (possible si una acció ha canviat la visibilitat pròpia de l'ítem actual, // edge case defensiu), salta al següent visible. - if (!stack_.empty()) { - Page& top = stack_.back(); + if (!stack.empty()) { + Page& top = stack.back(); if (!top.items.empty() && !isVisible(top.items[top.cursor])) { top.cursor = nextVisibleCursor(top, top.cursor, +1); } @@ -527,12 +527,12 @@ namespace Menu { // clip_x_min/clip_x_max limiten on es dibuixa text i la línia separadora. static void renderPageContent(Uint32* pixel_data, const Page& page, int box_x, int box_y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) { // Títol - int title_w = font_->width(page.title); + int title_w = font->width(page.title); int title_x = box_x + ((BOX_W - title_w) / 2) + x_offset; - font_->drawClipped(pixel_data, title_x, box_y + TITLE_PAD_Y, page.title, TITLE_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + font->drawClipped(pixel_data, title_x, box_y + TITLE_PAD_Y, page.title, TITLE_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); // Línia sota el títol (també lliscada) — clippada manualment - int title_line_y = box_y + TITLE_PAD_Y + font_->charHeight() + 2; + int title_line_y = box_y + TITLE_PAD_Y + font->charHeight() + 2; if (title_line_y >= clip_y_min && title_line_y < clip_y_max) { int line_x = box_x + 4 + x_offset; int line_w = BOX_W - 8; @@ -546,17 +546,17 @@ namespace Menu { // Subtítol opcional (sota la línia del títol, abans dels items) int items_y = title_line_y + 4; if (!page.subtitle.empty()) { - int sub_w = font_->width(page.subtitle.c_str()); + int sub_w = font->width(page.subtitle.c_str()); int sub_x = box_x + ((BOX_W - sub_w) / 2) + x_offset; - font_->drawClipped(pixel_data, sub_x, items_y, page.subtitle.c_str(), LABEL_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + font->drawClipped(pixel_data, sub_x, items_y, page.subtitle.c_str(), LABEL_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); items_y += SUBTITLE_H; } // Compta visibles — si cap, dibuixa placeholder (caixa totalment col·lapsada però oberta) - const int visible_count = static_cast(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); })); - if (visible_count == 0) { + const int VISIBLE_COUNT = static_cast(std::count_if(page.items.begin(), page.items.end(), [](const auto& it) { return isVisible(it); })); + if (VISIBLE_COUNT == 0) { const char* empty_text = Locale::get("menu.values.empty"); - int ew = font_->width(empty_text); - font_->drawClipped(pixel_data, box_x + ((BOX_W - ew) / 2) + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + int ew = font->width(empty_text); + font->drawClipped(pixel_data, box_x + ((BOX_W - ew) / 2) + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); return; } @@ -571,30 +571,30 @@ namespace Menu { bool selected = (static_cast(i) == page.cursor); Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR; - // Action: sense valor a la dreta — centrem el label amb el cursor just a l'esquerra. - if (item.kind == ItemKind::Action) { - int lw = font_->width(item.label); + // ACTION: sense valor a la dreta — centrem el label amb el cursor just a l'esquerra. + if (item.kind == ItemKind::ACTION) { + int lw = font->width(item.label); int lx = box_x + ((BOX_W - lw) / 2) + x_offset; if (selected) { - font_->drawClipped(pixel_data, lx - font_->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + font->drawClipped(pixel_data, lx - font->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); } - font_->drawClipped(pixel_data, lx, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + font->drawClipped(pixel_data, lx, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max); continue; } if (selected) { - font_->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + font->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max); } - font_->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + font->drawClipped(pixel_data, box_x + ITEM_PAD_X + x_offset, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max); - if (item.kind == ItemKind::Submenu) { + if (item.kind == ItemKind::SUBMENU) { const char* arrow = ">>"; - int aw = font_->width(arrow); + int aw = font->width(arrow); Uint32 ac = selected ? CURSOR_COLOR : VALUE_COLOR; - font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - aw + x_offset, y, arrow, ac, clip_x_min, clip_x_max, clip_y_min, clip_y_max); - } else if (item.kind == ItemKind::KeyBind) { - bool this_capturing = (capturing_ == item.scancode); + font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - aw + x_offset, y, arrow, ac, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + } else if (item.kind == ItemKind::KEY_BIND) { + bool this_capturing = (capturing == item.scancode); const char* text = nullptr; if (this_capturing) { text = Locale::get("menu.values.press_key"); @@ -606,83 +606,83 @@ namespace Menu { if ((text == nullptr) || (*text == 0)) { text = Locale::get("menu.values.unknown"); } - int tw = font_->width(text); + int tw = font->width(text); Uint32 tc = 0; if (this_capturing) { tc = 0xFF00FFFF; } else { tc = selected ? CURSOR_COLOR : VALUE_COLOR; } - font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - tw + x_offset, y, text, tc, clip_x_min, clip_x_max, clip_y_min, clip_y_max); - } else if (item.getValue) { - std::string value = item.getValue(); - int value_w = font_->width(value.c_str()); + font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - tw + x_offset, y, text, tc, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + } else if (item.get_value) { + std::string value = item.get_value(); + int value_w = font->width(value.c_str()); Uint32 value_color = selected ? CURSOR_COLOR : VALUE_COLOR; - font_->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - value_w + x_offset, y, value.c_str(), value_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - value_w + x_offset, y, value.c_str(), value_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max); } } } void render(Uint32* pixel_data) { - if (!isVisible() || !font_ || (pixel_data == nullptr)) { + if (!isVisible() || !font || (pixel_data == nullptr)) { return; } // Delta time Uint32 now = SDL_GetTicks(); - float dt = static_cast(now - last_ticks_) / 1000.0F; - last_ticks_ = now; - if (closing_) { - open_anim_ -= CLOSE_SPEED * dt; - if (open_anim_ <= 0.0F) { + float dt = static_cast(now - last_ticks) / 1000.0F; + last_ticks = now; + if (closing) { + open_anim -= CLOSE_SPEED * dt; + if (open_anim <= 0.0F) { // Animació de tancament completada — buida l'estat de veritat. - open_anim_ = 0.0F; - stack_.clear(); - animated_h_ = 0.0F; - closing_ = false; + open_anim = 0.0F; + stack.clear(); + animated_h = 0.0F; + closing = false; return; } - } else if (open_anim_ < 1.0F) { - open_anim_ += OPEN_SPEED * dt; - open_anim_ = std::min(open_anim_, 1.0F); + } else if (open_anim < 1.0F) { + open_anim += OPEN_SPEED * dt; + open_anim = std::min(open_anim, 1.0F); } // Avança transició - if (transition_active_) { - transition_progress_ += TRANSITION_SPEED * dt; - if (transition_progress_ >= 1.0F) { - transition_progress_ = 1.0F; - transition_active_ = false; + if (transition_active) { + transition_progress += TRANSITION_SPEED * dt; + if (transition_progress >= 1.0F) { + transition_progress = 1.0F; + transition_active = false; } } - const Page& page = stack_.back(); - const int current_h = boxHeight(page); + const Page& page = stack.back(); + const int CURRENT_H = boxHeight(page); // Smoothing exponencial de l'alçada cap al target (pàgina actual + ítems visibles). // Permet que el menú reaccione amb animació quan una opció canvia la visibilitat // d'altres ítems en calent (p. ex. shader=off → shader_type/preset/supersampling). - if (animated_h_ <= 0.0F) { - animated_h_ = static_cast(current_h); + if (animated_h <= 0.0F) { + animated_h = static_cast(CURRENT_H); } else { - float diff = static_cast(current_h) - animated_h_; + float diff = static_cast(CURRENT_H) - animated_h; if (std::fabs(diff) < 0.5F) { - animated_h_ = static_cast(current_h); + animated_h = static_cast(CURRENT_H); } else { float t = HEIGHT_RATE * dt; t = std::min(t, 1.0F); - animated_h_ += diff * t; + animated_h += diff * t; } } - float eased = Easing::outQuad(open_anim_); + float eased = Easing::outQuad(open_anim); // Calcula alçada (amb transició si escau) - int target_h = static_cast(animated_h_); - if (transition_active_) { - int outgoing_h = boxHeight(transition_outgoing_); - float tp = Easing::outQuad(transition_progress_); - target_h = Easing::lerpInt(outgoing_h, static_cast(animated_h_), tp); + int target_h = static_cast(animated_h); + if (transition_active) { + int outgoing_h = boxHeight(transition_outgoing); + float tp = Easing::outQuad(transition_progress); + target_h = Easing::lerpInt(outgoing_h, static_cast(animated_h), tp); } // Caixa creix verticalment durant l'obertura @@ -696,17 +696,17 @@ namespace Menu { blendRect(pixel_data, box_x, box_y, BOX_W, box_h, BG_COLOR, alpha); // El contingut només apareix quan la caixa és prou gran - if (open_anim_ >= 0.9F) { + if (open_anim >= 0.9F) { int clip_x_min = box_x + 1; int clip_x_max = box_x + BOX_W - 1; int clip_y_min = box_y + 1; int clip_y_max = box_y + box_h - 1; - if (transition_active_) { - float tp = Easing::outQuad(transition_progress_); - int out_offset = static_cast(-transition_dir_ * BOX_W * tp); - int new_offset = static_cast(transition_dir_ * BOX_W * (1.0F - tp)); - renderPageContent(pixel_data, transition_outgoing_, box_x, box_y, out_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max); + if (transition_active) { + float tp = Easing::outQuad(transition_progress); + int out_offset = static_cast(-transition_dir * BOX_W * tp); + int new_offset = static_cast(transition_dir * BOX_W * (1.0F - tp)); + renderPageContent(pixel_data, transition_outgoing, box_x, box_y, out_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max); renderPageContent(pixel_data, page, box_x, box_y, new_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max); } else { renderPageContent(pixel_data, page, box_x, box_y, 0, clip_x_min, clip_x_max, clip_y_min, clip_y_max);