service menu vitaminat: cliping, swapping animation i versió
This commit is contained in:
@@ -7,8 +7,10 @@
|
||||
#include "core/rendering/text.hpp"
|
||||
#include "game/ui/menu_option.hpp"
|
||||
#include "utils/color.hpp"
|
||||
#include "utils/defines.hpp" // Para Texts::VERSION
|
||||
#include "utils/param.hpp"
|
||||
#include "utils/utils.hpp"
|
||||
#include "version.h" // Para Version::GIT_HASH
|
||||
|
||||
// --- 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
|
||||
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
|
||||
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);
|
||||
@@ -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_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
|
||||
y = options_y_;
|
||||
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) {
|
||||
const bool IS_SELECTED = (i == menu_state->getSelectedIndex());
|
||||
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) {
|
||||
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);
|
||||
element_text_->writeColored(X, static_cast<int>(y + value_y_offset), 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_;
|
||||
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_);
|
||||
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_);
|
||||
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_)};
|
||||
|
||||
@@ -209,6 +248,18 @@ auto MenuRenderer::calculateNewRect(const ServiceMenu* menu_state) -> SDL_FRect
|
||||
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) {
|
||||
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
|
||||
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) {
|
||||
@@ -231,16 +282,35 @@ void MenuRenderer::setSize(const ServiceMenu* menu_state) {
|
||||
resize_animation_.stop();
|
||||
|
||||
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};
|
||||
}
|
||||
|
||||
// --- 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) {
|
||||
if (show_hide_animation_.active) {
|
||||
updateShowHideAnimation(delta_time);
|
||||
}
|
||||
if (swap_animation_.active) {
|
||||
updateSwapAnimation(delta_time);
|
||||
}
|
||||
if (resize_animation_.active) {
|
||||
updateResizeAnimation(delta_time);
|
||||
}
|
||||
@@ -272,7 +342,7 @@ void MenuRenderer::updateShowHideAnimation(float delta_time) {
|
||||
}
|
||||
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) {
|
||||
@@ -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);
|
||||
updatePosition();
|
||||
}
|
||||
options_y_ = rect_.y + upper_height_ + lower_padding_;
|
||||
options_y_ = rect_.y + upper_height_ + subtitle_offset_ + lower_padding_;
|
||||
}
|
||||
|
||||
void MenuRenderer::updatePosition() {
|
||||
@@ -341,6 +411,12 @@ void MenuRenderer::precalculateMenuWidths(const std::vector<std::unique_ptr<Menu
|
||||
if (menu_state->getCurrentGroupAlignment() == ServiceMenu::GroupAlignment::LEFT) {
|
||||
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_));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,6 +42,9 @@ class MenuRenderer {
|
||||
void onLayoutChanged(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
|
||||
[[nodiscard]] auto getRect() const -> const SDL_FRect& { return rect_; }
|
||||
|
||||
@@ -63,6 +66,7 @@ class MenuRenderer {
|
||||
size_t upper_height_ = 0;
|
||||
size_t lower_height_ = 0;
|
||||
size_t lower_padding_ = 0;
|
||||
size_t subtitle_offset_ = 0; // Altura reservada para subtítulo del grupo actual
|
||||
Uint32 color_counter_ = 0;
|
||||
bool visible_ = false;
|
||||
|
||||
@@ -102,6 +106,16 @@ class MenuRenderer {
|
||||
void stop();
|
||||
} 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 ---
|
||||
std::array<int, ServiceMenu::SETTINGS_GROUP_SIZE> group_menu_widths_ = {};
|
||||
|
||||
@@ -112,9 +126,14 @@ class MenuRenderer {
|
||||
void resize(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 updateResizeAnimation(float delta_time);
|
||||
void updateShowHideAnimation(float delta_time);
|
||||
void updateSwapAnimation(float delta_time);
|
||||
void updatePosition();
|
||||
|
||||
void precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state); // NOLINT(readability-avoid-const-params-in-decls)
|
||||
|
||||
@@ -347,6 +347,22 @@ void ServiceMenu::initializeOptions() {
|
||||
Options::gamepad_manager.swapPlayers();
|
||||
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
|
||||
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user