feat(service-menu): pobla SISTEMA amb reinici, eixir i confirmacions

This commit is contained in:
2026-05-24 12:18:39 +02:00
parent 8c21345f14
commit 22827c28fa
8 changed files with 338 additions and 38 deletions
+7
View File
@@ -71,6 +71,13 @@ service_menu:
audio_music_volume: "VOLUM MUSICA" audio_music_volume: "VOLUM MUSICA"
audio_sound: "EFECTES" audio_sound: "EFECTES"
audio_sound_volume: "VOLUM EFECTES" audio_sound_volume: "VOLUM EFECTES"
# Items del submenu SISTEMA
system_restart: "REINICIAR"
# Pagines de confirmacio (estructura: titol + NO/SI)
confirm_restart: "ESTAS SEGUR DE REINICIAR?"
confirm_exit: "ESTAS SEGUR DE EIXIR?"
confirm_no: "NO"
confirm_yes: "SI"
# Valors comuns # Valors comuns
value_on: "ACTIU" value_on: "ACTIU"
value_off: "INACTIU" value_off: "INACTIU"
+7
View File
@@ -70,6 +70,13 @@ service_menu:
audio_music_volume: "MUSIC VOLUME" audio_music_volume: "MUSIC VOLUME"
audio_sound: "SOUNDS" audio_sound: "SOUNDS"
audio_sound_volume: "SOUND VOLUME" audio_sound_volume: "SOUND VOLUME"
# Items of SYSTEM submenu
system_restart: "RESTART"
# Confirmation pages (structure: title + NO/YES)
confirm_restart: "REALLY RESTART?"
confirm_exit: "REALLY EXIT?"
confirm_no: "NO"
confirm_yes: "YES"
# Common values # Common values
value_on: "ON" value_on: "ON"
value_off: "OFF" value_off: "OFF"
+14 -7
View File
@@ -8,9 +8,12 @@
namespace Defaults::ServiceMenu { namespace Defaults::ServiceMenu {
// ---- Mides en coordenades logiques del joc (1280×720) ---- // ---- Mides en coordenades logiques del joc (1280×720) ----
constexpr int BOX_WIDTH = 460; // BOX_WIDTH_MIN es el minim: si el titol o algun item no hi caben, el
// marc s'expandeix dinamicament amb animacio (cf. WIDTH_RATE).
constexpr int BOX_WIDTH_MIN = 460;
constexpr int GAP_Y = 22; constexpr int GAP_Y = 22;
constexpr int TITLE_HEIGHT = 36; // scale 0.85 ≈ 34 px de text constexpr int TITLE_HEIGHT = 36; // scale 0.85 ≈ 34 px de text
constexpr int SUBTITLE_HEIGHT = 18; // scale 0.4 ≈ 16 px de text
constexpr int SEPARATOR_HEIGHT = 1; constexpr int SEPARATOR_HEIGHT = 1;
constexpr int ITEM_HEIGHT = 38; // scale 0.55 ≈ 22 px de text + padding per al highlight constexpr int ITEM_HEIGHT = 38; // scale 0.55 ≈ 22 px de text + padding per al highlight
constexpr int ITEM_GAP_Y = 6; constexpr int ITEM_GAP_Y = 6;
@@ -24,20 +27,23 @@ namespace Defaults::ServiceMenu {
constexpr float OPEN_SPEED = 8.0F; // ~125 ms a obrir constexpr float OPEN_SPEED = 8.0F; // ~125 ms a obrir
constexpr float CLOSE_SPEED = 10.0F; // ~100 ms a tancar constexpr float CLOSE_SPEED = 10.0F; // ~100 ms a tancar
constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada de la caixa constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada de la caixa
constexpr float WIDTH_RATE = 12.0F; // smoothing per a canvis d'ample entre pagines
// ---- Animacio del highlight (rectangle del cursor) ---- // ---- Animacio del highlight (rectangle del cursor) ----
// Rate=18 dona settling ~0.17 s al 95% (ease-out exponencial). // Rate=18 dona settling ~0.17 s al 95% (ease-out exponencial).
constexpr float HIGHLIGHT_RATE = 18.0F; constexpr float HIGHLIGHT_RATE = 18.0F;
constexpr int HIGHLIGHT_TICK_LEN = 10; // longitud dels ticks a cada cantonada constexpr int HIGHLIGHT_TICK_LEN = 10; // longitud dels ticks a cada cantonada
constexpr int HIGHLIGHT_THICKNESS = 1; constexpr int HIGHLIGHT_THICKNESS = 1;
constexpr int HIGHLIGHT_PAD_X = 18; // padding lateral del rect respecte al text constexpr int HIGHLIGHT_PAD_X = 18; // padding lateral del rect respecte al text
constexpr int HIGHLIGHT_PAD_Y = 4; // padding vertical constexpr int HIGHLIGHT_PAD_Y = 4; // padding vertical
constexpr int TEXT_INSET_X = 16; // marge intern del text dins del highlight (label esq / valor dre) constexpr int TEXT_INSET_X = 16; // marge intern del text dins del highlight (label esq / valor dre)
constexpr int MIN_LABEL_VALUE_GAP = 30; // mínim gap entre label i valor (per al càlcul d'ample dinàmic)
// ---- Colors RGBA ---- // ---- Colors RGBA ----
constexpr SDL_Color BG_COLOR{.r = 0, .g = 12, .b = 24, .a = 215}; constexpr SDL_Color BG_COLOR{.r = 0, .g = 12, .b = 24, .a = 215};
constexpr SDL_Color CORNER_COLOR{.r = 120, .g = 220, .b = 255, .a = 255}; // cian neon constexpr SDL_Color CORNER_COLOR{.r = 120, .g = 220, .b = 255, .a = 255}; // cian neon
constexpr SDL_Color TITLE_COLOR{.r = 200, .g = 240, .b = 255, .a = 255}; constexpr SDL_Color TITLE_COLOR{.r = 200, .g = 240, .b = 255, .a = 255};
constexpr SDL_Color SUBTITLE_COLOR{.r = 110, .g = 170, .b = 210, .a = 220}; // cian apagat
constexpr SDL_Color SEPARATOR_COLOR{.r = 60, .g = 120, .b = 180, .a = 180}; constexpr SDL_Color SEPARATOR_COLOR{.r = 60, .g = 120, .b = 180, .a = 180};
constexpr SDL_Color LABEL_COLOR{.r = 170, .g = 210, .b = 240, .a = 255}; constexpr SDL_Color LABEL_COLOR{.r = 170, .g = 210, .b = 240, .a = 255};
constexpr SDL_Color CURSOR_COLOR{.r = 255, .g = 230, .b = 120, .a = 255}; // groc per al text sel·leccionat constexpr SDL_Color CURSOR_COLOR{.r = 255, .g = 230, .b = 120, .a = 255}; // groc per al text sel·leccionat
@@ -45,8 +51,9 @@ namespace Defaults::ServiceMenu {
constexpr SDL_Color HIGHLIGHT_FILL{.r = 255, .g = 230, .b = 120, .a = 36}; // wash translucid constexpr SDL_Color HIGHLIGHT_FILL{.r = 255, .g = 230, .b = 120, .a = 36}; // wash translucid
// ---- Tipografia (VectorText). Scale 1.0 = caracter 20×40 px ---- // ---- Tipografia (VectorText). Scale 1.0 = caracter 20×40 px ----
constexpr float TITLE_SCALE = 0.85F; // mateixa escala que el HUD del scoreboard constexpr float TITLE_SCALE = 0.85F; // mateixa escala que el HUD del scoreboard
constexpr float ITEM_SCALE = 0.55F; // mateixa escala que les notificacions constexpr float SUBTITLE_SCALE = 0.40F; // sota el titol, info decorativa (versio/hash)
constexpr float ITEM_SCALE = 0.55F; // mateixa escala que les notificacions
constexpr float TEXT_SPACING = 2.0F; constexpr float TEXT_SPACING = 2.0F;
// ---- Sons UI (relatius a data/sounds/), portats d'aee_arcade ---- // ---- Sons UI (relatius a data/sounds/), portats d'aee_arcade ----
+61
View File
@@ -0,0 +1,61 @@
// relaunch.cpp - Implementacio del reinici en calent
// © 2026 JailDesigner
#include "core/system/relaunch.hpp"
#include <cerrno>
#include <cstdlib>
#include <cstring>
#include <iostream>
#ifdef _WIN32
#include <process.h> // _execv
#else
#include <unistd.h> // execv
#endif
namespace {
// Estat global (process-scope). Aquesta TU es la unica que gestiona el
// reinici, aixi que els static interns no s'escapen.
char** g_argv = nullptr;
bool g_requested = false;
} // namespace
namespace System::Relaunch {
void setArgv(int /*argc*/, char** argv) {
g_argv = argv;
}
void request() {
g_requested = true;
}
auto isRequested() -> bool {
return g_requested;
}
void execIfRequested() {
#ifdef __EMSCRIPTEN__
// Al navegador el reinici real seria location.reload(); aqui no fem res.
return;
#else
if (!g_requested || g_argv == nullptr || g_argv[0] == nullptr) {
return;
}
std::cout << "[Relaunch] Reiniciant " << g_argv[0] << "...\n";
#ifdef _WIN32
_execv(g_argv[0], g_argv);
#else
execv(g_argv[0], g_argv);
#endif
// Si arribem aqui, execv ha fallat. Tots els subsistemes ja estan
// destruits; sortim amb error i el shell rebra el codi.
std::cerr << "[Relaunch] Ha fallat: " << std::strerror(errno) << '\n';
std::exit(EXIT_FAILURE);
#endif
}
} // namespace System::Relaunch
+33
View File
@@ -0,0 +1,33 @@
// relaunch.hpp - Reinici en calent del proces (execv)
// © 2026 JailDesigner
//
// Helper desacoblat per a permetre que el menu de servei demani un reinici
// sense conèixer Director ni main.cpp. Patro:
//
// main() → Relaunch::setArgv(argc, argv) (a l'arrencada)
// ServiceMenu → Relaunch::request() (en activar REINICIAR)
// main() → Relaunch::execIfRequested() (a SDL_AppQuit)
//
// L'execv() reemplaca el proces actual: si torna, ha fallat. A EMSCRIPTEN
// no es pot reiniciar; isRequested() seguira dient true pero execIfRequested
// sera no-op.
#pragma once
namespace System::Relaunch {
// Emmagatzema l'argv original. Cal cridar-ho una vegada des de main.
void setArgv(int argc, char** argv);
// Demana un reinici (no actua immediatament; nomes marca el flag).
void request();
// Consulta del flag.
[[nodiscard]] auto isRequested() -> bool;
// Si hi ha reinici demanat i tenim argv valid, fa execv. En cas d'exit
// no torna. Si execv falla, registra l'error i torna; el caller hauria
// de sortir normalment.
void execIfRequested();
} // namespace System::Relaunch
+192 -28
View File
@@ -4,6 +4,7 @@
#include "core/system/service_menu.hpp" #include "core/system/service_menu.hpp"
#include <algorithm> #include <algorithm>
#include <cctype>
#include <cmath> #include <cmath>
#include <cstddef> #include <cstddef>
#include <format> #include <format>
@@ -16,8 +17,10 @@
#include "core/locale/locale.hpp" #include "core/locale/locale.hpp"
#include "core/rendering/sdl_manager.hpp" #include "core/rendering/sdl_manager.hpp"
#include "core/system/debug_overlay.hpp" #include "core/system/debug_overlay.hpp"
#include "core/system/relaunch.hpp"
#include "core/types.hpp" #include "core/types.hpp"
#include "game/config_yaml.hpp" #include "game/config_yaml.hpp"
#include "project.h"
namespace { namespace {
@@ -50,6 +53,17 @@ namespace {
} }
} }
// VectorText nomes admet ASCII en majuscules. El git hash sortit de git
// rev-parse es lowercase (a-f), aixi que el passem a uppercase per al
// display sense modificar Project::GIT_HASH.
auto toUpperAscii(const std::string& s) -> std::string {
std::string result = s;
for (char& c : result) {
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
}
return result;
}
} // namespace } // namespace
namespace System { namespace System {
@@ -86,6 +100,10 @@ namespace System {
animated_h_ = 0.0F; animated_h_ = 0.0F;
highlight_snap_ = true; // primera frame: enganxar el highlight al cursor highlight_snap_ = true; // primera frame: enganxar el highlight al cursor
buildRootPage(); buildRootPage();
// L'ample comença ja al valor objectiu (la caixa surt amb l'amplada
// final i nomes anima l'alçada). L'ample s'animarà despres entre
// pagines (push/pop).
animated_w_ = computeTargetWidth();
playAcceptSound(); playAcceptSound();
return; return;
} }
@@ -121,20 +139,12 @@ namespace System {
makeSubmenu("service_menu.video", [this] { pushPage(buildVideoPage()); }), makeSubmenu("service_menu.video", [this] { pushPage(buildVideoPage()); }),
makeSubmenu("service_menu.audio", [this] { pushPage(buildAudioPage()); }), makeSubmenu("service_menu.audio", [this] { pushPage(buildAudioPage()); }),
makeSubmenu("service_menu.options", [this] { pushPage(buildOptionsPage()); }), makeSubmenu("service_menu.options", [this] { pushPage(buildOptionsPage()); }),
makeSubmenu("service_menu.system", [this] { pushSubmenuPlaceholder("service_menu.system"); }), makeSubmenu("service_menu.system", [this] { pushPage(buildSystemPage()); }),
}; };
stack_.clear(); stack_.clear();
stack_.push_back(std::move(root)); stack_.push_back(std::move(root));
} }
void ServiceMenu::pushSubmenuPlaceholder(const std::string& title_key) {
Page page;
page.title_key = title_key;
// items buit: el submenu mostra nomes el titol (al·iteracions futures
// s'omplen amb opcions reals de Vsync/Zoom/Locale/Restart/Exit/etc.).
pushPage(std::move(page));
}
auto ServiceMenu::buildVideoPage() const -> Page { auto ServiceMenu::buildVideoPage() const -> Page {
// Helper: localitza ON/OFF per a TOGGLE items. // Helper: localitza ON/OFF per a TOGGLE items.
auto on_off_text = [](bool v) -> std::string { auto on_off_text = [](bool v) -> std::string {
@@ -346,6 +356,76 @@ namespace System {
return page; return page;
} }
auto ServiceMenu::buildSystemPage() -> Page {
Page page;
page.title_key = "service_menu.system";
// Versio + hash com a subtitol sota el titol (apagat, mes petit).
// Uppercase del hash perque VectorText nomes admet majuscules.
page.subtitle_provider = [] {
return std::format("V{} - {}", Project::VERSION, toUpperAscii(Project::GIT_HASH));
};
page.items = {
// REINICIAR (amb confirmacio).
Item{
.kind = Kind::ACTION,
.label_key = "service_menu.system_restart",
.selectable = true,
.on_activate = [this] {
pushConfirmPage("service_menu.confirm_restart", [] {
System::Relaunch::request();
SDL_Event quit_event{};
quit_event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&quit_event);
});
},
.get_value_text = {},
.on_change = {},
},
// EIXIR DEL JOC (amb confirmacio).
Item{
.kind = Kind::ACTION,
.label_key = "service_menu.exit",
.selectable = true,
.on_activate = [this] {
pushConfirmPage("service_menu.confirm_exit", [] {
SDL_Event quit_event{};
quit_event.type = SDL_EVENT_QUIT;
SDL_PushEvent(&quit_event);
});
},
.get_value_text = {},
.on_change = {},
},
};
return page;
}
void ServiceMenu::pushConfirmPage(const std::string& title_key, std::function<void()> on_yes) {
auto yes_callback = std::move(on_yes);
Page page;
page.title_key = title_key;
page.cursor = 0; // per defecte sobre NO (segur)
page.items = {
Item{
.kind = Kind::ACTION,
.label_key = "service_menu.confirm_no",
.selectable = true,
.on_activate = [this] { popPage(); },
.get_value_text = {},
.on_change = {},
},
Item{
.kind = Kind::ACTION,
.label_key = "service_menu.confirm_yes",
.selectable = true,
.on_activate = std::move(yes_callback),
.get_value_text = {},
.on_change = {},
},
};
pushPage(std::move(page));
}
void ServiceMenu::pushPage(Page page) { void ServiceMenu::pushPage(Page page) {
stack_.push_back(std::move(page)); stack_.push_back(std::move(page));
// El cursor salta a una pagina nova: enganxem el highlight per a // El cursor salta a una pagina nova: enganxem el highlight per a
@@ -463,8 +543,12 @@ namespace System {
} }
using namespace Defaults::ServiceMenu; using namespace Defaults::ServiceMenu;
const Page& page = stack_.back(); const Page& page = stack_.back();
int h = GAP_Y; // padding superior int h = GAP_Y; // padding superior
h += TITLE_HEIGHT + GAP_Y; // titol + gap h += TITLE_HEIGHT; // titol
if (page.subtitle_provider) {
h += GAP_Y / 2 + SUBTITLE_HEIGHT; // subtitol amb mig gap
}
h += GAP_Y; // gap abans del separador
h += SEPARATOR_HEIGHT + GAP_Y; // separador + gap h += SEPARATOR_HEIGHT + GAP_Y; // separador + gap
const auto N = static_cast<int>(page.items.size()); const auto N = static_cast<int>(page.items.size());
if (N > 0) { if (N > 0) {
@@ -473,15 +557,58 @@ namespace System {
return static_cast<float>(h); return static_cast<float>(h);
} }
auto ServiceMenu::computeItemTopY(float box_y, std::size_t index) -> float { auto ServiceMenu::computeTargetWidth() const -> float {
using namespace Defaults::ServiceMenu; using namespace Defaults::ServiceMenu;
const float ITEMS_Y0 = box_y + if (stack_.empty()) {
static_cast<float>(GAP_Y) + return static_cast<float>(BOX_WIDTH_MIN);
static_cast<float>(TITLE_HEIGHT) + }
const Page& page = stack_.back();
// Comencem amb l'ample del titol.
float content_w = Graphics::VectorText::getTextWidth(
Locale::get().text(page.title_key),
TITLE_SCALE,
TEXT_SPACING);
if (page.subtitle_provider) {
content_w = std::max(content_w, Graphics::VectorText::getTextWidth(page.subtitle_provider(), SUBTITLE_SCALE, TEXT_SPACING));
}
for (const Item& item : page.items) {
const std::string LABEL = item.label_key.empty()
? std::string{}
: Locale::get().text(item.label_key);
if (item.label_key.empty() && item.get_value_text) {
content_w = std::max(content_w, Graphics::VectorText::getTextWidth(item.get_value_text(), ITEM_SCALE, TEXT_SPACING));
} else if (item.get_value_text) {
const float LABEL_W = Graphics::VectorText::getTextWidth(LABEL, ITEM_SCALE, TEXT_SPACING);
const float VALUE_W = Graphics::VectorText::getTextWidth(
item.get_value_text(),
ITEM_SCALE,
TEXT_SPACING);
content_w = std::max(content_w,
LABEL_W + static_cast<float>(MIN_LABEL_VALUE_GAP) + VALUE_W);
} else {
content_w = std::max(content_w,
Graphics::VectorText::getTextWidth(LABEL, ITEM_SCALE, TEXT_SPACING));
}
}
// Padding total: highlight pad als dos costats + inset del text.
const float REQUIRED = content_w +
(2.0F * static_cast<float>(HIGHLIGHT_PAD_X)) +
(2.0F * static_cast<float>(TEXT_INSET_X));
return std::max(static_cast<float>(BOX_WIDTH_MIN), REQUIRED);
}
auto ServiceMenu::computeItemTopY(float box_y, std::size_t index, bool has_subtitle) -> float {
using namespace Defaults::ServiceMenu;
float items_y0 = box_y +
static_cast<float>(GAP_Y) + static_cast<float>(GAP_Y) +
static_cast<float>(TITLE_HEIGHT);
if (has_subtitle) {
items_y0 += static_cast<float>(GAP_Y / 2) + static_cast<float>(SUBTITLE_HEIGHT);
}
items_y0 += static_cast<float>(GAP_Y) +
static_cast<float>(SEPARATOR_HEIGHT) + static_cast<float>(SEPARATOR_HEIGHT) +
static_cast<float>(GAP_Y); static_cast<float>(GAP_Y);
return ITEMS_Y0 + (static_cast<float>(index) * static_cast<float>(ITEM_HEIGHT + ITEM_GAP_Y)); return items_y0 + (static_cast<float>(index) * static_cast<float>(ITEM_HEIGHT + ITEM_GAP_Y));
} }
void ServiceMenu::update(float delta_time) { void ServiceMenu::update(float delta_time) {
@@ -504,10 +631,13 @@ namespace System {
open_anim_ = std::min(1.0F, open_anim_ + (OPEN_SPEED * delta_time)); open_anim_ = std::min(1.0F, open_anim_ + (OPEN_SPEED * delta_time));
} }
// Smoothing exponencial cap a l'alçada objectiu de la pagina superior. // Smoothing exponencial cap a l'alçada i ample objectius de la pagina.
const float TARGET = closing_ ? 0.0F : computeTargetHeight(); const float TARGET_H_BOX = closing_ ? 0.0F : computeTargetHeight();
const float ALPHA = 1.0F - std::exp(-HEIGHT_RATE * delta_time); const float TARGET_W_BOX = closing_ ? animated_w_ : computeTargetWidth();
animated_h_ += (TARGET - animated_h_) * ALPHA; const float ALPHA_H = 1.0F - std::exp(-HEIGHT_RATE * delta_time);
const float ALPHA_W = 1.0F - std::exp(-WIDTH_RATE * delta_time);
animated_h_ += (TARGET_H_BOX - animated_h_) * ALPHA_H;
animated_w_ += (TARGET_W_BOX - animated_w_) * ALPHA_W;
// Highlight: lerp cap a la Y de l'item del cursor. Snapping nomes en // Highlight: lerp cap a la Y de l'item del cursor. Snapping nomes en
// obrir o canviar de pagina; en moure el cursor amb UP/DOWN, el rect // obrir o canviar de pagina; en moure el cursor amb UP/DOWN, el rect
@@ -522,7 +652,8 @@ namespace System {
} }
const float BOX_H_TARGET = computeTargetHeight(); const float BOX_H_TARGET = computeTargetHeight();
const float BOX_Y_TARGET = (CANVAS_H - BOX_H_TARGET) * 0.5F; const float BOX_Y_TARGET = (CANVAS_H - BOX_H_TARGET) * 0.5F;
const float ITEM_TOP = computeItemTopY(BOX_Y_TARGET, page.cursor); const bool HAS_SUBTITLE = static_cast<bool>(page.subtitle_provider);
const float ITEM_TOP = computeItemTopY(BOX_Y_TARGET, page.cursor, HAS_SUBTITLE);
const float TARGET_Y = ITEM_TOP - static_cast<float>(HIGHLIGHT_PAD_Y); const float TARGET_Y = ITEM_TOP - static_cast<float>(HIGHLIGHT_PAD_Y);
const float TARGET_H = static_cast<float>(ITEM_HEIGHT) + (2.0F * static_cast<float>(HIGHLIGHT_PAD_Y)); const float TARGET_H = static_cast<float>(ITEM_HEIGHT) + (2.0F * static_cast<float>(HIGHLIGHT_PAD_Y));
if (highlight_snap_) { if (highlight_snap_) {
@@ -603,9 +734,9 @@ namespace System {
return; return;
} }
const float BOX_X = (CANVAS_W - static_cast<float>(BOX_WIDTH)) * 0.5F; const float BOX_W = animated_w_;
const float BOX_X = (CANVAS_W - BOX_W) * 0.5F;
const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F; const float BOX_Y = (CANVAS_H - BOX_H) * 0.5F;
const auto BOX_W = static_cast<float>(BOX_WIDTH);
const float CENTER_X = BOX_X + (BOX_W * 0.5F); const float CENTER_X = BOX_X + (BOX_W * 0.5F);
// Fons semi-transparent. // Fons semi-transparent.
@@ -623,6 +754,7 @@ namespace System {
renderer_->pushClip(CLIP_X, CLIP_Y, CLIP_W, CLIP_H); renderer_->pushClip(CLIP_X, CLIP_Y, CLIP_W, CLIP_H);
const Page& page = stack_.back(); const Page& page = stack_.back();
const bool HAS_SUBTITLE = static_cast<bool>(page.subtitle_provider);
// Titol centrat al cim de la caixa. // Titol centrat al cim de la caixa.
const std::string TITLE = Locale::get().text(page.title_key); const std::string TITLE = Locale::get().text(page.title_key);
@@ -634,8 +766,27 @@ namespace System {
1.0F, 1.0F,
TITLE_COLOR); TITLE_COLOR);
// Separador horitzontal sota el titol. // Subtitol opcional: sota el titol, mes petit i apagat.
const float SEP_Y = BOX_Y + static_cast<float>(GAP_Y) + static_cast<float>(TITLE_HEIGHT) + (static_cast<float>(GAP_Y) * 0.5F); if (HAS_SUBTITLE) {
const float SUBTITLE_CY = BOX_Y + static_cast<float>(GAP_Y) +
static_cast<float>(TITLE_HEIGHT) +
(static_cast<float>(GAP_Y) / 4.0F) +
(static_cast<float>(SUBTITLE_HEIGHT) * 0.5F);
text_.renderCentered(page.subtitle_provider(),
Vec2{.x = CENTER_X, .y = SUBTITLE_CY},
SUBTITLE_SCALE,
TEXT_SPACING,
1.0F,
SUBTITLE_COLOR);
}
// Separador horitzontal sota el titol (o subtitol si n'hi ha).
float sep_y = BOX_Y + static_cast<float>(GAP_Y) + static_cast<float>(TITLE_HEIGHT);
if (HAS_SUBTITLE) {
sep_y += static_cast<float>(GAP_Y / 2) + static_cast<float>(SUBTITLE_HEIGHT);
}
sep_y += static_cast<float>(GAP_Y) * 0.5F;
const float SEP_Y = sep_y;
fillRect(renderer_, fillRect(renderer_,
BOX_X + static_cast<float>(GAP_Y), BOX_X + static_cast<float>(GAP_Y),
SEP_Y, SEP_Y,
@@ -659,11 +810,24 @@ namespace System {
for (std::size_t i = 0; i < page.items.size(); ++i) { for (std::size_t i = 0; i < page.items.size(); ++i) {
const Item& item = page.items[i]; const Item& item = page.items[i];
const SDL_Color COL = (i == page.cursor) ? CURSOR_COLOR : LABEL_COLOR; const SDL_Color COL = (i == page.cursor) ? CURSOR_COLOR : LABEL_COLOR;
const std::string LABEL = Locale::get().text(item.label_key); // Salta el Locale lookup si label_key esta buit (item nomes-valor).
const float ITEM_TOP = computeItemTopY(BOX_Y, i); const std::string LABEL = item.label_key.empty()
? std::string{}
: Locale::get().text(item.label_key);
const float ITEM_TOP = computeItemTopY(BOX_Y, i, HAS_SUBTITLE);
const float ITEM_CY = ITEM_TOP + (static_cast<float>(ITEM_HEIGHT) * 0.5F); const float ITEM_CY = ITEM_TOP + (static_cast<float>(ITEM_HEIGHT) * 0.5F);
if (item.get_value_text) { if (item.label_key.empty() && item.get_value_text) {
// Item nomes-valor (sense label_key): el text del valor es
// renderitza centrat com a label decoratiu. Util per a items
// d'informacio com la versio/hash a SISTEMA.
text_.renderCentered(item.get_value_text(),
Vec2{.x = CENTER_X, .y = ITEM_CY},
ITEM_SCALE,
TEXT_SPACING,
1.0F,
COL);
} else if (item.get_value_text) {
// Layout dues columnes: label esquerra, valor dreta. // Layout dues columnes: label esquerra, valor dreta.
const std::string VALUE = item.get_value_text(); const std::string VALUE = item.get_value_text();
const float TEXT_TOP_Y = ITEM_CY - TEXT_TOP_OFFSET; const float TEXT_TOP_Y = ITEM_CY - TEXT_TOP_OFFSET;
+16 -3
View File
@@ -62,6 +62,10 @@ namespace System {
struct Page { struct Page {
std::string title_key; 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<std::string()> subtitle_provider;
std::vector<Item> items; std::vector<Item> items;
std::size_t cursor = 0; std::size_t cursor = 0;
}; };
@@ -88,10 +92,13 @@ namespace System {
ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay); ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay);
void buildRootPage(); void buildRootPage();
void pushSubmenuPlaceholder(const std::string& title_key);
[[nodiscard]] auto buildVideoPage() const -> Page; [[nodiscard]] auto buildVideoPage() const -> Page;
[[nodiscard]] static auto buildAudioPage() -> Page; [[nodiscard]] static auto buildAudioPage() -> Page;
[[nodiscard]] auto buildOptionsPage() const -> Page; [[nodiscard]] auto buildOptionsPage() const -> Page;
[[nodiscard]] auto buildSystemPage() -> 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<void()> on_yes);
void pushPage(Page page); void pushPage(Page page);
void popPage(); void popPage();
void moveCursor(int direction); void moveCursor(int direction);
@@ -103,8 +110,13 @@ namespace System {
// Alçada objectiu de la caixa per a la pagina superior (sense animacio). // Alçada objectiu de la caixa per a la pagina superior (sense animacio).
[[nodiscard]] auto computeTargetHeight() const -> float; [[nodiscard]] auto computeTargetHeight() const -> float;
// Y (top) de l'item index dins una caixa col·locada a box_y. // Ample objectiu de la caixa per a la pagina superior (sense animacio).
[[nodiscard]] static auto computeItemTopY(float box_y, std::size_t index) -> float; // 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_; Rendering::Renderer* renderer_;
SDLManager* sdl_; SDLManager* sdl_;
@@ -116,6 +128,7 @@ namespace System {
bool closing_ = false; bool closing_ = false;
float open_anim_ = 0.0F; // 0..1 raw (sense easing) float open_anim_ = 0.0F; // 0..1 raw (sense easing)
float animated_h_ = 0.0F; // Alçada animada amb smoothing exponencial 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 // Estat del highlight (rectangle del cursor). Es lerpa cap a l'item
// actiu amb ease-out exponencial; quan el cursor "salta" (open o // actiu amb ease-out exponencial; quan el cursor "salta" (open o
+8
View File
@@ -13,8 +13,12 @@
#include <memory> #include <memory>
#include "core/system/director.hpp" #include "core/system/director.hpp"
#include "core/system/relaunch.hpp"
auto SDL_AppInit(void** appstate, int argc, char* argv[]) -> SDL_AppResult { auto SDL_AppInit(void** appstate, int argc, char* argv[]) -> SDL_AppResult {
// Desem argv perquè el menu de servei pugui demanar un reinici en calent
// (execv) sense haver de conèixer Director.
System::Relaunch::setArgv(argc, argv);
auto director = std::make_unique<Director>(argc, argv); auto director = std::make_unique<Director>(argc, argv);
*appstate = director.release(); *appstate = director.release();
return SDL_APP_CONTINUE; return SDL_APP_CONTINUE;
@@ -33,4 +37,8 @@ auto SDL_AppIterate(void* appstate) -> SDL_AppResult {
void SDL_AppQuit(void* appstate, SDL_AppResult /*result*/) { void SDL_AppQuit(void* appstate, SDL_AppResult /*result*/) {
// Reabsorbim la propietat: el destructor del Director allibera tot. // Reabsorbim la propietat: el destructor del Director allibera tot.
std::unique_ptr<Director> director(static_cast<Director*>(appstate)); std::unique_ptr<Director> director(static_cast<Director*>(appstate));
director.reset();
// Si el menu va demanar reinici, fem execv ara que tot esta net. En cas
// d'exit no torna; si falla, l'aplicacio surt amb codi d'error.
System::Relaunch::execIfRequested();
} }