379 lines
16 KiB
C++
379 lines
16 KiB
C++
#include "menu_renderer.h"
|
|
|
|
#include <algorithm>
|
|
#include <utility>
|
|
|
|
#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<Text> element_text, std::shared_ptr<Text> 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<size_t>(param.game.game_area.rect.w * 0.9F);
|
|
max_menu_height_ = static_cast<size_t>(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<ServiceMenu::SettingsGroup>(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<size_t>(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<std::unique_ptr<MenuOption>> &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<ServiceMenu::SettingsGroup>(group);
|
|
int max_option_width = 0, 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<int>(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<int>(max_menu_width_));
|
|
}
|
|
}
|
|
|
|
auto MenuRenderer::getMenuWidthForGroup(ServiceMenu::SettingsGroup group) const -> int { return group_menu_widths_[static_cast<int>(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<float>(value_width) / value.length();
|
|
auto max_chars = static_cast<size_t>(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<float>(value_width) / value.length();
|
|
auto max_chars = static_cast<size_t>(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; } |