reestructuració

This commit is contained in:
2026-04-14 13:26:22 +02:00
parent 4ac34b8583
commit 4429cd92c1
143 changed files with 0 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
#include "menu_option.hpp"
#include <algorithm> // Para max
#include <iterator> // Para distance
#include <ranges> // Para __find_fn, find
#include "text.hpp" // Para Text
auto ActionListOption::getValueAsString() const -> std::string {
if (value_getter_) {
return value_getter_();
}
if (current_index_ < options_.size()) {
return options_[current_index_];
}
return options_.empty() ? "" : options_[0];
}
auto ActionListOption::getMaxValueWidth(Text* text) const -> int {
int max_width = 0;
for (const auto& option : options_) {
int width = text->length(option, -2);
max_width = std::max(width, max_width);
}
return max_width;
}
void ActionListOption::adjustValue(bool up) {
if (options_.empty()) {
return;
}
if (up) {
current_index_ = (current_index_ + 1) % options_.size();
} else {
current_index_ = (current_index_ == 0) ? options_.size() - 1 : current_index_ - 1;
}
// Aplicar el cambio usando el setter
if (value_setter_ && current_index_ < options_.size()) {
value_setter_(options_[current_index_]);
}
}
void ActionListOption::executeAction() {
if (action_executor_) {
action_executor_();
}
}
void ActionListOption::sync() {
updateCurrentIndex();
}
void ActionListOption::updateCurrentIndex() {
current_index_ = findCurrentIndex();
}
auto ActionListOption::findCurrentIndex() const -> size_t {
if (!value_getter_ || options_.empty()) {
return 0;
}
const std::string CURRENT_VALUE = value_getter_();
auto it = std::ranges::find(options_, CURRENT_VALUE);
if (it != options_.end()) {
return static_cast<size_t>(std::distance(options_.begin(), it));
}
return 0; // Valor por defecto si no se encuentra
}

View File

@@ -0,0 +1,241 @@
#pragma once
#include <algorithm> // Para max, clamp
#include <cstddef> // Para size_t
#include <functional> // Para function
#include <string> // Para allocator, string, basic_string, to_string, operator==, char_traits
#include <utility> // Para move
#include <vector> // Para vector
#include "lang.hpp" // Para getText
#include "text.hpp" // Para Text
#include "ui/service_menu.hpp" // Para ServiceMenu
// --- Clase MenuOption: interfaz base para todas las opciones del menú ---
class MenuOption {
public:
// --- Enums ---
enum class Behavior {
ADJUST, // Solo puede ajustar valor (como IntOption, BoolOption, ListOption)
SELECT, // Solo puede ejecutar acción (como ActionOption, FolderOption)
BOTH // Puede tanto ajustar como ejecutar acción (como ActionListOption)
};
// --- Constructor y destructor ---
MenuOption(std::string caption, ServiceMenu::SettingsGroup group, bool hidden = false)
: caption_(std::move(caption)),
group_(group),
hidden_(hidden) {}
virtual ~MenuOption() = default;
// --- Getters ---
[[nodiscard]] auto getCaption() const -> const std::string& { return caption_; }
[[nodiscard]] auto getGroup() const -> ServiceMenu::SettingsGroup { return group_; }
[[nodiscard]] auto isHidden() const -> bool { return hidden_; }
void setHidden(bool hidden) { hidden_ = hidden; }
[[nodiscard]] virtual auto getBehavior() const -> Behavior = 0;
[[nodiscard]] virtual auto getValueAsString() const -> std::string { return ""; }
virtual void adjustValue(bool adjust_up) {}
[[nodiscard]] virtual auto getTargetGroup() const -> ServiceMenu::SettingsGroup { return ServiceMenu::SettingsGroup::MAIN; }
virtual void executeAction() {}
virtual auto getMaxValueWidth(Text* text_renderer) const -> int { return 0; } // Método virtual para que cada opción calcule el ancho de su valor más largo
protected:
// --- Variables ---
std::string caption_;
ServiceMenu::SettingsGroup group_;
bool hidden_;
};
// --- Clases Derivadas ---
class BoolOption : public MenuOption {
public:
BoolOption(const std::string& cap, ServiceMenu::SettingsGroup grp, bool* var)
: MenuOption(cap, grp),
linked_variable_(var) {}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::ADJUST; }
[[nodiscard]] auto getValueAsString() const -> std::string override {
return *linked_variable_ ? Lang::getText("[SERVICE_MENU] ON") : Lang::getText("[SERVICE_MENU] OFF");
}
void adjustValue(bool /*adjust_up*/) override {
*linked_variable_ = !*linked_variable_;
}
auto getMaxValueWidth(Text* text_renderer) const -> int override {
return std::max(
text_renderer->length(Lang::getText("[SERVICE_MENU] ON"), -2),
text_renderer->length(Lang::getText("[SERVICE_MENU] OFF"), -2));
}
private:
bool* linked_variable_;
};
class IntOption : public MenuOption {
public:
IntOption(const std::string& cap, ServiceMenu::SettingsGroup grp, int* var, int min, int max, int step)
: MenuOption(cap, grp),
linked_variable_(var),
min_value_(min),
max_value_(max),
step_value_(step) {}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::ADJUST; }
[[nodiscard]] auto getValueAsString() const -> std::string override { return std::to_string(*linked_variable_); }
void adjustValue(bool adjust_up) override {
int new_value = *linked_variable_ + (adjust_up ? step_value_ : -step_value_);
*linked_variable_ = std::clamp(new_value, min_value_, max_value_);
}
auto getMaxValueWidth(Text* text_renderer) const -> int override {
int max_width = 0;
// Iterar por todos los valores posibles en el rango
for (int value = min_value_; value <= max_value_; value += step_value_) {
int width = text_renderer->length(std::to_string(value), -2);
max_width = std::max(max_width, width);
}
return max_width;
}
private:
int* linked_variable_;
int min_value_, max_value_, step_value_;
};
class ListOption : public MenuOption {
public:
ListOption(const std::string& cap, ServiceMenu::SettingsGroup grp, std::vector<std::string> values, std::function<std::string()> current_value_getter, std::function<void(const std::string&)> new_value_setter)
: MenuOption(cap, grp),
value_list_(std::move(values)),
getter_(std::move(current_value_getter)),
setter_(std::move(new_value_setter)) {
sync();
}
void sync() {
std::string current_value = getter_();
for (size_t i = 0; i < value_list_.size(); ++i) {
if (value_list_[i] == current_value) {
list_index_ = i;
return;
}
}
}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::ADJUST; }
[[nodiscard]] auto getValueAsString() const -> std::string override {
return value_list_.empty() ? "" : value_list_[list_index_];
}
void adjustValue(bool adjust_up) override {
if (value_list_.empty()) {
return;
}
size_t size = value_list_.size();
list_index_ = adjust_up ? (list_index_ + 1) % size
: (list_index_ + size - 1) % size;
setter_(value_list_[list_index_]);
}
auto getMaxValueWidth(Text* text_renderer) const -> int override {
int max_w = 0;
for (const auto& val : value_list_) {
max_w = std::max(max_w, text_renderer->length(val, -2));
}
return max_w;
}
private:
std::vector<std::string> value_list_;
std::function<std::string()> getter_;
std::function<void(const std::string&)> setter_;
size_t list_index_{0};
};
class FolderOption : public MenuOption {
public:
FolderOption(const std::string& cap, ServiceMenu::SettingsGroup grp, ServiceMenu::SettingsGroup target)
: MenuOption(cap, grp),
target_group_(target) {}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::SELECT; }
[[nodiscard]] auto getTargetGroup() const -> ServiceMenu::SettingsGroup override { return target_group_; }
private:
ServiceMenu::SettingsGroup target_group_;
};
class ActionOption : public MenuOption {
public:
ActionOption(const std::string& cap, ServiceMenu::SettingsGroup grp, std::function<void()> action, bool hidden = false)
: MenuOption(cap, grp, hidden),
action_(std::move(action)) {}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::SELECT; }
void executeAction() override {
if (action_) {
action_();
}
}
private:
std::function<void()> action_;
};
// Opción de lista con acción
class ActionListOption : public MenuOption {
public:
using ValueGetter = std::function<std::string()>;
using ValueSetter = std::function<void(const std::string&)>;
using ActionExecutor = std::function<void()>;
ActionListOption(const std::string& caption, ServiceMenu::SettingsGroup group, std::vector<std::string> options, ValueGetter getter, ValueSetter setter, ActionExecutor action_executor, bool hidden = false)
: MenuOption(caption, group, hidden),
options_(std::move(options)),
value_getter_(std::move(getter)),
value_setter_(std::move(setter)),
action_executor_(std::move(action_executor)) {
updateCurrentIndex();
}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::BOTH; }
[[nodiscard]] auto getValueAsString() const -> std::string override;
[[nodiscard]] auto getMaxValueWidth(Text* text) const -> int override;
void adjustValue(bool up) override;
void executeAction() override;
void sync(); // Sincroniza con el valor actual
private:
std::vector<std::string> options_;
ValueGetter value_getter_;
ValueSetter value_setter_;
ActionExecutor action_executor_;
size_t current_index_{0};
void updateCurrentIndex();
[[nodiscard]] auto findCurrentIndex() const -> size_t;
};
// Opción genérica con callbacks: getter para mostrar, adjuster(bool up) para cambiar valor
class CallbackOption : public MenuOption {
public:
CallbackOption(const std::string& cap, ServiceMenu::SettingsGroup grp, std::function<std::string()> getter, std::function<void(bool)> adjuster, std::function<int(Text*)> max_width_fn = nullptr)
: MenuOption(cap, grp),
getter_(std::move(getter)),
adjuster_(std::move(adjuster)),
max_width_fn_(std::move(max_width_fn)) {}
[[nodiscard]] auto getBehavior() const -> Behavior override { return Behavior::ADJUST; }
[[nodiscard]] auto getValueAsString() const -> std::string override { return getter_(); }
void adjustValue(bool adjust_up) override { adjuster_(adjust_up); }
auto getMaxValueWidth(Text* text_renderer) const -> int override {
return max_width_fn_ ? max_width_fn_(text_renderer) : 0;
}
private:
std::function<std::string()> getter_;
std::function<void(bool)> adjuster_;
std::function<int(Text*)> max_width_fn_;
};

View File

