#include "screen.h" #include #include // for max, min #include // for basic_ostream, operator<<, cout, endl #include // for basic_string, char_traits, string #include "asset.h" // for Asset #include "mouse.hpp" // for Mouse::cursorVisible, Mouse::lastMouseMoveTime #include "text.h" // for Text, TXT_CENTER, TXT_COLOR, TXT_STROKE #ifdef __EMSCRIPTEN__ #include #include // --- Fix per a fullscreen/resize en Emscripten --- // // SDL3 + Emscripten no emet de forma fiable SDL_EVENT_WINDOW_LEAVE_FULLSCREEN // (libsdl-org/SDL#13300) ni SDL_EVENT_WINDOW_PIXEL_SIZE_CHANGED / // SDL_EVENT_DISPLAY_ORIENTATION (libsdl-org/SDL#11389). Quan l'usuari ix de // fullscreen amb Esc o rota el mòbil, el canvas HTML torna al tamany correcte // però l'estat intern de SDL creu que segueix en fullscreen amb la resolució // anterior i el viewport queda desencuadrat. // // Solució: registrar callbacks natius d'Emscripten, diferir la feina un tick // del event loop (el canvas encara no està estable en el moment del callback) // i cridar setVideoMode() amb el flag de fullscreen actualitzat. La crida // interna a SDL_SetWindowFullscreen(false) és la peça que realment fa eixir // SDL del seu estat intern de fullscreen — sense això res més funciona. namespace { Screen *g_screen_instance = nullptr; void deferredCanvasResize(void * /*userData*/) { if (g_screen_instance) { g_screen_instance->handleCanvasResized(); } } EM_BOOL onEmFullscreenChange(int /*eventType*/, const EmscriptenFullscreenChangeEvent *event, void * /*userData*/) { if (g_screen_instance && event) { g_screen_instance->syncFullscreenFlagFromBrowser(event->isFullscreen != 0); } emscripten_async_call(deferredCanvasResize, nullptr, 0); return EM_FALSE; } EM_BOOL onEmResize(int /*eventType*/, const EmscriptenUiEvent * /*event*/, void * /*userData*/) { emscripten_async_call(deferredCanvasResize, nullptr, 0); return EM_FALSE; } EM_BOOL onEmOrientationChange(int /*eventType*/, const EmscriptenOrientationChangeEvent * /*event*/, void * /*userData*/) { emscripten_async_call(deferredCanvasResize, nullptr, 0); return EM_FALSE; } } // namespace #endif // __EMSCRIPTEN__ // Constructor Screen::Screen(SDL_Window *window, SDL_Renderer *renderer, Asset *asset, options_t *options) { // Inicializa variables this->window = window; this->renderer = renderer; this->options = options; this->asset = asset; gameCanvasWidth = options->gameWidth; gameCanvasHeight = options->gameHeight; borderWidth = options->borderWidth * 2; borderHeight = options->borderHeight * 2; // Define el color del borde para el modo de pantalla completa borderColor = {0x00, 0x00, 0x00}; // Crea la textura donde se dibujan los graficos del juego gameCanvas = SDL_CreateTexture(renderer, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, gameCanvasWidth, gameCanvasHeight); if (gameCanvas != nullptr) { SDL_SetTextureScaleMode(gameCanvas, options->filter == FILTER_NEAREST ? SDL_SCALEMODE_NEAREST : SDL_SCALEMODE_LINEAR); } if (gameCanvas == nullptr) { if (options->console) { std::cout << "TitleSurface could not be created!\nSDL Error: " << SDL_GetError() << std::endl; } } // Establece el modo de video setVideoMode(options->videoMode); // Inicializa el sistema de notificaciones notificationText = new Text(asset->get("8bithud.png"), asset->get("8bithud.txt"), renderer); notificationMessage = ""; notificationTextColor = {0xFF, 0xFF, 0xFF}; notificationOutlineColor = {0x00, 0x00, 0x00}; notificationEndTime = 0; notificationY = 2; // Registra callbacks natius d'Emscripten per a fullscreen/resize/orientation registerEmscriptenEventCallbacks(); } // Destructor Screen::~Screen() { delete notificationText; SDL_DestroyTexture(gameCanvas); } // Limpia la pantalla void Screen::clean(color_t color) { SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, 0xFF); SDL_RenderClear(renderer); } // Prepara para empezar a dibujar en la textura de juego void Screen::start() { SDL_SetRenderTarget(renderer, gameCanvas); } // Vuelca el contenido del renderizador en pantalla void Screen::blit() { // Dibuja la notificación activa sobre el gameCanvas antes de presentar SDL_SetRenderTarget(renderer, gameCanvas); renderNotification(); // Vuelve a dejar el renderizador en modo normal SDL_SetRenderTarget(renderer, nullptr); // Borra el contenido previo SDL_SetRenderDrawColor(renderer, borderColor.r, borderColor.g, borderColor.b, 0xFF); SDL_RenderClear(renderer); // Copia la textura de juego en el renderizador en la posición adecuada SDL_FRect fdest = {(float)dest.x, (float)dest.y, (float)dest.w, (float)dest.h}; SDL_RenderTexture(renderer, gameCanvas, nullptr, &fdest); // Muestra por pantalla el renderizador SDL_RenderPresent(renderer); } // Establece el modo de video void Screen::setVideoMode(int videoMode) { // Aplica el modo de video SDL_SetWindowFullscreen(window, videoMode != 0); // Si está activo el modo ventana quita el borde if (videoMode == 0) { // Muestra el puntero y reinicia el temporizador de inactividad SDL_ShowCursor(); Mouse::cursorVisible = true; Mouse::lastMouseMoveTime = SDL_GetTicks(); // Esconde la ventana // SDL_HideWindow(window); if (options->borderEnabled) { windowWidth = gameCanvasWidth + borderWidth; windowHeight = gameCanvasHeight + borderHeight; dest = {0 + (borderWidth / 2), 0 + (borderHeight / 2), gameCanvasWidth, gameCanvasHeight}; } else { windowWidth = gameCanvasWidth; windowHeight = gameCanvasHeight; dest = {0, 0, gameCanvasWidth, gameCanvasHeight}; } #ifdef __EMSCRIPTEN__ // En WASM el tamaño de ventana está fijado a 1x, así que // escalamos el renderizado por 3 aprovechando el modo NEAREST // de la textura del juego para que los píxeles salgan nítidos. constexpr int WASM_RENDER_SCALE = 3; windowWidth *= WASM_RENDER_SCALE; windowHeight *= WASM_RENDER_SCALE; dest.w *= WASM_RENDER_SCALE; dest.h *= WASM_RENDER_SCALE; #endif // Modifica el tamaño de la ventana SDL_SetWindowSize(window, windowWidth * options->windowSize, windowHeight * options->windowSize); SDL_SetWindowPosition(window, SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED); // Muestra la ventana // SDL_ShowWindow(window); } // Si está activo el modo de pantalla completa añade el borde else if (videoMode == SDL_WINDOW_FULLSCREEN) { // Oculta el puntero SDL_HideCursor(); Mouse::cursorVisible = false; // Obten el alto y el ancho de la ventana SDL_GetWindowSize(window, &windowWidth, &windowHeight); // Aplica el escalado al rectangulo donde se pinta la textura del juego if (options->integerScale) { // Calcula el tamaño de la escala máxima int scale = 0; while (((gameCanvasWidth * (scale + 1)) <= windowWidth) && ((gameCanvasHeight * (scale + 1)) <= windowHeight)) { scale++; } dest.w = gameCanvasWidth * scale; dest.h = gameCanvasHeight * scale; dest.x = (windowWidth - dest.w) / 2; dest.y = (windowHeight - dest.h) / 2; } else if (options->keepAspect) { float ratio = (float)gameCanvasWidth / (float)gameCanvasHeight; if ((windowWidth - gameCanvasWidth) >= (windowHeight - gameCanvasHeight)) { dest.h = windowHeight; dest.w = (int)((windowHeight * ratio) + 0.5f); dest.x = (windowWidth - dest.w) / 2; dest.y = (windowHeight - dest.h) / 2; } else { dest.w = windowWidth; dest.h = (int)((windowWidth / ratio) + 0.5f); dest.x = (windowWidth - dest.w) / 2; dest.y = (windowHeight - dest.h) / 2; } } else { dest.w = windowWidth; dest.h = windowHeight; dest.x = dest.y = 0; } } // Modifica el tamaño del renderizador SDL_SetRenderLogicalPresentation(renderer, windowWidth, windowHeight, SDL_LOGICAL_PRESENTATION_LETTERBOX); // Actualiza las opciones options->videoMode = videoMode; options->screen.windowWidth = windowWidth; options->screen.windowHeight = windowHeight; } // Camibia entre pantalla completa y ventana void Screen::switchVideoMode() { options->videoMode = (options->videoMode == 0) ? SDL_WINDOW_FULLSCREEN : 0; setVideoMode(options->videoMode); } // Cambia el tamaño de la ventana void Screen::setWindowSize(int size) { options->windowSize = size; setVideoMode(0); } // Reduce el tamaño de la ventana void Screen::decWindowSize() { --options->windowSize; options->windowSize = std::max(options->windowSize, 1); setVideoMode(0); } // Aumenta el tamaño de la ventana void Screen::incWindowSize() { ++options->windowSize; options->windowSize = std::min(options->windowSize, 4); setVideoMode(0); } // Cambia el color del borde void Screen::setBorderColor(color_t color) { borderColor = color; } // Cambia el tipo de mezcla void Screen::setBlendMode(SDL_BlendMode blendMode) { SDL_SetRenderDrawBlendMode(renderer, blendMode); } // Establece el tamaño del borde void Screen::setBorderWidth(int s) { options->borderWidth = s; } // Establece el tamaño del borde void Screen::setBorderHeight(int s) { options->borderHeight = s; } // Establece si se ha de ver el borde en el modo ventana void Screen::setBorderEnabled(bool value) { options->borderEnabled = value; } // Cambia entre borde visible y no visible void Screen::switchBorder() { options->borderEnabled = !options->borderEnabled; setVideoMode(0); } // Muestra una notificación en la línea superior durante durationMs void Screen::notify(const std::string &text, color_t textColor, color_t outlineColor, Uint32 durationMs) { notificationMessage = text; notificationTextColor = textColor; notificationOutlineColor = outlineColor; notificationEndTime = SDL_GetTicks() + durationMs; } // Limpia la notificación actual void Screen::clearNotification() { notificationEndTime = 0; notificationMessage.clear(); } // --- Fix per a fullscreen/resize en Emscripten --- // Vore el bloc de comentaris a dalt i l'anonymous namespace amb els callbacks. void Screen::handleCanvasResized() { #ifdef __EMSCRIPTEN__ // La crida a SDL_SetWindowFullscreen + SDL_SetRenderLogicalPresentation // que fa setVideoMode és l'única manera de resincronitzar l'estat intern // de SDL amb el canvas HTML real. setVideoMode(options->videoMode); #endif } void Screen::syncFullscreenFlagFromBrowser(bool isFullscreen) { #ifdef __EMSCRIPTEN__ options->videoMode = isFullscreen ? SDL_WINDOW_FULLSCREEN : 0; #else (void)isFullscreen; #endif } void Screen::registerEmscriptenEventCallbacks() { #ifdef __EMSCRIPTEN__ g_screen_instance = this; emscripten_set_fullscreenchange_callback(EMSCRIPTEN_EVENT_TARGET_DOCUMENT, nullptr, EM_FALSE, onEmFullscreenChange); emscripten_set_resize_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, nullptr, EM_FALSE, onEmResize); emscripten_set_orientationchange_callback(nullptr, EM_FALSE, onEmOrientationChange); #endif } // Dibuja la notificación activa (si la hay) sobre el gameCanvas void Screen::renderNotification() { if (SDL_GetTicks() >= notificationEndTime) { return; } notificationText->writeDX(TXT_CENTER | TXT_COLOR | TXT_STROKE, gameCanvasWidth / 2, notificationY, notificationMessage, 1, notificationTextColor, 1, notificationOutlineColor); }