Files
aee-arcade/source/game/ui/service_menu.hpp
T

217 lines
9.0 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#pragma once
#include <SDL3/SDL.h>
#include <cstddef>
#include <functional>
#include <memory>
#include <string>
#include <vector>
#include "core/rendering/surface.hpp" // Palette
class Text;
// Menú de servei (F9). Caixa centrada amb títol i items navegables per
// fletxes + Retorn; Retrocés / Escape tanca la pàgina actual o el menú.
// És accessible només dins de `GameScene` (els placeholders LOGO/TITLE/INTRO
// duren 5 s i no tenen sentit com a entrada al menú; veure plan §11).
//
// La pila de pàgines (`stack_`) permet submenús; la pàgina de dalt és la
// que rep input i es dibuixa. `pushPage` apila, `popPage` treu la de dalt;
// en tornar a quedar amb una sola pàgina, backspace/escape tanca el menú.
class ServiceMenu {
public:
static auto get() -> ServiceMenu&;
ServiceMenu(const ServiceMenu&) = delete;
ServiceMenu(ServiceMenu&&) = delete;
auto operator=(const ServiceMenu&) -> ServiceMenu& = delete;
auto operator=(ServiceMenu&&) -> ServiceMenu& = delete;
// Injecta les dues fonts del menú i la paleta UI. Ha de cridar-se una
// sola vegada (des de Director) abans de qualsevol `render()` o
// `toggle()`. Cada font pot portar el seu kerning pre-configurat
// (via `Text::setDefaultKerning`).
void init(std::shared_ptr<Text> title_font,
std::shared_ptr<Text> items_font,
const Palette& ui_palette);
// "Obert" = el menú accepta input. Torna `false` durant l'animació de
// tancament (la caixa es veu però ja no respon).
[[nodiscard]] auto isOpen() const -> bool { return open_; }
void toggle();
void close();
// Avança animacions d'obertura, tancament i alçada. Ha de cridar-se
// cada frame des del Director (no des de cada escena).
void update(int delta_ms);
void render();
// Retorna true si el menú ha consumit l'esdeveniment. Quan el menú està
// obert, consumeix tots els KEY_DOWN perquè les hotkeys globals (F1–F8,
// F11, F12) no es disparin per sota. Quan està tancat sempre retorna
// false; és el caller qui decideix obrir-lo amb la hotkey.
auto handleEvent(const SDL_Event& ev) -> bool;
private:
// --- Struct d'un item d'una pàgina ---
//
// La unió de totes les "variants" (toggle / cycle / ...) en un sol
// struct amb `std::function` nominalment opcionals simplifica la
// construcció dels items (nomes s'omplen els slots del Kind rellevant)
// a canvi d'una mica d'overhead. Amb una dotzena d'items no és un
// problema de rendiment.
struct Item {
enum class Kind : std::uint8_t {
LABEL, // Text pur, no interacciona; ideal per a versions/notes.
TOGGLE, // ON / OFF amb bool getter/setter
CYCLE, // Índex dins d'una llista; L=prev, R=next (wrap)
INT_RANGE, // L/R descent/ascent per step, clamp a [min,max]
SUBMENU, // RETURN/R: push la pàgina construïda per `pushSub`
ACTION, // RETURN: dispara `action`
};
Kind kind{Kind::TOGGLE};
// Per a tots els Kind menys LABEL, `label_key` és una clau de Locale.
// Per a LABEL, és text literal (no passa per Locale) perquè típicament
// porta valors dinàmics (versió, hash de commit, etc.).
std::string label_key;
std::function<bool()> get_bool;
std::function<void(bool)> set_bool;
std::function<std::size_t()> get_index;
std::function<std::size_t()> get_count;
std::function<void(std::size_t)> set_index;
std::function<std::string(std::size_t)> value_label;
std::function<int()> get_int;
std::function<void(int)> set_int;
int min_value{0};
int max_value{0};
int step{1};
std::function<std::string(int)> int_display;
std::function<void()> push_sub;
std::function<void()> action;
// Si està buit, l'item sempre és visible. Si retorna false, l'item
// es salta tant al render com a la navegació (cursor salta).
std::function<bool()> visible;
// Si `false`, el cursor no pot aterrar-hi: es salta a l'hora de
// navegar amb ↑/↓. Default `true` per la gran majoria d'items; les
// etiquetes LABEL el deixen `false` perquè no tenen interacció.
bool selectable{true};
};
struct Page {
std::string title_key; // "menu.titles.video" etc.
std::string subtitle; // Text ja format (buit si no aplica).
std::vector<Item> items;
std::size_t cursor{0};
// Si `true`, els items es dibuixen centrats dins de la caixa i el
// cursor es col·loca immediatament a l'esquerra de l'etiqueta.
// Adequat per a pàgines que només tenen botons d'acció (p.ex. Sistema).
bool center_items{false};
};
ServiceMenu() = default;
~ServiceMenu() = default;
// --- Factories d'Items ---
//
// Cada factory omple els slots rellevants del Kind i deixa la resta
// default-construïts. Redueix el soroll al lloc d'ús i, el més important,
// elimina el warning `-Wmissing-designated-field-initializers` que
// apareixeria amb designated-initializers parcials.
[[nodiscard]] static auto makeToggle(
std::string label_key,
std::function<bool()> getter,
std::function<void(bool)> setter,
std::function<bool()> visible = {}) -> Item;
[[nodiscard]] static auto makeCycle(
std::string label_key,
std::function<std::size_t()> get_index,
std::function<std::size_t()> get_count,
std::function<void(std::size_t)> set_index,
std::function<std::string(std::size_t)> value_label,
std::function<bool()> visible = {}) -> Item;
[[nodiscard]] static auto makeIntRange(
std::string label_key,
std::function<int()> get_int,
std::function<void(int)> set_int,
int min_value,
int max_value,
int step,
std::function<std::string(int)> int_display = {},
std::function<bool()> visible = {}) -> Item;
[[nodiscard]] static auto makeSubmenu(
std::string label_key,
std::function<void()> push_sub) -> Item;
[[nodiscard]] static auto makeAction(
std::string label_key,
std::function<void()> action) -> Item;
// Etiqueta no seleccionable. `text` és el literal a mostrar (no passa
// per Locale), perquè l'ús principal és mostrar versions o hashos que
// ja es calculen a codi.
[[nodiscard]] static auto makeLabel(
std::string text,
std::function<bool()> visible = {}) -> Item;
// --- Navegació i estat ---
void pushPage(Page page);
void popPage();
[[nodiscard]] auto top() -> Page&;
[[nodiscard]] auto topConst() const -> const Page&;
[[nodiscard]] static auto isItemVisible(const Item& item) -> bool;
void moveCursor(int direction); // +1 / -1, salta items no visibles
void activateCurrent(int direction); // Aplica l'acció segons el Kind
static void activateCycle(const Item& item, int direction);
static void activateIntRange(const Item& item, int direction);
// --- Constructors de pàgines (viuen a .cpp) ---
// Són static perquè no necessiten estat d'instància: tots els getters
// i setters que capturen dins dels items accedeixen via `Options::` o
// `ServiceMenu::get()` directament.
[[nodiscard]] static auto buildRoot() -> Page;
[[nodiscard]] static auto buildVideo() -> Page;
[[nodiscard]] static auto buildAudio() -> Page;
[[nodiscard]] static auto buildSystem() -> Page;
// Helpers privats d'items de vídeo: aïllen la lògica de
// `shader_type` i `preset` fora de `buildVideo` perquè no exploti la
// complexitat cognitiva del constructor de pàgines.
[[nodiscard]] static auto makeShaderTypeItem() -> Item;
[[nodiscard]] static auto makeShaderPresetItem() -> Item;
// --- Render ---
// "Visible" inclou l'animació de tancament: retorna true mentre la caixa
// encara ha de dibuixar-se, encara que open_ siga false.
[[nodiscard]] auto isVisible() const -> bool;
[[nodiscard]] auto targetBoxHeight(const Page& page) const -> int;
void renderBox(int box_x, int box_y, int box_w, int box_h, float eased) const;
void renderPage(const Page& page, int box_x, int box_y, int box_w) const;
[[nodiscard]] static auto formatValue(const Item& item) -> std::string;
// --- Estat ---
std::shared_ptr<Text> font_; // Títol del menú (`ari-w9500-display`).
std::shared_ptr<Text> items_font_; // Items del menú (`ari-w9500-condensed-display`).
Palette ui_palette_{};
std::vector<Page> stack_;
bool open_{false};
bool closing_{false}; // `true` entre `close()` i fi de l'animació.
float open_anim_{0.0F}; // 0 = invisible, 1 = totalment obert.
float animated_h_{0.0F}; // Alçada suavitzada cap a `targetBoxHeight`.
bool transition_active_{false}; // Push/pop de pàgina en curs.
float transition_progress_{0.0F}; // 0 → 1 durant la transició de pàgina.
};