reestructuració
This commit is contained in:
74
source/game/ui/menu_option.cpp
Normal file
74
source/game/ui/menu_option.cpp
Normal 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
|
||||
}
|
||||
241
source/game/ui/menu_option.hpp
Normal file
241
source/game/ui/menu_option.hpp
Normal 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_;
|
||||
};
|
||||
417
source/game/ui/menu_renderer.cpp
Normal file
417
source/game/ui/menu_renderer.cpp
Normal 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; }
|
||||
129
source/game/ui/menu_renderer.hpp
Normal file
129
source/game/ui/menu_renderer.hpp
Normal 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
310
source/game/ui/notifier.cpp
Normal 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
112
source/game/ui/notifier.hpp
Normal 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
|
||||
};
|
||||
733
source/game/ui/service_menu.cpp
Normal file
733
source/game/ui/service_menu.cpp
Normal 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_);
|
||||
}
|
||||
}
|
||||
}
|
||||
129
source/game/ui/service_menu.hpp
Normal file
129
source/game/ui/service_menu.hpp
Normal 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;
|
||||
};
|
||||
98
source/game/ui/ui_message.cpp
Normal file
98
source/game/ui/ui_message.cpp
Normal 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;
|
||||
}
|
||||
56
source/game/ui/ui_message.hpp
Normal file
56
source/game/ui/ui_message.hpp
Normal 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);
|
||||
};
|
||||
416
source/game/ui/window_message.cpp
Normal file
416
source/game/ui/window_message.cpp
Normal 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);
|
||||
}
|
||||
244
source/game/ui/window_message.hpp
Normal file
244
source/game/ui/window_message.hpp
Normal 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;
|
||||
};
|
||||
Reference in New Issue
Block a user