@@ -0,0 +1,417 @@
#include "menu_renderer.hpp"
#include <algorithm>
#include <utility>
#include "color.hpp"
#include "menu_option.hpp"
#include "param.hpp"
#include "screen.hpp"
#include "text.hpp"
#include "utils.hpp"
// --- Implementación de las estructuras de animación ---
void MenuRenderer::ResizeAnimation::start(float from_w, float from_h, float to_w, float to_h) {
start_width = from_w;
start_height = from_h;
target_width = to_w;
target_height = to_h;
elapsed = 0.0F;
active = true;
}
void MenuRenderer::ResizeAnimation::stop() {
active = false;
elapsed = 0.0F;
}
void MenuRenderer::ShowHideAnimation::startShow(float to_w, float to_h) {
type = Type::SHOWING;
target_width = to_w;
target_height = to_h;
elapsed = 0.0F;
active = true;
}
void MenuRenderer::ShowHideAnimation::startHide() {
type = Type::HIDING;
elapsed = 0.0F;
active = true;
}
void MenuRenderer::ShowHideAnimation::stop() {
type = Type::NONE;
active = false;
elapsed = 0.0F;
}
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();
setPosition(param.game.game_area.center_x, param.game.game_area.center_y, PositionMode::CENTERED);
}
void MenuRenderer::render(const ServiceMenu* menu_state) {
if (!visible_) {
return;
}
// Dibuja la sombra
if (param.service_menu.drop_shadow) {
SDL_FRect shadow_rect = {.x = rect_.x + 5, .y = rect_.y + 5, .w = rect_.w, .h = 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_);
// Solo renderizar contenido si la animación lo permite
if (shouldShowContent()) {
// 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);
// 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();
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) {
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);
} 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_;
}
}
}
void MenuRenderer::update(const ServiceMenu* menu_state, float delta_time) {
updateAnimations(delta_time);
if (visible_) {
updateColorCounter();
param.service_menu.selected_color = getAnimatedSelectedColor();
}
}
// --- Nuevos métodos de control ---
void MenuRenderer::show(const ServiceMenu* menu_state) {
if (visible_) {
return;
}
visible_ = true;
// Calcula el tamaño final y lo usa para la animación
SDL_FRect target_rect = calculateNewRect(menu_state);
// Detener cualquier animación anterior
resize_animation_.stop();
// Iniciar animación de mostrar
show_hide_animation_.startShow(target_rect.w, target_rect.h);
// El tamaño inicial es cero para la animación
rect_.w = 0.0F;
rect_.h = 0.0F;
updatePosition();
}
void MenuRenderer::hide() {
if (!visible_ || show_hide_animation_.type == ShowHideAnimation::Type::HIDING) {
return;
}
// Detener animación de resize si la hubiera
resize_animation_.stop();
// Guardar tamaño actual para la animación de ocultar
show_hide_animation_.target_width = rect_.w;
show_hide_animation_.target_height = rect_.h;
show_hide_animation_.startHide();
}
void MenuRenderer::setPosition(float x, float y, PositionMode mode) {
anchor_x_ = x;
anchor_y_ = y;
position_mode_ = mode;
updatePosition();
}
// --- Métodos de layout ---
void MenuRenderer::onLayoutChanged(const ServiceMenu* menu_state) {
precalculateMenuWidths(menu_state->getAllOptions(), menu_state);
setAnchors(menu_state);
resize(menu_state);
}
void MenuRenderer::setLayout(const ServiceMenu* menu_state) {
precalculateMenuWidths(menu_state->getAllOptions(), menu_state);
setAnchors(menu_state);
setSize(menu_state);
}
void MenuRenderer::initializeMaxSizes() {
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) {
max_entries = std::max(max_entries, menu_state->countOptionsInGroup(static_cast<ServiceMenu::SettingsGroup>(i)));
}
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_);
SDL_FRect new_rect = {.x = 0, .y = 0, .w = static_cast<float>(width_), .h = static_cast<float>(height_)};
// La posición x, y se establecerá en `updatePosition`
return new_rect;
}
void MenuRenderer::resize(const ServiceMenu* menu_state) {
SDL_FRect new_rect = calculateNewRect(menu_state);
if (rect_.w != new_rect.w || rect_.h != new_rect.h) {
// En lugar de la animación antigua, usamos la nueva
resize_animation_.start(rect_.w, rect_.h, new_rect.w, new_rect.h);
} else {
// Si no hay cambio de tamaño, solo actualizamos la posición
updatePosition();
}
options_y_ = rect_.y + upper_height_ + lower_padding_;
}
void MenuRenderer::setSize(const ServiceMenu* menu_state) {
SDL_FRect new_rect = calculateNewRect(menu_state);
rect_.w = new_rect.w;
rect_.h = new_rect.h;
show_hide_animation_.stop();
resize_animation_.stop();
updatePosition();
options_y_ = rect_.y + upper_height_ + 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::updateAnimations(float delta_time) {
if (show_hide_animation_.active) {
updateShowHideAnimation(delta_time);
}
if (resize_animation_.active) {
updateResizeAnimation(delta_time);
}
}
void MenuRenderer::updateShowHideAnimation(float delta_time) {
show_hide_animation_.elapsed += delta_time;
float duration = show_hide_animation_.duration;
if (show_hide_animation_.elapsed >= duration) {
if (show_hide_animation_.type == ShowHideAnimation::Type::SHOWING) {
rect_.w = show_hide_animation_.target_width;
rect_.h = show_hide_animation_.target_height;
} else if (show_hide_animation_.type == ShowHideAnimation::Type::HIDING) {
rect_.w = 0.0F;
rect_.h = 0.0F;
visible_ = false;
}
show_hide_animation_.stop();
updatePosition();
} else {
float progress = easeOut(show_hide_animation_.elapsed / duration);
if (show_hide_animation_.type == ShowHideAnimation::Type::SHOWING) {
rect_.w = show_hide_animation_.target_width * progress;
rect_.h = show_hide_animation_.target_height * progress;
} else if (show_hide_animation_.type == ShowHideAnimation::Type::HIDING) {
rect_.w = show_hide_animation_.target_width * (1.0F - progress);
rect_.h = show_hide_animation_.target_height * (1.0F - progress);
}
updatePosition();
}
options_y_ = rect_.y + upper_height_ + lower_padding_;
}
void MenuRenderer::updateResizeAnimation(float delta_time) {
resize_animation_.elapsed += delta_time;
float duration = resize_animation_.duration;
if (resize_animation_.elapsed >= duration) {
rect_.w = resize_animation_.target_width;
rect_.h = resize_animation_.target_height;
resize_animation_.stop();
updatePosition();
} else {
float progress = easeOut(resize_animation_.elapsed / duration);
rect_.w = resize_animation_.start_width + ((resize_animation_.target_width - resize_animation_.start_width) * progress);
rect_.h = resize_animation_.start_height + ((resize_animation_.target_height - resize_animation_.start_height) * progress);
updatePosition();
}
options_y_ = rect_.y + upper_height_ + lower_padding_;
}
void MenuRenderer::updatePosition() {
switch (position_mode_) {
case PositionMode::CENTERED:
rect_.x = anchor_x_ - (rect_.w / 2.0F);
rect_.y = anchor_y_ - (rect_.h / 2.0F);
break;
case PositionMode::FIXED:
rect_.x = anchor_x_;
rect_.y = anchor_y_;
break;
}
// Actualizar el rectángulo del borde junto con el principal
border_rect_ = {.x = rect_.x - 1, .y = rect_.y + 1, .w = rect_.w + 2, .h = rect_.h - 2};
}
// Resto de métodos (sin cambios significativos)
void MenuRenderer::precalculateMenuWidths(const std::vector<std::unique_ptr<MenuOption>>& all_options, const ServiceMenu* menu_state) { // NOLINT(readability-named-parameter)
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) {
// Usar getMaxValueWidth() para considerar TODOS los valores posibles de la opción
int option_max_value_width = option->getMaxValueWidth(element_text_.get());
int max_available_value_width = static_cast<int>(max_menu_width_) - max_option_width - (ServiceMenu::OPTIONS_HORIZONTAL_PADDING * 2) - ServiceMenu::MIN_GAP_OPTION_VALUE;
if (option_max_value_width <= max_available_value_width) {
// Si el valor más largo cabe, usar su ancho real
max_value_width = std::max(max_value_width, option_max_value_width);
} else {
// Si no cabe, usar el ancho disponible (será truncado)
max_value_width = std::max(max_value_width, max_available_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(static_cast<int>(ServiceMenu::MIN_WIDTH), static_cast<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();
if (SDL_GetTicks() - last_update_ >= 50) {
color_counter_++;
last_update_ = SDL_GetTicks();
}
}
auto MenuRenderer::getAnimatedSelectedColor() const -> Color {
static auto color_cycle_ = Colors::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_ = {.x = rect.x - 1, .y = rect.y + 1, .w = rect.w + 2, .h = 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;
}
auto MenuRenderer::easeOut(float t) -> float { return 1.0F - ((1.0F - t) * (1.0F - t)); }
auto MenuRenderer::shouldShowContent() const -> bool { return !show_hide_animation_.active; }

View File

@@ -0,0 +1,129 @@
#pragma once
#include <SDL3/SDL.h>
#include <array>
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include "color.hpp"
#include "ui/service_menu.hpp"
class MenuOption;
class Text;
class MenuRenderer {
public:
// --- Nuevo: Enum para el modo de posicionamiento ---
enum class PositionMode {
CENTERED, // La ventana se centra en el punto especificado
FIXED // La esquina superior izquierda coincide con el punto
};
MenuRenderer(const ServiceMenu* menu_state, std::shared_ptr<Text> element_text, std::shared_ptr<Text> title_text);
// --- Métodos principales de la vista ---
void render(const ServiceMenu* menu_state);
void update(const ServiceMenu* menu_state, float delta_time);
// --- Nuevos: Métodos de control de visibilidad y animación ---
void show(const ServiceMenu* menu_state);
void hide();
[[nodiscard]] auto isVisible() const -> bool { return visible_; }
[[nodiscard]] auto isFullyVisible() const -> bool { return visible_ && !show_hide_animation_.active && !resize_animation_.active; }
[[nodiscard]] auto isAnimating() const -> bool { return resize_animation_.active || show_hide_animation_.active; }
// --- Nuevos: Métodos de configuración de posición ---
void setPosition(float x, float y, PositionMode mode);
// Método para notificar al renderer que el layout puede haber cambiado
void onLayoutChanged(const ServiceMenu* menu_state);
void setLayout(const ServiceMenu* menu_state);
// Getters
[[nodiscard]] auto getRect() const -> const SDL_FRect& { return rect_; }
private:
// --- Referencias a los renderizadores de texto ---
std::shared_ptr<Text> element_text_;
std::shared_ptr<Text> title_text_;
// --- Variables de estado de la vista (layout y animación) ---
SDL_FRect rect_{};
SDL_FRect border_rect_{};
size_t width_ = 0;
size_t height_ = 0;
size_t options_height_ = 0;
size_t options_padding_ = 0;
size_t options_y_ = 0;
size_t title_height_ = 0;
size_t title_padding_ = 0;
size_t upper_height_ = 0;
size_t lower_height_ = 0;
size_t lower_padding_ = 0;
Uint32 color_counter_ = 0;
bool visible_ = false;
// --- Posicionamiento ---
PositionMode position_mode_ = PositionMode::CENTERED;
float anchor_x_ = 0.0F;
float anchor_y_ = 0.0F;
// --- Límites de tamaño máximo ---
size_t max_menu_width_ = 0;
size_t max_menu_height_ = 0;
// --- Estructuras de Animación ---
struct ResizeAnimation {
bool active = false;
float start_width, start_height;
float target_width, target_height;
float elapsed = 0.0F;
float duration = 0.2F;
void start(float from_w, float from_h, float to_w, float to_h);
void stop();
} resize_animation_;
struct ShowHideAnimation {
enum class Type { NONE,
SHOWING,
HIDING };
Type type = Type::NONE;
bool active = false;
float target_width, target_height;
float elapsed = 0.0F;
float duration = 0.25F;
void startShow(float to_w, float to_h);
void startHide();
void stop();
} show_hide_animation_;
// --- Anchos precalculados ---
std::array<int, ServiceMenu::SETTINGS_GROUP_SIZE> group_menu_widths_ = {};
// --- Métodos privados de la vista ---
void initializeMaxSizes();
void setAnchors(const ServiceMenu* menu_state);
auto calculateNewRect(const ServiceMenu* menu_state) -> SDL_FRect;
void resize(const ServiceMenu* menu_state);
void setSize(const ServiceMenu* menu_state);
void updateAnimations(float delta_time);
void updateResizeAnimation(float delta_time);
void updateShowHideAnimation(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)
[[nodiscard]] auto getMenuWidthForGroup(ServiceMenu::SettingsGroup group) const -> int;
[[nodiscard]] auto getAnimatedSelectedColor() const -> Color;
void updateColorCounter();
auto setRect(SDL_FRect rect) -> SDL_FRect;
[[nodiscard]] auto getTruncatedValueWidth(const std::string& value, int available_width) const -> int;
[[nodiscard]] auto getTruncatedValue(const std::string& value, int available_width) const -> std::string;
[[nodiscard]] static auto easeOut(float t) -> float;
[[nodiscard]] auto shouldShowContent() const -> bool;
};

310
source/game/ui/notifier.cpp Normal file
View File

@@ -0,0 +1,310 @@
#include "notifier.hpp"
#include <SDL3/SDL.h> // Para SDL_RenderFillRect, SDL_FRect, SDL_RenderClear
#include <algorithm> // Para remove_if, min
#include <string> // Para basic_string, string
#include <utility>
#include <vector> // Para vector
#include "audio.hpp" // Para Audio
#include "param.hpp" // Para Param, param, ParamNotification, ParamGame
#include "screen.hpp" // Para Screen
#include "sprite.hpp" // Para Sprite
#include "text.hpp" // Para Text
#include "texture.hpp" // Para Texture
// Singleton
Notifier* Notifier::instance = nullptr;
// Inicializa la instancia única del singleton
void Notifier::init(const std::string& icon_file, std::shared_ptr<Text> text) { Notifier::instance = new Notifier(icon_file, std::move(text)); }
// Libera la instancia
void Notifier::destroy() { delete Notifier::instance; }
// Obtiene la instancia
auto Notifier::get() -> Notifier* { return Notifier::instance; }
// Constructor
Notifier::Notifier(const std::string& icon_file, std::shared_ptr<Text> text)
: renderer_(Screen::get()->getRenderer()),
icon_texture_(!icon_file.empty() ? std::make_unique<Texture>(renderer_, icon_file) : nullptr),
text_(std::move(text)),
bg_color_(param.notification.color),
stack_(false),
has_icons_(!icon_file.empty()) {}
// Dibuja las notificaciones por pantalla
void Notifier::render() {
for (int i = static_cast<int>(notifications_.size()) - 1; i >= 0; --i) {
notifications_[i].sprite->render();
}
}
// Actualiza el estado de las notificaciones
void Notifier::update(float delta_time) {
for (int i = 0; std::cmp_less(i, notifications_.size()); ++i) {
if (!shouldProcessNotification(i)) {
break;
}
processNotification(i, delta_time);
}
clearFinishedNotifications();
}
auto Notifier::shouldProcessNotification(int index) const -> bool {
// Si la notificación anterior está "saliendo", no hagas nada
return index <= 0 || notifications_[index - 1].state != State::RISING;
}
void Notifier::processNotification(int index, float delta_time) {
auto& notification = notifications_[index];
notification.timer += delta_time;
playNotificationSoundIfNeeded(notification);
updateNotificationState(index, delta_time);
notification.sprite->setPosition(notification.rect);
}
void Notifier::playNotificationSoundIfNeeded(const Notification& notification) {
// Hace sonar la notificación al inicio
if (notification.timer <= 0.016F &&
param.notification.sound &&
notification.state == State::RISING) {
Audio::get()->playSound("notify.wav", Audio::Group::INTERFACE);
}
}
void Notifier::updateNotificationState(int index, float delta_time) {
auto& notification = notifications_[index];
switch (notification.state) {
case State::RISING:
handleRisingState(index, delta_time);
break;
case State::STAY:
handleStayState(index);
break;
case State::VANISHING:
handleVanishingState(index, delta_time);
break;
default:
break;
}
}
void Notifier::handleRisingState(int index, float delta_time) {
auto& notification = notifications_[index];
const float PIXELS_TO_MOVE = ANIMATION_SPEED_PX_PER_S * delta_time;
const float PROGRESS = notification.timer * ANIMATION_SPEED_PX_PER_S / notification.travel_dist;
const int ALPHA = static_cast<int>(255 * std::min(PROGRESS, 1.0F));
moveNotificationVertically(notification, param.notification.pos_v == Position::TOP ? PIXELS_TO_MOVE : -PIXELS_TO_MOVE);
notification.texture->setAlpha(ALPHA);
if ((param.notification.pos_v == Position::TOP && notification.rect.y >= notification.y) ||
(param.notification.pos_v == Position::BOTTOM && notification.rect.y <= notification.y)) {
transitionToStayState(index);
}
}
void Notifier::handleStayState(int index) {
auto& notification = notifications_[index];
if (notification.timer >= STAY_DURATION_S) {
notification.state = State::VANISHING;
notification.timer = 0.0F;
}
}
void Notifier::handleVanishingState(int index, float delta_time) {
auto& notification = notifications_[index];
const float PIXELS_TO_MOVE = ANIMATION_SPEED_PX_PER_S * delta_time;
const float PROGRESS = notification.timer * ANIMATION_SPEED_PX_PER_S / notification.travel_dist;
const int ALPHA = static_cast<int>(255 * (1 - std::min(PROGRESS, 1.0F)));
moveNotificationVertically(notification, param.notification.pos_v == Position::TOP ? -PIXELS_TO_MOVE : PIXELS_TO_MOVE);
notification.texture->setAlpha(ALPHA);
if (PROGRESS >= 1.0F) {
notification.state = State::FINISHED;
}
}
void Notifier::moveNotificationVertically(Notification& notification, float pixels_to_move) {
notification.rect.y += pixels_to_move;
}
void Notifier::transitionToStayState(int index) {
auto& notification = notifications_[index];
notification.state = State::STAY;
notification.texture->setAlpha(255);
notification.rect.y = static_cast<float>(notification.y); // Asegurar posición exacta
notification.timer = 0.0F;
}
// Elimina las notificaciones finalizadas
void Notifier::clearFinishedNotifications() {
for (int i = static_cast<int>(notifications_.size()) - 1; i >= 0; --i) {
if (notifications_[i].state == State::FINISHED) {
notifications_.erase(notifications_.begin() + i);
}
}
}
void Notifier::show(std::vector<std::string> texts, int icon, const std::string& code) {
// Si no hay texto, acaba
if (texts.empty()) {
return;
}
// Si las notificaciones no se apilan, elimina las anteriores
if (!stack_) {
clearAllNotifications();
}
// Elimina las cadenas vacías
texts.erase(std::ranges::remove_if(texts, [](const std::string& s) -> bool { return s.empty(); }).begin(), texts.end());
// Encuentra la cadena más larga
std::string longest;
for (const auto& text : texts) {
if (text.length() > longest.length()) {
longest = text;
}
}
// Inicializa variables
constexpr int ICON_SIZE = 16;
constexpr int PADDING_OUT = 1;
const float PADDING_IN_H = text_->getCharacterSize();
const float PADDING_IN_V = text_->getCharacterSize() / 2;
const int ICON_SPACE = icon >= 0 ? ICON_SIZE + PADDING_IN_H : 0;
const float WIDTH = text_->length(longest) + (PADDING_IN_H * 2) + ICON_SPACE;
const float HEIGHT = (text_->getCharacterSize() * texts.size()) + (PADDING_IN_V * 2);
const auto SHAPE = Shape::SQUARED;
// Posición horizontal
float desp_h = 0;
switch (param.notification.pos_h) {
case Position::LEFT:
desp_h = PADDING_OUT;
break;
case Position::MIDDLE:
desp_h = ((param.game.width / 2) - (WIDTH / 2));
break;
case Position::RIGHT:
desp_h = param.game.width - WIDTH - PADDING_OUT;
break;
default:
desp_h = 0;
break;
}
// Posición vertical
const int DESP_V = (param.notification.pos_v == Position::TOP) ? PADDING_OUT : (param.game.height - HEIGHT - PADDING_OUT);
// Offset
const auto TRAVEL_DIST = HEIGHT + PADDING_OUT;
auto offset = notifications_.empty()
? DESP_V
: notifications_.back().y + (param.notification.pos_v == Position::TOP ? TRAVEL_DIST : -TRAVEL_DIST);
// Crea la notificacion
Notification n;
// Inicializa variables
n.code = code;
n.y = offset;
n.travel_dist = TRAVEL_DIST;
n.texts = texts;
n.shape = SHAPE;
const float POS_Y = offset + (param.notification.pos_v == Position::TOP ? -TRAVEL_DIST : TRAVEL_DIST);
n.rect = {.x = desp_h, .y = POS_Y, .w = WIDTH, .h = HEIGHT};
// Crea la textura
n.texture = std::make_shared<Texture>(renderer_);
n.texture->createBlank(WIDTH, HEIGHT, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET);
n.texture->setBlendMode(SDL_BLENDMODE_BLEND);
// Prepara para dibujar en la textura
n.texture->setAsRenderTarget(renderer_);
// Dibuja el fondo de la notificación
SDL_SetRenderDrawColor(renderer_, bg_color_.r, bg_color_.g, bg_color_.b, 255);
SDL_FRect rect;
if (SHAPE == Shape::ROUNDED) {
rect = {.x = 4, .y = 0, .w = WIDTH - (4 * 2), .h = HEIGHT};
SDL_RenderFillRect(renderer_, &rect);
rect = {.x = 4 / 2, .y = 1, .w = WIDTH - 4, .h = HEIGHT - 2};
SDL_RenderFillRect(renderer_, &rect);
rect = {.x = 1, .y = 4 / 2, .w = WIDTH - 2, .h = HEIGHT - 4};
SDL_RenderFillRect(renderer_, &rect);
rect = {.x = 0, .y = 4, .w = WIDTH, .h = HEIGHT - (4 * 2)};
SDL_RenderFillRect(renderer_, &rect);
}
else if (SHAPE == Shape::SQUARED) {
SDL_RenderClear(renderer_);
}
// Dibuja el icono de la notificación
if (has_icons_ && icon >= 0 && texts.size() >= 2) {
auto sp = std::make_unique<Sprite>(icon_texture_, (SDL_FRect){.x = 0, .y = 0, .w = ICON_SIZE, .h = ICON_SIZE});
sp->setPosition({.x = PADDING_IN_H, .y = PADDING_IN_V, .w = ICON_SIZE, .h = ICON_SIZE});
sp->setSpriteClip(SDL_FRect{
.x = static_cast<float>(ICON_SIZE * (icon % 10)),
.y = static_cast<float>(ICON_SIZE * (icon / 10)),
.w = ICON_SIZE,
.h = ICON_SIZE});
sp->render();
}
// Escribe el texto de la notificación
const Color COLOR{255, 255, 255};
int iterator = 0;
for (const auto& text : texts) {
text_->writeColored(PADDING_IN_H + ICON_SPACE, PADDING_IN_V + (iterator * (text_->getCharacterSize() + 1)), text, COLOR);
++iterator;
}
// Deja de dibujar en la textura
SDL_SetRenderTarget(renderer_, nullptr);
// Crea el sprite de la notificación
n.sprite = std::make_shared<Sprite>(n.texture, n.rect);
// Deja la notificación invisible
n.texture->setAlpha(0);
// Añade la notificación a la lista
notifications_.emplace_back(n);
}
// Finaliza y elimnina todas las notificaciones activas
void Notifier::clearAllNotifications() {
for (auto& notification : notifications_) {
notification.state = State::FINISHED;
}
clearFinishedNotifications();
}
// Obtiene los códigos de las notificaciones
auto Notifier::getCodes() -> std::vector<std::string> {
std::vector<std::string> codes;
codes.reserve(notifications_.size());
for (const auto& notification : notifications_) {
codes.emplace_back(notification.code);
}
return codes;
}

112
source/game/ui/notifier.hpp Normal file
View File

@@ -0,0 +1,112 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FRect, SDL_Renderer
#include <memory> // Para shared_ptr
#include <string> // Para basic_string, string
#include <vector> // Para vector
#include "color.hpp" // Para stringInVector, Color
#include "utils.hpp"
class Sprite;
class Text;
class Texture;
// --- Clase Notifier: gestiona las notificaciones en pantalla (singleton) ---
class Notifier {
public:
// --- Enums ---
enum class Position {
TOP, // Parte superior
BOTTOM, // Parte inferior
LEFT, // Lado izquierdo
MIDDLE, // Centro
RIGHT, // Lado derecho
};
// --- Métodos de singleton ---
static void init(const std::string& icon_file, std::shared_ptr<Text> text); // Inicializa el singleton
static void destroy(); // Libera el singleton
static auto get() -> Notifier*; // Obtiene la instancia
// --- Métodos principales ---
void render(); // Dibuja las notificaciones por pantalla
void update(float delta_time); // Actualiza el estado de las notificaciones
// --- Gestión de notificaciones ---
void show(std::vector<std::string> texts, int icon = -1, const std::string& code = std::string()); // Muestra una notificación de texto por pantalla
[[nodiscard]] auto isActive() const -> bool { return !notifications_.empty(); } // Indica si hay notificaciones activas
auto getCodes() -> std::vector<std::string>; // Obtiene los códigos de las notificaciones activas
auto checkCode(const std::string& code) -> bool { return stringInVector(getCodes(), code); } // Comprueba si hay alguna notificación con un código concreto
private:
// --- Constantes de tiempo (en segundos) ---
static constexpr float STAY_DURATION_S = 2.5F; // Tiempo que se ve la notificación (150 frames @ 60fps)
static constexpr float ANIMATION_SPEED_PX_PER_S = 60.0F; // Velocidad de animación (1 pixel/frame @ 60fps)
// --- Enums privados ---
enum class State {
RISING, // Apareciendo
STAY, // Visible
VANISHING, // Desapareciendo
FINISHED, // Terminada
};
enum class Shape {
ROUNDED, // Forma redondeada
SQUARED, // Forma cuadrada
};
// --- Estructuras privadas ---
struct Notification {
std::shared_ptr<Texture> texture; // Textura de la notificación
std::shared_ptr<Sprite> sprite; // Sprite asociado
std::vector<std::string> texts; // Textos a mostrar
SDL_FRect rect; // Rectángulo de la notificación
std::string code; // Código identificador de la notificación
State state{State::RISING}; // Estado de la notificación
Shape shape{Shape::SQUARED}; // Forma de la notificación
float timer{0.0F}; // Timer en segundos
int y{0}; // Posición vertical
int travel_dist{0}; // Distancia a recorrer
// Constructor
explicit Notification()
: texture(nullptr),
sprite(nullptr),
rect{.x = 0, .y = 0, .w = 0, .h = 0} {}
};
// --- Objetos y punteros ---
SDL_Renderer* renderer_; // El renderizador de la ventana
std::shared_ptr<Texture> icon_texture_; // Textura para los iconos de las notificaciones
std::shared_ptr<Text> text_; // Objeto para dibujar texto
// --- Variables de estado ---
std::vector<Notification> notifications_; // Lista de notificaciones activas
Color bg_color_; // Color de fondo de las notificaciones
// Nota: wait_time_ eliminado, ahora se usa STAY_DURATION_S
bool stack_; // Indica si las notificaciones se apilan
bool has_icons_; // Indica si el notificador tiene textura para iconos
// --- Métodos internos ---
void clearFinishedNotifications(); // Elimina las notificaciones cuyo estado es FINISHED
void clearAllNotifications(); // Elimina todas las notificaciones activas, sin importar el estado
[[nodiscard]] auto shouldProcessNotification(int index) const -> bool; // Determina si una notificación debe ser procesada (según su estado y posición)
void processNotification(int index, float delta_time); // Procesa una notificación en la posición dada: actualiza su estado y comportamiento visual
static void playNotificationSoundIfNeeded(const Notification& notification); // Reproduce sonido asociado si es necesario (dependiendo del estado o contenido)
void updateNotificationState(int index, float delta_time); // Actualiza el estado interno de una notificación (ej. de RISING a STAY)
void handleRisingState(int index, float delta_time); // Lógica de animación para el estado RISING (apareciendo)
void handleStayState(int index); // Lógica para mantener una notificación visible en el estado STAY
void handleVanishingState(int index, float delta_time); // Lógica de animación para el estado VANISHING (desapareciendo)
static void moveNotificationVertically(Notification& notification, float pixels_to_move); // Mueve verticalmente una notificación con la cantidad de pixels especificada
void transitionToStayState(int index); // Cambia el estado de una notificación de RISING a STAY cuando ha alcanzado su posición final
// --- Constructores y destructor privados (singleton) ---
Notifier(const std::string& icon_file, std::shared_ptr<Text> text); // Constructor privado
~Notifier() = default; // Destructor privado
// --- Instancia singleton ---
static Notifier* instance; // Instancia única de Notifier
};

View File

@@ -0,0 +1,733 @@
#include "ui/service_menu.hpp"
#include <utility>
#include "audio.hpp" // Para Audio
#include "define_buttons.hpp" // Para DefineButtons
#include "difficulty.hpp" // Para getCodeFromName, getNameFromCode
#include "input.hpp" // Para Input
#include "input_types.hpp" // Para InputAction
#include "lang.hpp" // Para getText, getCodeFromName, getNameFromCode
#include "menu_option.hpp" // Para MenuOption, ActionOption, BoolOption, ListOption, FolderOption, IntOption, ActionListOption
#include "menu_renderer.hpp" // Para MenuRenderer
#include "options.hpp" // Para GamepadManager, gamepad_manager, PendingChanges, Video, pending_changes, video, Audio, Gamepad, Settings, audio, checkPendingChanges, settings, Window, getPlayerWhoUsesKeyboard, playerIdToString, stringToPlayerId, window, Keyboard, Music, Sound, keyboard
#include "param.hpp" // Para Param, param, ParamGame, ParamServiceMenu
#include "player.hpp" // Para Player
#include "resource.hpp" // Para Resource
#include "screen.hpp" // Para Screen
#include "section.hpp" // Para Name, name, Options, options
#include "ui/ui_message.hpp" // Para UIMessage
#include "utils.hpp" // Para Zone
// Singleton
ServiceMenu* ServiceMenu::instance = nullptr;
void ServiceMenu::init() { ServiceMenu::instance = new ServiceMenu(); }
void ServiceMenu::destroy() { delete ServiceMenu::instance; }
auto ServiceMenu::get() -> ServiceMenu* { return ServiceMenu::instance; }
// Constructor
ServiceMenu::ServiceMenu()
: current_settings_group_(SettingsGroup::MAIN),
previous_settings_group_(current_settings_group_) {
auto element_text = Resource::get()->getText("04b_25_flat");
auto title_text = Resource::get()->getText("04b_25_flat_2x");
// El renderer ahora se inicializa con su configuración
renderer_ = std::make_unique<MenuRenderer>(this, element_text, title_text);
restart_message_ui_ = std::make_unique<UIMessage>(element_text, Lang::getText("[SERVICE_MENU] NEED_RESTART_MESSAGE"), param.service_menu.title_color);
define_buttons_ = std::make_unique<DefineButtons>();
reset();
}
void ServiceMenu::toggle() {
if (define_buttons_ && define_buttons_->isEnabled()) {
return;
}
if (isAnimating() && !define_buttons_->isEnabled()) {
return;
}
if (!enabled_) { // Si está cerrado, abrir
reset();
Options::gamepad_manager.assignAndLinkGamepads();
renderer_->show(this);
setEnabledInternal(true);
playSelectSound();
} else { // Si está abierto, cerrar
renderer_->hide();
setEnabledInternal(false);
playBackSound();
}
}
void ServiceMenu::render() {
// Condición corregida: renderiza si está habilitado O si se está animando
if (enabled_ || isAnimating()) {
renderer_->render(this);
} else {
return; // Si no está ni habilitado ni animándose, no dibujes nada.
}
// El mensaje de reinicio y otros elementos solo deben aparecer si está completamente visible,
// no durante la animación.
if (enabled_ && !isAnimating()) {
const float MSG_X = param.game.game_area.center_x;
const float MSG_Y = renderer_->getRect().y + 39.0F;
restart_message_ui_->setPosition(MSG_X, MSG_Y);
restart_message_ui_->render();
if (define_buttons_ && define_buttons_->isEnabled()) {
define_buttons_->render();
}
}
}
void ServiceMenu::update(float delta_time) {
// El renderer siempre se actualiza para manejar sus animaciones
renderer_->update(this, delta_time);
if (!enabled_) {
return;
}
// Lógica de actualización del mensaje de reinicio y botones
bool now_pending = Options::pending_changes.has_pending_changes;
if (now_pending != last_pending_changes_) {
now_pending ? restart_message_ui_->show() : restart_message_ui_->hide();
last_pending_changes_ = now_pending;
}
restart_message_ui_->update(delta_time);
if (define_buttons_) {
define_buttons_->update(delta_time);
if (define_buttons_->isEnabled() && define_buttons_->isReadyToClose()) {
define_buttons_->disable();
}
}
}
void ServiceMenu::reset() {
selected_ = 0;
main_menu_selected_ = 0;
current_settings_group_ = SettingsGroup::MAIN;
previous_settings_group_ = SettingsGroup::MAIN;
initializeOptions();
updateMenu();
renderer_->setLayout(this);
}
void ServiceMenu::moveBack() {
// Si estamos en una subpantalla, no llamamos a toggle
if (current_settings_group_ != SettingsGroup::MAIN) {
playBackSound();
current_settings_group_ = previous_settings_group_;
selected_ = (current_settings_group_ == SettingsGroup::MAIN) ? main_menu_selected_ : 0;
updateMenu();
return;
}
// Si estamos en la pantalla principal, llamamos a toggle() para cerrar con animación.
toggle();
}
// --- Lógica de Navegación ---
void ServiceMenu::setSelectorUp() {
if (display_options_.empty()) {
return;
}
selected_ = (selected_ > 0) ? selected_ - 1 : display_options_.size() - 1;
playMoveSound();
}
void ServiceMenu::setSelectorDown() {
if (display_options_.empty()) {
return;
}
selected_ = (selected_ + 1) % display_options_.size();
playMoveSound();
}
void ServiceMenu::adjustOption(bool adjust_up) {
if (display_options_.empty()) {
return;
}
auto& selected_option = display_options_.at(selected_);
if (selected_option->getBehavior() == MenuOption::Behavior::ADJUST) {
selected_option->adjustValue(adjust_up);
applySettings();
playAdjustSound();
}
}
void ServiceMenu::selectOption() {
if (display_options_.empty()) {
return;
}
if (current_settings_group_ == SettingsGroup::MAIN) {
main_menu_selected_ = selected_;
}
auto* selected_option = display_options_.at(selected_);
if (selected_option == nullptr) {
// This shouldn't happen in normal operation, but protects against null pointer
return;
}
if (auto* folder = dynamic_cast<FolderOption*>(selected_option)) {
previous_settings_group_ = current_settings_group_;
current_settings_group_ = folder->getTargetGroup();
selected_ = 0;
updateMenu();
} else if (selected_option->getBehavior() == MenuOption::Behavior::SELECT or selected_option->getBehavior() == MenuOption::Behavior::BOTH) {
selected_option->executeAction();
}
playSelectSound();
}
// --- Lógica Interna ---
void ServiceMenu::updateDisplayOptions() {
display_options_.clear();
for (auto& option : options_) {
if (option->getGroup() == current_settings_group_ && !option->isHidden()) {
display_options_.push_back(option.get());
}
}
updateOptionPairs();
}
void ServiceMenu::updateOptionPairs() {
option_pairs_.clear();
for (const auto& option : display_options_) {
option_pairs_.emplace_back(option->getCaption(), option->getValueAsString());
}
}
void ServiceMenu::updateMenu() {
title_ = settingsGroupToString(current_settings_group_);
adjustListValues();
// Actualiza las opciones visibles
updateDisplayOptions();
// Notifica al renderer del cambio de layout
renderer_->onLayoutChanged(this);
}
void ServiceMenu::applySettings() {
if (current_settings_group_ == SettingsGroup::CONTROLS) {
applyControlsSettings();
}
if (current_settings_group_ == SettingsGroup::VIDEO) {
applyVideoSettings();
}
if (current_settings_group_ == SettingsGroup::AUDIO) {
applyAudioSettings();
}
if (current_settings_group_ == SettingsGroup::SETTINGS) {
applySettingsSettings();
}
// Actualiza los valores de las opciones
updateOptionPairs();
}
void ServiceMenu::applyControlsSettings() {}
void ServiceMenu::applyVideoSettings() {
Screen::get()->applySettings();
setHiddenOptions();
}
void ServiceMenu::applyAudioSettings() {
Audio::get()->applySettings();
}
void ServiceMenu::applySettingsSettings() {
setHiddenOptions();
}
auto ServiceMenu::getOptionByCaption(const std::string& caption) const -> MenuOption* {
for (const auto& option : options_) {
if (option->getCaption() == caption) {
return option.get();
}
}
return nullptr;
}
// --- Getters y otros ---
auto ServiceMenu::getCurrentGroupAlignment() const -> ServiceMenu::GroupAlignment {
switch (current_settings_group_) {
case SettingsGroup::CONTROLS:
case SettingsGroup::VIDEO:
case SettingsGroup::AUDIO:
case SettingsGroup::SETTINGS:
return GroupAlignment::LEFT;
default:
return GroupAlignment::CENTERED;
}
}
auto ServiceMenu::countOptionsInGroup(SettingsGroup group) const -> size_t {
size_t count = 0;
for (const auto& option : options_) {
if (option->getGroup() == group && !option->isHidden()) {
count++;
}
}
return count;
}
// Inicializa todas las opciones del menú
void ServiceMenu::initializeOptions() {
options_.clear();
// CONTROLS - Usando ActionListOption para mandos
options_.push_back(std::make_unique<ActionListOption>(
Lang::getText("[SERVICE_MENU] CONTROLLER1"),
SettingsGroup::CONTROLS,
Input::get()->getControllerNames(),
[]() -> std::string {
return Options::gamepad_manager.getGamepad(Player::Id::PLAYER1).name;
},
[](const std::string& val) -> void {
Options::gamepad_manager.assignGamepadToPlayer(Player::Id::PLAYER1, Input::get()->getGamepadByName(val), val);
},
[this]() -> void {
// Acción: configurar botones del mando del jugador 1
auto* gamepad = &Options::gamepad_manager.getGamepad(Player::Id::PLAYER1);
if ((gamepad != nullptr) && gamepad->instance) {
define_buttons_->enable(gamepad);
}
}));
options_.push_back(std::make_unique<ActionListOption>(
Lang::getText("[SERVICE_MENU] CONTROLLER2"),
SettingsGroup::CONTROLS,
Input::get()->getControllerNames(),
[]() -> std::string {
return Options::gamepad_manager.getGamepad(Player::Id::PLAYER2).name;
},
[](const std::string& val) -> void {
Options::gamepad_manager.assignGamepadToPlayer(Player::Id::PLAYER2, Input::get()->getGamepadByName(val), val);
},
[this]() -> void {
// Acción: configurar botones del mando del jugador 2
auto* gamepad = &Options::gamepad_manager.getGamepad(Player::Id::PLAYER2);
if ((gamepad != nullptr) && gamepad->instance) {
define_buttons_->enable(gamepad);
}
}));
// CONTROLS - Opción para teclado (solo lista, sin acción)
options_.push_back(std::make_unique<ListOption>(
Lang::getText("[SERVICE_MENU] KEYBOARD"),
SettingsGroup::CONTROLS,
std::vector<std::string>{
Lang::getText("[SERVICE_MENU] PLAYER1"),
Lang::getText("[SERVICE_MENU] PLAYER2")},
[]() -> std::string {
// Devolver el jugador actual asignado al teclado
return Options::playerIdToString(Options::getPlayerWhoUsesKeyboard());
},
[](const std::string& val) -> void {
// Asignar el teclado al jugador seleccionado
Options::keyboard.assignTo(Options::stringToPlayerId(val));
}));
// CONTROLS - Acción para intercambiar mandos
options_.push_back(std::make_unique<ActionOption>(
Lang::getText("[SERVICE_MENU] SWAP_CONTROLLERS"),
SettingsGroup::CONTROLS,
[this]() -> void {
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
}));
// VIDEO
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] FULLSCREEN"),
SettingsGroup::VIDEO,
&Options::video.fullscreen));
options_.push_back(std::make_unique<IntOption>(
Lang::getText("[SERVICE_MENU] WINDOW_SIZE"),
SettingsGroup::VIDEO,
&Options::window.zoom,
1,
Options::window.max_zoom,
1));
// Shader: Desactivat / PostFX / CrtPi
{
std::string disabled_text = Lang::getText("[SERVICE_MENU] SHADER_DISABLED");
std::vector<std::string> shader_values = {disabled_text, "PostFX", "CrtPi"};
auto shader_getter = [disabled_text]() -> std::string {
// NOLINTNEXTLINE(performance-no-automatic-move) -- captura por valor en lambda const, no se puede mover
if (!Options::video.shader.enabled) { return disabled_text; }
return (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) ? "CrtPi" : "PostFX";
};
auto shader_setter = [disabled_text](const std::string& val) {
if (val == disabled_text) {
Options::video.shader.enabled = false;
} else {
Options::video.shader.enabled = true;
const auto TYPE = (val == "CrtPi") ? Rendering::ShaderType::CRTPI : Rendering::ShaderType::POSTFX;
Options::video.shader.current_shader = TYPE;
auto* screen = Screen::get();
if (screen != nullptr) {
screen->applySettings();
}
}
Screen::initShaders();
};
options_.push_back(std::make_unique<ListOption>(
Lang::getText("[SERVICE_MENU] SHADER"),
SettingsGroup::VIDEO,
shader_values,
shader_getter,
shader_setter));
}
// Preset: muestra nombre, cicla circularmente entre presets del shader activo
{
auto preset_getter = []() -> std::string {
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
if (Options::crtpi_presets.empty()) { return ""; }
return Options::crtpi_presets.at(static_cast<size_t>(Options::video.shader.current_crtpi_preset)).name;
}
if (Options::postfx_presets.empty()) { return ""; }
return Options::postfx_presets.at(static_cast<size_t>(Options::video.shader.current_postfx_preset)).name;
};
auto preset_adjuster = [](bool up) {
if (Options::video.shader.current_shader == Rendering::ShaderType::CRTPI) {
if (Options::crtpi_presets.empty()) { return; }
const int SIZE = static_cast<int>(Options::crtpi_presets.size());
Options::video.shader.current_crtpi_preset = up
? (Options::video.shader.current_crtpi_preset + 1) % SIZE
: (Options::video.shader.current_crtpi_preset + SIZE - 1) % SIZE;
} else {
if (Options::postfx_presets.empty()) { return; }
const int SIZE = static_cast<int>(Options::postfx_presets.size());
Options::video.shader.current_postfx_preset = up
? (Options::video.shader.current_postfx_preset + 1) % SIZE
: (Options::video.shader.current_postfx_preset + SIZE - 1) % SIZE;
}
Screen::initShaders();
};
auto preset_max_width = [](Text* text) -> int {
int max_w = 0;
for (const auto& p : Options::postfx_presets) { max_w = std::max(max_w, text->length(p.name, -2)); }
for (const auto& p : Options::crtpi_presets) { max_w = std::max(max_w, text->length(p.name, -2)); }
return max_w;
};
options_.push_back(std::make_unique<CallbackOption>(
Lang::getText("[SERVICE_MENU] SHADER_PRESET"),
SettingsGroup::VIDEO,
preset_getter,
preset_adjuster,
preset_max_width));
}
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] SUPERSAMPLING"),
SettingsGroup::VIDEO,
&Options::video.supersampling.enabled));
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] VSYNC"),
SettingsGroup::VIDEO,
&Options::video.vsync));
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] INTEGER_SCALE"),
SettingsGroup::VIDEO,
&Options::video.integer_scale));
// AUDIO
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] AUDIO"),
SettingsGroup::AUDIO,
&Options::audio.enabled));
options_.push_back(std::make_unique<IntOption>(
Lang::getText("[SERVICE_MENU] MAIN_VOLUME"),
SettingsGroup::AUDIO,
&Options::audio.volume,
0,
100,
5));
options_.push_back(std::make_unique<IntOption>(
Lang::getText("[SERVICE_MENU] MUSIC_VOLUME"),
SettingsGroup::AUDIO,
&Options::audio.music.volume,
0,
100,
5));
options_.push_back(std::make_unique<IntOption>(
Lang::getText("[SERVICE_MENU] SFX_VOLUME"),
SettingsGroup::AUDIO,
&Options::audio.sound.volume,
0,
100,
5));
// SETTINGS
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] AUTOFIRE"),
SettingsGroup::SETTINGS,
&Options::settings.autofire));
options_.push_back(std::make_unique<ListOption>(
Lang::getText("[SERVICE_MENU] LANGUAGE"),
SettingsGroup::SETTINGS,
std::vector<std::string>{
Lang::getText("[SERVICE_MENU] LANG_ES"),
Lang::getText("[SERVICE_MENU] LANG_BA"),
Lang::getText("[SERVICE_MENU] LANG_EN")},
[]() -> std::string {
return Lang::getNameFromCode(Options::pending_changes.new_language);
},
[](const std::string& val) -> void {
Options::pending_changes.new_language = Lang::getCodeFromName(val);
Options::checkPendingChanges();
}));
options_.push_back(std::make_unique<ListOption>(
Lang::getText("[SERVICE_MENU] DIFFICULTY"),
SettingsGroup::SETTINGS,
std::vector<std::string>{
Lang::getText("[SERVICE_MENU] EASY"),
Lang::getText("[SERVICE_MENU] NORMAL"),
Lang::getText("[SERVICE_MENU] HARD")},
[]() -> std::string {
return Difficulty::getNameFromCode(Options::pending_changes.new_difficulty);
},
[](const std::string& val) -> void {
Options::pending_changes.new_difficulty = Difficulty::getCodeFromName(val);
Options::checkPendingChanges();
}));
options_.push_back(std::make_unique<BoolOption>(
Lang::getText("[SERVICE_MENU] ENABLE_SHUTDOWN"),
SettingsGroup::SETTINGS,
&Options::settings.shutdown_enabled));
// SYSTEM
options_.push_back(std::make_unique<ActionOption>(
Lang::getText("[SERVICE_MENU] RESET"),
SettingsGroup::SYSTEM,
[this]() -> void {
Section::name = Section::Name::RESET;
toggle();
}));
options_.push_back(std::make_unique<ActionOption>(
Lang::getText("[SERVICE_MENU] QUIT"),
SettingsGroup::SYSTEM,
[]() -> void {
Section::name = Section::Name::QUIT;
Section::options = Section::Options::NONE;
}));
options_.push_back(std::make_unique<ActionOption>(
Lang::getText("[SERVICE_MENU] SHUTDOWN"),
SettingsGroup::SYSTEM,
[]() -> void {
Section::name = Section::Name::QUIT;
Section::options = Section::Options::SHUTDOWN;
},
!Options::settings.shutdown_enabled));
// MAIN MENU
options_.push_back(std::make_unique<FolderOption>(
Lang::getText("[SERVICE_MENU] CONTROLS"),
SettingsGroup::MAIN,
SettingsGroup::CONTROLS));
options_.push_back(std::make_unique<FolderOption>(
Lang::getText("[SERVICE_MENU] VIDEO"),
SettingsGroup::MAIN,
SettingsGroup::VIDEO));
options_.push_back(std::make_unique<FolderOption>(
Lang::getText("[SERVICE_MENU] AUDIO"),
SettingsGroup::MAIN,
SettingsGroup::AUDIO));
options_.push_back(std::make_unique<FolderOption>(
Lang::getText("[SERVICE_MENU] SETTINGS"),
SettingsGroup::MAIN,
SettingsGroup::SETTINGS));
options_.push_back(std::make_unique<FolderOption>(
Lang::getText("[SERVICE_MENU] SYSTEM"),
SettingsGroup::MAIN,
SettingsGroup::SYSTEM));
// Oculta opciones según configuración
setHiddenOptions();
}
// Sincroniza los valores de las opciones tipo lista
void ServiceMenu::adjustListValues() {
for (auto& option : options_) {
if (auto* list_option = dynamic_cast<ListOption*>(option.get())) {
list_option->sync();
}
}
}
// Reproduce el sonido de navegación del menú
void ServiceMenu::playAdjustSound() { Audio::get()->playSound("service_menu_adjust.wav", Audio::Group::INTERFACE); }
void ServiceMenu::playMoveSound() { Audio::get()->playSound("service_menu_move.wav", Audio::Group::INTERFACE); }
void ServiceMenu::playSelectSound() { Audio::get()->playSound("service_menu_select.wav", Audio::Group::INTERFACE); }
void ServiceMenu::playBackSound() { Audio::get()->playSound("service_menu_back.wav", Audio::Group::INTERFACE); }
// Devuelve el nombre del grupo como string para el título
auto ServiceMenu::settingsGroupToString(SettingsGroup group) -> std::string {
switch (group) {
case SettingsGroup::MAIN:
return Lang::getText("[SERVICE_MENU] TITLE");
case SettingsGroup::CONTROLS:
return Lang::getText("[SERVICE_MENU] CONTROLS");
case SettingsGroup::VIDEO:
return Lang::getText("[SERVICE_MENU] VIDEO");
case SettingsGroup::AUDIO:
return Lang::getText("[SERVICE_MENU] AUDIO");
case SettingsGroup::SETTINGS:
return Lang::getText("[SERVICE_MENU] SETTINGS");
case SettingsGroup::SYSTEM:
return Lang::getText("[SERVICE_MENU] SYSTEM");
default:
return Lang::getText("[SERVICE_MENU] TITLE");
}
}
// Establece el estado de oculto de ciertas opciones
void ServiceMenu::setHiddenOptions() {
{
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] WINDOW_SIZE"));
if (option != nullptr) {
option->setHidden(Options::video.fullscreen);
}
}
{
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] SHUTDOWN"));
if (option != nullptr) {
option->setHidden(!Options::settings.shutdown_enabled);
}
}
{
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] SHADER_PRESET"));
if (option != nullptr) {
option->setHidden(!Options::video.shader.enabled);
}
}
{
auto* option = getOptionByCaption(Lang::getText("[SERVICE_MENU] SUPERSAMPLING"));
if (option != nullptr) {
option->setHidden(!Options::video.shader.enabled || Options::video.shader.current_shader != Rendering::ShaderType::POSTFX);
}
}
updateMenu(); // El menú debe refrescarse si algo se oculta
}
void ServiceMenu::handleEvent(const SDL_Event& event) {
if (!enabled_) {
return;
}
// Si DefineButtons está activo, que maneje todos los eventos
if (define_buttons_ && define_buttons_->isEnabled()) {
define_buttons_->handleEvents(event);
}
}
auto ServiceMenu::checkInput() -> bool {
// --- Guardas ---
// No procesar input si el menú no está habilitado, si se está animando o si se definen botones
if (!enabled_ || isAnimating() || (define_buttons_ && define_buttons_->isEnabled())) {
return false;
}
static auto* input_ = Input::get();
using Action = Input::Action;
const std::vector<std::pair<Action, std::function<void()>>> ACTIONS = {
{Action::UP, [this]() -> void { setSelectorUp(); }},
{Action::DOWN, [this]() -> void { setSelectorDown(); }},
{Action::RIGHT, [this]() -> void { adjustOption(true); }},
{Action::LEFT, [this]() -> void { adjustOption(false); }},
{Action::SM_SELECT, [this]() -> void { selectOption(); }},
{Action::SM_BACK, [this]() -> void { moveBack(); }},
};
// Teclado
for (const auto& [action, func] : ACTIONS) {
if (input_->checkAction(action, Input::DO_NOT_ALLOW_REPEAT, Input::CHECK_KEYBOARD)) {
func();
return true;
}
}
// Mandos
for (const auto& gamepad : input_->getGamepads()) {
for (const auto& [action, func] : ACTIONS) {
if (input_->checkAction(action, Input::DO_NOT_ALLOW_REPEAT, Input::DO_NOT_CHECK_KEYBOARD, gamepad)) {
func();
return true;
}
}
}
return false;
}
// --- Nuevo Getter ---
auto ServiceMenu::isAnimating() const -> bool {
return renderer_ && renderer_->isAnimating();
}
auto ServiceMenu::isDefiningButtons() const -> bool {
return define_buttons_ && define_buttons_->isEnabled();
}
void ServiceMenu::refresh() {
// Este método está diseñado para ser llamado desde fuera, por ejemplo,
// cuando un mando se conecta o desconecta mientras el menú está abierto.
// La función updateMenu() es la forma más completa de refrescar, ya que
// sincroniza los valores, actualiza la lista de opciones visibles y notifica
// al renderer de cualquier cambio de layout que pueda haber ocurrido.
updateMenu();
}
// Método para registrar callback
void ServiceMenu::setStateChangeCallback(StateChangeCallback callback) {
state_change_callback_ = std::move(callback);
}
// Método interno que cambia estado y notifica
void ServiceMenu::setEnabledInternal(bool enabled) {
if (enabled_ != enabled) { // Solo si realmente cambia
enabled_ = enabled;
// Notifica el cambio si hay callback registrado
if (state_change_callback_) {
state_change_callback_(enabled_);
}
}
}

