service menu vitaminat: cliping, swapping animation i versió

This commit is contained in:
2026-04-14 19:41:17 +02:00
parent 25a36d5064
commit 10a3e2fedd
3 changed files with 118 additions and 7 deletions

View File

@@ -7,8 +7,10 @@
#include "core/rendering/text.hpp" #include "core/rendering/text.hpp"
#include "game/ui/menu_option.hpp" #include "game/ui/menu_option.hpp"
#include "utils/color.hpp" #include "utils/color.hpp"
#include "utils/defines.hpp" // Para Texts::VERSION
#include "utils/param.hpp" #include "utils/param.hpp"
#include "utils/utils.hpp" #include "utils/utils.hpp"
#include "version.h" // Para Version::GIT_HASH
// --- Implementación de las estructuras de animación --- // --- Implementación de las estructuras de animación ---
@@ -74,6 +76,17 @@ void MenuRenderer::render(const ServiceMenu* menu_state) {
// Solo renderizar contenido si la animación lo permite // Solo renderizar contenido si la animación lo permite
if (shouldShowContent()) { if (shouldShowContent()) {
// Recorta todas las operaciones de texto y líneas al rect actual del panel
// para que durante el resize animation nada se pinte fuera del borde.
auto* renderer = Screen::get()->getRenderer();
const SDL_Rect CLIP_RECT = {
.x = static_cast<int>(rect_.x),
.y = static_cast<int>(rect_.y),
.w = static_cast<int>(rect_.w),
.h = static_cast<int>(rect_.h),
};
SDL_SetRenderClipRect(renderer, &CLIP_RECT);
// Dibuja el título // Dibuja el título
float y = rect_.y + title_padding_; 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); title_text_->writeDX(Text::COLOR | Text::CENTER, rect_.x + (rect_.w / 2.0F), y, menu_state->getTitle(), -4, param.service_menu.title_color);
@@ -83,27 +96,52 @@ void MenuRenderer::render(const ServiceMenu* menu_state) {
SDL_SetRenderDrawColor(Screen::get()->getRenderer(), BORDER_COLOR.r, BORDER_COLOR.g, BORDER_COLOR.b, 255); 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); SDL_RenderLine(Screen::get()->getRenderer(), rect_.x + ServiceMenu::OPTIONS_HORIZONTAL_PADDING, y, rect_.x + rect_.w - ServiceMenu::OPTIONS_HORIZONTAL_PADDING, y);
// Dibuja el subtítulo del grupo (versión + hash en SYSTEM)
if (groupHasSubtitle(menu_state->getCurrentGroup())) {
const std::string SUBTITLE = "ver. " + std::string(Texts::VERSION) + " (" + std::string(Version::GIT_HASH) + ")";
element_text_->writeDX(
Text::CENTER | Text::COLOR,
rect_.x + (rect_.w / 2.0F),
y + options_padding_,
SUBTITLE,
-2,
param.service_menu.text_color);
}
// Dibuja las opciones // Dibuja las opciones
y = options_y_; y = options_y_;
const auto& option_pairs = menu_state->getOptionPairs(); const auto& option_pairs = menu_state->getOptionPairs();
const float ROW_HEIGHT = static_cast<float>(options_height_ + options_padding_);
for (size_t i = 0; i < option_pairs.size(); ++i) { for (size_t i = 0; i < option_pairs.size(); ++i) {
const bool IS_SELECTED = (i == menu_state->getSelectedIndex()); const bool IS_SELECTED = (i == menu_state->getSelectedIndex());
const Color& current_color = IS_SELECTED ? param.service_menu.selected_color : param.service_menu.text_color; const Color& current_color = IS_SELECTED ? param.service_menu.selected_color : param.service_menu.text_color;
// Offset Y del valor durante la animación de swap: va desde la fila
// origen (la otra implicada) hasta su fila natural con easeOut.
float value_y_offset = 0.0F;
if (swap_animation_.active && (i == swap_animation_.idx_a || i == swap_animation_.idx_b)) {
const size_t OTHER = (i == swap_animation_.idx_a) ? swap_animation_.idx_b : swap_animation_.idx_a;
const float PROGRESS = easeOut(swap_animation_.elapsed / swap_animation_.duration);
const float DELTA_ROWS = static_cast<float>(OTHER) - static_cast<float>(i);
value_y_offset = DELTA_ROWS * ROW_HEIGHT * (1.0F - PROGRESS);
}
if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) { 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; 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); 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); 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); 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); element_text_->writeColored(X, static_cast<int>(y + value_y_offset), truncated_value, current_color, -2);
} else { } else {
const int AVAILABLE_WIDTH = rect_.w - (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2); const int AVAILABLE_WIDTH = rect_.w - (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2);
std::string truncated_caption = getTruncatedValue(option_pairs.at(i).first, AVAILABLE_WIDTH); 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); element_text_->writeDX(Text::CENTER | Text::COLOR, rect_.x + (rect_.w / 2.0F), y, truncated_caption, -2, current_color);
} }
y += options_height_ + options_padding_; y += ROW_HEIGHT;
} }
SDL_SetRenderClipRect(renderer, nullptr);
} }
} }
@@ -201,7 +239,8 @@ auto MenuRenderer::calculateNewRect(const ServiceMenu* menu_state) -> SDL_FRect
width_ = std::min(static_cast<size_t>(getMenuWidthForGroup(menu_state->getCurrentGroup())), max_menu_width_); width_ = std::min(static_cast<size_t>(getMenuWidthForGroup(menu_state->getCurrentGroup())), max_menu_width_);
const auto& display_options = menu_state->getDisplayOptions(); 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); 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_); subtitle_offset_ = getGroupSubtitleHeight(menu_state);
height_ = std::min(upper_height_ + subtitle_offset_ + lower_height_, max_menu_height_);
SDL_FRect new_rect = {.x = 0, .y = 0, .w = static_cast<float>(width_), .h = static_cast<float>(height_)}; SDL_FRect new_rect = {.x = 0, .y = 0, .w = static_cast<float>(width_), .h = static_cast<float>(height_)};
@@ -209,6 +248,18 @@ auto MenuRenderer::calculateNewRect(const ServiceMenu* menu_state) -> SDL_FRect
return new_rect; return new_rect;
} }
// Grupos que muestran una línea de subtítulo bajo el título (antes de las opciones).
auto MenuRenderer::groupHasSubtitle(ServiceMenu::SettingsGroup group) -> bool {
return group == ServiceMenu::SettingsGroup::SYSTEM;
}
// Altura extra reservada para el subtítulo del grupo actual.
// Reservamos el alto de una línea de element_text_ más un pequeño padding.
auto MenuRenderer::getGroupSubtitleHeight(const ServiceMenu* menu_state) const -> size_t {
if (!groupHasSubtitle(menu_state->getCurrentGroup())) { return 0; }
return element_text_->getCharacterSize() + options_padding_;
}
void MenuRenderer::resize(const ServiceMenu* menu_state) { void MenuRenderer::resize(const ServiceMenu* menu_state) {
SDL_FRect new_rect = calculateNewRect(menu_state); SDL_FRect new_rect = calculateNewRect(menu_state);
@@ -219,7 +270,7 @@ void MenuRenderer::resize(const ServiceMenu* menu_state) {
// Si no hay cambio de tamaño, solo actualizamos la posición // Si no hay cambio de tamaño, solo actualizamos la posición
updatePosition(); updatePosition();
} }
options_y_ = rect_.y + upper_height_ + lower_padding_; options_y_ = rect_.y + upper_height_ + subtitle_offset_ + lower_padding_;
} }
void MenuRenderer::setSize(const ServiceMenu* menu_state) { void MenuRenderer::setSize(const ServiceMenu* menu_state) {
@@ -231,16 +282,35 @@ void MenuRenderer::setSize(const ServiceMenu* menu_state) {
resize_animation_.stop(); resize_animation_.stop();
updatePosition(); updatePosition();
options_y_ = rect_.y + upper_height_ + lower_padding_; options_y_ = rect_.y + upper_height_ + subtitle_offset_ + lower_padding_;
border_rect_ = {.x = rect_.x - 1, .y = rect_.y + 1, .w = rect_.w + 2, .h = rect_.h - 2}; border_rect_ = {.x = rect_.x - 1, .y = rect_.y + 1, .w = rect_.w + 2, .h = rect_.h - 2};
} }
// --- Métodos de animación y posición --- // --- Métodos de animación y posición ---
void MenuRenderer::startSwapAnimation(size_t idx_a, size_t idx_b) {
if (idx_a == idx_b) { return; }
swap_animation_.active = true;
swap_animation_.idx_a = idx_a;
swap_animation_.idx_b = idx_b;
swap_animation_.elapsed = 0.0F;
}
void MenuRenderer::updateSwapAnimation(float delta_time) {
swap_animation_.elapsed += delta_time;
if (swap_animation_.elapsed >= swap_animation_.duration) {
swap_animation_.active = false;
swap_animation_.elapsed = 0.0F;
}
}
void MenuRenderer::updateAnimations(float delta_time) { void MenuRenderer::updateAnimations(float delta_time) {
if (show_hide_animation_.active) { if (show_hide_animation_.active) {
updateShowHideAnimation(delta_time); updateShowHideAnimation(delta_time);
} }
if (swap_animation_.active) {
updateSwapAnimation(delta_time);
}
if (resize_animation_.active) { if (resize_animation_.active) {
updateResizeAnimation(delta_time); updateResizeAnimation(delta_time);
} }
@@ -272,7 +342,7 @@ void MenuRenderer::updateShowHideAnimation(float delta_time) {
} }
updatePosition(); updatePosition();
} }
options_y_ = rect_.y + upper_height_ + lower_padding_; options_y_ = rect_.y + upper_height_ + subtitle_offset_ + lower_padding_;
} }
void MenuRenderer::updateResizeAnimation(float delta_time) { void MenuRenderer::updateResizeAnimation(float delta_time) {
@@ -290,7 +360,7 @@ void MenuRenderer::updateResizeAnimation(float delta_time) {
rect_.h = resize_animation_.start_height + ((resize_animation_.target_height - resize_animation_.start_height) * progress); rect_.h = resize_animation_.start_height + ((resize_animation_.target_height - resize_animation_.start_height) * progress);
updatePosition(); updatePosition();
} }
options_y_ = rect_.y + upper_height_ + lower_padding_; options_y_ = rect_.y + upper_height_ + subtitle_offset_ + lower_padding_;
} }
void MenuRenderer::updatePosition() { void MenuRenderer::updatePosition() {
@@ -341,6 +411,12 @@ void MenuRenderer::precalculateMenuWidths(const std::vector<std::unique_ptr<Menu
if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) { if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) {
total_width += ServiceMenu::MIN_GAP_OPTION_VALUE + max_value_width; total_width += ServiceMenu::MIN_GAP_OPTION_VALUE + max_value_width;
} }
// Si el grupo tiene subtítulo, ensanchamos lo necesario para que quepa
if (groupHasSubtitle(sg)) {
const std::string SUBTITLE = "ver. " + std::string(Texts::VERSION) + " (" + std::string(Version::GIT_HASH) + ")";
const size_t SUBTITLE_WIDTH = static_cast<size_t>(element_text_->length(SUBTITLE, -2)) + (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2);
total_width = std::max(total_width, SUBTITLE_WIDTH);
}
group_menu_widths_[group] = std::min(std::max(static_cast<int>(ServiceMenu::MIN_WIDTH), static_cast<int>(total_width)), static_cast<int>(max_menu_width_)); group_menu_widths_[group] = std::min(std::max(static_cast<int>(ServiceMenu::MIN_WIDTH), static_cast<int>(total_width)), static_cast<int>(max_menu_width_));
} }
} }

