Files
coffee_crisis_arcade_edition/source/ui/menu_renderer.cpp
Sergio Valor 100b7265d5 Text: revisada la classe
window_message: correcions
2025-08-07 12:40:24 +02:00

292 lines
13 KiB
C++

#include "menu_renderer.h"
#include <algorithm> // Para max, min
#include <utility> // Para pair, move
#include "color.h" // Para Color, generateMirroredCycle, ColorCycleStyle
#include "menu_option.h" // Para MenuOption
#include "param.h" // Para Param, param, ParamServiceMenu, ParamGame
#include "screen.h" // Para Screen
#include "text.h" // Para Text, Text::CENTER, Text::COLOR
#include "utils.h" // Para Zone, truncateWithEllipsis
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();
}
void MenuRenderer::render(const ServiceMenu *menu_state) {
// 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_);
// Dibuja el título
float y = rect_.y + title_padding_;
title_text_->writeDX(Text::COLOR | Text::CENTER, param.game.game_area.center_x, 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();
const auto &display_options = menu_state->getDisplayOptions();
for (size_t i = 0; i < option_pairs.size(); ++i) {
const bool IS_SELECTED = (i == menu_state->getSelectedIndex());
const Color &current_color = IS_SELECTED ? param.service_menu.selected_color : param.service_menu.text_color;
if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) {
// Para opciones alineadas a la izquierda, truncamos el valor si es necesario
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);
// Si la opción tiene Behavior::BOTH, añadir indicador visual
if (i < display_options.size() && display_options[i]->getBehavior() == MenuOption::Behavior::BOTH) {
truncated_value = "-" + truncated_value + "-";
}
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 {
// Para opciones centradas, también truncamos si es necesario
const int available_width = rect_.w - (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2);
std::string truncated_caption = getTruncatedValue(option_pairs.at(i).first, available_width);
// Si la opción tiene Behavior::BOTH, añadir indicador visual
if (i < display_options.size() && display_options[i]->getBehavior() == MenuOption::Behavior::BOTH) {
truncated_caption = "-" + truncated_caption + "-";
}
element_text_->writeDX(Text::CENTER | Text::COLOR, rect_.x + rect_.w / 2, y, truncated_caption, -2, current_color);
}
y += options_height_ + options_padding_;
}
}
void MenuRenderer::update(const ServiceMenu *menu_state) {
if (resizing_) {
updateResizeAnimation();
}
updateColorCounter();
param.service_menu.selected_color = getAnimatedSelectedColor();
}
void MenuRenderer::onLayoutChanged(const ServiceMenu *menu_state) {
// Cuando la lógica del menú notifica un cambio, el renderer recalcula su layout
precalculateMenuWidths(menu_state->getAllOptions(), menu_state);
setAnchors(menu_state);
resize(menu_state);
}
void MenuRenderer::setLayout(const ServiceMenu *menu_state) {
// Cuando la lógica del menú notifica un cambio, el renderer recalcula su layout
precalculateMenuWidths(menu_state->getAllOptions(), menu_state);
setAnchors(menu_state);
setSize(menu_state);
}
void MenuRenderer::initializeMaxSizes() {
// Establecemos los límites máximos basados en el tamaño de la pantalla
// Dejamos un margen del 10% en cada lado para que el menú no ocupe toda la pantalla
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) {
size_t count = menu_state->countOptionsInGroup(static_cast<ServiceMenu::SettingsGroup>(i));
if (count > max_entries) {
max_entries = count;
}
}
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_);
return {(param.game.width - width_) / 2.0F, (param.game.height - height_) / 2.0F, (float)width_, (float)height_};
}
void MenuRenderer::resize(const ServiceMenu *menu_state) {
SDL_FRect new_rect = calculateNewRect(menu_state);
if (rect_.x != new_rect.x || rect_.y != new_rect.y || rect_.w != new_rect.w || rect_.h != new_rect.h) {
rect_anim_from_ = rect_;
rect_anim_to_ = new_rect;
resize_anim_step_ = 0;
resizing_ = true;
} else {
rect_ = setRect(new_rect);
resizing_ = false;
}
options_y_ = new_rect.y + upper_height_ + lower_padding_;
}
void MenuRenderer::setSize(const ServiceMenu *menu_state) {
rect_ = setRect(calculateNewRect(menu_state));
resizing_ = false;
options_y_ = rect_.y + upper_height_ + lower_padding_;
}
void MenuRenderer::updateResizeAnimation() {
if (!resizing_) {
return;
}
++resize_anim_step_;
float t = static_cast<float>(resize_anim_step_) / resize_anim_steps_;
if (t >= 1.0F) {
rect_ = setRect(rect_anim_to_);
resizing_ = false;
return;
}
float ease = 1 - (1 - t) * (1 - t);
SDL_FRect rect =
{rect_anim_from_.x + (rect_anim_to_.x - rect_anim_from_.x) * ease,
rect_anim_from_.y + (rect_anim_to_.y - rect_anim_from_.y) * ease,
rect_anim_from_.w + (rect_anim_to_.w - rect_anim_from_.w) * ease,
rect_anim_from_.h + (rect_anim_to_.h - rect_anim_from_.h) * ease};
rect_ = setRect(rect);
}
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;
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) {
// Para calcular el ancho máximo, necesitamos considerar la truncación
int max_available_value_width = static_cast<int>(max_menu_width_) - max_option_width -
(ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2) -
ServiceMenu::MIN_GAP_OPTION_VALUE;
int actual_value_width = getTruncatedValueWidth(option->getValueAsString(), max_available_value_width);
max_value_width = std::max(max_value_width, actual_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();
Uint64 current_ticks = SDL_GetTicks();
if (current_ticks - last_update_ >= 50) {
color_counter_++;
last_update_ = current_ticks;
}
}
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;
}