View File

@@ -0,0 +1,129 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_Event
#include <cstddef> // Para size_t
#include <cstdint> // Para std::uint8_t
#include <functional> // Para function
#include <iterator> // Para pair
#include <memory> // Para unique_ptr
#include <string> // Para basic_string, string
#include <utility> // Para pair
#include <vector> // Para vector
#include "define_buttons.hpp" // for DefineButtons
#include "ui_message.hpp" // for UIMessage
class MenuOption;
class MenuRenderer;
class ServiceMenu {
public:
// --- Enums y constantes ---
enum class SettingsGroup : std::uint8_t {
CONTROLS,
VIDEO,
AUDIO,
SETTINGS,
SYSTEM,
MAIN
};
enum class GroupAlignment : std::uint8_t {
CENTERED,
LEFT
};
static constexpr size_t OPTIONS_HORIZONTAL_PADDING = 20;
static constexpr size_t MIN_WIDTH = 240;
static constexpr size_t MIN_GAP_OPTION_VALUE = 30;
static constexpr size_t SETTINGS_GROUP_SIZE = 6;
using StateChangeCallback = std::function<void(bool is_active)>;
// --- Métodos de singleton ---
static void init();
static void destroy();
static auto get() -> ServiceMenu*;
ServiceMenu(const ServiceMenu&) = delete;
auto operator=(const ServiceMenu&) -> ServiceMenu& = delete;
// --- Métodos principales ---
void toggle();
void render();
void update(float delta_time);
void reset();
// --- Lógica de navegación ---
void setSelectorUp();
void setSelectorDown();
void adjustOption(bool adjust_up);
void selectOption();
void moveBack();
// --- Método para manejar eventos ---
void handleEvent(const SDL_Event& event);
auto checkInput() -> bool;
// --- Método principal para refresco externo ---
void refresh(); // Refresca los valores y el layout del menú bajo demanda
// --- Método para registrar el callback ---
void setStateChangeCallback(StateChangeCallback callback);
// --- Getters para el estado ---
[[nodiscard]] auto isDefiningButtons() const -> bool;
[[nodiscard]] auto isAnimating() const -> bool; // Nuevo getter
// --- Getters para que el Renderer pueda leer el estado ---
[[nodiscard]] auto isEnabled() const -> bool { return enabled_; }
[[nodiscard]] auto getTitle() const -> const std::string& { return title_; }
[[nodiscard]] auto getCurrentGroup() const -> SettingsGroup { return current_settings_group_; }
[[nodiscard]] auto getCurrentGroupAlignment() const -> GroupAlignment;
[[nodiscard]] auto getDisplayOptions() const -> const std::vector<MenuOption*>& { return display_options_; }
[[nodiscard]] auto getAllOptions() const -> const std::vector<std::unique_ptr<MenuOption>>& { return options_; }
[[nodiscard]] auto getSelectedIndex() const -> size_t { return selected_; }
[[nodiscard]] auto getOptionPairs() const -> const std::vector<std::pair<std::string, std::string>>& { return option_pairs_; }
[[nodiscard]] auto countOptionsInGroup(SettingsGroup group) const -> size_t;
private:
bool enabled_ = false;
std::vector<std::unique_ptr<MenuOption>> options_;
std::vector<MenuOption*> display_options_;
std::vector<std::pair<std::string, std::string>> option_pairs_;
SettingsGroup current_settings_group_;
SettingsGroup previous_settings_group_;
std::string title_;
size_t selected_ = 0;
size_t main_menu_selected_ = 0;
std::unique_ptr<UIMessage> restart_message_ui_;
bool last_pending_changes_ = false;
std::unique_ptr<DefineButtons> define_buttons_;
std::unique_ptr<MenuRenderer> renderer_;
StateChangeCallback state_change_callback_;
// --- Métodos de lógica interna ---
void updateDisplayOptions();
void updateOptionPairs();
void initializeOptions();
void updateMenu();
void applySettings();
void applyControlsSettings();
void applyVideoSettings();
static void applyAudioSettings();
void applySettingsSettings();
[[nodiscard]] auto getOptionByCaption(const std::string& caption) const -> MenuOption*;
void adjustListValues();
static void playMoveSound();
static void playAdjustSound();
static void playSelectSound();
static void playBackSound();
[[nodiscard]] static auto settingsGroupToString(SettingsGroup group) -> std::string;
void setHiddenOptions();
void setEnabledInternal(bool enabled); // Método privado para cambiar estado y notificar
// --- Constructores y destructor privados (singleton) ---
ServiceMenu();
~ServiceMenu() = default;
// --- Instancia singleton ---
static ServiceMenu* instance;
};

