feat(service-menu): pobla SISTEMA amb reinici, eixir i confirmacions
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 ----
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user