#include "ui_manager.hpp" #include #include #include "ball.hpp" // for Ball #include "defines.hpp" // for TEXT_DURATION, NOTIFICATION_DURATION, AppMode, SimulationMode #include "engine.hpp" // for Engine (info de sistema) #include "scene/scene_manager.hpp" // for SceneManager #include "shapes/shape.hpp" // for Shape #include "text/textrenderer.hpp" // for TextRenderer #include "theme_manager.hpp" // for ThemeManager #include "notifier.hpp" // for Notifier #include "help_overlay.hpp" // for HelpOverlay // ============================================================================ // HELPER: Obtener viewport en coordenadas físicas (no lógicas) // ============================================================================ // SDL_GetRenderViewport() devuelve coordenadas LÓGICAS cuando hay presentación // lógica activa. Para obtener coordenadas FÍSICAS, necesitamos deshabilitar // temporalmente la presentación lógica. static SDL_Rect getPhysicalViewport(SDL_Renderer* renderer) { // Guardar estado actual de presentación lógica int logical_w = 0, logical_h = 0; SDL_RendererLogicalPresentation presentation_mode; SDL_GetRenderLogicalPresentation(renderer, &logical_w, &logical_h, &presentation_mode); // Deshabilitar presentación lógica temporalmente SDL_SetRenderLogicalPresentation(renderer, 0, 0, SDL_LOGICAL_PRESENTATION_DISABLED); // Obtener viewport en coordenadas físicas (píxeles reales) SDL_Rect physical_viewport; SDL_GetRenderViewport(renderer, &physical_viewport); // Restaurar presentación lógica SDL_SetRenderLogicalPresentation(renderer, logical_w, logical_h, presentation_mode); return physical_viewport; } UIManager::UIManager() : text_renderer_debug_(nullptr) , text_renderer_notifier_(nullptr) , notifier_(nullptr) , help_overlay_(nullptr) , show_debug_(false) , fps_last_time_(0) , fps_frame_count_(0) , fps_current_(0) , fps_text_("FPS: 0") , vsync_text_("VSYNC ON") , renderer_(nullptr) , theme_manager_(nullptr) , physical_window_width_(0) , physical_window_height_(0) , current_font_size_(18) { // Tamaño por defecto (medium) } UIManager::~UIManager() { // Limpieza: Los objetos creados con new deben ser eliminados delete text_renderer_debug_; delete text_renderer_notifier_; delete notifier_; delete help_overlay_; } void UIManager::initialize(SDL_Renderer* renderer, ThemeManager* theme_manager, int physical_width, int physical_height) { renderer_ = renderer; theme_manager_ = theme_manager; physical_window_width_ = physical_width; physical_window_height_ = physical_height; // Calcular tamaño de fuente apropiado según dimensiones físicas current_font_size_ = calculateFontSize(physical_width, physical_height); // Crear renderers de texto text_renderer_debug_ = new TextRenderer(); text_renderer_notifier_ = new TextRenderer(); // Inicializar renderers con tamaño dinámico text_renderer_debug_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", current_font_size_, true); text_renderer_notifier_->init(renderer, "data/fonts/FunnelSans-Regular.ttf", current_font_size_, true); // Crear y configurar sistema de notificaciones notifier_ = new Notifier(); notifier_->init(renderer, text_renderer_notifier_, theme_manager_, physical_width, physical_height); // Crear y configurar sistema de ayuda (overlay) help_overlay_ = new HelpOverlay(); help_overlay_->initialize(renderer, theme_manager_, physical_width, physical_height, current_font_size_); // Inicializar FPS counter fps_last_time_ = SDL_GetTicks(); fps_frame_count_ = 0; fps_current_ = 0; } void UIManager::update(Uint64 current_time, float delta_time) { // Calcular FPS fps_frame_count_++; if (current_time - fps_last_time_ >= 1000) { // Actualizar cada segundo fps_current_ = fps_frame_count_; fps_frame_count_ = 0; fps_last_time_ = current_time; fps_text_ = "fps: " + std::to_string(fps_current_); } // Actualizar sistema de notificaciones notifier_->update(current_time); } void UIManager::render(SDL_Renderer* renderer, const Engine* engine, const SceneManager* scene_manager, SimulationMode current_mode, AppMode current_app_mode, const Shape* active_shape, float shape_convergence, int physical_width, int physical_height, int current_screen_width) { // Actualizar dimensiones físicas (puede cambiar en fullscreen) physical_window_width_ = physical_width; physical_window_height_ = physical_height; // Renderizar debug HUD si está activo if (show_debug_) { renderDebugHUD(engine, scene_manager, current_mode, current_app_mode, active_shape, shape_convergence); } // Renderizar notificaciones (siempre al final, sobre todo lo demás) notifier_->render(); // Renderizar ayuda (siempre última, sobre todo incluso notificaciones) if (help_overlay_) { help_overlay_->render(renderer); } } void UIManager::toggleDebug() { show_debug_ = !show_debug_; } void UIManager::toggleHelp() { if (help_overlay_) { help_overlay_->toggle(); } } void UIManager::showNotification(const std::string& text, Uint64 duration) { if (duration == 0) { duration = NOTIFICATION_DURATION; } notifier_->show(text, duration); } void UIManager::updateVSyncText(bool enabled) { vsync_text_ = enabled ? "V-Sync: On" : "V-Sync: Off"; } void UIManager::updatePhysicalWindowSize(int width, int height) { physical_window_width_ = width; physical_window_height_ = height; // Calcular nuevo tamaño de fuente apropiado int new_font_size = calculateFontSize(width, height); // Si el tamaño cambió, reinicializar todos los text renderers if (new_font_size != current_font_size_) { current_font_size_ = new_font_size; // Reinicializar text renderers con nuevo tamaño if (text_renderer_debug_) { text_renderer_debug_->reinitialize(current_font_size_); } if (text_renderer_notifier_) { text_renderer_notifier_->reinitialize(current_font_size_); } } // Actualizar help overlay con font size actual Y nuevas dimensiones (atómicamente) if (help_overlay_) { help_overlay_->updateAll(current_font_size_, width, height); } // Actualizar otros componentes de UI con nuevas dimensiones notifier_->updateWindowSize(width, height); } // === Métodos privados === void UIManager::renderDebugHUD(const Engine* engine, const SceneManager* scene_manager, SimulationMode current_mode, AppMode current_app_mode, const Shape* active_shape, float shape_convergence) { // Obtener altura de línea para espaciado dinámico int line_height = text_renderer_debug_->getTextHeight(); int margin = 8; // Margen constante en píxeles físicos // Obtener viewport FÍSICO (píxeles reales, no lógicos) // CRÍTICO: En F3, SDL_GetRenderViewport() devuelve coordenadas LÓGICAS, // pero printAbsolute() trabaja en píxeles FÍSICOS. Usar helper para obtener // viewport en coordenadas físicas. SDL_Rect physical_viewport = getPhysicalViewport(renderer_); // =========================== // COLUMNA LEFT (Sistema) // =========================== int left_y = margin; // AppMode (antes estaba centrado, ahora va a la izquierda) std::string appmode_text; SDL_Color appmode_color = {255, 255, 255, 255}; // Blanco por defecto if (current_app_mode == AppMode::LOGO) { appmode_text = "AppMode: LOGO"; appmode_color = {255, 128, 0, 255}; // Naranja } else if (current_app_mode == AppMode::DEMO) { appmode_text = "AppMode: DEMO"; appmode_color = {255, 165, 0, 255}; // Naranja } else if (current_app_mode == AppMode::DEMO_LITE) { appmode_text = "AppMode: DEMO LITE"; appmode_color = {255, 200, 0, 255}; // Amarillo-naranja } else { appmode_text = "AppMode: SANDBOX"; appmode_color = {0, 255, 128, 255}; // Verde claro } text_renderer_debug_->printAbsolute(margin, left_y, appmode_text.c_str(), appmode_color); left_y += line_height; // SimulationMode std::string simmode_text; if (current_mode == SimulationMode::PHYSICS) { simmode_text = "SimMode: PHYSICS"; } else if (current_mode == SimulationMode::SHAPE) { if (active_shape) { simmode_text = std::string("SimMode: SHAPE (") + active_shape->getName() + ")"; } else { simmode_text = "SimMode: SHAPE"; } } else if (current_mode == SimulationMode::BOIDS) { simmode_text = "SimMode: BOIDS"; } text_renderer_debug_->printAbsolute(margin, left_y, simmode_text.c_str(), {0, 255, 255, 255}); // Cian left_y += line_height; // Número de pelotas (escenario actual) size_t ball_count = scene_manager->getBallCount(); std::string balls_text; if (ball_count >= 1000) { // Formatear con separador de miles (ejemplo: 5,000 o 50,000) std::string count_str = std::to_string(ball_count); std::string formatted; int digits = count_str.length(); for (int i = 0; i < digits; i++) { if (i > 0 && (digits - i) % 3 == 0) { formatted += ','; } formatted += count_str[i]; } balls_text = "Balls: " + formatted; } else { balls_text = "Balls: " + std::to_string(ball_count); } text_renderer_debug_->printAbsolute(margin, left_y, balls_text.c_str(), {128, 255, 128, 255}); // Verde claro left_y += line_height; // V-Sync text_renderer_debug_->printAbsolute(margin, left_y, vsync_text_.c_str(), {0, 255, 255, 255}); // Cian left_y += line_height; // Modo de escalado (INTEGER/LETTERBOX/STRETCH o WINDOWED si no está en fullscreen) std::string scaling_text; if (engine->getFullscreenEnabled() || engine->getRealFullscreenEnabled()) { ScalingMode scaling = engine->getCurrentScalingMode(); if (scaling == ScalingMode::INTEGER) { scaling_text = "Scaling: INTEGER"; } else if (scaling == ScalingMode::LETTERBOX) { scaling_text = "Scaling: LETTERBOX"; } else if (scaling == ScalingMode::STRETCH) { scaling_text = "Scaling: STRETCH"; } } else { scaling_text = "Scaling: WINDOWED"; } text_renderer_debug_->printAbsolute(margin, left_y, scaling_text.c_str(), {255, 255, 0, 255}); // Amarillo left_y += line_height; // Resolución física (píxeles reales de la ventana) std::string phys_res_text = "Physical: " + std::to_string(physical_window_width_) + "x" + std::to_string(physical_window_height_); text_renderer_debug_->printAbsolute(margin, left_y, phys_res_text.c_str(), {255, 128, 255, 255}); // Magenta claro left_y += line_height; // Resolución lógica (resolución interna del renderizador) std::string logic_res_text = "Logical: " + std::to_string(engine->getCurrentScreenWidth()) + "x" + std::to_string(engine->getCurrentScreenHeight()); text_renderer_debug_->printAbsolute(margin, left_y, logic_res_text.c_str(), {255, 128, 255, 255}); // Magenta claro left_y += line_height; // Display refresh rate (obtener de SDL) std::string refresh_text; int num_displays = 0; SDL_DisplayID* displays = SDL_GetDisplays(&num_displays); if (displays && num_displays > 0) { const auto* dm = SDL_GetCurrentDisplayMode(displays[0]); if (dm) { refresh_text = "Refresh: " + std::to_string(static_cast(dm->refresh_rate)) + " Hz"; } else { refresh_text = "Refresh: N/A"; } SDL_free(displays); } else { refresh_text = "Refresh: N/A"; } text_renderer_debug_->printAbsolute(margin, left_y, refresh_text.c_str(), {255, 255, 128, 255}); // Amarillo claro left_y += line_height; // Tema actual (delegado a ThemeManager) std::string theme_text = std::string("Theme: ") + theme_manager_->getCurrentThemeNameEN(); text_renderer_debug_->printAbsolute(margin, left_y, theme_text.c_str(), {128, 255, 255, 255}); // Cian claro left_y += line_height; // =========================== // COLUMNA RIGHT (Primera pelota) // =========================== int right_y = margin; // FPS counter (esquina superior derecha) int fps_text_width = text_renderer_debug_->getTextWidthPhysical(fps_text_.c_str()); int fps_x = physical_viewport.w - fps_text_width - margin; text_renderer_debug_->printAbsolute(fps_x, right_y, fps_text_.c_str(), {255, 255, 0, 255}); // Amarillo right_y += line_height; // Info de la primera pelota (si existe) const Ball* first_ball = scene_manager->getFirstBall(); if (first_ball != nullptr) { // Posición X, Y SDL_FRect pos = first_ball->getPosition(); std::string pos_text = "Pos: (" + std::to_string(static_cast(pos.x)) + ", " + std::to_string(static_cast(pos.y)) + ")"; int pos_width = text_renderer_debug_->getTextWidthPhysical(pos_text.c_str()); text_renderer_debug_->printAbsolute(physical_viewport.w - pos_width - margin, right_y, pos_text.c_str(), {255, 128, 128, 255}); // Rojo claro right_y += line_height; // Velocidad X int vx_int = static_cast(first_ball->getVelocityX()); std::string vx_text = "VelX: " + std::to_string(vx_int); int vx_width = text_renderer_debug_->getTextWidthPhysical(vx_text.c_str()); text_renderer_debug_->printAbsolute(physical_viewport.w - vx_width - margin, right_y, vx_text.c_str(), {128, 255, 128, 255}); // Verde claro right_y += line_height; // Velocidad Y int vy_int = static_cast(first_ball->getVelocityY()); std::string vy_text = "VelY: " + std::to_string(vy_int); int vy_width = text_renderer_debug_->getTextWidthPhysical(vy_text.c_str()); text_renderer_debug_->printAbsolute(physical_viewport.w - vy_width - margin, right_y, vy_text.c_str(), {128, 255, 128, 255}); // Verde claro right_y += line_height; // Fuerza de gravedad int grav_int = static_cast(first_ball->getGravityForce()); std::string grav_text = "Gravity: " + std::to_string(grav_int); int grav_width = text_renderer_debug_->getTextWidthPhysical(grav_text.c_str()); text_renderer_debug_->printAbsolute(physical_viewport.w - grav_width - margin, right_y, grav_text.c_str(), {255, 255, 128, 255}); // Amarillo claro right_y += line_height; // Estado superficie std::string surface_text = first_ball->isOnSurface() ? "Surface: YES" : "Surface: NO"; int surface_width = text_renderer_debug_->getTextWidthPhysical(surface_text.c_str()); text_renderer_debug_->printAbsolute(physical_viewport.w - surface_width - margin, right_y, surface_text.c_str(), {255, 200, 128, 255}); // Naranja claro right_y += line_height; // Coeficiente de rebote (loss) float loss_val = first_ball->getLossCoefficient(); std::string loss_text = "Loss: " + std::to_string(loss_val).substr(0, 4); int loss_width = text_renderer_debug_->getTextWidthPhysical(loss_text.c_str()); text_renderer_debug_->printAbsolute(physical_viewport.w - loss_width - margin, right_y, loss_text.c_str(), {255, 128, 255, 255}); // Magenta right_y += line_height; // Dirección de gravedad std::string gravity_dir_text = "Dir: " + gravityDirectionToString(static_cast(scene_manager->getCurrentGravity())); int dir_width = text_renderer_debug_->getTextWidthPhysical(gravity_dir_text.c_str()); text_renderer_debug_->printAbsolute(physical_viewport.w - dir_width - margin, right_y, gravity_dir_text.c_str(), {128, 255, 255, 255}); // Cian claro right_y += line_height; } // Convergencia en modo LOGO (solo cuando está activo) - Parte inferior derecha if (current_app_mode == AppMode::LOGO && current_mode == SimulationMode::SHAPE) { int convergence_percent = static_cast(shape_convergence * 100.0f); std::string convergence_text = "Convergence: " + std::to_string(convergence_percent) + "%"; int conv_width = text_renderer_debug_->getTextWidthPhysical(convergence_text.c_str()); text_renderer_debug_->printAbsolute(physical_viewport.w - conv_width - margin, right_y, convergence_text.c_str(), {255, 128, 0, 255}); // Naranja right_y += line_height; } } std::string UIManager::gravityDirectionToString(int direction) const { switch (direction) { case 0: return "Abajo"; // DOWN case 1: return "Arriba"; // UP case 2: return "Izquierda"; // LEFT case 3: return "Derecha"; // RIGHT default: return "Desconocida"; } } int UIManager::calculateFontSize(int physical_width, int physical_height) const { // Calcular área física de la ventana int area = physical_width * physical_height; // Stepped scaling con 3 tamaños: // - SMALL: < 800x600 (480,000 pixels) → 14px // - MEDIUM: 800x600 a 1920x1080 (2,073,600 pixels) → 18px // - LARGE: > 1920x1080 → 24px if (area < 480000) { return 14; // Ventanas pequeñas } else if (area < 2073600) { return 18; // Ventanas medianas (default) } else { return 24; // Ventanas grandes } }