View File

@@ -0,0 +1,98 @@
#include "ui_message.hpp"
#include <algorithm>
#include <cmath> // Para pow
#include <utility>
#include "text.hpp" // Para Text::CENTER, Text::COLOR, Text
// Constructor: inicializa el renderizador, el texto y el color del mensaje
UIMessage::UIMessage(std::shared_ptr<Text> text_renderer, std::string message_text, const Color& color)
: text_renderer_(std::move(text_renderer)),
text_(std::move(message_text)),
color_(color) {}
// Muestra el mensaje en la posición base_x, base_y con animación de entrada desde arriba
void UIMessage::show() {
if (visible_ && target_y_ == 0.0F) {
return; // Ya está visible y quieto
}
start_y_ = DESP; // Empieza 8 píxeles arriba de la posición base
target_y_ = 0.0F; // La posición final es la base
y_offset_ = start_y_;
animation_timer_ = 0.0F;
animating_ = true;
visible_ = true;
}
// Oculta el mensaje con animación de salida hacia arriba
void UIMessage::hide() {
if (!visible_) {
return;
}
start_y_ = y_offset_; // Comienza desde la posición actual
target_y_ = DESP; // Termina 8 píxeles arriba de la base
animation_timer_ = 0.0F;
animating_ = true;
}
// Actualiza el estado de la animación (debe llamarse cada frame)
void UIMessage::update(float delta_time) {
if (animating_) {
updateAnimation(delta_time);
}
}
// Interpola la posición vertical del mensaje usando ease out cubic
void UIMessage::updateAnimation(float delta_time) {
animation_timer_ += delta_time;
float t = animation_timer_ / ANIMATION_DURATION_S;
// Clamp t entre 0 y 1
t = std::min(t, 1.0F);
if (target_y_ > start_y_) {
// Animación de entrada (ease out cubic)
t = 1 - pow(1 - t, 3);
} else {
// Animación de salida (ease in cubic)
t = pow(t, 3);
}
y_offset_ = start_y_ + ((target_y_ - start_y_) * t);
if (animation_timer_ >= ANIMATION_DURATION_S) {
y_offset_ = target_y_;
animating_ = false;
animation_timer_ = 0.0F; // Reset timer
if (target_y_ < 0.0F) {
visible_ = false;
}
}
}
// Dibuja el mensaje en pantalla si está visible
void UIMessage::render() {
if (visible_) {
text_renderer_->writeDX(
Text::COLOR | Text::CENTER,
base_x_,
base_y_ + y_offset_,
text_,
-2,
color_);
}
}
// Devuelve true si el mensaje está visible actualmente
auto UIMessage::isVisible() const -> bool {
return visible_;
}
// Permite actualizar la posición del mensaje (por ejemplo, si el menú se mueve)
void UIMessage::setPosition(float new_base_x, float new_base_y) {
base_x_ = new_base_x;
base_y_ = new_base_y;
}

