#include "menu_renderer.h" #include #include #include "color.h" #include "menu_option.h" #include "param.h" #include "screen.h" #include "text.h" #include "utils.h" // --- Implementación de las estructuras de animación --- void MenuRenderer::ResizeAnimation::start(float from_w, float from_h, float to_w, float to_h) { start_width = from_w; start_height = from_h; target_width = to_w; target_height = to_h; elapsed = 0.0F; active = true; } void MenuRenderer::ResizeAnimation::stop() { active = false; elapsed = 0.0F; } void MenuRenderer::ShowHideAnimation::startShow(float to_w, float to_h) { type = Type::SHOWING; target_width = to_w; target_height = to_h; elapsed = 0.0F; active = true; } void MenuRenderer::ShowHideAnimation::startHide() { type = Type::HIDING; elapsed = 0.0F; active = true; } void MenuRenderer::ShowHideAnimation::stop() { type = Type::NONE; active = false; elapsed = 0.0F; } MenuRenderer::MenuRenderer(const ServiceMenu *menu_state, std::shared_ptr element_text, std::shared_ptr title_text) : element_text_(std::move(element_text)), title_text_(std::move(title_text)) { initializeMaxSizes(); setPosition(param.game.game_area.center_x, param.game.game_area.center_y, PositionMode::CENTERED); } void MenuRenderer::render(const ServiceMenu *menu_state) { if (!visible_) { return; } // Dibuja la sombra if (param.service_menu.drop_shadow) { SDL_FRect shadow_rect = {rect_.x + 5, rect_.y + 5, rect_.w, rect_.h}; SDL_SetRenderDrawColor(Screen::get()->getRenderer(), 0, 0, 0, 64); SDL_RenderFillRect(Screen::get()->getRenderer(), &shadow_rect); } // Dibuja el fondo SDL_SetRenderDrawColor(Screen::get()->getRenderer(), param.service_menu.bg_color.r, param.service_menu.bg_color.g, param.service_menu.bg_color.b, param.service_menu.bg_color.a); SDL_RenderFillRect(Screen::get()->getRenderer(), &rect_); // Dibuja el borde const Color BORDER_COLOR = param.service_menu.title_color.DARKEN(); SDL_SetRenderDrawColor(Screen::get()->getRenderer(), BORDER_COLOR.r, BORDER_COLOR.g, BORDER_COLOR.b, 255); SDL_RenderRect(Screen::get()->getRenderer(), &rect_); SDL_RenderRect(Screen::get()->getRenderer(), &border_rect_); // Solo renderizar contenido si la animación lo permite if (shouldShowContent()) { // Dibuja el título float y = rect_.y + title_padding_; title_text_->writeDX(Text::COLOR | Text::CENTER, rect_.x + rect_.w / 2.0F, y, menu_state->getTitle(), -4, param.service_menu.title_color); // Dibuja la línea separadora y = rect_.y + upper_height_; SDL_SetRenderDrawColor(Screen::get()->getRenderer(), BORDER_COLOR.r, BORDER_COLOR.g, BORDER_COLOR.b, 255); SDL_RenderLine(Screen::get()->getRenderer(), rect_.x + ServiceMenu::OPTIONS_HORIZONTAL_PADDING, y, rect_.x + rect_.w - ServiceMenu::OPTIONS_HORIZONTAL_PADDING, y); // Dibuja las opciones y = options_y_; const auto &option_pairs = menu_state->getOptionPairs(); for (size_t i = 0; i < option_pairs.size(); ++i) { const bool IS_SELECTED = (i == menu_state->getSelectedIndex()); const Color ¤t_color = IS_SELECTED ? param.service_menu.selected_color : param.service_menu.text_color; if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) { const int AVAILABLE_WIDTH = rect_.w - (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2) - element_text_->length(option_pairs.at(i).first, -2) - ServiceMenu::MIN_GAP_OPTION_VALUE; std::string truncated_value = getTruncatedValue(option_pairs.at(i).second, AVAILABLE_WIDTH); element_text_->writeColored(rect_.x + ServiceMenu::OPTIONS_HORIZONTAL_PADDING, y, option_pairs.at(i).first, current_color, -2); const int X = rect_.x + rect_.w - ServiceMenu::OPTIONS_HORIZONTAL_PADDING - element_text_->length(truncated_value, -2); element_text_->writeColored(X, y, truncated_value, current_color, -2); } else { const int AVAILABLE_WIDTH = rect_.w - (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2); std::string truncated_caption = getTruncatedValue(option_pairs.at(i).first, AVAILABLE_WIDTH); element_text_->writeDX(Text::CENTER | Text::COLOR, rect_.x + rect_.w / 2.0F, y, truncated_caption, -2, current_color); } y += options_height_ + options_padding_; } } } void MenuRenderer::update(const ServiceMenu *menu_state) { float delta_time = 1.0F / 60.0F; // Asumiendo 60 FPS updateAnimations(delta_time); if (visible_) { updateColorCounter(); param.service_menu.selected_color = getAnimatedSelectedColor(); } } // --- Nuevos métodos de control --- void MenuRenderer::show(const ServiceMenu *menu_state) { if (visible_) { return; } visible_ = true; // Calcula el tamaño final y lo usa para la animación SDL_FRect target_rect = calculateNewRect(menu_state); // Detener cualquier animación anterior resize_animation_.stop(); // Iniciar animación de mostrar show_hide_animation_.startShow(target_rect.w, target_rect.h); // El tamaño inicial es cero para la animación rect_.w = 0.0F; rect_.h = 0.0F; updatePosition(); } void MenuRenderer::hide() { if (!visible_ || show_hide_animation_.type == ShowHideAnimation::Type::HIDING) { return; } // Detener animación de resize si la hubiera resize_animation_.stop(); // Guardar tamaño actual para la animación de ocultar show_hide_animation_.target_width = rect_.w; show_hide_animation_.target_height = rect_.h; show_hide_animation_.startHide(); } void MenuRenderer::setPosition(float x, float y, PositionMode mode) { anchor_x_ = x; anchor_y_ = y; position_mode_ = mode; updatePosition(); } // --- Métodos de layout --- void MenuRenderer::onLayoutChanged(const ServiceMenu *menu_state) { precalculateMenuWidths(menu_state->getAllOptions(), menu_state); setAnchors(menu_state); resize(menu_state); } void MenuRenderer::setLayout(const ServiceMenu *menu_state) { precalculateMenuWidths(menu_state->getAllOptions(), menu_state); setAnchors(menu_state); setSize(menu_state); } void MenuRenderer::initializeMaxSizes() { max_menu_width_ = static_cast(param.game.game_area.rect.w * 0.9F); max_menu_height_ = static_cast(param.game.game_area.rect.h * 0.9F); } void MenuRenderer::setAnchors(const ServiceMenu *menu_state) { size_t max_entries = 0; for (int i = 0; i < 5; ++i) { max_entries = std::max(max_entries, menu_state->countOptionsInGroup(static_cast(i))); } options_height_ = element_text_->getCharacterSize(); options_padding_ = 5; title_height_ = title_text_->getCharacterSize(); title_padding_ = title_height_ / 2; upper_height_ = (title_padding_ * 2) + title_height_; lower_padding_ = (options_padding_ * 3); lower_height_ = ((max_entries > 0 ? max_entries - 1 : 0) * (options_height_ + options_padding_)) + options_height_ + (lower_padding_ * 2); width_ = ServiceMenu::MIN_WIDTH; height_ = upper_height_ + lower_height_; } auto MenuRenderer::calculateNewRect(const ServiceMenu *menu_state) -> SDL_FRect { width_ = std::min(static_cast(getMenuWidthForGroup(menu_state->getCurrentGroup())), max_menu_width_); const auto &display_options = menu_state->getDisplayOptions(); lower_height_ = ((!display_options.empty() ? display_options.size() - 1 : 0) * (options_height_ + options_padding_)) + options_height_ + (lower_padding_ * 2); height_ = std::min(upper_height_ + lower_height_, max_menu_height_); SDL_FRect new_rect = {0, 0, (float)width_, (float)height_}; // La posición x, y se establecerá en `updatePosition` return new_rect; } void MenuRenderer::resize(const ServiceMenu *menu_state) { SDL_FRect new_rect = calculateNewRect(menu_state); if (rect_.w != new_rect.w || rect_.h != new_rect.h) { // En lugar de la animación antigua, usamos la nueva resize_animation_.start(rect_.w, rect_.h, new_rect.w, new_rect.h); } else { // Si no hay cambio de tamaño, solo actualizamos la posición updatePosition(); } options_y_ = rect_.y + upper_height_ + lower_padding_; } void MenuRenderer::setSize(const ServiceMenu *menu_state) { SDL_FRect new_rect = calculateNewRect(menu_state); rect_.w = new_rect.w; rect_.h = new_rect.h; show_hide_animation_.stop(); resize_animation_.stop(); updatePosition(); options_y_ = rect_.y + upper_height_ + lower_padding_; border_rect_ = {rect_.x - 1, rect_.y + 1, rect_.w + 2, rect_.h - 2}; } // --- Métodos de animación y posición --- void MenuRenderer::updateAnimations(float delta_time) { if (show_hide_animation_.active) { updateShowHideAnimation(delta_time); } if (resize_animation_.active) { updateResizeAnimation(delta_time); } } void MenuRenderer::updateShowHideAnimation(float delta_time) { show_hide_animation_.elapsed += delta_time; float duration = show_hide_animation_.duration; if (show_hide_animation_.elapsed >= duration) { if (show_hide_animation_.type == ShowHideAnimation::Type::SHOWING) { rect_.w = show_hide_animation_.target_width; rect_.h = show_hide_animation_.target_height; } else if (show_hide_animation_.type == ShowHideAnimation::Type::HIDING) { rect_.w = 0.0F; rect_.h = 0.0F; visible_ = false; } show_hide_animation_.stop(); updatePosition(); } else { float progress = easeOut(show_hide_animation_.elapsed / duration); if (show_hide_animation_.type == ShowHideAnimation::Type::SHOWING) { rect_.w = show_hide_animation_.target_width * progress; rect_.h = show_hide_animation_.target_height * progress; } else if (show_hide_animation_.type == ShowHideAnimation::Type::HIDING) { rect_.w = show_hide_animation_.target_width * (1.0F - progress); rect_.h = show_hide_animation_.target_height * (1.0F - progress); } updatePosition(); } options_y_ = rect_.y + upper_height_ + lower_padding_; } void MenuRenderer::updateResizeAnimation(float delta_time) { resize_animation_.elapsed += delta_time; float duration = resize_animation_.duration; if (resize_animation_.elapsed >= duration) { rect_.w = resize_animation_.target_width; rect_.h = resize_animation_.target_height; resize_animation_.stop(); updatePosition(); } else { float progress = easeOut(resize_animation_.elapsed / duration); rect_.w = resize_animation_.start_width + (resize_animation_.target_width - resize_animation_.start_width) * progress; rect_.h = resize_animation_.start_height + (resize_animation_.target_height - resize_animation_.start_height) * progress; updatePosition(); } options_y_ = rect_.y + upper_height_ + lower_padding_; } void MenuRenderer::updatePosition() { switch (position_mode_) { case PositionMode::CENTERED: rect_.x = anchor_x_ - rect_.w / 2.0F; rect_.y = anchor_y_ - rect_.h / 2.0F; break; case PositionMode::FIXED: rect_.x = anchor_x_; rect_.y = anchor_y_; break; } // Actualizar el rectángulo del borde junto con el principal border_rect_ = {rect_.x - 1, rect_.y + 1, rect_.w + 2, rect_.h - 2}; } // Resto de métodos (sin cambios significativos) void MenuRenderer::precalculateMenuWidths(const std::vector> &all_options, const ServiceMenu *menu_state) { for (int &w : group_menu_widths_) { w = ServiceMenu::MIN_WIDTH; } for (int group = 0; group < 5; ++group) { auto sg = static_cast(group); int max_option_width = 0; int max_value_width = 0; for (const auto &option : all_options) { if (option->getGroup() != sg) { continue; } max_option_width = std::max(max_option_width, element_text_->length(option->getCaption(), -2)); if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) { int max_available_value_width = static_cast(max_menu_width_) - max_option_width - (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2) - ServiceMenu::MIN_GAP_OPTION_VALUE; max_value_width = std::max(max_value_width, getTruncatedValueWidth(option->getValueAsString(), max_available_value_width)); } } size_t total_width = max_option_width + (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2); if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) { total_width += ServiceMenu::MIN_GAP_OPTION_VALUE + max_value_width; } group_menu_widths_[group] = std::min(std::max((int)ServiceMenu::MIN_WIDTH, (int)total_width), static_cast(max_menu_width_)); } } auto MenuRenderer::getMenuWidthForGroup(ServiceMenu::SettingsGroup group) const -> int { return group_menu_widths_[static_cast(group)]; } void MenuRenderer::updateColorCounter() { static Uint64 last_update_ = SDL_GetTicks(); if (SDL_GetTicks() - last_update_ >= 50) { color_counter_++; last_update_ = SDL_GetTicks(); } } auto MenuRenderer::getAnimatedSelectedColor() const -> Color { static auto color_cycle_ = generateMirroredCycle(param.service_menu.selected_color, ColorCycleStyle::HUE_WAVE); return color_cycle_.at(color_counter_ % color_cycle_.size()); } auto MenuRenderer::setRect(SDL_FRect rect) -> SDL_FRect { border_rect_ = {rect.x - 1, rect.y + 1, rect.w + 2, rect.h - 2}; return rect; } auto MenuRenderer::getTruncatedValueWidth(const std::string &value, int available_width) const -> int { int value_width = element_text_->length(value, -2); if (value_width <= available_width) { return value_width; } // Calculamos cuántos caracteres podemos mostrar más los puntos suspensivos // Estimamos el ancho de los puntos suspensivos como 3 caracteres promedio int ellipsis_width = element_text_->length("...", -2); int available_for_text = available_width - ellipsis_width; if (available_for_text <= 0) { return ellipsis_width; // Solo mostramos los puntos suspensivos } // Calculamos aproximadamente cuántos caracteres caben float char_width = static_cast(value_width) / value.length(); auto max_chars = static_cast(available_for_text / char_width); // Verificamos el ancho real del texto truncado std::string truncated = truncateWithEllipsis(value, max_chars); return element_text_->length(truncated, -2); } auto MenuRenderer::getTruncatedValue(const std::string &value, int available_width) const -> std::string { int value_width = element_text_->length(value, -2); if (value_width <= available_width) { return value; } // Calculamos cuántos caracteres podemos mostrar int ellipsis_width = element_text_->length("...", -2); int available_for_text = available_width - ellipsis_width; if (available_for_text <= 0) { return "..."; // Solo mostramos los puntos suspensivos } // Calculamos aproximadamente cuántos caracteres caben float char_width = static_cast(value_width) / value.length(); auto max_chars = static_cast(available_for_text / char_width); // Ajustamos iterativamente hasta que el texto quepa std::string truncated = truncateWithEllipsis(value, max_chars); while (element_text_->length(truncated, -2) > available_width && max_chars > 1) { max_chars--; truncated = truncateWithEllipsis(value, max_chars); } return truncated; } auto MenuRenderer::easeOut(float t) const -> float { return 1.0F - (1.0F - t) * (1.0F - t); } auto MenuRenderer::shouldShowContent() const -> bool { return !show_hide_animation_.active; }