// service_menu.hpp - Menu de servei (singleton) // © 2026 JailDesigner // // Overlay de configuracio global accessible amb F12 des de qualsevol escena // (LOGO, TITLE, GAME). Captura tots els KEY_DOWN excepte F1-F12 i ESC, que // continuen arribant a GlobalEvents. Mentre esta obert, GameScene::update() // fa early return per pausar el joc; LOGO i TITLE continuen renderitzant-se // sota el menu. // // Arquitectura inspirada en aee_arcade service_menu.{hpp,cpp}: pila de // pagines amb cursor, animacio open/close amb easing easeOutQuad i clipping // del contingut mentre la caixa creix/decreix. // // API singleton equivalent a Notifier: init() al startup amb un renderer, // get() retorna el punter, destroy() al teardown. #pragma once #include #include #include #include #include #include #include #include "core/graphics/vector_text.hpp" #include "core/rendering/render_context.hpp" class SDLManager; namespace System { class DebugOverlay; class ServiceMenu { public: // Tipus d'item de menu. En aquesta iteracio nomes s'usen SUBMENU i // LABEL; la resta queden reservats per a iteracions futures (toggles // de vsync/zoom, picker d'idioma, restart, exit...). enum class Kind : std::uint8_t { LABEL, // No interactiu, nomes es dibuixa TOGGLE, // bool flip — reservat CYCLE, // index amb modul — reservat INT_RANGE, // step ± — reservat SUBMENU, // pushPage en activar — usat ACTION // call al lambda en activar — reservat }; struct Item { Kind kind = Kind::LABEL; std::string label_key; // Clau de locale (s'ignora si label_text no esta buit) std::string label_text; // Text literal (no locale). Util per a labels que no necessiten traduccio (resolucions, etc.) bool selectable = true; // SUBMENU / ACTION: callback en ENTER / RIGHT. std::function on_activate; // TOGGLE / CYCLE / INT_RANGE: text del valor actual (renderitzat a la dreta). std::function get_value_text; // TOGGLE / CYCLE / INT_RANGE: callback amb +1 (RIGHT/ENTER) o -1 (LEFT). std::function on_change; }; struct Page { std::string title_key; // Subtitol opcional, renderitzat sota el titol amb tipografia mes // petita i color apagat. Es una funcio perque pot ser dinamic // (versio+hash, etc.). Si esta buit, no es renderitza. std::function subtitle_provider; std::vector items; std::size_t cursor = 0; }; // Inicialitza el singleton amb el renderer global, l'SDLManager (video // toggles: fullscreen, vsync, AA, postfx, zoom) i el DebugOverlay // (toggle del HUD de debug a OPCIONS). Tots propietat del Director. static void init(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay); static void destroy(); [[nodiscard]] static auto get() -> ServiceMenu*; // F12: alterna obrir/tancar amb animacio. void toggle(); [[nodiscard]] auto isOpen() const -> bool; void update(float delta_time); void draw() const; // Processa events de navegacio. Retorna true si l'event s'ha consumit. // Accepta: // - SDL_EVENT_KEY_DOWN: UP/DOWN/ENTER/RIGHT/LEFT/BACKSPACE. // - SDL_EVENT_GAMEPAD_BUTTON_DOWN: DPAD per nav, FIRE = ENTER, // ACCELERATE = BACK. La resta de botons s'ignoren. // - SDL_EVENT_GAMEPAD_AXIS_MOTION: stick X/Y amb edge-detect. auto handleEvent(const SDL_Event& event) -> bool; private: ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay); // Sub-handlers de handleEvent. Privats, no son part de l'API publica. auto handleKeyDown(const SDL_Event& event) -> bool; auto handleGamepadButton(const SDL_Event& event) -> bool; auto handleGamepadAxis(const SDL_Event& event) -> bool; // Helpers per a cada eix; permeten que handleGamepadAxis es quedi // com a dispatcher i no bote el llindar de complexitat. void processStickX(Sint16 val); void processStickY(Sint16 val); void processTriggerEdge(SDL_JoystickID which, Sint16 val, int virtual_button, bool& held); void buildRootPage(); [[nodiscard]] auto buildVideoPage() -> Page; [[nodiscard]] auto buildResolutionPage() const -> Page; [[nodiscard]] static auto buildAudioPage() -> Page; [[nodiscard]] auto buildOptionsPage() const -> Page; [[nodiscard]] auto buildSystemPage() -> Page; [[nodiscard]] auto buildControlsPage() -> Page; // Llista de mandos detectats per a un jugador. Cada item assigna el // pad triat (amb swap automatic si l'altre jugador ja el tenia) i // tanca la picker amb popPage. L'ultim item es "SENSE MANDO" per a // desasignar. [[nodiscard]] auto buildPadPickerPage(int player_index) -> Page; // Pagina de confirmacio "ESTAS SEGUR? NO/SI". on_yes s'executa si // l'usuari selecciona SI; el cursor per defecte apunta a NO. void pushConfirmPage(const std::string& title_key, std::function on_yes); void pushPage(Page page); void popPage(); void moveCursor(int direction); void activateCurrent(); // RIGHT (direction=+1) / LEFT (direction=-1). Per a TOGGLE/CYCLE/INT_RANGE // crida on_change. Per a SUBMENU/ACTION nomes +1 (entra/activa). void changeValue(int direction); // Alçada objectiu de la caixa per a la pagina superior (sense animacio). [[nodiscard]] auto computeTargetHeight() const -> float; // Ample objectiu de la caixa per a la pagina superior (sense animacio). // Pren com a base BOX_WIDTH_MIN i s'eixampla si algun text no hi cap. [[nodiscard]] auto computeTargetWidth() const -> float; // Y (top) de l'item index dins una caixa col·locada a box_y. Si la // pagina te subtitol, els items es desplacen cap avall. [[nodiscard]] static auto computeItemTopY(float box_y, std::size_t index, bool has_subtitle) -> float; Rendering::Renderer* renderer_; SDLManager* sdl_; DebugOverlay* debug_overlay_; Graphics::VectorText text_; std::vector stack_; bool open_ = false; bool closing_ = false; float open_anim_ = 0.0F; // 0..1 raw (sense easing) float animated_h_ = 0.0F; // Alçada animada amb smoothing exponencial float animated_w_ = 0.0F; // Ample animat (eixampla segons contingut) // Estat del highlight (rectangle del cursor). Es lerpa cap a l'item // actiu amb ease-out exponencial; quan el cursor "salta" (open o // push/pop de pagina), s'enganxa directament al nou objectiu. float highlight_y_ = 0.0F; float highlight_h_ = 0.0F; bool highlight_snap_ = true; // Edge-detect de stick analogic per a navegacio. Una sola activacio // per direccio: cal tornar a centre (sota el llindar) per disparar // una altra. Compartit entre tots els pads — qualsevol jugador pot // navegar el menu. bool stick_left_held_ = false; bool stick_right_held_ = false; bool stick_up_held_ = false; bool stick_down_held_ = false; // Edge-detect dels triggers L2/R2 com a botons virtuals. SDL3 no // emet button events per als triggers; els llegim com a axis i // sintetitzem una pulsacio quan creuen el llindar. bool trigger_l2_held_ = false; bool trigger_r2_held_ = false; static std::unique_ptr instance; }; } // namespace System