feat(menu): primera versió del menú de servei

This commit is contained in:
JailDesigner committed 2026-04-22 18:15:00 +02:00
1 parent e4a8b4cd3e
commit 350625dec0
21 files changed
+1477 -16

No files matched your search

+216
View File
@@ -0,0 +1,216 @@
#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.
};