View File

@@ -0,0 +1,56 @@
#pragma once
#include <memory> // Para shared_ptr
#include <string> // Para string
#include "color.hpp" // Para Color
class Text;
// Clase para mostrar mensajes animados en la interfaz de usuario
class UIMessage {
public:
// Constructor: recibe el renderizador de texto, el mensaje y el color
UIMessage(std::shared_ptr<Text> text_renderer, std::string message_text, const Color& color);
// Muestra el mensaje con animación de entrada
void show();
// Oculta el mensaje con animación de salida
void hide();
// Actualiza el estado de la animación (debe llamarse cada frame)
void update(float delta_time);
// Dibuja el mensaje en pantalla si está visible
void render();
// Indica si el mensaje está visible actualmente
[[nodiscard]] auto isVisible() const -> bool;
// Permite actualizar la posición del mensaje (por ejemplo, si el menú se mueve)
void setPosition(float new_base_x, float new_base_y);
private:
// --- Configuración ---
std::shared_ptr<Text> text_renderer_; // Renderizador de texto
std::string text_; // Texto del mensaje a mostrar
Color color_; // Color del texto
// --- Estado ---
bool visible_ = false; // Indica si el mensaje está visible
bool animating_ = false; // Indica si el mensaje está en proceso de animación
float base_x_ = 0.0F; // Posición X base donde se muestra el mensaje
float base_y_ = 0.0F; // Posición Y base donde se muestra el mensaje
float y_offset_ = 0.0F; // Desplazamiento vertical actual del mensaje (para animación)
// --- Animación ---
float start_y_ = 0.0F; // Posición Y inicial de la animación
float target_y_ = 0.0F; // Posición Y objetivo de la animación
float animation_timer_ = 0.0F; // Timer actual de la animación en segundos
static constexpr float ANIMATION_DURATION_S = 0.133F; // Duración total de la animación (8 frames @ 60fps)
static constexpr float DESP = -8.0F; // Distancia a desplazarse
// Actualiza la interpolación de la animación (ease out/in cubic)
void updateAnimation(float delta_time);
};

