#include "help_overlay.hpp" #include // for std::min #include // for std::array #include "defines.hpp" #include "text/textrenderer.hpp" #include "theme_manager.hpp" HelpOverlay::HelpOverlay() : renderer_(nullptr), theme_mgr_(nullptr), text_renderer_(nullptr), physical_width_(0), physical_height_(0), visible_(false), box_width_(0), box_height_(0), box_x_(0), box_y_(0), column1_width_(0), column2_width_(0), column3_width_(0), cached_texture_(nullptr), last_category_color_({.r = 0, .g = 0, .b = 0, .a = 255}), last_content_color_({.r = 0, .g = 0, .b = 0, .a = 255}), last_bg_color_({.r = 0, .g = 0, .b = 0, .a = 255}), texture_needs_rebuild_(true) { // Llenar lista de controles (organizados por categoría, equilibrado en 3 columnas) key_bindings_ = { // COLUMNA 1: SIMULACIÓN {.key = "SIMULACIÓN", .description = ""}, {.key = "1-8", .description = "Escenarios (10 a 50.000 pelotas)"}, {.key = "F", .description = "Cambia entre figura y física"}, {.key = "B", .description = "Cambia entre boids y física"}, {.key = "ESPACIO", .description = "Impulso contra la gravedad"}, {.key = "G", .description = "Activar / Desactivar gravedad"}, {.key = "CURSORES", .description = "Dirección de la gravedad"}, {.key = "", .description = ""}, // Separador // COLUMNA 1: FIGURAS 3D {.key = "FIGURAS 3D", .description = ""}, {.key = "Q/W/E/R", .description = "Esfera / Lissajous / Hélice / Toroide"}, {.key = "T/Y/U/I", .description = "Cubo / Cilindro / Icosaedro / Átomo"}, {.key = "Num+/-", .description = "Escalar figura"}, {.key = "Num*", .description = "Reset escala"}, {.key = "Num/", .description = "Activar / Desactivar profundidad"}, {.key = "[new_col]", .description = ""}, // CAMBIO DE COLUMNA -> COLUMNA 2 // COLUMNA 2: MODOS {.key = "MODOS", .description = ""}, {.key = "D", .description = "Activar / Desactivar modo demo"}, {.key = "L", .description = "Activar / Desactivar modo demo lite"}, {.key = "K", .description = "Activar / Desactivar modo logo"}, {.key = "", .description = ""}, // Separador // COLUMNA 2: VISUAL {.key = "VISUAL", .description = ""}, {.key = "C", .description = "Tema siguiente"}, {.key = "Shift+C", .description = "Tema anterior"}, {.key = "NumEnter", .description = "Página de temas"}, {.key = "Shift+D", .description = "Pausar tema dinámico"}, {.key = "N", .description = "Cambiar tamaño de pelota"}, {.key = "X", .description = "Ciclar presets PostFX"}, {.key = "[new_col]", .description = ""}, // CAMBIO DE COLUMNA -> COLUMNA 3 // COLUMNA 3: PANTALLA {.key = "PANTALLA", .description = ""}, {.key = "F1", .description = "Disminuye ventana"}, {.key = "F2", .description = "Aumenta ventana"}, {.key = "F3", .description = "Pantalla completa"}, {.key = "F4", .description = "Pantalla completa real"}, {.key = "F5", .description = "Activar / Desactivar PostFX"}, {.key = "F6", .description = "Cambia el escalado de pantalla"}, {.key = "V", .description = "Activar / Desactivar V-Sync"}, {.key = "", .description = ""}, // Separador // COLUMNA 3: DEBUG/AYUDA {.key = "DEBUG / AYUDA", .description = ""}, {.key = "F12", .description = "Activar / Desactivar info debug"}, {.key = "H", .description = "Esta ayuda"}, {.key = "ESC", .description = "Salir"}}; } HelpOverlay::~HelpOverlay() { // Destruir textura cacheada si existe if (cached_texture_ != nullptr) { SDL_DestroyTexture(cached_texture_); cached_texture_ = nullptr; } delete text_renderer_; } void HelpOverlay::toggle() { visible_ = !visible_; } void HelpOverlay::initialize(SDL_Renderer* renderer, ThemeManager* theme_mgr, int physical_width, int physical_height, int font_size) { renderer_ = renderer; theme_mgr_ = theme_mgr; physical_width_ = physical_width; physical_height_ = physical_height; // Crear renderer de texto con tamaño dinámico text_renderer_ = new TextRenderer(); text_renderer_->init(renderer, APP_FONT, font_size, true); calculateBoxDimensions(); } void HelpOverlay::updatePhysicalWindowSize(int physical_width, int physical_height) { physical_width_ = physical_width; physical_height_ = physical_height; calculateBoxDimensions(); // Marcar textura para regeneración (dimensiones han cambiado) texture_needs_rebuild_ = true; } void HelpOverlay::reinitializeFontSize(int new_font_size) { if (text_renderer_ == nullptr) { return; } // Reinicializar text renderer con nuevo tamaño text_renderer_->reinitialize(new_font_size); // NOTA: NO recalcular dimensiones aquí porque physical_width_ y physical_height_ // pueden tener valores antiguos. updatePhysicalWindowSize() se llamará después // con las dimensiones correctas y recalculará todo apropiadamente. // Marcar textura para regeneración completa texture_needs_rebuild_ = true; } void HelpOverlay::updateAll(int font_size, int physical_width, int physical_height) { // Actualizar dimensiones físicas PRIMERO physical_width_ = physical_width; physical_height_ = physical_height; // Reinicializar text renderer con nuevo tamaño (si cambió) if (text_renderer_ != nullptr) { text_renderer_->reinitialize(font_size); } // Recalcular dimensiones del box con nuevo font y nuevas dimensiones calculateBoxDimensions(); // Marcar textura para regeneración completa texture_needs_rebuild_ = true; } void HelpOverlay::calculateTextDimensions(int& max_width, int& total_height) { if (text_renderer_ == nullptr) { max_width = 0; total_height = 0; return; } int line_height = text_renderer_->getTextHeight(); // Padding dinámico basado en altura física: 25px para >= 600px, escalado proporcionalmente para menores int padding = (physical_height_ >= 600) ? 25 : std::max(10, physical_height_ / 24); // Calcular ancho máximo por columna int max_col1_width = 0; int max_col2_width = 0; int max_col3_width = 0; int current_column = 0; for (const auto& binding : key_bindings_) { // Cambio de columna if (strcmp(binding.key, "[new_col]") == 0) { current_column++; continue; } // Separador vacío (no tiene key ni description) if (binding.key[0] == '\0') { continue; } int line_width = 0; if (binding.description[0] == '\0') { // Es un encabezado (solo tiene key, sin description) line_width = text_renderer_->getTextWidthPhysical(binding.key); } else { // Es una línea normal con key + description int key_width = text_renderer_->getTextWidthPhysical(binding.key); int desc_width = text_renderer_->getTextWidthPhysical(binding.description); line_width = key_width + 10 + desc_width; // 10px de separación } // Actualizar máximo de columna correspondiente if (current_column == 0) { max_col1_width = std::max(max_col1_width, line_width); } else if (current_column == 1) { max_col2_width = std::max(max_col2_width, line_width); } else { max_col3_width = std::max(max_col3_width, line_width); } } // Almacenar anchos de columnas en miembros para uso posterior column1_width_ = max_col1_width; column2_width_ = max_col2_width; column3_width_ = max_col3_width; // Gap entre columnas: doble del padding para dar más respiro int col_gap = padding * 2; // Ancho total: 3 columnas + padding izq/der + 2 gaps entre columnas max_width = max_col1_width + max_col2_width + max_col3_width + padding * 2 + col_gap * 2; // Calcular altura real simulando exactamente lo que hace el render std::array col_heights = {0, 0, 0}; current_column = 0; for (const auto& binding : key_bindings_) { if (strcmp(binding.key, "[new_col]") == 0) { current_column++; continue; } if (binding.key[0] == '\0') { col_heights[current_column] += line_height; // separador vacío } else if (binding.description[0] == '\0') { col_heights[current_column] += line_height; // encabezado } else { col_heights[current_column] += line_height; // línea normal } } int content_height = std::max({col_heights[0], col_heights[1], col_heights[2]}); // Eliminar el line_gap de la última línea: ese gap es espacio entre líneas, // pero la última línea no tiene siguiente, así que queda como padding muerto. int glyph_height = text_renderer_->getGlyphHeight(); int visual_content_height = content_height - (line_height - glyph_height); total_height = visual_content_height + padding * 2; } void HelpOverlay::calculateBoxDimensions() { // Calcular dimensiones necesarias según el texto int text_width; int text_height; calculateTextDimensions(text_width, text_height); // Aplicar límites máximos: 95% ancho, 90% altura int max_width = static_cast(physical_width_ * 0.95f); int max_height = static_cast(physical_height_ * 0.90f); box_width_ = std::min(text_width, max_width); box_height_ = std::min(text_height, max_height); // Centrar en pantalla box_x_ = (physical_width_ - box_width_) / 2; box_y_ = (physical_height_ - box_height_) / 2; } void HelpOverlay::rebuildCachedTexture() { if ((renderer_ == nullptr) || (theme_mgr_ == nullptr) || (text_renderer_ == nullptr)) { return; } // Destruir textura anterior si existe if (cached_texture_ != nullptr) { SDL_DestroyTexture(cached_texture_); cached_texture_ = nullptr; } // Crear nueva textura del tamaño del overlay cached_texture_ = SDL_CreateTexture(renderer_, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, box_width_, box_height_); if (cached_texture_ == nullptr) { SDL_Log("Error al crear textura cacheada: %s", SDL_GetError()); return; } // Habilitar alpha blending en la textura SDL_SetTextureBlendMode(cached_texture_, SDL_BLENDMODE_BLEND); // Guardar render target actual SDL_Texture* prev_target = SDL_GetRenderTarget(renderer_); // Cambiar render target a la textura cacheada SDL_SetRenderTarget(renderer_, cached_texture_); // Limpiar textura (completamente transparente) SDL_SetRenderDrawColor(renderer_, 0, 0, 0, 0); SDL_RenderClear(renderer_); // Habilitar alpha blending SDL_SetRenderDrawBlendMode(renderer_, SDL_BLENDMODE_BLEND); // Obtener colores actuales del tema int notif_bg_r; int notif_bg_g; int notif_bg_b; theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b); // Renderizar fondo del overlay a la textura float alpha = 0.85f; std::array bg_vertices{}; float r = notif_bg_r / 255.0f; float g = notif_bg_g / 255.0f; float b = notif_bg_b / 255.0f; // Vértices del fondo (posición relativa 0,0 porque estamos renderizando a textura) bg_vertices[0].position = {.x = 0, .y = 0}; bg_vertices[0].tex_coord = {.x = 0.0f, .y = 0.0f}; bg_vertices[0].color = {.r = r, .g = g, .b = b, .a = alpha}; bg_vertices[1].position = {.x = static_cast(box_width_), .y = 0}; bg_vertices[1].tex_coord = {.x = 1.0f, .y = 0.0f}; bg_vertices[1].color = {.r = r, .g = g, .b = b, .a = alpha}; bg_vertices[2].position = {.x = static_cast(box_width_), .y = static_cast(box_height_)}; bg_vertices[2].tex_coord = {.x = 1.0f, .y = 1.0f}; bg_vertices[2].color = {.r = r, .g = g, .b = b, .a = alpha}; bg_vertices[3].position = {.x = 0, .y = static_cast(box_height_)}; bg_vertices[3].tex_coord = {.x = 0.0f, .y = 1.0f}; bg_vertices[3].color = {.r = r, .g = g, .b = b, .a = alpha}; std::array bg_indices = {0, 1, 2, 2, 3, 0}; SDL_RenderGeometry(renderer_, nullptr, bg_vertices.data(), 4, bg_indices.data(), 6); // Renderizar texto del overlay (ajustando coordenadas para que sean relativas a 0,0) // Necesito renderizar el texto igual que en renderHelpText() pero con coordenadas ajustadas // Obtener colores para el texto int text_r; int text_g; int text_b; theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b); SDL_Color category_color = {static_cast(text_r), static_cast(text_g), static_cast(text_b), 255}; Color ball_color = theme_mgr_->getInterpolatedColor(0); SDL_Color content_color = {static_cast(ball_color.r), static_cast(ball_color.g), static_cast(ball_color.b), 255}; // Guardar colores actuales para comparación futura last_category_color_ = category_color; last_content_color_ = content_color; last_bg_color_ = {.r = static_cast(notif_bg_r), .g = static_cast(notif_bg_g), .b = static_cast(notif_bg_b), .a = 255}; // Configuración de espaciado int line_height = text_renderer_->getTextHeight(); int padding = (physical_height_ >= 600) ? 25 : std::max(10, physical_height_ / 24); int col_gap = padding * 2; // Posición X de inicio de cada columna std::array col_start{}; col_start[0] = padding; col_start[1] = padding + column1_width_ + col_gap; col_start[2] = padding + column1_width_ + col_gap + column2_width_ + col_gap; // Ancho de cada columna (para centrado interno) std::array col_width = {column1_width_, column2_width_, column3_width_}; int glyph_height = text_renderer_->getGlyphHeight(); int current_y = padding; int current_column = 0; int content_start_y = current_y; // Renderizar cada línea for (const auto& binding : key_bindings_) { if (strcmp(binding.key, "[new_col]") == 0 && binding.description[0] == '\0') { if (current_column < 2) { current_column++; current_y = content_start_y; } continue; } // CHECK PADDING INFERIOR ANTES de escribir la línea // Usamos glyph_height (no line_height) porque el gap después de la última línea no ocupa espacio visual if (current_y + glyph_height > box_height_ - padding) { continue; } int cx = col_start[current_column]; int cw = col_width[current_column]; if (binding.description[0] == '\0') { if (binding.key[0] == '\0') { // Separador vacío: avanzar una línea completa current_y += line_height; } else { // Encabezado de sección — centrado en la columna int w = text_renderer_->getTextWidthPhysical(binding.key); text_renderer_->printAbsolute(cx + ((cw - w) / 2), current_y, binding.key, category_color); current_y += line_height; } continue; } // Par tecla+descripción — centrado como bloque en la columna int key_width = text_renderer_->getTextWidthPhysical(binding.key); int desc_width = text_renderer_->getTextWidthPhysical(binding.description); int total_width = key_width + 10 + desc_width; int line_x = cx + ((cw - total_width) / 2); text_renderer_->printAbsolute(line_x, current_y, binding.key, category_color); text_renderer_->printAbsolute(line_x + key_width + 10, current_y, binding.description, content_color); current_y += line_height; } // Restaurar render target original SDL_SetRenderTarget(renderer_, prev_target); // Marcar que ya no necesita rebuild texture_needs_rebuild_ = false; } void HelpOverlay::render(SDL_Renderer* renderer) { if (!visible_) { return; } // Obtener colores actuales del tema int notif_bg_r; int notif_bg_g; int notif_bg_b; theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b); int text_r; int text_g; int text_b; theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b); Color ball_color = theme_mgr_->getInterpolatedColor(0); // Crear colores actuales para comparación SDL_Color current_bg = {static_cast(notif_bg_r), static_cast(notif_bg_g), static_cast(notif_bg_b), 255}; SDL_Color current_category = {static_cast(text_r), static_cast(text_g), static_cast(text_b), 255}; SDL_Color current_content = {static_cast(ball_color.r), static_cast(ball_color.g), static_cast(ball_color.b), 255}; // Detectar si los colores han cambiado significativamente (umbral: 5/255) constexpr int COLOR_CHANGE_THRESHOLD = 5; bool colors_changed = (abs(current_bg.r - last_bg_color_.r) > COLOR_CHANGE_THRESHOLD || abs(current_bg.g - last_bg_color_.g) > COLOR_CHANGE_THRESHOLD || abs(current_bg.b - last_bg_color_.b) > COLOR_CHANGE_THRESHOLD || abs(current_category.r - last_category_color_.r) > COLOR_CHANGE_THRESHOLD || abs(current_category.g - last_category_color_.g) > COLOR_CHANGE_THRESHOLD || abs(current_category.b - last_category_color_.b) > COLOR_CHANGE_THRESHOLD || abs(current_content.r - last_content_color_.r) > COLOR_CHANGE_THRESHOLD || abs(current_content.g - last_content_color_.g) > COLOR_CHANGE_THRESHOLD || abs(current_content.b - last_content_color_.b) > COLOR_CHANGE_THRESHOLD); // Regenerar textura si es necesario (colores cambiaron O flag de rebuild activo) if (texture_needs_rebuild_ || colors_changed || (cached_texture_ == nullptr)) { rebuildCachedTexture(); } // Si no hay textura cacheada (error), salir if (cached_texture_ == nullptr) { return; } // CRÍTICO: Habilitar alpha blending para que la transparencia funcione SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_BLEND); // Obtener viewport actual (en modo letterbox F3 tiene offset para centrar imagen) SDL_Rect viewport; SDL_GetRenderViewport(renderer, &viewport); // Calcular posición centrada dentro del VIEWPORT, no de la pantalla física // viewport.w y viewport.h son las dimensiones del área visible // viewport.x y viewport.y son el offset de las barras negras int centered_x = viewport.x + ((viewport.w - box_width_) / 2); int centered_y = viewport.y + ((viewport.h - box_height_) / 2); // Renderizar la textura cacheada centrada en el viewport SDL_FRect dest_rect; dest_rect.x = static_cast(centered_x); dest_rect.y = static_cast(centered_y); dest_rect.w = static_cast(box_width_); dest_rect.h = static_cast(box_height_); SDL_RenderTexture(renderer, cached_texture_, nullptr, &dest_rect); }