diff --git a/data/locale/ca.yaml b/data/locale/ca.yaml index 6b74413..cd5afa9 100644 --- a/data/locale/ca.yaml +++ b/data/locale/ca.yaml @@ -14,7 +14,7 @@ input: shader_off: "Shaders desactivats" vsync_on: "VSync activat" vsync_off: "VSync desactivat" - screenshot: "Captura" + screenshot: "Imatge {} guardada a {}" shader: "Shader" preset: "Preset" scaling_disabled: "Escalat sense" diff --git a/data/locale/en.yaml b/data/locale/en.yaml index 8ea27ad..fcea27f 100644 --- a/data/locale/en.yaml +++ b/data/locale/en.yaml @@ -14,7 +14,7 @@ input: shader_off: "Shaders off" vsync_on: "VSync on" vsync_off: "VSync off" - screenshot: "Screenshot" + screenshot: "Image {} saved at {}" shader: "Shader" preset: "Preset" scaling_disabled: "Scaling off" diff --git a/source/core/input/global_inputs.cpp b/source/core/input/global_inputs.cpp index 96454a1..bc8d0dc 100644 --- a/source/core/input/global_inputs.cpp +++ b/source/core/input/global_inputs.cpp @@ -3,6 +3,7 @@ #include #include +#include #include #include "core/input/key_config.hpp" @@ -69,15 +70,26 @@ namespace GlobalInputs { notifyLocale(NEW_VSYNC ? "input.vsync_on" : "input.vsync_off"); } - // Volca el canvas ARGB actual a PNG dins de `screenshots/`. Si ha - // fallat l'escriptura, el handler no mostra notificació (retorn buit). + // Volca el canvas ARGB actual a PNG dins del directori de + // captures (configurat pel Director). La notificació informa + // del nom i de la ruta absoluta — així l'usuari sap on anar + // a buscar-la encara que el binari s'executi des d'un sitio + // estrany. Si l'escriptura falla, no es mostra notificació. void onScreenshot() { const auto* buffer = Screen::get().getCanvasArgb(); const int W = Screen::get().getCanvasWidth(); const int H = Screen::get().getCanvasHeight(); - const std::string FILENAME = Screenshot::save(buffer, W, H); - if (!FILENAME.empty()) { - Notification::notify(Locale::get().text("input.screenshot") + ' ' + FILENAME); + const auto R = Screenshot::save(buffer, W, H); + if (!R.filename.empty()) { + // `make_format_args` retorna un store que conté referències + // als arguments; cal lligar-lo a una lvalue local perquè + // sobrevisqui fins que `vformat` el consumeixi. Passar-lo + // directament a `vformat` és UB en alguns compiladors + // (DR de C++20 / fix de C++23) i ha provocat un segfault + // post-captura en aquest projecte. + const std::string TEMPLATE = Locale::get().text("input.screenshot"); + auto args = std::make_format_args(R.filename, R.folder); + Notification::notify(std::vformat(TEMPLATE, args)); } } diff --git a/source/core/rendering/notifier.cpp b/source/core/rendering/notifier.cpp index 02bedc1..6133969 100644 --- a/source/core/rendering/notifier.cpp +++ b/source/core/rendering/notifier.cpp @@ -12,11 +12,52 @@ namespace { // Parteix `text` en una llista de línies que caben cadascuna dins - // `max_width`. Algoritme de projecte_2026: tall per espais, la paraula - // que no cap passa a la línia següent. Les paraules aïllades més - // llargues que `max_width` es queden a la seva pròpia línia sense - // trencar-les (cas improbable amb els pretty_names de gamepad retallats - // a ~25 caràcters). + // Trenca una "paraula" llarga (sense espais) en trossos contigus + // que càpiguen a `max_width`. Si un sol caràcter ja excedeix + // (font enorme), el deixa sol per garantir progrés. La part final + // que encara cap queda a `current` per a fusionar-se amb paraules + // posteriors via espai. + void hardWrapWord(const std::string& word, int max_width, const Text& font, int kerning, std::vector& lines, std::string& current) { + std::string buf; + for (char c : word) { + std::string candidate = buf; + candidate += c; + if (buf.empty() || font.length(candidate, kerning) <= max_width) { + buf = std::move(candidate); + } else { + lines.push_back(buf); + buf = std::string(1, c); + } + } + current = std::move(buf); + } + + // Posa la `word` a la `current` line si hi cap (amb separador + // implícit si current no està buida), o fa flush + reintenta a + // línia neta. Si tampoc cap sola, hard-wrap caràcter a caràcter. + void appendWord(const std::string& word, int max_width, const Text& font, int kerning, std::vector& lines, std::string& current) { + if (!current.empty()) { + std::string candidate = current; + candidate += ' '; + candidate += word; + if (font.length(candidate, kerning) <= max_width) { + current = std::move(candidate); + return; + } + lines.push_back(current); + current.clear(); + } + if (font.length(word, kerning) <= max_width) { + current = word; + } else { + hardWrapWord(word, max_width, font, kerning, lines, current); + } + } + + // `max_width`. Tall per espais; les paraules més llargues que + // `max_width` (rutes, URLs...) es trenquen caràcter a caràcter + // — sense això, una caixa que excedís el canvas faria que + // `fillRect` rebés rangs invertits i petés. auto wrapToWidth(const std::string& text, int max_width, const Text& font, int kerning) -> std::vector { std::vector lines; if (text.empty()) { @@ -30,26 +71,12 @@ namespace { const std::size_t SPACE_POS = text.find(' ', i); const bool LAST = (SPACE_POS == std::string::npos); const std::string WORD = text.substr(i, LAST ? std::string::npos : SPACE_POS - i); - - if (current.empty()) { - current = WORD; - } else { - std::string candidate = current; - candidate += ' '; - candidate += WORD; - if (font.length(candidate, kerning) <= max_width) { - current = std::move(candidate); - } else { - lines.push_back(current); - current = WORD; - } - } - + appendWord(WORD, max_width, font, kerning, lines, current); if (LAST) { break; } i = SPACE_POS + 1; } - lines.push_back(current); + if (!current.empty()) { lines.push_back(current); } return lines; } diff --git a/source/core/rendering/screen.cpp b/source/core/rendering/screen.cpp index eb1c504..b1392aa 100644 --- a/source/core/rendering/screen.cpp +++ b/source/core/rendering/screen.cpp @@ -226,6 +226,12 @@ void Screen::fillRect(int x, int y, int w, int h, std::uint32_t argb) { const int Y0 = std::max(y, CLIP.y); const int X1 = std::min(x + w, CLIP.x + CLIP.w); const int Y1 = std::min(y + h, CLIP.y + CLIP.h); + // Bail-out defensiu: si el rang és buit o invertit després del clip, + // `std::fill(first, last)` amb `first > last` és UB i a la pràctica + // entra en bucle infinit (vist en un crash a l'overflow del Notifier + // quan la caixa quedava més ampla que el canvas). El `blendRect` + // d'aquí baix ja és segur perquè usa `for(ix; ix < X1; ...)`. + if (X1 <= X0 || Y1 <= Y0) { return; } for (int iy = Y0; iy < Y1; ++iy) { std::uint32_t* row = canvas_.data() + (static_cast(iy) * canvas_width_); std::fill(row + X0, row + X1, argb); diff --git a/source/core/rendering/screenshot.cpp b/source/core/rendering/screenshot.cpp index c520563..32242b1 100644 --- a/source/core/rendering/screenshot.cpp +++ b/source/core/rendering/screenshot.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include "external/stb_image_write.h" @@ -13,21 +14,18 @@ namespace Screenshot { namespace { - // Crea la carpeta `screenshots/` si no existeix i torna la ruta - // completa al fitxer. El paràmetre `filename` rep el nom nu (sense - // directori) per poder mostrar-lo a la notificació. - auto generateFilePath(std::string& filename) -> std::string { - std::filesystem::create_directories("screenshots"); + // Estat de mòdul: ruta base on s'escriu la subcarpeta + // `screenshots/`. Buida per defecte ⇒ relativa al CWD (fallback + // si el Director no ha pogut establir la ruta per plataforma). + std::string g_base_dir; // NOLINT(*-avoid-non-const-global-variables) - const auto NOW = std::chrono::system_clock::now(); - const std::time_t TIME = std::chrono::system_clock::to_time_t(NOW); - const std::tm* tm = std::localtime(&TIME); - - char timestamp[20]; - std::strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm); - - filename = std::string("scr_") + timestamp + ".png"; - return "screenshots/" + filename; + // Construeix la ruta del directori on van les captures: + // `/screenshots/`. Si la base és buida, retorna + // "screenshots/" relativa al CWD. + auto screenshotsDir() -> std::string { + if (g_base_dir.empty()) { return "screenshots/"; } + const bool ENDS_WITH_SEP = (g_base_dir.back() == '/' || g_base_dir.back() == '\\'); + return g_base_dir + (ENDS_WITH_SEP ? "" : "/") + "screenshots/"; } // Converteix ARGB8888 → RGBA8888 in-place i escriu el PNG. stb_image_write @@ -47,14 +45,28 @@ namespace Screenshot { } // namespace - auto save(const std::uint32_t* buffer, int width, int height) -> std::string { + void setBaseDir(std::string dir) { + g_base_dir = std::move(dir); + } + + auto save(const std::uint32_t* buffer, int width, int height) -> Result { + const std::string FOLDER = screenshotsDir(); + std::error_code ec; + std::filesystem::create_directories(FOLDER, ec); + + const auto NOW = std::chrono::system_clock::now(); + const std::time_t TIME = std::chrono::system_clock::to_time_t(NOW); + const std::tm* tm = std::localtime(&TIME); + char timestamp[20]; + std::strftime(timestamp, sizeof(timestamp), "%Y%m%d_%H%M%S", tm); + + const std::string FILENAME = std::string("scr_") + timestamp + ".png"; + const std::string FILEPATH = FOLDER + FILENAME; + // Còpia local per no tocar el canvas original durant la conversió. std::vector copy(buffer, buffer + (static_cast(width) * height)); - - std::string filename; - const std::string FILEPATH = generateFilePath(filename); - - return writePng(copy.data(), width, height, FILEPATH) ? filename : ""; + if (!writePng(copy.data(), width, height, FILEPATH)) { return {}; } + return {.filename = FILENAME, .folder = FOLDER}; } } // namespace Screenshot diff --git a/source/core/rendering/screenshot.hpp b/source/core/rendering/screenshot.hpp index 624e69c..1a91588 100644 --- a/source/core/rendering/screenshot.hpp +++ b/source/core/rendering/screenshot.hpp @@ -3,11 +3,30 @@ #include #include -// Volca un buffer ARGB8888 a un fitxer PNG dins de la carpeta `screenshots/` -// (relativa al CWD). El nom es genera amb un timestamp i la funció retorna -// el nom del fitxer creat, o una cadena buida si l'escriptura ha fallat. +// Volca un buffer ARGB8888 a un fitxer PNG dins de la subcarpeta +// `screenshots/` del directori configurat amb `setBaseDir`. El nom es +// genera amb un timestamp i la funció retorna el basename i la ruta +// del directori on s'ha escrit (o un Result amb `filename` buit si +// l'escriptura ha fallat). namespace Screenshot { - auto save(const std::uint32_t* buffer, int width, int height) -> std::string; + // Configura el directori base on s'escriuran les captures (s'hi + // crea una subcarpeta `screenshots/`). El Director el crida una + // vegada al boot amb `system_folder_` perquè les captures vagin + // al mateix lloc per plataforma que la resta de fitxers generats + // (config.yaml, gamepad_configs.yaml, shaders/, etc.). Si no es + // crida, el fallback és el CWD — comportament antic. + void setBaseDir(std::string dir); + + // Resultat d'una captura: el `filename` és el basename del PNG + // (per a notificacions/títol) i el `folder` és la ruta absoluta + // on s'ha guardat (per a poder dir "guardada a {folder}"). Si + // l'escriptura falla, ambdós camps tornen buits. + struct Result { + std::string filename; + std::string folder; + }; + + auto save(const std::uint32_t* buffer, int width, int height) -> Result; } // namespace Screenshot diff --git a/source/core/system/director.cpp b/source/core/system/director.cpp index b7e46a8..bb42584 100644 --- a/source/core/system/director.cpp +++ b/source/core/system/director.cpp @@ -22,6 +22,7 @@ #include "core/rendering/notifier.hpp" #include "core/rendering/render_info.hpp" #include "core/rendering/screen.hpp" +#include "core/rendering/screenshot.hpp" #include "core/rendering/surface.hpp" #include "core/rendering/text.hpp" #include "core/resources/resource_cache.hpp" @@ -379,6 +380,12 @@ void Director::createSystemFolder() { if (error) { Log::error("Director", "no s'ha pogut crear {}: {}", system_folder_, error.message()); } + + // Les captures de pantalla viuen sota la mateixa carpeta per + // plataforma. Sense aquesta crida, `Screenshot::save` cauria al + // CWD del procés (raïz del projecte si llances `build/aee_arcade` + // a mà, lloc imprevisible en una release de macOS/Windows). + Screenshot::setBaseDir(system_folder_); } void Director::processEvents() {