View File

@@ -0,0 +1,416 @@
#include "window_message.hpp"
#include <algorithm>
#include <utility>
#include "param.hpp"
#include "screen.hpp"
#include "text.hpp"
WindowMessage::WindowMessage(
std::shared_ptr<Text> text_renderer,
std::string title,
const Config& config)
: text_renderer_(std::move(text_renderer)),
config_(config),
title_(std::move(title)),
title_style_(Text::CENTER | Text::COLOR, config_.title_color, config_.title_color, 0, -2),
text_style_(Text::CENTER | Text::COLOR, config_.text_color, config_.text_color, 0, -2) {
}
void WindowMessage::render() {
if (!visible_) {
return;
}
SDL_Renderer* renderer = Screen::get()->getRenderer();
// Dibujar fondo con transparencia
SDL_SetRenderDrawColor(renderer, config_.bg_color.r, config_.bg_color.g, config_.bg_color.b, config_.bg_color.a);
SDL_RenderFillRect(renderer, &rect_);
// Dibujar borde
SDL_SetRenderDrawColor(renderer, config_.border_color.r, config_.border_color.g, config_.border_color.b, config_.border_color.a);
SDL_RenderRect(renderer, &rect_);
// Solo mostrar contenido si no estamos en animación de show/hide
if (shouldShowContent()) {
float current_y = rect_.y + config_.padding;
float available_width = getAvailableTextWidth();
// Dibujar título si existe
if (!title_.empty()) {
std::string visible_title = getTruncatedText(title_, available_width);
if (!visible_title.empty()) {
text_renderer_->writeStyle(
rect_.x + (rect_.w / 2.0F),
current_y,
visible_title,
title_style_);
}
current_y += text_renderer_->getCharacterSize() + config_.title_separator_spacing;
// Línea separadora debajo del título (solo si hay título visible)
if (!visible_title.empty()) {
SDL_SetRenderDrawColor(renderer, config_.border_color.r, config_.border_color.g, config_.border_color.b, config_.border_color.a);
SDL_RenderLine(renderer,
rect_.x + config_.padding,
current_y - (config_.title_separator_spacing / 2.0F),
rect_.x + rect_.w - config_.padding,
current_y - (config_.title_separator_spacing / 2.0F));
}
}
// Dibujar textos
for (const auto& text : texts_) {
std::string visible_text = getTruncatedText(text, available_width);
if (!visible_text.empty()) {
text_renderer_->writeStyle(
rect_.x + (rect_.w / 2.0F),
current_y,
visible_text,
text_style_);
}
current_y += text_renderer_->getCharacterSize() + config_.line_spacing;
}
}
}
void WindowMessage::update(float delta_time) {
// Actualizar animaciones
if (show_hide_animation_.active || resize_animation_.active) {
updateAnimation(delta_time);
}
}
void WindowMessage::show() {
if (visible_) {
return; // Ya visible
}
visible_ = true;
ensureTextFits();
// Detener cualquier animación anterior
resize_animation_.stop();
// Iniciar animación de mostrar desde tamaño 0
show_hide_animation_.startShow(rect_.w, rect_.h);
rect_.w = 0.0F;
rect_.h = 0.0F;
updatePosition(); // Reposicionar con tamaño 0
}
void WindowMessage::hide() {
if (!visible_) {
return; // Ya oculto
}
// Detener cualquier animación anterior
resize_animation_.stop();
// Guardar el tamaño actual para la animación
show_hide_animation_.target_width = rect_.w;
show_hide_animation_.target_height = rect_.h;
// Iniciar animación de ocultar hacia tamaño 0
show_hide_animation_.startHide();
}
void WindowMessage::setTitle(const std::string& title) {
title_ = title;
triggerAutoResize();
}
void WindowMessage::setText(const std::string& text) {
texts_.clear();
texts_.push_back(text);
triggerAutoResize();
}
void WindowMessage::setTexts(const std::vector<std::string>& texts) {
texts_ = texts;
triggerAutoResize();
}
void WindowMessage::addText(const std::string& text) {
texts_.push_back(text);
triggerAutoResize();
}
void WindowMessage::clearTexts() {
texts_.clear();
triggerAutoResize();
}
void WindowMessage::setPosition(float x, float y, PositionMode mode) {
anchor_ = {.x = x, .y = y};
position_mode_ = mode;
updatePosition();
}
void WindowMessage::setSize(float width, float height) {
rect_.w = width;
rect_.h = height;
updatePosition(); // Reposicionar después de cambiar el tamaño
}
void WindowMessage::centerOnScreen() {
setPosition(getScreenWidth() / 2.0F, getScreenHeight() / 2.0F, PositionMode::CENTERED);
}
void WindowMessage::autoSize() {
if (show_hide_animation_.active) {
return; // No redimensionar durante show/hide
}
if (resize_animation_.active) {
resize_animation_.stop(); // Detener animación anterior
}
float old_width = rect_.w;
float old_height = rect_.h;
calculateAutoSize();
// Solo animar si hay cambio en el tamaño y la ventana está visible
if (visible_ && (old_width != rect_.w || old_height != rect_.h)) {
resize_animation_.start(old_width, old_height, rect_.w, rect_.h);
// Restaurar el tamaño anterior para que la animación funcione
rect_.w = old_width;
rect_.h = old_height;
} else {
updatePosition(); // Reposicionar después de ajustar el tamaño
}
}
void WindowMessage::updateStyles() {
title_style_ = Text::Style(Text::CENTER | Text::COLOR, config_.title_color, config_.title_color, 0, -2);
text_style_ = Text::Style(Text::CENTER | Text::COLOR, config_.text_color, config_.text_color, 0, -2);
}
void WindowMessage::updatePosition() {
switch (position_mode_) {
case PositionMode::CENTERED:
rect_.x = anchor_.x - (rect_.w / 2.0F);
rect_.y = anchor_.y - (rect_.h / 2.0F);
break;
case PositionMode::FIXED:
rect_.x = anchor_.x;
rect_.y = anchor_.y;
break;
}
// Asegurar que la ventana esté dentro de los límites de la pantalla
rect_.x = std::max(0.0F, std::min(rect_.x, getScreenWidth() - rect_.w));
rect_.y = std::max(0.0F, std::min(rect_.y, getScreenHeight() - rect_.h));
}
void WindowMessage::ensureTextFits() {
float required_width = calculateContentWidth() + (config_.padding * 2) + config_.text_safety_margin;
float required_height = calculateContentHeight() + (config_.padding * 2) + config_.text_safety_margin;
// Verificar si el tamaño actual es suficiente
if (rect_.w < required_width || rect_.h < required_height) {
autoSize(); // Recalcular tamaño automáticamente
}
}
void WindowMessage::calculateAutoSize() {
float content_width = calculateContentWidth();
float content_height = calculateContentHeight();
// Calcular dimensiones con padding y margen de seguridad
rect_.w = content_width + (config_.padding * 2) + config_.text_safety_margin;
rect_.h = content_height + (config_.padding * 2);
// Aplicar límites mínimos
rect_.w = std::max(rect_.w, config_.min_width);
rect_.h = std::max(rect_.h, config_.min_height);
// Aplicar límites máximos basados en el tamaño de pantalla
float max_width = getScreenWidth() * config_.max_width_ratio;
float max_height = getScreenHeight() * config_.max_height_ratio;
rect_.w = std::min(rect_.w, max_width);
rect_.h = std::min(rect_.h, max_height);
}
auto WindowMessage::calculateContentHeight() const -> float {
float height = 0;
// Altura del título
if (!title_.empty()) {
height += text_renderer_->getCharacterSize() + config_.title_separator_spacing;
}
// Altura de los textos
if (!texts_.empty()) {
height += (texts_.size() * text_renderer_->getCharacterSize());
if (texts_.size() > 1) {
height += ((texts_.size() - 1) * config_.line_spacing);
}
}
return height;
}
auto WindowMessage::calculateContentWidth() const -> float {
float max_width = config_.min_width - (config_.padding * 2); // Ancho mínimo sin padding
// Ancho del título
if (!title_.empty()) {
float title_width = text_renderer_->length(title_, -2);
max_width = std::max(max_width, title_width);
}
// Ancho de los textos
for (const auto& text : texts_) {
float text_width = text_renderer_->length(text, -2);
max_width = std::max(max_width, text_width);
}
return max_width;
}
auto WindowMessage::getScreenWidth() -> float {
return param.game.width;
}
auto WindowMessage::getScreenHeight() -> float {
return param.game.height;
}
void WindowMessage::triggerAutoResize() {
if (auto_resize_enabled_) {
autoSize();
}
}
void WindowMessage::updateAnimation(float delta_time) {
if (show_hide_animation_.active) {
updateShowHideAnimation(delta_time);
}
if (resize_animation_.active) {
updateResizeAnimation(delta_time);
}
}
void WindowMessage::updateShowHideAnimation(float delta_time) {
if (!show_hide_animation_.active) {
return;
}
show_hide_animation_.elapsed += delta_time;
if (show_hide_animation_.isFinished(config_.animation_duration)) {
// Animación terminada
if (show_hide_animation_.type == ShowHideAnimation::Type::SHOWING) {
// Mostrar completado
rect_.w = show_hide_animation_.target_width;
rect_.h = show_hide_animation_.target_height;
} else if (show_hide_animation_.type == ShowHideAnimation::Type::HIDING) {
// Ocultar completado
rect_.w = 0.0F;
rect_.h = 0.0F;
visible_ = false;
}
show_hide_animation_.stop();
updatePosition();
} else {
// Interpolar el tamaño
float progress = easeOut(show_hide_animation_.getProgress(config_.animation_duration));
if (show_hide_animation_.type == ShowHideAnimation::Type::SHOWING) {
// Crecer desde 0 hasta el tamaño objetivo
rect_.w = show_hide_animation_.target_width * progress;
rect_.h = show_hide_animation_.target_height * progress;
} else if (show_hide_animation_.type == ShowHideAnimation::Type::HIDING) {
// Decrecer desde el tamaño actual hasta 0
rect_.w = show_hide_animation_.target_width * (1.0F - progress);
rect_.h = show_hide_animation_.target_height * (1.0F - progress);
}
updatePosition(); // Mantener la posición centrada durante la animación
}
}
void WindowMessage::updateResizeAnimation(float delta_time) {
if (!resize_animation_.active) {
return;
}
resize_animation_.elapsed += delta_time;
if (resize_animation_.isFinished(config_.animation_duration)) {
// Animación terminada
rect_.w = resize_animation_.target_width;
rect_.h = resize_animation_.target_height;
resize_animation_.stop();
updatePosition();
} else {
// Interpolar el tamaño
float progress = easeOut(resize_animation_.getProgress(config_.animation_duration));
rect_.w = resize_animation_.start_width +
((resize_animation_.target_width - resize_animation_.start_width) * progress);
rect_.h = resize_animation_.start_height +
((resize_animation_.target_height - resize_animation_.start_height) * progress);
updatePosition(); // Mantener la posición centrada durante la animación
}
}
auto WindowMessage::shouldShowContent() const -> bool {
// No mostrar contenido durante animaciones de show/hide
return !show_hide_animation_.active;
}
auto WindowMessage::easeOut(float t) -> float {
// Función de suavizado ease-out cuadrática
return 1.0F - ((1.0F - t) * (1.0F - t));
}
auto WindowMessage::getAvailableTextWidth() const -> float {
// Ancho disponible = ancho total - padding en ambos lados
return rect_.w - (config_.padding * 2.0F);
}
auto WindowMessage::getTruncatedText(const std::string& text, float available_width) const -> std::string {
if (text.empty()) {
return text;
}
// Si el texto completo cabe, devolverlo tal como está
int text_width = text_renderer_->length(text, -2);
if (text_width <= available_width) {
return text;
}
// Si no hay espacio suficiente, devolver string vacío
if (available_width < 10.0F) { // Mínimo espacio para al menos un carácter
return "";
}
// Buscar cuántos caracteres caben usando búsqueda binaria
int left = 0;
int right = text.length();
int best_length = 0;
while (left <= right) {
int mid = (left + right) / 2;
std::string partial = text.substr(0, mid);
int partial_width = text_renderer_->length(partial, -2);
if (partial_width <= available_width) {
best_length = mid;
left = mid + 1;
} else {
right = mid - 1;
}
}
return text.substr(0, best_length);
}

