diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index a49f63b..737566c 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -71,6 +71,13 @@ service_menu: audio_music_volume: "VOLUM MUSICA" audio_sound: "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 value_on: "ACTIU" value_off: "INACTIU" diff --git a/data/locale/en.yaml b/data/locale/en.yaml index a10340e..73a4b0b 100644 --- a/data/locale/en.yaml +++ b/data/locale/en.yaml @@ -70,6 +70,13 @@ service_menu: audio_music_volume: "MUSIC VOLUME" audio_sound: "SOUNDS" 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 value_on: "ON" value_off: "OFF" diff --git a/source/core/defaults/service_menu.hpp b/source/core/defaults/service_menu.hpp index 72e02c8..cc3614c 100644 --- a/source/core/defaults/service_menu.hpp +++ b/source/core/defaults/service_menu.hpp @@ -8,9 +8,12 @@ namespace Defaults::ServiceMenu { // ---- 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 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 ITEM_HEIGHT = 38; // scale 0.55 ≈ 22 px de text + padding per al highlight 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 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 WIDTH_RATE = 12.0F; // smoothing per a canvis d'ample entre pagines // ---- Animacio del highlight (rectangle del cursor) ---- // Rate=18 dona settling ~0.17 s al 95% (ease-out exponencial). constexpr float HIGHLIGHT_RATE = 18.0F; constexpr int HIGHLIGHT_TICK_LEN = 10; // longitud dels ticks a cada cantonada constexpr int HIGHLIGHT_THICKNESS = 1; - constexpr int HIGHLIGHT_PAD_X = 18; // padding lateral del rect respecte al text - 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 HIGHLIGHT_PAD_X = 18; // padding lateral del rect respecte al text + 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 MIN_LABEL_VALUE_GAP = 30; // mínim gap entre label i valor (per al càlcul d'ample dinàmic) // ---- Colors RGBA ---- 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 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 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 @@ -45,8 +51,9 @@ namespace Defaults::ServiceMenu { constexpr SDL_Color HIGHLIGHT_FILL{.r = 255, .g = 230, .b = 120, .a = 36}; // wash translucid // ---- Tipografia (VectorText). Scale 1.0 = caracter 20×40 px ---- - 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 TITLE_SCALE = 0.85F; // mateixa escala que el HUD del scoreboard + 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; // ---- Sons UI (relatius a data/sounds/), portats d'aee_arcade ---- diff --git a/source/core/system/relaunch.cpp b/source/core/system/relaunch.cpp new file mode 100644 index 0000000..a6e13ed --- /dev/null +++ b/source/core/system/relaunch.cpp @@ -0,0 +1,61 @@ +// relaunch.cpp - Implementacio del reinici en calent +// © 2026 JailDesigner + +#include "core/system/relaunch.hpp" + +#include +#include +#include +#include + +#ifdef _WIN32 +#include // _execv +#else +#include // 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 diff --git a/source/core/system/relaunch.hpp b/source/core/system/relaunch.hpp new file mode 100644 index 0000000..b3df0b1 --- /dev/null +++ b/source/core/system/relaunch.hpp @@ -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 diff --git a/source/core/system/service_menu.cpp b/source/core/system/service_menu.cpp index 5e5cef6..7f78842 100644 --- a/source/core/system/service_menu.cpp +++ b/source/core/system/service_menu.cpp @@ -4,6 +4,7 @@ #include "core/system/service_menu.hpp" #include +#include #include #include #include @@ -16,8 +17,10 @@ #include "core/locale/locale.hpp" #include "core/rendering/sdl_manager.hpp" #include "core/system/debug_overlay.hpp" +#include "core/system/relaunch.hpp" #include "core/types.hpp" #include "game/config_yaml.hpp" +#include "project.h" 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(std::toupper(static_cast(c))); + } + return result; + } + } // namespace namespace System { @@ -86,6 +100,10 @@ namespace System { animated_h_ = 0.0F; highlight_snap_ = true; // primera frame: enganxar el highlight al cursor 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(); return; } @@ -121,20 +139,12 @@ namespace System { makeSubmenu("service_menu.video", [this] { pushPage(buildVideoPage()); }), makeSubmenu("service_menu.audio", [this] { pushPage(buildAudioPage()); }), 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_.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 { // Helper: localitza ON/OFF per a TOGGLE items. auto on_off_text = [](bool v) -> std::string { @@ -346,6 +356,76 @@ namespace System { 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 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) { stack_.push_back(std::move(page)); // El cursor salta a una pagina nova: enganxem el highlight per a @@ -463,8 +543,12 @@ namespace System { } using namespace Defaults::ServiceMenu; const Page& page = stack_.back(); - int h = GAP_Y; // padding superior - h += TITLE_HEIGHT + GAP_Y; // titol + gap + int h = GAP_Y; // padding superior + 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 const auto N = static_cast(page.items.size()); if (N > 0) { @@ -473,15 +557,58 @@ namespace System { return static_cast(h); } - auto ServiceMenu::computeItemTopY(float box_y, std::size_t index) -> float { + auto ServiceMenu::computeTargetWidth() const -> float { using namespace Defaults::ServiceMenu; - const float ITEMS_Y0 = box_y + - static_cast(GAP_Y) + - static_cast(TITLE_HEIGHT) + + if (stack_.empty()) { + return static_cast(BOX_WIDTH_MIN); + } + 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(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(HIGHLIGHT_PAD_X)) + + (2.0F * static_cast(TEXT_INSET_X)); + return std::max(static_cast(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(GAP_Y) + + static_cast(TITLE_HEIGHT); + if (has_subtitle) { + items_y0 += static_cast(GAP_Y / 2) + static_cast(SUBTITLE_HEIGHT); + } + items_y0 += static_cast(GAP_Y) + static_cast(SEPARATOR_HEIGHT) + static_cast(GAP_Y); - return ITEMS_Y0 + (static_cast(index) * static_cast(ITEM_HEIGHT + ITEM_GAP_Y)); + return items_y0 + (static_cast(index) * static_cast(ITEM_HEIGHT + ITEM_GAP_Y)); } void ServiceMenu::update(float delta_time) { @@ -504,10 +631,13 @@ namespace System { open_anim_ = std::min(1.0F, open_anim_ + (OPEN_SPEED * delta_time)); } - // Smoothing exponencial cap a l'alçada objectiu de la pagina superior. - const float TARGET = closing_ ? 0.0F : computeTargetHeight(); - const float ALPHA = 1.0F - std::exp(-HEIGHT_RATE * delta_time); - animated_h_ += (TARGET - animated_h_) * ALPHA; + // Smoothing exponencial cap a l'alçada i ample objectius de la pagina. + const float TARGET_H_BOX = closing_ ? 0.0F : computeTargetHeight(); + const float TARGET_W_BOX = closing_ ? animated_w_ : computeTargetWidth(); + 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 // 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_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(page.subtitle_provider); + const float ITEM_TOP = computeItemTopY(BOX_Y_TARGET, page.cursor, HAS_SUBTITLE); const float TARGET_Y = ITEM_TOP - static_cast(HIGHLIGHT_PAD_Y); const float TARGET_H = static_cast(ITEM_HEIGHT) + (2.0F * static_cast(HIGHLIGHT_PAD_Y)); if (highlight_snap_) { @@ -603,9 +734,9 @@ namespace System { return; } - const float BOX_X = (CANVAS_W - static_cast(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 auto BOX_W = static_cast(BOX_WIDTH); const float CENTER_X = BOX_X + (BOX_W * 0.5F); // Fons semi-transparent. @@ -623,6 +754,7 @@ namespace System { renderer_->pushClip(CLIP_X, CLIP_Y, CLIP_W, CLIP_H); const Page& page = stack_.back(); + const bool HAS_SUBTITLE = static_cast(page.subtitle_provider); // Titol centrat al cim de la caixa. const std::string TITLE = Locale::get().text(page.title_key); @@ -634,8 +766,27 @@ namespace System { 1.0F, TITLE_COLOR); - // Separador horitzontal sota el titol. - const float SEP_Y = BOX_Y + static_cast(GAP_Y) + static_cast(TITLE_HEIGHT) + (static_cast(GAP_Y) * 0.5F); + // Subtitol opcional: sota el titol, mes petit i apagat. + if (HAS_SUBTITLE) { + const float SUBTITLE_CY = BOX_Y + static_cast(GAP_Y) + + static_cast(TITLE_HEIGHT) + + (static_cast(GAP_Y) / 4.0F) + + (static_cast(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(GAP_Y) + static_cast(TITLE_HEIGHT); + if (HAS_SUBTITLE) { + sep_y += static_cast(GAP_Y / 2) + static_cast(SUBTITLE_HEIGHT); + } + sep_y += static_cast(GAP_Y) * 0.5F; + const float SEP_Y = sep_y; fillRect(renderer_, BOX_X + static_cast(GAP_Y), SEP_Y, @@ -659,11 +810,24 @@ namespace System { for (std::size_t i = 0; i < page.items.size(); ++i) { const Item& item = page.items[i]; const SDL_Color COL = (i == page.cursor) ? CURSOR_COLOR : LABEL_COLOR; - const std::string LABEL = Locale::get().text(item.label_key); - const float ITEM_TOP = computeItemTopY(BOX_Y, i); + // Salta el Locale lookup si label_key esta buit (item nomes-valor). + 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(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. const std::string VALUE = item.get_value_text(); const float TEXT_TOP_Y = ITEM_CY - TEXT_TOP_OFFSET; diff --git a/source/core/system/service_menu.hpp b/source/core/system/service_menu.hpp index 3c8d128..0b385f9 100644 --- a/source/core/system/service_menu.hpp +++ b/source/core/system/service_menu.hpp @@ -62,6 +62,10 @@ namespace System { struct Page { 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 subtitle_provider; std::vector items; std::size_t cursor = 0; }; @@ -88,10 +92,13 @@ namespace System { ServiceMenu(Rendering::Renderer* renderer, SDLManager* sdl, DebugOverlay* debug_overlay); void buildRootPage(); - void pushSubmenuPlaceholder(const std::string& title_key); [[nodiscard]] auto buildVideoPage() const -> Page; [[nodiscard]] static auto buildAudioPage() -> 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 on_yes); void pushPage(Page page); void popPage(); void moveCursor(int direction); @@ -103,8 +110,13 @@ namespace System { // Alçada objectiu de la caixa per a la pagina superior (sense animacio). [[nodiscard]] auto computeTargetHeight() const -> float; - // Y (top) de l'item index dins una caixa col·locada a box_y. - [[nodiscard]] static auto computeItemTopY(float box_y, std::size_t index) -> float; + // Ample objectiu de la caixa per a la pagina superior (sense animacio). + // 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_; SDLManager* sdl_; @@ -116,6 +128,7 @@ namespace System { bool closing_ = false; float open_anim_ = 0.0F; // 0..1 raw (sense easing) 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 // actiu amb ease-out exponencial; quan el cursor "salta" (open o diff --git a/source/main.cpp b/source/main.cpp index c03c04d..3cb4764 100644 --- a/source/main.cpp +++ b/source/main.cpp @@ -13,8 +13,12 @@ #include #include "core/system/director.hpp" +#include "core/system/relaunch.hpp" 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(argc, argv); *appstate = director.release(); return SDL_APP_CONTINUE; @@ -33,4 +37,8 @@ auto SDL_AppIterate(void* appstate) -> SDL_AppResult { void SDL_AppQuit(void* appstate, SDL_AppResult /*result*/) { // Reabsorbim la propietat: el destructor del Director allibera tot. std::unique_ptr director(static_cast(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(); }