feat(menu): submenú Controls i bloqueig de polling amb menú obert

This commit is contained in:
JailDesigner committed 2026-04-22 19:05:36 +02:00
1 parent 350625dec0
commit fb40d0effb
8 files changed
+229 -7

No files matched your search

+6 -1
View File
@@ -8,6 +8,7 @@
#include <stdexcept>
#include <utility>
#include "core/input/input_state.hpp"
#include "core/resources/resource_cache.hpp"
#include "core/resources/resource_helper.hpp"
#include "external/fkyaml_node.hpp"
@@ -57,7 +58,11 @@ Player::Player(const std::string& anim_key, float x, float y, const Controls& co
}
void Player::update(float delta_time) {
const bool* keys = SDL_GetKeyboardState(nullptr);
// Via `InputState` en lloc de `SDL_GetKeyboardState` directe: quan el
// service menu (o qualsevol UI modal) està obert, el wrapper retorna
// un buffer tot-a-zero perquè el jugador no es mogui mentre l'usuari
// navega el menú. Polling-blocked; veure `core/input/input_state.hpp`.
const bool* keys = InputState::getKeyboardState();
float vx = 0.0F;
float vy = 0.0F;
if (keys[controls_.left]) { vx -= SPEED_PX_PER_S; }
+100 -6
View File
@@ -7,6 +7,7 @@
#include <utility>
#include "core/audio/audio.hpp"
#include "core/input/input_state.hpp"
#include "core/input/key_config.hpp"
#include "core/locale/locale.hpp"
#include "core/rendering/canvas.hpp"
@@ -33,6 +34,13 @@ namespace {
constexpr int PAD_X = 10;
constexpr int GAP_Y = 6;
constexpr int ITEM_GAP_Y = 2;
// Les fonts display reserven ~2 px de descendent al fons de la seua
// caixa (per a `g`, `p`, `q`...). Quan el títol no porta cap d'aquests
// glifs, eixos 2 px queden buits i el gap visual sota el títol sembla
// més gran que el del damunt. Compensem-ho restant-los al càlcul
// d'alçada i al posicionament del separador; així el títol queda
// òpticament centrat entre la vora superior i la línia divisòria.
constexpr int TITLE_DESCENDER_TRIM = 2;
constexpr int MIN_ZOOM = 1;
constexpr int MAX_ZOOM = 6;
@@ -188,6 +196,22 @@ auto ServiceMenu::makeLabel(std::string text, std::function<bool()> visible) ->
return item;
}
auto ServiceMenu::makeKeyBind(std::string label_key,
std::function<SDL_Scancode()> getter,
std::function<void(SDL_Scancode)> setter) -> Item {
Item item;
item.kind = Item::Kind::KEY_BIND;
item.label_key = std::move(label_key);
item.get_scancode = std::move(getter);
item.set_scancode = std::move(setter);
// Si no hi ha setter, l'item queda purament informatiu: no pot capturar
// una tecla nova. Així la primera iteració de la pàgina Controls pot
// mostrar les vinculacions actuals sense tenir encara la infraestructura
// de mutació a `Options::`.
item.selectable = static_cast<bool>(item.set_scancode);
return item;
}
// --- Cicle de vida ---
auto ServiceMenu::get() -> ServiceMenu& {
@@ -212,6 +236,11 @@ void ServiceMenu::toggle() {
open_ = true;
closing_ = false;
animated_h_ = 0.0F; // Comença des de 0 perquè la caixa crega.
// Bloqueja l'input de polling: els consumidors com `Player` veuran
// l'estat del teclat tot-a-zero mentre el menú sigui obert. Els
// events (KEY_DOWN, etc.) ja els consumeix `processMenuInput`,
// així que la navegació del menú no es veu afectada.
InputState::setBlocked(true);
}
}
@@ -219,6 +248,9 @@ void ServiceMenu::close() {
if (!open_) { return; }
open_ = false;
closing_ = true; // `open_anim_` decaurà a update; la caixa es veu mentre dura.
// Allibera el polling de seguida — la closing animation és purament
// visual i no ha d'impedir que el jugador es mogui un cop ha tancat.
InputState::setBlocked(false);
}
// `isVisible` inclou l'animació de tancament: retorna true mentre open_anim_>0
@@ -381,6 +413,12 @@ void ServiceMenu::activateCurrent(int direction) {
case Item::Kind::ACTION:
if (direction > 0 && item.action) { item.action(); }
break;
case Item::Kind::KEY_BIND:
// TODO: entrar en mode captura quan hi hagi `set_scancode`.
// De moment no fem res — els items sense setter ja no són
// seleccionables (veure `makeKeyBind`), per tant aquest case
// només es trepitja si algú en fa un amb setter en el futur.
break;
}
}
@@ -432,6 +470,8 @@ auto ServiceMenu::buildRoot() -> Page {
[]() { ServiceMenu::get().pushPage(buildVideo()); }));
page.items.push_back(makeSubmenu("menu.items.audio_submenu",
[]() { ServiceMenu::get().pushPage(buildAudio()); }));
page.items.push_back(makeSubmenu("menu.items.controls_submenu",
[]() { ServiceMenu::get().pushPage(buildControls()); }));
page.items.push_back(makeSubmenu("menu.items.system_submenu",
[]() { ServiceMenu::get().pushPage(buildSystem()); }));
return page;
@@ -639,6 +679,55 @@ auto ServiceMenu::buildAudio() -> Page {
return page;
}
auto ServiceMenu::buildControls() -> Page {
Page page;
page.title_key = "menu.titles.controls";
page.items.push_back(makeSubmenu("menu.items.keyboard_submenu",
[]() { ServiceMenu::get().pushPage(buildKeyboardControls()); }));
// Comandament: la pàgina existeix però encara no té infraestructura
// d'integració (no detectem gamepads ni mapegem botons). Entra-hi per
// veure un placeholder fins que la lògica arribi.
page.items.push_back(makeSubmenu("menu.items.gamepad_submenu",
[]() { ServiceMenu::get().pushPage(buildGamepadControls()); }));
return page;
}
auto ServiceMenu::buildKeyboardControls() -> Page {
Page page;
page.title_key = "menu.titles.keyboard";
// Etiqueta divisòria per al jugador 1. Quan s'afegeixi el jugador 2,
// només s'ha d'apilar un segon bloc "Jugador 2" amb les seves tecles
// per sota, sense tocar cap altra cosa.
page.items.push_back(makeLabel(Locale::get().text("menu.labels.player_1")));
// Vinculacions actuals: les llegim directament de `Defaults::KeysGame`.
// Encara no hi ha setter, per tant els items surten com a informatius
// (no seleccionables). La lògica de captura i persistència arribarà en
// una segona iteració amb `Options::keysGameP1()`.
page.items.push_back(makeKeyBind("menu.items.move_up",
[]() { return Defaults::KeysGame::UP; }));
page.items.push_back(makeKeyBind("menu.items.move_down",
[]() { return Defaults::KeysGame::DOWN; }));
page.items.push_back(makeKeyBind("menu.items.move_left",
[]() { return Defaults::KeysGame::LEFT; }));
page.items.push_back(makeKeyBind("menu.items.move_right",
[]() { return Defaults::KeysGame::RIGHT; }));
return page;
}
auto ServiceMenu::buildGamepadControls() -> Page {
Page page;
page.title_key = "menu.titles.gamepad";
page.center_items = true;
// Placeholder: un sol LABEL que indica que la pàgina encara no té
// contingut real. Quan s'afegeixi el suport de gamepad, aquesta
// pàgina es substituirà pel llistat real de botons mapejables.
page.items.push_back(makeLabel(Locale::get().text("menu.labels.gamepad_unavailable")));
return page;
}
auto ServiceMenu::buildSystem() -> Page {
Page page;
page.title_key = "menu.titles.system";
@@ -690,6 +779,8 @@ auto ServiceMenu::formatValue(const Item& item) -> std::string {
return Locale::get().text("menu.values.submenu_arrow");
case Item::Kind::ACTION:
return {};
case Item::Kind::KEY_BIND:
return item.get_scancode ? SDL_GetScancodeName(item.get_scancode()) : std::string{};
}
return {};
}
@@ -703,10 +794,10 @@ auto ServiceMenu::targetBoxHeight(const Page& page) const -> int {
// Layout: GAP_Y enmig dels blocs estructurals (top, títol↔separador,
// separador↔primer item, últim item↔bottom). Entre items consecutius,
// ITEM_GAP_Y (més petit, perquè els items no queden massa airejats).
int h = GAP_Y; // marge superior
h += TITLE_H + GAP_Y; // títol + gap estructural
int h = GAP_Y; // marge superior
h += TITLE_H + GAP_Y - TITLE_DESCENDER_TRIM; // títol + gap (compensant descendent)
if (!page.subtitle.empty()) {
h += TITLE_H + GAP_Y; // subtítol + gap estructural
h += TITLE_H + GAP_Y - TITLE_DESCENDER_TRIM; // subtítol idem
}
h += 1 + GAP_Y; // separador d'1 px + gap estructural
if (VISIBLE_ITEMS > 0) {
@@ -752,7 +843,7 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w)
Locale::get().text(page.title_key),
TITLE_KERNING,
UiColor::MENU_TITLE);
cursor_y += TITLE_H + GAP_Y;
cursor_y += TITLE_H + GAP_Y - TITLE_DESCENDER_TRIM;
if (!page.subtitle.empty()) {
font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG,
@@ -761,7 +852,7 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w)
page.subtitle,
TITLE_KERNING,
UiColor::MENU_TITLE);
cursor_y += TITLE_H + GAP_Y;
cursor_y += TITLE_H + GAP_Y - TITLE_DESCENDER_TRIM;
}
Screen::get().fillRect(box_x + PAD_X, cursor_y, box_w - (2 * PAD_X), 1, ui_palette_[UiColor::MENU_BORDER]);
@@ -783,7 +874,10 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w)
Uint8 label_color = UiColor::MENU_LABEL;
if (item.kind == Item::Kind::LABEL) {
label_color = UiColor::MENU_DISABLED; // to gris per diferenciar-lo dels botons actius
// Mateix to que el títol de la pàgina: les LABELs actuen com
// a subtítol-divisor (p.ex. "Jugador 1" a Teclat, "ver.X.Y" a
// Sistema) i han de tenir la mateixa presència visual.
label_color = UiColor::MENU_TITLE;
} else if (SELECTED) {
label_color = UiColor::MENU_CURSOR;
}
+19
View File
@@ -72,6 +72,7 @@ class ServiceMenu {
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`
KEY_BIND, // Mostra el nom d'una tecla; RETURN captura-ne de nova.
};
Kind kind{Kind::TOGGLE};
// Per a tots els Kind menys LABEL, `label_key` és una clau de Locale.
@@ -97,6 +98,12 @@ class ServiceMenu {
std::function<void()> push_sub;
std::function<void()> action;
// Per a KEY_BIND: getter que retorna l'scancode actual i setter
// opcional que el muta. Si `set_scancode` és buit, l'item es veu
// però la captura queda deshabilitada (placeholder).
std::function<SDL_Scancode()> get_scancode;
std::function<void(SDL_Scancode)> set_scancode;
// 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;
@@ -166,6 +173,15 @@ class ServiceMenu {
std::string text,
std::function<bool()> visible = {}) -> Item;
// Item de vinculació de tecla. Mostra el nom de l'scancode al costat
// dret. Si `setter` és buit, l'item surt com a informatiu (no captura).
// Quan el setter sigui operatiu, RETURN entrarà en mode captura i la
// pròxima tecla que es premi sobreescriurà la vinculació.
[[nodiscard]] static auto makeKeyBind(
std::string label_key,
std::function<SDL_Scancode()> getter,
std::function<void(SDL_Scancode)> setter = {}) -> Item;
// --- Navegació i estat ---
void pushPage(Page page);
void popPage();
@@ -184,6 +200,9 @@ class ServiceMenu {
[[nodiscard]] static auto buildRoot() -> Page;
[[nodiscard]] static auto buildVideo() -> Page;
[[nodiscard]] static auto buildAudio() -> Page;
[[nodiscard]] static auto buildControls() -> Page;
[[nodiscard]] static auto buildKeyboardControls() -> Page;
[[nodiscard]] static auto buildGamepadControls() -> Page;
[[nodiscard]] static auto buildSystem() -> Page;
// Helpers privats d'items de vídeo: aïllen la lògica de