Files
vibe3_physics/source/ui/help_overlay.cpp
Sergio Valor 33cb995872 refactor: unificar log de consola y centralizar fuente de UI
- Formato uniforme [Tipo] nombre (pack/disco) en texture, textrenderer, png_shape
- Eliminar logs verbosos de logo_scaler y app_logo (resolución, escalado, etc.)
- Centralizar fuente de UI en APP_FONT (defines.hpp) con las 8 opciones comentadas
- Actualizar carpeta data/fonts con nuevas fuentes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-20 23:28:32 +01:00

475 lines
18 KiB
C++

#include "help_overlay.hpp"
#include <algorithm> // for std::min
#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_({0, 0, 0, 255}),
last_content_color_({0, 0, 0, 255}),
last_bg_color_({0, 0, 0, 255}),
texture_needs_rebuild_(true) {
// Llenar lista de controles (organizados por categoría, equilibrado en 3 columnas)
key_bindings_ = {
// COLUMNA 1: SIMULACIÓN
{"SIMULACIÓN", ""},
{"1-8", "Escenarios (10 a 50.000 pelotas)"},
{"F", "Cambia entre figura y física"},
{"B", "Cambia entre boids y física"},
{"ESPACIO", "Impulso contra la gravedad"},
{"G", "Activar / Desactivar gravedad"},
{"CURSORES", "Dirección de la gravedad"},
{"", ""}, // Separador
// COLUMNA 1: FIGURAS 3D
{"FIGURAS 3D", ""},
{"Q/W/E/R", "Esfera / Lissajous / Hélice / Toroide"},
{"T/Y/U/I", "Cubo / Cilindro / Icosaedro / Átomo"},
{"Num+/-", "Escalar figura"},
{"Num*", "Reset escala"},
{"Num/", "Activar / Desactivar profundidad"},
{"[new_col]", ""}, // CAMBIO DE COLUMNA -> COLUMNA 2
// COLUMNA 2: MODOS
{"MODOS", ""},
{"D", "Activar / Desactivar modo demo"},
{"L", "Activar / Desactivar modo demo lite"},
{"K", "Activar / Desactivar modo logo"},
{"", ""}, // Separador
// COLUMNA 2: VISUAL
{"VISUAL", ""},
{"C", "Tema siguiente"},
{"Shift+C", "Tema anterior"},
{"NumEnter", "Página de temas"},
{"Shift+D", "Pausar tema dinámico"},
{"N", "Cambiar tamaño de pelota"},
{"X", "Ciclar presets PostFX"},
{"[new_col]", ""}, // CAMBIO DE COLUMNA -> COLUMNA 3
// COLUMNA 3: PANTALLA
{"PANTALLA", ""},
{"F1", "Disminuye ventana"},
{"F2", "Aumenta ventana"},
{"F3", "Pantalla completa"},
{"F4", "Pantalla completa real"},
{"F5", "Activar / Desactivar PostFX"},
{"F6", "Cambia el escalado de pantalla"},
{"V", "Activar / Desactivar V-Sync"},
{"", ""}, // Separador
// COLUMNA 3: DEBUG/AYUDA
{"DEBUG / AYUDA", ""},
{"F12", "Activar / Desactivar info debug"},
{"H", "Esta ayuda"},
{"ESC", "Salir"}};
}
HelpOverlay::~HelpOverlay() {
// Destruir textura cacheada si existe
if (cached_texture_) {
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_) 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_) {
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_) {
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
int col_heights[3] = {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, text_height;
calculateTextDimensions(text_width, text_height);
// Aplicar límites máximos: 95% ancho, 90% altura
int max_width = static_cast<int>(physical_width_ * 0.95f);
int max_height = static_cast<int>(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_ || !theme_mgr_ || !text_renderer_) return;
// Destruir textura anterior si existe
if (cached_texture_) {
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_) {
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, notif_bg_g, 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;
SDL_Vertex bg_vertices[4];
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 = {0, 0};
bg_vertices[0].tex_coord = {0.0f, 0.0f};
bg_vertices[0].color = {r, g, b, alpha};
bg_vertices[1].position = {static_cast<float>(box_width_), 0};
bg_vertices[1].tex_coord = {1.0f, 0.0f};
bg_vertices[1].color = {r, g, b, alpha};
bg_vertices[2].position = {static_cast<float>(box_width_), static_cast<float>(box_height_)};
bg_vertices[2].tex_coord = {1.0f, 1.0f};
bg_vertices[2].color = {r, g, b, alpha};
bg_vertices[3].position = {0, static_cast<float>(box_height_)};
bg_vertices[3].tex_coord = {0.0f, 1.0f};
bg_vertices[3].color = {r, g, b, alpha};
int bg_indices[6] = {0, 1, 2, 2, 3, 0};
SDL_RenderGeometry(renderer_, nullptr, bg_vertices, 4, bg_indices, 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, text_g, text_b;
theme_mgr_->getCurrentThemeTextColor(text_r, text_g, text_b);
SDL_Color category_color = {static_cast<Uint8>(text_r), static_cast<Uint8>(text_g), static_cast<Uint8>(text_b), 255};
Color ball_color = theme_mgr_->getInterpolatedColor(0);
SDL_Color content_color = {static_cast<Uint8>(ball_color.r), static_cast<Uint8>(ball_color.g), static_cast<Uint8>(ball_color.b), 255};
// Guardar colores actuales para comparación futura
last_category_color_ = category_color;
last_content_color_ = content_color;
last_bg_color_ = {static_cast<Uint8>(notif_bg_r), static_cast<Uint8>(notif_bg_g), static_cast<Uint8>(notif_bg_b), 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
int col_start[3];
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)
int col_width[3] = {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, notif_bg_g, notif_bg_b;
theme_mgr_->getCurrentNotificationBackgroundColor(notif_bg_r, notif_bg_g, notif_bg_b);
int text_r, text_g, 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<Uint8>(notif_bg_r), static_cast<Uint8>(notif_bg_g), static_cast<Uint8>(notif_bg_b), 255};
SDL_Color current_category = {static_cast<Uint8>(text_r), static_cast<Uint8>(text_g), static_cast<Uint8>(text_b), 255};
SDL_Color current_content = {static_cast<Uint8>(ball_color.r), static_cast<Uint8>(ball_color.g), static_cast<Uint8>(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_) {
rebuildCachedTexture();
}
// Si no hay textura cacheada (error), salir
if (!cached_texture_) 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<float>(centered_x);
dest_rect.y = static_cast<float>(centered_y);
dest_rect.w = static_cast<float>(box_width_);
dest_rect.h = static_cast<float>(box_height_);
SDL_RenderTexture(renderer, cached_texture_, nullptr, &dest_rect);
}