service menu vitaminat: cliping, swapping animation i versió
This commit is contained in:
@@ -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_));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user