menu ara permet amagar items en funció d'altres items
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
#include "core/rendering/menu.hpp"
|
#include "core/rendering/menu.hpp"
|
||||||
|
|
||||||
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <functional>
|
#include <functional>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
@@ -39,6 +40,7 @@ namespace Menu {
|
|||||||
|
|
||||||
// --- Animació ---
|
// --- Animació ---
|
||||||
static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s
|
static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s
|
||||||
|
static constexpr float HEIGHT_RATE = 12.0F; // smoothing exponencial de l'alçada (~150 ms al 90%)
|
||||||
|
|
||||||
// --- Items ---
|
// --- Items ---
|
||||||
enum class ItemKind { Toggle,
|
enum class ItemKind { Toggle,
|
||||||
@@ -54,6 +56,7 @@ namespace Menu {
|
|||||||
std::function<void(int dir)> change; // per Toggle/Cycle/IntRange
|
std::function<void(int dir)> change; // per Toggle/Cycle/IntRange
|
||||||
std::function<void()> enter; // per Submenu
|
std::function<void()> enter; // per Submenu
|
||||||
SDL_Scancode* scancode{nullptr}; // per KeyBind
|
SDL_Scancode* scancode{nullptr}; // per KeyBind
|
||||||
|
std::function<bool()> visible; // nullptr ⇒ sempre visible
|
||||||
};
|
};
|
||||||
|
|
||||||
struct Page {
|
struct Page {
|
||||||
@@ -62,10 +65,25 @@ namespace Menu {
|
|||||||
int cursor{0};
|
int cursor{0};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
static bool isVisible(const Item& it) { return !it.visible || it.visible(); }
|
||||||
|
|
||||||
|
// Troba el pròxim ítem visible en direcció `dir` (±1) a partir de `from`.
|
||||||
|
// Si cap és visible retorna `from`.
|
||||||
|
static int nextVisibleCursor(const Page& p, int from, int dir) {
|
||||||
|
const int n = static_cast<int>(p.items.size());
|
||||||
|
if (n <= 0) return from;
|
||||||
|
for (int i = 1; i <= n; ++i) {
|
||||||
|
int idx = ((from + dir * i) % n + n) % n;
|
||||||
|
if (isVisible(p.items[idx])) return idx;
|
||||||
|
}
|
||||||
|
return from;
|
||||||
|
}
|
||||||
|
|
||||||
// --- Estat ---
|
// --- Estat ---
|
||||||
static std::vector<Page> stack_;
|
static std::vector<Page> stack_;
|
||||||
static std::unique_ptr<Text> font_;
|
static std::unique_ptr<Text> font_;
|
||||||
static float open_anim_{0.0F}; // 0 = tancat, 1 = obert
|
static float open_anim_{0.0F}; // 0 = tancat, 1 = obert
|
||||||
|
static float animated_h_{0.0F}; // alçada actual animada (smoothing cap al target visible)
|
||||||
static Uint32 last_ticks_{0};
|
static Uint32 last_ticks_{0};
|
||||||
static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar
|
static SDL_Scancode* capturing_{nullptr}; // != null → esperant tecla per assignar
|
||||||
|
|
||||||
@@ -116,44 +134,60 @@ namespace Menu {
|
|||||||
static Page buildVideo() {
|
static Page buildVideo() {
|
||||||
Page p{Locale::get("menu.titles.video"), {}, 0};
|
Page p{Locale::get("menu.titles.video"), {}, 0};
|
||||||
|
|
||||||
|
// Zoom i fullscreen: sense sentit a WASM (el navegador posseix el canvas)
|
||||||
|
#ifndef __EMSCRIPTEN__
|
||||||
p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::IntRange, [] {
|
p.items.push_back({Locale::get("menu.items.zoom"), ItemKind::IntRange, [] {
|
||||||
char buf[16];
|
char buf[16];
|
||||||
std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
|
std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
|
||||||
return std::string(buf); }, [](int dir) {
|
return std::string(buf); }, [](int dir) {
|
||||||
if (dir < 0) Screen::get()->decZoom();
|
if (dir < 0) Screen::get()->decZoom();
|
||||||
else if (dir > 0) Screen::get()->incZoom(); }, nullptr});
|
else if (dir > 0) Screen::get()->incZoom(); }, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.screen"), ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.screen"), ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? Locale::get("menu.values.fullscreen") : Locale::get("menu.values.windowed")); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr, nullptr, nullptr});
|
||||||
|
#endif
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr});
|
// Opcions visuals generals (sempre visibles)
|
||||||
|
p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.aspect_4_3"), ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::Toggle, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.integer_scale"), ItemKind::Toggle, [] { return onOff(Options::video.integer_scale); }, [](int) { Screen::get()->toggleIntegerScale(); }, nullptr, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.vsync"), ItemKind::Toggle, [] { return onOff(Options::video.vsync); }, [](int) { Screen::get()->toggleVSync(); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.stretch_filter"), ItemKind::Toggle, [] { return std::string(Options::video.stretch_filter_linear ? Locale::get("menu.values.linear") : Locale::get("menu.values.nearest")); }, [](int) { Screen::get()->toggleStretchFilter(); }, nullptr, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.integer_scale"), ItemKind::Toggle, [] { return onOff(Options::video.integer_scale); }, [](int) { Screen::get()->toggleIntegerScale(); }, nullptr});
|
// Bloc shaders: no disponible a WASM (NO_SHADERS, sense SDL3 GPU a WebGL2)
|
||||||
|
#ifndef __EMSCRIPTEN__
|
||||||
|
p.items.push_back({Locale::get("menu.items.shader"), ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) {
|
p.items.push_back({Locale::get("menu.items.shader_type"), ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) {
|
||||||
if (dir < 0) Screen::get()->prevShaderType();
|
if (dir < 0) Screen::get()->prevShaderType();
|
||||||
else Screen::get()->nextShaderType(); }, nullptr});
|
else Screen::get()->nextShaderType(); }, nullptr, nullptr,
|
||||||
|
[] { return Options::video.shader_enabled; }});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.preset"), ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) {
|
p.items.push_back({Locale::get("menu.items.preset"), ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) {
|
||||||
if (dir < 0) Screen::get()->prevPreset();
|
if (dir < 0) Screen::get()->prevPreset();
|
||||||
else Screen::get()->nextPreset(); }, nullptr});
|
else Screen::get()->nextPreset(); }, nullptr, nullptr,
|
||||||
|
[] { return Options::video.shader_enabled; }});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.stretch_filter"), ItemKind::Toggle, [] { return std::string(Options::video.stretch_filter_linear ? Locale::get("menu.values.linear") : Locale::get("menu.values.nearest")); }, [](int) { Screen::get()->toggleStretchFilter(); }, nullptr});
|
p.items.push_back({Locale::get("menu.items.supersampling"), ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr, nullptr,
|
||||||
|
[] {
|
||||||
|
if (!Options::video.shader_enabled) return false;
|
||||||
|
const char* name = Screen::get()->getActiveShaderName();
|
||||||
|
return name && std::string(name) == "POSTFX";
|
||||||
|
}});
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Informació de render
|
||||||
p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::Cycle, [] {
|
p.items.push_back({Locale::get("menu.items.render_info"), ItemKind::Cycle, [] {
|
||||||
switch (Options::render_info.position) {
|
switch (Options::render_info.position) {
|
||||||
case Options::RenderInfoPosition::OFF: return std::string(Locale::get("menu.values.off"));
|
case Options::RenderInfoPosition::OFF: return std::string(Locale::get("menu.values.off"));
|
||||||
case Options::RenderInfoPosition::TOP: return std::string(Locale::get("menu.values.top"));
|
case Options::RenderInfoPosition::TOP: return std::string(Locale::get("menu.values.top"));
|
||||||
case Options::RenderInfoPosition::BOTTOM: return std::string(Locale::get("menu.values.bottom"));
|
case Options::RenderInfoPosition::BOTTOM: return std::string(Locale::get("menu.values.bottom"));
|
||||||
}
|
}
|
||||||
return std::string(Locale::get("menu.values.off")); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr});
|
return std::string(Locale::get("menu.values.off")); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr, nullptr, nullptr});
|
||||||
|
|
||||||
p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::Toggle, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr});
|
p.items.push_back({Locale::get("menu.items.uptime"), ItemKind::Toggle, [] { return onOff(Options::render_info.show_time); }, [](int) { Options::render_info.show_time = !Options::render_info.show_time; }, nullptr, nullptr,
|
||||||
|
[] { return Options::render_info.position != Options::RenderInfoPosition::OFF; }});
|
||||||
|
|
||||||
return p;
|
return p;
|
||||||
}
|
}
|
||||||
@@ -263,9 +297,12 @@ namespace Menu {
|
|||||||
fillRect(buf, x + w - 1, y, 1, h, color);
|
fillRect(buf, x + w - 1, y, 1, h, color);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mida final de la caixa segons el nombre d'items
|
// Mida final de la caixa segons el nombre d'items *visibles*
|
||||||
static int boxHeight(const Page& page) {
|
static int boxHeight(const Page& page) {
|
||||||
int n = static_cast<int>(page.items.size());
|
int n = 0;
|
||||||
|
for (const auto& it : page.items) {
|
||||||
|
if (isVisible(it)) ++n;
|
||||||
|
}
|
||||||
int body = (n == 0) ? ITEM_SPACING : n * ITEM_SPACING;
|
int body = (n == 0) ? ITEM_SPACING : n * ITEM_SPACING;
|
||||||
return HEADER_H + body + BOTTOM_PAD;
|
return HEADER_H + body + BOTTOM_PAD;
|
||||||
}
|
}
|
||||||
@@ -294,6 +331,7 @@ namespace Menu {
|
|||||||
} else {
|
} else {
|
||||||
stack_.push_back(buildRoot());
|
stack_.push_back(buildRoot());
|
||||||
open_anim_ = 0.0F;
|
open_anim_ = 0.0F;
|
||||||
|
animated_h_ = static_cast<float>(boxHeight(stack_.back()));
|
||||||
last_ticks_ = SDL_GetTicks();
|
last_ticks_ = SDL_GetTicks();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -301,6 +339,7 @@ namespace Menu {
|
|||||||
void close() {
|
void close() {
|
||||||
stack_.clear();
|
stack_.clear();
|
||||||
open_anim_ = 0.0F;
|
open_anim_ = 0.0F;
|
||||||
|
animated_h_ = 0.0F;
|
||||||
capturing_ = nullptr;
|
capturing_ = nullptr;
|
||||||
transition_active_ = false;
|
transition_active_ = false;
|
||||||
transition_progress_ = 1.0F;
|
transition_progress_ = 1.0F;
|
||||||
@@ -334,13 +373,17 @@ namespace Menu {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const int n = static_cast<int>(page.items.size());
|
// Si el cursor està sobre un ítem ocultat (p. ex. una acció anterior el va ocultar),
|
||||||
|
// reubica'l al pròxim visible abans de processar l'entrada.
|
||||||
|
if (!isVisible(page.items[page.cursor])) {
|
||||||
|
page.cursor = nextVisibleCursor(page, page.cursor, +1);
|
||||||
|
}
|
||||||
switch (sc) {
|
switch (sc) {
|
||||||
case SDL_SCANCODE_UP:
|
case SDL_SCANCODE_UP:
|
||||||
page.cursor = (page.cursor - 1 + n) % n;
|
page.cursor = nextVisibleCursor(page, page.cursor, -1);
|
||||||
break;
|
break;
|
||||||
case SDL_SCANCODE_DOWN:
|
case SDL_SCANCODE_DOWN:
|
||||||
page.cursor = (page.cursor + 1) % n;
|
page.cursor = nextVisibleCursor(page, page.cursor, +1);
|
||||||
break;
|
break;
|
||||||
case SDL_SCANCODE_LEFT:
|
case SDL_SCANCODE_LEFT:
|
||||||
if (page.items[page.cursor].kind != ItemKind::Submenu &&
|
if (page.items[page.cursor].kind != ItemKind::Submenu &&
|
||||||
@@ -373,6 +416,15 @@ namespace Menu {
|
|||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
// Després de qualsevol acció, si el cursor quedara sobre un ítem ocult
|
||||||
|
// (possible si una acció ha canviat la visibilitat pròpia de l'ítem actual,
|
||||||
|
// edge case defensiu), salta al següent visible.
|
||||||
|
if (!stack_.empty()) {
|
||||||
|
Page& top = stack_.back();
|
||||||
|
if (!top.items.empty() && !isVisible(top.items[top.cursor])) {
|
||||||
|
top.cursor = nextVisibleCursor(top, top.cursor, +1);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip.
|
// Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip.
|
||||||
@@ -398,17 +450,23 @@ namespace Menu {
|
|||||||
|
|
||||||
// Items o placeholder buit
|
// Items o placeholder buit
|
||||||
int items_y = title_line_y + 4;
|
int items_y = title_line_y + 4;
|
||||||
if (page.items.empty()) {
|
// Compta visibles — si cap, dibuixa placeholder (caixa totalment col·lapsada però oberta)
|
||||||
|
int visible_count = 0;
|
||||||
|
for (const auto& it : page.items) if (isVisible(it)) ++visible_count;
|
||||||
|
if (visible_count == 0) {
|
||||||
const char* empty_text = Locale::get("menu.values.empty");
|
const char* empty_text = Locale::get("menu.values.empty");
|
||||||
int ew = font_->width(empty_text);
|
int ew = font_->width(empty_text);
|
||||||
font_->drawClipped(pixel_data, box_x + (BOX_W - ew) / 2 + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
font_->drawClipped(pixel_data, box_x + (BOX_W - ew) / 2 + x_offset, items_y + 2, empty_text, EMPTY_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int y_slot = 0; // índex de fila visible (independent de l'índex real de l'ítem)
|
||||||
for (size_t i = 0; i < page.items.size(); i++) {
|
for (size_t i = 0; i < page.items.size(); i++) {
|
||||||
int y = items_y + static_cast<int>(i) * ITEM_SPACING;
|
|
||||||
bool selected = (static_cast<int>(i) == page.cursor);
|
|
||||||
const Item& item = page.items[i];
|
const Item& item = page.items[i];
|
||||||
|
if (!isVisible(item)) continue;
|
||||||
|
int y = items_y + y_slot * ITEM_SPACING;
|
||||||
|
++y_slot;
|
||||||
|
bool selected = (static_cast<int>(i) == page.cursor);
|
||||||
|
|
||||||
if (selected) {
|
if (selected) {
|
||||||
font_->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
font_->drawClipped(pixel_data, box_x + 4 + x_offset, y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
|
||||||
@@ -462,14 +520,30 @@ namespace Menu {
|
|||||||
const Page& page = stack_.back();
|
const Page& page = stack_.back();
|
||||||
const int current_h = boxHeight(page);
|
const int current_h = boxHeight(page);
|
||||||
|
|
||||||
|
// Smoothing exponencial de l'alçada cap al target (pàgina actual + ítems visibles).
|
||||||
|
// Permet que el menú reaccione amb animació quan una opció canvia la visibilitat
|
||||||
|
// d'altres ítems en calent (p. ex. shader=off → shader_type/preset/supersampling).
|
||||||
|
if (animated_h_ <= 0.0F) {
|
||||||
|
animated_h_ = static_cast<float>(current_h);
|
||||||
|
} else {
|
||||||
|
float diff = static_cast<float>(current_h) - animated_h_;
|
||||||
|
if (std::fabs(diff) < 0.5F) {
|
||||||
|
animated_h_ = static_cast<float>(current_h);
|
||||||
|
} else {
|
||||||
|
float t = HEIGHT_RATE * dt;
|
||||||
|
if (t > 1.0F) t = 1.0F;
|
||||||
|
animated_h_ += diff * t;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float eased = Easing::outQuad(open_anim_);
|
float eased = Easing::outQuad(open_anim_);
|
||||||
|
|
||||||
// Calcula alçada (amb transició si escau)
|
// Calcula alçada (amb transició si escau)
|
||||||
int target_h = current_h;
|
int target_h = static_cast<int>(animated_h_);
|
||||||
if (transition_active_) {
|
if (transition_active_) {
|
||||||
int outgoing_h = boxHeight(transition_outgoing_);
|
int outgoing_h = boxHeight(transition_outgoing_);
|
||||||
float tp = Easing::outQuad(transition_progress_);
|
float tp = Easing::outQuad(transition_progress_);
|
||||||
target_h = Easing::lerpInt(outgoing_h, current_h, tp);
|
target_h = Easing::lerpInt(outgoing_h, static_cast<int>(animated_h_), tp);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Caixa creix verticalment durant l'obertura
|
// Caixa creix verticalment durant l'obertura
|
||||||
|
|||||||
Reference in New Issue
Block a user