View File

@@ -0,0 +1,244 @@
#pragma once
#include <SDL3/SDL.h> // Para SDL_FPoint, SDL_FRect
#include <algorithm> // Para min
#include <cstdint> // Para std::uint8_t
#include <memory> // Para allocator, shared_ptr
#include <string> // Para string
#include <vector> // Para vector
#include "color.hpp" // Para Color
#include "param.hpp" // Para param
#include "text.hpp" // Para Text
class WindowMessage {
public:
enum class PositionMode : std::uint8_t {
CENTERED, // La ventana se centra en el punto especificado
FIXED // La esquina superior izquierda coincide con el punto
};
struct Config {
// Colores
Color bg_color;
Color border_color;
Color title_color;
Color text_color;
// Espaciado y dimensiones
float padding{15.0F};
float line_spacing{5.0F};
float title_separator_spacing{10.0F}; // Espacio extra para separador del título
// Límites de tamaño
float min_width{200.0F};
float min_height{100.0F};
float max_width_ratio{0.8F}; // % máximo de ancho de pantalla
float max_height_ratio{0.8F}; // % máximo de alto de pantalla
// Margen de seguridad para texto
float text_safety_margin{20.0F}; // Margen extra para evitar texto cortado
// Animaciones
float animation_duration{0.3F}; // Duración en segundos para todas las animaciones
// Constructor con valores por defecto
Config()
: bg_color{40, 40, 60, 220},
border_color{100, 100, 120, 255},
title_color{255, 255, 255, 255},
text_color{200, 200, 200, 255} {}
// Constructor que convierte desde ParamServiceMenu::WindowMessage
Config(const ParamServiceMenu::WindowMessage& param_config)
: bg_color(param_config.bg_color),
border_color(param_config.border_color),
title_color(param_config.title_color),
text_color(param_config.text_color),
padding(param_config.padding),
line_spacing(param_config.line_spacing),
title_separator_spacing(param_config.title_separator_spacing),
min_width(param_config.min_width),
min_height(param_config.min_height),
max_width_ratio(param_config.max_width_ratio),
max_height_ratio(param_config.max_height_ratio),
text_safety_margin(param_config.text_safety_margin),
animation_duration(param_config.animation_duration) {}
};
WindowMessage(
std::shared_ptr<Text> text_renderer,
std::string title = "",
const Config& config = Config{});
// Métodos principales
void render();
void update(float delta_time);
// Control de visibilidad
void show();
void hide();
[[nodiscard]] auto isVisible() const -> bool { return visible_; }
[[nodiscard]] auto isFullyVisible() const -> bool { return visible_ && !show_hide_animation_.active; }
[[nodiscard]] auto isAnimating() const -> bool { return resize_animation_.active || show_hide_animation_.active; }
// Configuración de contenido
void setTitle(const std::string& title);
void setText(const std::string& text);
void setTexts(const std::vector<std::string>& texts);
void addText(const std::string& text);
void clearTexts();
// Control de redimensionado automático
void enableAutoResize(bool enabled) { auto_resize_enabled_ = enabled; }
[[nodiscard]] auto isAutoResizeEnabled() const -> bool { return auto_resize_enabled_; }
// Configuración de posición y tamaño
void setPosition(float x, float y, PositionMode mode = PositionMode::CENTERED);
void setSize(float width, float height);
void centerOnScreen();
void autoSize(); // Ajusta automáticamente al contenido y reposiciona si es necesario
// Configuración de colores
void setBackgroundColor(const Color& color) { config_.bg_color = color; }
void setBorderColor(const Color& color) { config_.border_color = color; }
void setTitleColor(const Color& color) {
config_.title_color = color;
updateStyles();
}
void setTextColor(const Color& color) {
config_.text_color = color;
updateStyles();
}
// Configuración de espaciado
void setPadding(float padding) { config_.padding = padding; }
void setLineSpacing(float spacing) { config_.line_spacing = spacing; }
// Configuración avanzada
void setConfig(const Config& config) {
config_ = config;
updateStyles();
}
[[nodiscard]] auto getConfig() const -> const Config& { return config_; }
// Getters
[[nodiscard]] auto getRect() const -> const SDL_FRect& { return rect_; }
[[nodiscard]] auto getPositionMode() const -> PositionMode { return position_mode_; }
[[nodiscard]] auto getAnchorPoint() const -> SDL_FPoint { return anchor_; }
private:
std::shared_ptr<Text> text_renderer_;
Config config_;
// Estado de visibilidad y redimensionado
bool visible_ = false;
bool auto_resize_enabled_ = true; // Por defecto habilitado
// Contenido
std::string title_;
std::vector<std::string> texts_;
// Posición y tamaño
SDL_FRect rect_{.x = 0, .y = 0, .w = 300, .h = 200};
PositionMode position_mode_ = PositionMode::CENTERED;
SDL_FPoint anchor_{.x = 0.0F, .y = 0.0F};
// Animación de redimensionado
struct ResizeAnimation {
bool active = false;
float start_width, start_height;
float target_width, target_height;
float elapsed = 0.0F;
void start(float from_w, float from_h, float to_w, float to_h) {
start_width = from_w;
start_height = from_h;
target_width = to_w;
target_height = to_h;
elapsed = 0.0F;
active = true;
}
void stop() {
active = false;
elapsed = 0.0F;
}
[[nodiscard]] auto isFinished(float duration) const -> bool {
return elapsed >= duration;
}
[[nodiscard]] auto getProgress(float duration) const -> float {
return std::min(elapsed / duration, 1.0F);
}
} resize_animation_;
// Animación de mostrar/ocultar
struct ShowHideAnimation {
enum class Type : std::uint8_t { NONE,
SHOWING,
HIDING };
Type type = Type::NONE;
bool active = false;
float target_width, target_height; // Tamaño final al mostrar
float elapsed = 0.0F;
void startShow(float to_w, float to_h) {
type = Type::SHOWING;
target_width = to_w;
target_height = to_h;
elapsed = 0.0F;
active = true;
}
void startHide() {
type = Type::HIDING;
elapsed = 0.0F;
active = true;
}
void stop() {
type = Type::NONE;
active = false;
elapsed = 0.0F;
}
[[nodiscard]] auto isFinished(float duration) const -> bool {
return elapsed >= duration;
}
[[nodiscard]] auto getProgress(float duration) const -> float {
return std::min(elapsed / duration, 1.0F);
}
} show_hide_animation_;
// Estilos
Text::Style title_style_;
Text::Style text_style_;
// Métodos privados
void calculateAutoSize();
void updatePosition(); // Actualiza la posición según el modo y punto de anclaje
void updateStyles(); // Actualiza los estilos de texto cuando cambian los colores
void ensureTextFits(); // Verifica y ajusta para que todo el texto sea visible
void triggerAutoResize(); // Inicia redimensionado automático si está habilitado
void updateAnimation(float delta_time); // Actualiza la animación de redimensionado
void updateShowHideAnimation(float delta_time); // Actualiza la animación de mostrar/ocultar
void updateResizeAnimation(float delta_time); // Actualiza la animación de redimensionado
// Función de suavizado (ease-out)
[[nodiscard]] static auto easeOut(float t) -> float;
// Métodos para manejo de texto durante animación
[[nodiscard]] auto getTruncatedText(const std::string& text, float available_width) const -> std::string;
[[nodiscard]] auto getAvailableTextWidth() const -> float;
[[nodiscard]] auto shouldShowContent() const -> bool; // Si mostrar el contenido (texto, líneas, etc.)
[[nodiscard]] auto calculateContentHeight() const -> float;
[[nodiscard]] auto calculateContentWidth() const -> float;
[[nodiscard]] static auto getScreenWidth() -> float;
[[nodiscard]] static auto getScreenHeight() -> float;
};