feat(menu): submenú Controls i bloqueig de polling amb menú obert
This commit is contained in:
1 parent
350625dec0
commit
fb40d0effb
8 files changed
+229
-7
No files matched your search
@@ -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;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user