menu i opcions de audio

This commit is contained in:
2026-04-05 00:07:10 +02:00
parent be4b62414e
commit 6d42f848a5
5 changed files with 233 additions and 86 deletions

View File

@@ -18,9 +18,6 @@ namespace Menu {
static constexpr int SCREEN_H = 200;
static constexpr int BOX_W = 220;
static constexpr int BOX_H = 150;
static constexpr int BOX_X = (SCREEN_W - BOX_W) / 2; // 50
static constexpr int BOX_Y = (SCREEN_H - BOX_H) / 2; // 25
static constexpr Uint32 BG_COLOR = 0xFF1A0E0E; // fons marró fosc (ABGR)
static constexpr Uint8 BG_ALPHA = 220; // semi-transparent
@@ -29,87 +26,144 @@ namespace Menu {
static constexpr Uint32 LABEL_COLOR = 0xFFCCCCCC; // gris clar
static constexpr Uint32 VALUE_COLOR = 0xFFFFFF00; // cyan
static constexpr Uint32 CURSOR_COLOR = 0xFF00FFFF; // groc
static constexpr Uint32 FOOTER_COLOR = 0xFF888888; // gris
static constexpr Uint32 EMPTY_COLOR = 0xFF888888; // gris
static constexpr int TITLE_PAD_Y = 4;
static constexpr int ITEM_PAD_X = 10;
static constexpr int ITEM_SPACING = 11; // 8 px glifo + 3 pad
static constexpr int FOOTER_PAD_Y = 4;
static constexpr int ITEM_SPACING = 11;
static constexpr int BOTTOM_PAD = 6;
static constexpr int HEADER_H = TITLE_PAD_Y + 8 /*charH*/ + 2 + 4; // títol + línia + gap
// --- Animació ---
static constexpr float OPEN_SPEED = 8.0F; // 1.0 / 0.125s
// --- Items ---
enum class ItemKind { Toggle,
Cycle,
IntRange };
IntRange,
Submenu };
struct Item {
const char* label;
ItemKind kind;
std::function<std::string()> getValue;
std::function<void(int dir)> change; // dir: -1=left, +1=right
std::function<std::string()> getValue; // opcional
std::function<void(int dir)> change; // per Toggle/Cycle/IntRange
std::function<void()> enter; // per Submenu
};
struct Page {
const char* title;
std::vector<Item> items;
int cursor{0};
};
// --- Estat ---
static bool open_ = false;
static int cursor_ = 0;
static std::vector<Item> items_;
static std::vector<Page> stack_;
static std::unique_ptr<Text> font_;
static float open_anim_{0.0F}; // 0 = tancat, 1 = obert
static Uint32 last_ticks_{0};
// --- Helpers ---
static std::string yesNo(bool b) { return b ? "SI" : "NO"; }
static std::string onOff(bool b) { return b ? "ON" : "OFF"; }
// Construeix la llista d'items (Video)
static void buildItems() {
items_.clear();
// --- Builders de pàgines ---
// ZOOM
items_.push_back({"ZOOM", ItemKind::IntRange, [] {
static Page buildVideo();
static Page buildAudio();
static Page buildRoot() {
Page p{"OPCIONS", {}, 0};
p.items.push_back({"VIDEO", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildVideo()); }});
p.items.push_back({"AUDIO", ItemKind::Submenu, nullptr, nullptr, [] { stack_.push_back(buildAudio()); }});
return p;
}
static Page buildVideo() {
Page p{"VIDEO", {}, 0};
p.items.push_back({"ZOOM", ItemKind::IntRange, [] {
char buf[16];
std::snprintf(buf, sizeof(buf), "%dX", Screen::get()->getZoom());
return std::string(buf); }, [](int dir) {
if (dir < 0) Screen::get()->decZoom();
else if (dir > 0) Screen::get()->incZoom(); }});
else if (dir > 0) Screen::get()->incZoom(); }, nullptr});
// PANTALLA (fullscreen)
items_.push_back({"PANTALLA", ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? "COMPLETA" : "FINESTRA"); }, [](int) { Screen::get()->toggleFullscreen(); }});
p.items.push_back({"PANTALLA", ItemKind::Toggle, [] { return std::string(Screen::get()->isFullscreen() ? "COMPLETA" : "FINESTRA"); }, [](int) { Screen::get()->toggleFullscreen(); }, nullptr});
// SHADER
items_.push_back({"SHADER", ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }});
p.items.push_back({"SHADER", ItemKind::Toggle, [] { return onOff(Options::video.shader_enabled); }, [](int) { Screen::get()->toggleShaders(); }, nullptr});
// ASPECTE 4:3
items_.push_back({"ASPECTE 4:3", ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }});
p.items.push_back({"ASPECTE 4:3", ItemKind::Toggle, [] { return yesNo(Options::video.aspect_ratio_4_3); }, [](int) { Screen::get()->toggleAspectRatio(); }, nullptr});
// SUPERSAMPLING
items_.push_back({"SUPERSAMPLING", ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }});
p.items.push_back({"SUPERSAMPLING", ItemKind::Toggle, [] { return onOff(Options::video.supersampling); }, [](int) { Screen::get()->toggleSupersampling(); }, nullptr});
// TIPUS SHADER
items_.push_back({"TIPUS SHADER", ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) {
p.items.push_back({"TIPUS SHADER", ItemKind::Cycle, [] { return std::string(Screen::get()->getActiveShaderName()); }, [](int dir) {
if (dir < 0) Screen::get()->prevShaderType();
else Screen::get()->nextShaderType(); }});
else Screen::get()->nextShaderType(); }, nullptr});
// PRESET
items_.push_back({"PRESET", ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) {
p.items.push_back({"PRESET", ItemKind::Cycle, [] { return std::string(Screen::get()->getCurrentPresetName()); }, [](int dir) {
if (dir < 0) Screen::get()->prevPreset();
else Screen::get()->nextPreset(); }});
else Screen::get()->nextPreset(); }, nullptr});
// FILTRE 4:3
items_.push_back({"FILTRE 4:3", ItemKind::Toggle, [] { return std::string(Options::video.stretch_filter_linear ? "LINEAR" : "NEAREST"); }, [](int) { Screen::get()->toggleStretchFilter(); }});
p.items.push_back({"FILTRE 4:3", ItemKind::Toggle, [] { return std::string(Options::video.stretch_filter_linear ? "LINEAR" : "NEAREST"); }, [](int) { Screen::get()->toggleStretchFilter(); }, nullptr});
// RENDER INFO
items_.push_back({"RENDER INFO", ItemKind::Cycle, [] {
p.items.push_back({"RENDER INFO", ItemKind::Cycle, [] {
switch (Options::render_info.position) {
case Options::RenderInfoPosition::OFF: return std::string("OFF");
case Options::RenderInfoPosition::TOP: return std::string("TOP");
case Options::RenderInfoPosition::BOTTOM: return std::string("BOTTOM");
}
return std::string("OFF"); }, [](int dir) { Overlay::cycleRenderInfo(dir); }});
return std::string("OFF"); }, [](int dir) { Overlay::cycleRenderInfo(dir); }, nullptr});
return p;
}
// Converteix volum 0..1 a percentatge i ho formata com "50%"
static std::string volPct(float v) {
int pct = static_cast<int>(v * 100.0F + 0.5F);
if (pct < 0) pct = 0;
if (pct > 100) pct = 100;
char buf[8];
std::snprintf(buf, sizeof(buf), "%d%%", pct);
return std::string(buf);
}
// Canvi +/- d'un volum en steps de 0.05 (5%) amb clamping
static void stepVolume(float& v, int dir) {
v += (dir >= 0 ? 0.05F : -0.05F);
if (v < 0.0F) v = 0.0F;
if (v > 1.0F) v = 1.0F;
Options::applyAudio();
}
static Page buildAudio() {
Page p{"AUDIO", {}, 0};
p.items.push_back({"AUDIO", ItemKind::Toggle, [] { return onOff(Options::audio.enabled); }, [](int) {
Options::audio.enabled = !Options::audio.enabled;
Options::applyAudio(); }, nullptr});
p.items.push_back({"MASTER", ItemKind::IntRange, [] { return volPct(Options::audio.volume); }, [](int dir) { stepVolume(Options::audio.volume, dir); }, nullptr});
p.items.push_back({"MUSICA", ItemKind::Toggle, [] { return onOff(Options::audio.music_enabled); }, [](int) {
Options::audio.music_enabled = !Options::audio.music_enabled;
Options::applyAudio(); }, nullptr});
p.items.push_back({"VOL MUSICA", ItemKind::IntRange, [] { return volPct(Options::audio.music_volume); }, [](int dir) { stepVolume(Options::audio.music_volume, dir); }, nullptr});
p.items.push_back({"SONS", ItemKind::Toggle, [] { return onOff(Options::audio.sound_enabled); }, [](int) {
Options::audio.sound_enabled = !Options::audio.sound_enabled;
Options::applyAudio(); }, nullptr});
p.items.push_back({"VOL SONS", ItemKind::IntRange, [] { return volPct(Options::audio.sound_volume); }, [](int dir) { stepVolume(Options::audio.sound_volume, dir); }, nullptr});
return p;
}
// --- Dibuix ---
// Alpha blending per pixel sobre el buffer ARGB (ABGR en memòria)
// src_argb és el color desitjat (canal alpha ignorat, s'usa src_alpha)
static void blendRect(Uint32* buf, int x, int y, int w, int h, Uint32 src_argb, Uint8 src_alpha) {
const Uint8 sa = src_alpha;
const Uint8 sr = src_argb & 0xFF;
@@ -144,54 +198,94 @@ namespace Menu {
}
static void drawBorder(Uint32* buf, int x, int y, int w, int h, Uint32 color) {
fillRect(buf, x, y, w, 1, color); // top
fillRect(buf, x, y + h - 1, w, 1, color); // bottom
fillRect(buf, x, y, 1, h, color); // left
fillRect(buf, x + w - 1, y, 1, h, color); // right
fillRect(buf, x, y, w, 1, color);
fillRect(buf, x, y + h - 1, w, 1, color);
fillRect(buf, x, y, 1, h, color);
fillRect(buf, x + w - 1, y, 1, h, color);
}
// Mida final de la caixa segons el nombre d'items
static int boxHeight(const Page& page) {
int n = static_cast<int>(page.items.size());
int body = (n == 0) ? ITEM_SPACING : n * ITEM_SPACING;
return HEADER_H + body + BOTTOM_PAD;
}
// --- API pública ---
void init() {
font_ = std::make_unique<Text>("fonts/8bithud.fnt", "fonts/8bithud.gif");
buildItems();
cursor_ = 0;
open_ = false;
stack_.clear();
open_anim_ = 0.0F;
last_ticks_ = SDL_GetTicks();
}
void destroy() {
font_.reset();
items_.clear();
stack_.clear();
}
auto isOpen() -> bool {
return open_;
return !stack_.empty();
}
void toggle() {
open_ = !open_;
if (isOpen()) {
close();
} else {
stack_.push_back(buildRoot());
open_anim_ = 0.0F;
last_ticks_ = SDL_GetTicks();
}
}
void close() {
open_ = false;
stack_.clear();
open_anim_ = 0.0F;
}
void handleKey(SDL_Scancode sc) {
if (!open_ || items_.empty()) return;
if (!isOpen()) return;
Page& page = stack_.back();
if (page.items.empty()) {
// Pàgina buida — només backspace surt
if (sc == SDL_SCANCODE_BACKSPACE) {
stack_.pop_back();
if (stack_.empty()) close();
}
return;
}
const int n = static_cast<int>(page.items.size());
switch (sc) {
case SDL_SCANCODE_UP:
cursor_ = (cursor_ - 1 + static_cast<int>(items_.size())) % static_cast<int>(items_.size());
page.cursor = (page.cursor - 1 + n) % n;
break;
case SDL_SCANCODE_DOWN:
cursor_ = (cursor_ + 1) % static_cast<int>(items_.size());
page.cursor = (page.cursor + 1) % n;
break;
case SDL_SCANCODE_LEFT:
items_[cursor_].change(-1);
if (page.items[page.cursor].kind != ItemKind::Submenu &&
page.items[page.cursor].change) {
page.items[page.cursor].change(-1);
}
break;
case SDL_SCANCODE_RIGHT:
if (page.items[page.cursor].kind != ItemKind::Submenu &&
page.items[page.cursor].change) {
page.items[page.cursor].change(+1);
}
break;
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
items_[cursor_].change(+1);
if (page.items[page.cursor].kind == ItemKind::Submenu) {
if (page.items[page.cursor].enter) page.items[page.cursor].enter();
} else if (page.items[page.cursor].change) {
page.items[page.cursor].change(+1);
}
break;
case SDL_SCANCODE_BACKSPACE:
stack_.pop_back();
if (stack_.empty()) close();
break;
default:
break;
@@ -199,50 +293,83 @@ namespace Menu {
}
void render(Uint32* pixel_data) {
if (!open_ || !font_ || !pixel_data) return;
if (!isOpen() || !font_ || !pixel_data) return;
// Fons semi-transparent
blendRect(pixel_data, BOX_X, BOX_Y, BOX_W, BOX_H, BG_COLOR, BG_ALPHA);
// Actualitza animació d'obertura
Uint32 now = SDL_GetTicks();
float dt = static_cast<float>(now - last_ticks_) / 1000.0F;
last_ticks_ = now;
if (open_anim_ < 1.0F) {
open_anim_ += OPEN_SPEED * dt;
if (open_anim_ > 1.0F) open_anim_ = 1.0F;
}
const Page& page = stack_.back();
const int target_h = boxHeight(page);
// Ease-out quadratic: f(t) = 1 - (1-t)^2
float t = open_anim_;
float eased = 1.0F - (1.0F - t) * (1.0F - t);
// Caixa creix verticalment des del centre
int box_h = static_cast<int>(target_h * eased);
if (box_h < 2) box_h = 2;
int box_x = (SCREEN_W - BOX_W) / 2;
int box_y = (SCREEN_H - box_h) / 2;
// Fons semi-transparent (alpha escalat per l'animació)
Uint8 alpha = static_cast<Uint8>(BG_ALPHA * eased);
blendRect(pixel_data, box_x, box_y, BOX_W, box_h, BG_COLOR, alpha);
// Vora
drawBorder(pixel_data, BOX_X, BOX_Y, BOX_W, BOX_H, BORDER_COLOR);
drawBorder(pixel_data, box_x, box_y, BOX_W, box_h, BORDER_COLOR);
// El contingut només apareix quan la caixa és prou gran
if (open_anim_ < 0.9F) return;
// Títol
const char* title = "OPCIONS DE VIDEO";
int title_w = font_->width(title);
font_->draw(pixel_data, BOX_X + (BOX_W - title_w) / 2, BOX_Y + TITLE_PAD_Y, title, TITLE_COLOR);
int title_w = font_->width(page.title);
font_->draw(pixel_data, box_x + (BOX_W - title_w) / 2, box_y + TITLE_PAD_Y, page.title, TITLE_COLOR);
// Línia sota el títol
int title_line_y = BOX_Y + TITLE_PAD_Y + font_->charHeight() + 2;
fillRect(pixel_data, BOX_X + 4, title_line_y, BOX_W - 8, 1, BORDER_COLOR);
int title_line_y = box_y + TITLE_PAD_Y + font_->charHeight() + 2;
fillRect(pixel_data, box_x + 4, title_line_y, BOX_W - 8, 1, BORDER_COLOR);
// Items
int items_y = title_line_y + 4;
for (size_t i = 0; i < items_.size(); i++) {
int y = items_y + static_cast<int>(i) * ITEM_SPACING;
bool selected = (static_cast<int>(i) == cursor_);
// Cursor
if (selected) {
font_->draw(pixel_data, BOX_X + 4, y, ">", CURSOR_COLOR);
}
// Label
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
font_->draw(pixel_data, BOX_X + ITEM_PAD_X, y, items_[i].label, label_color);
// Valor (dreta)
std::string value = items_[i].getValue();
int value_w = font_->width(value.c_str());
Uint32 value_color = selected ? CURSOR_COLOR : VALUE_COLOR;
font_->draw(pixel_data, BOX_X + BOX_W - ITEM_PAD_X - value_w, y, value.c_str(), value_color);
if (page.items.empty()) {
const char* empty_text = "(BUIT)";
int ew = font_->width(empty_text);
font_->draw(pixel_data, box_x + (BOX_W - ew) / 2, items_y + 2, empty_text, EMPTY_COLOR);
return;
}
// Peu
const char* footer = "^v:MOU <>:CANVIA ESC:IX";
int footer_w = font_->width(footer);
int footer_y = BOX_Y + BOX_H - font_->charHeight() - FOOTER_PAD_Y;
font_->draw(pixel_data, BOX_X + (BOX_W - footer_w) / 2, footer_y, footer, FOOTER_COLOR);
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];
if (selected) {
font_->draw(pixel_data, box_x + 4, y, ">", CURSOR_COLOR);
}
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
font_->draw(pixel_data, box_x + ITEM_PAD_X, y, item.label, label_color);
// Valor (dreta) — només per items no-submenu
if (item.kind == ItemKind::Submenu) {
// Fletxa indicant que entra a un submenú
const char* arrow = ">>";
int aw = font_->width(arrow);
Uint32 ac = selected ? CURSOR_COLOR : VALUE_COLOR;
font_->draw(pixel_data, box_x + BOX_W - ITEM_PAD_X - aw, y, arrow, ac);
} else if (item.getValue) {
std::string value = item.getValue();
int value_w = font_->width(value.c_str());
Uint32 value_color = selected ? CURSOR_COLOR : VALUE_COLOR;
font_->draw(pixel_data, box_x + BOX_W - ITEM_PAD_X - value_w, y, value.c_str(), value_color);
}
}
}
} // namespace Menu