feat(screenshot): desa-les a la carpeta de sistema per plataforma
This commit is contained in:
1 parent
06a49e09ba
commit
f7c4d62234
8 files changed
+135
-52
No files matched your search
+1
-1
@@ -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"
|
||||
|
||||
+1
-1
@@ -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"
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
#include <SDL3/SDL.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <format>
|
||||
#include <string>
|
||||
|
||||
#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));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<std::string>& 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<std::string>& 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::string> {
|
||||
std::vector<std::string> 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<std::ptrdiff_t>(iy) * canvas_width_);
|
||||
std::fill(row + X0, row + X1, argb);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include <ctime>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#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:
|
||||
// `<base>/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<std::uint32_t> copy(buffer, buffer + (static_cast<size_t>(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
|
||||
@@ -3,11 +3,30 @@
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// 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
|
||||
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user