View File

@@ -42,6 +42,9 @@ class MenuRenderer {
void onLayoutChanged(const ServiceMenu* menu_state); void onLayoutChanged(const ServiceMenu* menu_state);
void setLayout(const ServiceMenu* menu_state); void setLayout(const ServiceMenu* menu_state);
// Animación de intercambio de valores entre dos filas del menú actual
void startSwapAnimation(size_t idx_a, size_t idx_b);
// Getters // Getters
[[nodiscard]] auto getRect() const -> const SDL_FRect& { return rect_; } [[nodiscard]] auto getRect() const -> const SDL_FRect& { return rect_; }
@@ -63,6 +66,7 @@ class MenuRenderer {
size_t upper_height_ = 0; size_t upper_height_ = 0;
size_t lower_height_ = 0; size_t lower_height_ = 0;
size_t lower_padding_ = 0; size_t lower_padding_ = 0;
size_t subtitle_offset_ = 0; // Altura reservada para subtítulo del grupo actual
Uint32 color_counter_ = 0; Uint32 color_counter_ = 0;
bool visible_ = false; bool visible_ = false;
@@ -102,6 +106,16 @@ class MenuRenderer {
void stop(); void stop();
} show_hide_animation_; } show_hide_animation_;
// Animación de intercambio: interpola la Y del VALOR de dos filas
// durante su intercambio. El caption se queda estático.
struct SwapAnimation {
bool active = false;
size_t idx_a = 0;
size_t idx_b = 0;
float elapsed = 0.0F;
float duration = 0.4F;
} swap_animation_;
// --- Anchos precalculados --- // --- Anchos precalculados ---
std::array<int, ServiceMenu::SETTINGS_GROUP_SIZE> group_menu_widths_ = {}; std::array<int, ServiceMenu::SETTINGS_GROUP_SIZE> group_menu_widths_ = {};
@@ -112,9 +126,14 @@ class MenuRenderer {
void resize(const ServiceMenu* menu_state); void resize(const ServiceMenu* menu_state);
void setSize(const ServiceMenu* menu_state); void setSize(const ServiceMenu* menu_state);
// Altura extra ocupada por el subtítulo del grupo actual (versión + hash en SYSTEM)
[[nodiscard]] auto getGroupSubtitleHeight(const ServiceMenu* menu_state) const -> size_t;
[[nodiscard]] static auto groupHasSubtitle(ServiceMenu::SettingsGroup group) -> bool;
void updateAnimations(float delta_time); void updateAnimations(float delta_time);
void updateResizeAnimation(float delta_time); void updateResizeAnimation(float delta_time);
void updateShowHideAnimation(float delta_time); void updateShowHideAnimation(float delta_time);
void updateSwapAnimation(float delta_time);
void updatePosition(); void updatePosition();
void precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state); // NOLINT(readability-avoid-const-params-in-decls) void precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state); // NOLINT(readability-avoid-const-params-in-decls)

View File

@@ -347,6 +347,22 @@ void ServiceMenu::initializeOptions() {
Options::gamepad_manager.swapPlayers(); Options::gamepad_manager.swapPlayers();
adjustListValues(); // Sincroniza el valor de las opciones de lista (como MANDO1) con los datos reales adjustListValues(); // Sincroniza el valor de las opciones de lista (como MANDO1) con los datos reales
updateOptionPairs(); // Actualiza los pares de texto <opción, valor> que se van a dibujar updateOptionPairs(); // Actualiza los pares de texto <opción, valor> que se van a dibujar
// Feedback visual: anima el intercambio de los valores entre
// las filas de MANDO 1 y MANDO 2, imprescindible cuando los dos
// mandos tienen el mismo nombre (el texto no cambia al swap).
const std::string CAPTION1 = Lang::getText("[SERVICE_MENU] CONTROLLER1");
const std::string CAPTION2 = Lang::getText("[SERVICE_MENU] CONTROLLER2");
size_t idx1 = display_options_.size();
size_t idx2 = display_options_.size();
for (size_t i = 0; i < display_options_.size(); ++i) {
const auto& caption = display_options_[i]->getCaption();
if (caption == CAPTION1) { idx1 = i; }
if (caption == CAPTION2) { idx2 = i; }
}
if (idx1 < display_options_.size() && idx2 < display_options_.size()) {
renderer_->startSwapAnimation(idx1, idx2);
}
})); }));
// VIDEO // VIDEO