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 "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_));
}
}