refactor: extreure helpers per reduir complexitat cognitiva (tidy net)

This commit is contained in:
2026-05-16 16:13:57 +02:00
parent b984e6041e
commit e1bc1b597f
31 changed files with 1145 additions and 1332 deletions
+126 -100
View File
@@ -60,10 +60,10 @@ namespace Menu {
const char* label;
ItemKind kind;
std::function<std::string()> get_value; // opcional
std::function<void(int dir)> change; // per TOGGLE/CYCLE/INT_RANGE
std::function<void()> enter; // per SUBMENU i ACTION
SDL_Scancode* scancode{nullptr}; // per KEY_BIND
std::function<bool()> visible; // nullptr ⇒ sempre visible
std::function<void(int dir)> change; // per TOGGLE/CYCLE/INT_RANGE
std::function<void()> enter; // per SUBMENU i ACTION
SDL_Scancode* scancode{nullptr}; // per KEY_BIND
std::function<bool()> visible; // nullptr ⇒ sempre visible
};
struct Page {
@@ -448,27 +448,30 @@ namespace Menu {
capturing = nullptr;
}
void handleKey(SDL_Scancode sc) {
if (!isOpen()) {
return;
static void backOrClose() {
if (stack.size() > 1) {
popPage();
} else {
close();
}
Page& page = stack.back();
if (page.items.empty()) {
// Pàgina buida — només backspace surt
if (sc == SDL_SCANCODE_BACKSPACE) {
if (stack.size() > 1) {
popPage();
} else {
close();
}
}
// Activació d'un ítem (RETURN/KP_ENTER): SUBMENU/ACTION criden enter,
// KEY_BIND inicia captura, la resta avança change(+1).
static void activateItem(Item& item) {
if (item.kind == ItemKind::SUBMENU || item.kind == ItemKind::ACTION) {
if (item.enter) {
item.enter();
}
return;
}
// 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);
} else if (item.kind == ItemKind::KEY_BIND) {
capturing = item.scancode;
} else if (item.change) {
item.change(+1);
}
}
static void applyKeyToItem(Page& page, SDL_Scancode sc) {
Item& item = page.items[page.cursor];
switch (sc) {
case SDL_SCANCODE_UP:
page.cursor = nextVisibleCursor(page, page.cursor, -1);
@@ -477,43 +480,44 @@ namespace Menu {
page.cursor = nextVisibleCursor(page, page.cursor, +1);
break;
case SDL_SCANCODE_LEFT:
if (page.items[page.cursor].kind != ItemKind::SUBMENU &&
page.items[page.cursor].change) {
page.items[page.cursor].change(-1);
if (item.kind != ItemKind::SUBMENU && item.change) {
item.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);
if (item.kind != ItemKind::SUBMENU && item.change) {
item.change(+1);
}
break;
case SDL_SCANCODE_RETURN:
case SDL_SCANCODE_KP_ENTER:
if (page.items[page.cursor].kind == ItemKind::SUBMENU ||
page.items[page.cursor].kind == ItemKind::ACTION) {
if (page.items[page.cursor].enter) {
page.items[page.cursor].enter();
}
} else if (page.items[page.cursor].kind == ItemKind::KEY_BIND) {
capturing = page.items[page.cursor].scancode;
} else if (page.items[page.cursor].change) {
page.items[page.cursor].change(+1);
}
activateItem(item);
break;
case SDL_SCANCODE_BACKSPACE:
if (stack.size() > 1) {
popPage();
} else {
close();
}
backOrClose();
break;
default:
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.
}
void handleKey(SDL_Scancode sc) {
if (!isOpen()) {
return;
}
Page& page = stack.back();
if (page.items.empty()) {
if (sc == SDL_SCANCODE_BACKSPACE) {
backOrClose();
}
return;
}
if (!isVisible(page.items[page.cursor])) {
page.cursor = nextVisibleCursor(page, page.cursor, +1);
}
applyKeyToItem(page, sc);
// Defensa: si una acció ha amagat l'ítem que tenim sota el cursor,
// saltem al pròxim visible.
if (!stack.empty()) {
Page& top = stack.back();
if (!top.items.empty() && !isVisible(top.items[top.cursor])) {
@@ -522,6 +526,9 @@ namespace Menu {
}
}
// Forward decl: renderOneItem viu sota renderPageContent però aquest l'ha de cridar.
static void renderOneItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max);
// Dibuixa el contingut d'una pàgina amb un offset horitzontal i un rang de clip.
// box_x/box_y són les coordenades de la caixa (per calcular posicions relatives);
// clip_x_min/clip_x_max limiten on es dibuixa text i la línia separadora.
@@ -560,69 +567,88 @@ namespace Menu {
return;
}
int y_slot = 0; // índex de fila visible (independent de l'índex real de l'ítem)
int y_slot = 0;
for (size_t i = 0; i < page.items.size(); i++) {
const Item& item = page.items[i];
if (!isVisible(item)) {
continue;
}
int y = items_y + (y_slot * ITEM_SPACING);
const int Y = items_y + (y_slot * ITEM_SPACING);
++y_slot;
bool selected = (static_cast<int>(i) == page.cursor);
Uint32 label_color = selected ? CURSOR_COLOR : LABEL_COLOR;
// ACTION: sense valor a la dreta — centrem el label amb el cursor just a l'esquerra.
if (item.kind == ItemKind::ACTION) {
int lw = font->width(item.label);
int lx = box_x + ((BOX_W - lw) / 2) + x_offset;
if (selected) {
font->drawClipped(pixel_data, lx - font->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
font->drawClipped(pixel_data, lx, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
continue;
}
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 + ITEM_PAD_X + x_offset, y, item.label, label_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
if (item.kind == ItemKind::SUBMENU) {
const char* arrow = ">>";
int aw = font->width(arrow);
Uint32 ac = selected ? CURSOR_COLOR : VALUE_COLOR;
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - aw + x_offset, y, arrow, ac, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
} else if (item.kind == ItemKind::KEY_BIND) {
bool this_capturing = (capturing == item.scancode);
const char* text = nullptr;
if (this_capturing) {
text = Locale::get("menu.values.press_key");
} else if (item.scancode != nullptr) {
text = SDL_GetScancodeName(*item.scancode);
} else {
text = "";
}
if ((text == nullptr) || (*text == 0)) {
text = Locale::get("menu.values.unknown");
}
int tw = font->width(text);
Uint32 tc = 0;
if (this_capturing) {
tc = 0xFF00FFFF;
} else {
tc = selected ? CURSOR_COLOR : VALUE_COLOR;
}
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - tw + x_offset, y, text, tc, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
} else if (item.get_value) {
std::string value = item.get_value();
int value_w = font->width(value.c_str());
Uint32 value_color = selected ? CURSOR_COLOR : VALUE_COLOR;
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - value_w + x_offset, y, value.c_str(), value_color, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
const bool SELECTED = (static_cast<int>(i) == page.cursor);
renderOneItem(pixel_data, item, SELECTED, box_x, Y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
}
static auto keybindText(const Item& item, bool this_capturing) -> const char* {
const char* text = nullptr;
if (this_capturing) {
text = Locale::get("menu.values.press_key");
} else if (item.scancode != nullptr) {
text = SDL_GetScancodeName(*item.scancode);
} else {
text = "";
}
if ((text == nullptr) || (*text == 0)) {
text = Locale::get("menu.values.unknown");
}
return text;
}
static void renderActionItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
const Uint32 LABEL_COLOR_LOCAL = selected ? CURSOR_COLOR : LABEL_COLOR;
const int LW = font->width(item.label);
const int LX = box_x + ((BOX_W - LW) / 2) + x_offset;
if (selected) {
font->drawClipped(pixel_data, LX - font->width("> "), y, ">", CURSOR_COLOR, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
font->drawClipped(pixel_data, LX, y, item.label, LABEL_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
static auto keybindColor(bool this_capturing, bool selected) -> Uint32 {
if (this_capturing) {
return 0xFF00FFFF;
}
return selected ? CURSOR_COLOR : VALUE_COLOR;
}
static void renderItemValue(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
if (item.kind == ItemKind::SUBMENU) {
const char* arrow = ">>";
const int AW = font->width(arrow);
const Uint32 AC = selected ? CURSOR_COLOR : VALUE_COLOR;
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - AW + x_offset, y, arrow, AC, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
return;
}
if (item.kind == ItemKind::KEY_BIND) {
const bool THIS_CAPTURING = (capturing == item.scancode);
const char* text = keybindText(item, THIS_CAPTURING);
const int TW = font->width(text);
const Uint32 TC = keybindColor(THIS_CAPTURING, selected);
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - TW + x_offset, y, text, TC, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
return;
}
if (item.get_value) {
const std::string VALUE = item.get_value();
const int VALUE_W = font->width(VALUE.c_str());
const Uint32 VALUE_COLOR_LOCAL = selected ? CURSOR_COLOR : VALUE_COLOR;
font->drawClipped(pixel_data, box_x + BOX_W - ITEM_PAD_X - VALUE_W + x_offset, y, VALUE.c_str(), VALUE_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
}
static void renderOneItem(Uint32* pixel_data, const Item& item, bool selected, int box_x, int y, int x_offset, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) {
if (item.kind == ItemKind::ACTION) {
renderActionItem(pixel_data, item, selected, box_x, y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
return;
}
const Uint32 LABEL_COLOR_LOCAL = selected ? CURSOR_COLOR : LABEL_COLOR;
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 + ITEM_PAD_X + x_offset, y, item.label, LABEL_COLOR_LOCAL, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
renderItemValue(pixel_data, item, selected, box_x, y, x_offset, clip_x_min, clip_x_max, clip_y_min, clip_y_max);
}
void render(Uint32* pixel_data) {
if (!isVisible() || !font || (pixel_data == nullptr)) {
return;
+218 -235
View File
@@ -114,258 +114,241 @@ namespace Overlay {
}
}
static void updateNotifFsm(Notification& notif, float dt) {
switch (notif.status) {
case Status::RISING:
notif.anim += SLIDE_SPEED * dt;
if (notif.anim >= 1.0F) {
notif.anim = 1.0F;
notif.status = Status::STAY;
notif.timer = 0.0F;
}
break;
case Status::STAY:
notif.timer += dt;
if (notif.timer >= notif.duration) {
notif.status = Status::VANISHING;
}
break;
case Status::VANISHING:
notif.anim -= SLIDE_SPEED * dt;
if (notif.anim <= 0.0F) {
notif.status = Status::FINISHED;
}
break;
case Status::FINISHED:
break;
}
}
static void computeNotifBoxPos(const Notification& notif, int& box_x, int& box_y) {
switch (notif.pos) {
case NotifPosition::TOP_LEFT_SLIDE:
box_x = NOTIF_MARGIN_X - static_cast<int>((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X));
box_y = NOTIF_MARGIN_Y;
break;
case NotifPosition::TOP_CENTER_DROP:
box_x = (SCREEN_W - notif.box_w) / 2;
box_y = NOTIF_MARGIN_Y - static_cast<int>((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y));
break;
}
}
static void drawNotifTextLine(Uint32* pixel_data, const std::string& line, int line_x, int line_y, const Notification& notif) {
if (notif.style == NotifStyle::SHADOW) {
font->draw(pixel_data, line_x + 1, line_y + 1, line.c_str(), notif.accent_color);
} else if (notif.style == NotifStyle::OUTLINE) {
font->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color);
}
font->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color);
}
static void renderOneNotification(Uint32* pixel_data, const Notification& notif) {
int box_x = 0;
int box_y = 0;
computeNotifBoxPos(notif, box_x, box_y);
if (notif.style == NotifStyle::BOX) {
drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color);
}
const int LINE_H = font->charHeight();
int line_y = box_y + NOTIF_PADDING_V;
for (const auto& line : notif.lines) {
const int LINE_W = font->width(line.c_str());
const int LINE_X = box_x + ((notif.box_w - LINE_W) / 2);
drawNotifTextLine(pixel_data, line, LINE_X, line_y, notif);
line_y += LINE_H + 1;
}
}
static void updateRenderInfoFsm(float dt) {
const auto DESIRED = Options::render_info.position;
if (DESIRED == info_visible_pos) {
if (info_anim < 1.0F) {
info_anim = std::min(info_anim + (INFO_SLIDE_SPEED * dt), 1.0F);
}
} else if (info_visible_pos == Options::RenderInfoPosition::OFF) {
info_visible_pos = DESIRED;
info_anim = 0.0F;
} else {
info_anim -= INFO_SLIDE_SPEED * dt;
if (info_anim <= 0.0F) {
info_anim = 0.0F;
info_visible_pos = DESIRED;
}
}
for (auto& seg : info_segments) {
const float TARGET = seg.visible ? 1.0F : 0.0F;
if (seg.anim < TARGET) {
seg.anim = std::min(seg.anim + (SEG_SPEED * dt), TARGET);
} else if (seg.anim > TARGET) {
seg.anim = std::max(seg.anim - (SEG_SPEED * dt), TARGET);
}
}
}
static auto computeInfoTotalWidth(int digit_cell) -> float {
float total_w = 0.0F;
for (const auto& seg : info_segments) {
if (seg.anim > 0.0F && !seg.text.empty()) {
const int W = seg.mono_digits
? font->widthMonoDigits(seg.text.c_str(), digit_cell)
: font->width(seg.text.c_str());
total_w += static_cast<float>(W) * Easing::outQuad(seg.anim);
}
}
return total_w;
}
static void drawInfoSegment(Uint32* pixel_data, const InfoSegment& seg, int xi, int info_y, int digit_cell) {
if (seg.mono_digits) {
font->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, digit_cell);
font->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, digit_cell);
} else {
font->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color);
font->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color);
}
}
static void renderRenderInfo(Uint32* pixel_data) {
if (info_visible_pos == Options::RenderInfoPosition::OFF || info_anim <= 0.0F) {
return;
}
const int DIGIT_CELL = font->charBoxWidth() - 1;
const float TOTAL_W = computeInfoTotalWidth(DIGIT_CELL);
if (TOTAL_W <= 0.0F) {
return;
}
const float EASED_Y = Easing::outQuad(info_anim);
const int CH = font->charHeight();
const int FINAL_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? 1 : SCREEN_H - CH - 1;
const int START_Y = (info_visible_pos == Options::RenderInfoPosition::TOP) ? -CH - 1 : SCREEN_H;
const int INFO_Y = START_Y + static_cast<int>(static_cast<float>(FINAL_Y - START_Y) * EASED_Y);
float cur_x = (SCREEN_W - TOTAL_W) / 2.0F;
for (const auto& seg : info_segments) {
if (seg.anim <= 0.01F || seg.text.empty()) {
continue;
}
const int XI = static_cast<int>(cur_x);
const int SEG_W = seg.mono_digits
? font->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
: font->width(seg.text.c_str());
drawInfoSegment(pixel_data, seg, XI, INFO_Y, DIGIT_CELL);
cur_x += static_cast<float>(SEG_W) * Easing::outQuad(seg.anim);
}
}
static void renderPauseIndicator(Uint32* pixel_data) {
if ((Director::get() == nullptr) || !Director::get()->isPaused()) {
return;
}
const char* pause_text = Locale::get("notifications.pause");
const int W = font->width(pause_text);
const int X = SCREEN_W - W - 4;
const int Y = 4;
font->draw(pixel_data, X, Y - 1, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X, Y + 1, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X - 1, Y, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X + 1, Y, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, X, Y, pause_text, 0xFF0000FF);
}
static void emitCreditsLines(const char* role_key, const char* name_key) {
showNotification(
{std::string(Locale::get(role_key)), std::string(Locale::get(name_key))},
CREDITS_HOLD,
NotifPosition::TOP_CENTER_DROP,
NotifStyle::OUTLINE,
CREDITS_BG,
CREDITS_FG);
}
static void advanceCredits(float dt) {
if (credits_phase == CreditsPhase::IDLE) {
return;
}
credits_timer += dt;
switch (credits_phase) {
case CreditsPhase::DELAY:
if (credits_timer >= CREDITS_DELAY) {
emitCreditsLines("credits.port_role", "credits.port_name");
credits_phase = CreditsPhase::PLAYING_1;
credits_timer = 0.0F;
}
break;
case CreditsPhase::PLAYING_1:
if (notifications.empty()) {
credits_phase = CreditsPhase::GAP;
credits_timer = 0.0F;
}
break;
case CreditsPhase::GAP:
if (credits_timer >= CREDITS_GAP) {
emitCreditsLines("credits.modern_role", "credits.modern_name");
credits_phase = CreditsPhase::PLAYING_2;
credits_timer = 0.0F;
}
break;
case CreditsPhase::PLAYING_2:
if (notifications.empty()) {
credits_phase = CreditsPhase::IDLE;
credits_timer = 0.0F;
}
break;
case CreditsPhase::IDLE:
break;
}
}
void render(Uint32* pixel_data) {
if (!font || (pixel_data == nullptr)) {
return;
}
const Uint32 NOW = SDL_GetTicks();
const float DT = static_cast<float>(NOW - last_ticks) / 1000.0F;
last_ticks = NOW;
// Calcula delta time
Uint32 now = SDL_GetTicks();
float dt = static_cast<float>(now - last_ticks) / 1000.0F;
last_ticks = now;
// Actualitza i pinta cada notificació
for (auto& notif : notifications) {
switch (notif.status) {
case Status::RISING:
notif.anim += SLIDE_SPEED * dt;
if (notif.anim >= 1.0F) {
notif.anim = 1.0F;
notif.status = Status::STAY;
notif.timer = 0.0F;
}
break;
case Status::STAY:
notif.timer += dt;
if (notif.timer >= notif.duration) {
notif.status = Status::VANISHING;
}
break;
case Status::VANISHING:
notif.anim -= SLIDE_SPEED * dt;
if (notif.anim <= 0.0F) {
notif.status = Status::FINISHED;
}
break;
case Status::FINISHED:
break;
}
if (notif.status == Status::FINISHED) {
continue;
}
// Posició segons el tipus
int box_x = 0;
int box_y = 0;
switch (notif.pos) {
case NotifPosition::TOP_LEFT_SLIDE: {
int target_x = NOTIF_MARGIN_X;
int target_y = NOTIF_MARGIN_Y;
box_x = target_x - static_cast<int>((1.0F - notif.anim) * (notif.box_w + NOTIF_MARGIN_X));
box_y = target_y;
break;
}
case NotifPosition::TOP_CENTER_DROP: {
int target_y = NOTIF_MARGIN_Y;
box_x = (SCREEN_W - notif.box_w) / 2;
// Baixa des de sobre de la pantalla fins a target_y
box_y = target_y - static_cast<int>((1.0F - notif.anim) * (notif.box_h + NOTIF_MARGIN_Y));
break;
}
}
// Pinta fons (si BOX)
if (notif.style == NotifStyle::BOX) {
drawRect(pixel_data, box_x, box_y, notif.box_w, notif.box_h, notif.accent_color);
}
// Pinta el text línia a línia (amb ombra o contorn segons style)
int line_h = font->charHeight();
int line_y = box_y + NOTIF_PADDING_V;
for (const auto& line : notif.lines) {
int line_w = font->width(line.c_str());
int line_x = box_x + ((notif.box_w - line_w) / 2); // centrat dins la caixa
if (notif.style == NotifStyle::SHADOW) {
font->draw(pixel_data, line_x + 1, line_y + 1, line.c_str(), notif.accent_color);
} else if (notif.style == NotifStyle::OUTLINE) {
// Contorn 4-direccional (N, S, E, W)
font->draw(pixel_data, line_x, line_y - 1, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x, line_y + 1, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x - 1, line_y, line.c_str(), notif.accent_color);
font->draw(pixel_data, line_x + 1, line_y, line.c_str(), notif.accent_color);
}
font->draw(pixel_data, line_x, line_y, line.c_str(), notif.text_color);
line_y += line_h + 1;
updateNotifFsm(notif, DT);
if (notif.status != Status::FINISHED) {
renderOneNotification(pixel_data, notif);
}
}
// Render info (FPS, driver, shader) — animat amb slide vertical
// State machine: visible_pos s'actualitza cap a DESIRED quan anim arriba a 0
{
const auto DESIRED = Options::render_info.position;
if (DESIRED == info_visible_pos) {
// Mateix lloc: entra fins a 1
if (info_anim < 1.0F) {
info_anim += INFO_SLIDE_SPEED * dt;
info_anim = std::min(info_anim, 1.0F);
}
} else {
// Canvi: si visible_pos està OFF, commuta directament
if (info_visible_pos == Options::RenderInfoPosition::OFF) {
info_visible_pos = DESIRED;
info_anim = 0.0F;
} else {
// Ix del lloc actual
info_anim -= INFO_SLIDE_SPEED * dt;
if (info_anim <= 0.0F) {
info_anim = 0.0F;
info_visible_pos = DESIRED;
}
}
}
updateRenderInfoFsm(DT);
renderRenderInfo(pixel_data);
// Actualitza animacions individuals dels segments
for (auto& seg : info_segments) {
float target = seg.visible ? 1.0F : 0.0F;
if (seg.anim < target) {
seg.anim += SEG_SPEED * dt;
seg.anim = std::min(seg.anim, target);
} else if (seg.anim > target) {
seg.anim -= SEG_SPEED * dt;
seg.anim = std::max(seg.anim, target);
}
}
// Render si hi ha alguna cosa visible
if (info_visible_pos != Options::RenderInfoPosition::OFF && info_anim > 0.0F) {
const int DIGIT_CELL = font->charBoxWidth() - 1; // amplada uniforme per dígit
// Calcula amplada total interpolant cada segment per la seva anim
float total_w = 0.0F;
for (const auto& seg : info_segments) {
if (seg.anim > 0.0F && !seg.text.empty()) {
int w = seg.mono_digits
? font->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
: font->width(seg.text.c_str());
total_w += w * Easing::outQuad(seg.anim);
}
}
if (total_w > 0.0F) {
float eased_y = Easing::outQuad(info_anim);
int ch = font->charHeight();
int final_y;
int start_y;
if (info_visible_pos == Options::RenderInfoPosition::TOP) {
final_y = 1;
start_y = -ch - 1;
} else {
final_y = SCREEN_H - ch - 1;
start_y = SCREEN_H;
}
int info_y = start_y + static_cast<int>((final_y - start_y) * eased_y);
// Dibuixa cada segment en la seva posició x acumulada
float cur_x = (SCREEN_W - total_w) / 2.0F;
for (const auto& seg : info_segments) {
if (seg.anim > 0.01F && !seg.text.empty()) {
int xi = static_cast<int>(cur_x);
int seg_w = seg.mono_digits
? font->widthMonoDigits(seg.text.c_str(), DIGIT_CELL)
: font->width(seg.text.c_str());
if (seg.mono_digits) {
font->drawMonoDigits(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color, DIGIT_CELL);
font->drawMonoDigits(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color, DIGIT_CELL);
} else {
font->draw(pixel_data, xi + 1, info_y + 1, seg.text.c_str(), Options::render_info.shadow_color);
font->draw(pixel_data, xi, info_y, seg.text.c_str(), Options::render_info.text_color);
}
cur_x += seg_w * Easing::outQuad(seg.anim);
}
}
}
}
}
// Elimina les acabades
std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; });
// Si la notificació d'ESC ha desaparegut, reseteja l'estat
if (esc_waiting && notifications.empty()) {
esc_waiting = false;
}
// Indicador de pausa persistent (cantó superior dret)
if ((Director::get() != nullptr) && Director::get()->isPaused()) {
const char* pause_text = Locale::get("notifications.pause");
int w = font->width(pause_text);
int x = SCREEN_W - w - 4;
int y = 4;
// Contorn blanc 4-direccional
font->draw(pixel_data, x, y - 1, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, x, y + 1, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, x - 1, y, pause_text, 0xFFFFFFFF);
font->draw(pixel_data, x + 1, y, pause_text, 0xFFFFFFFF);
// Text en roig
font->draw(pixel_data, x, y, pause_text, 0xFF0000FF);
}
renderPauseIndicator(pixel_data);
advanceCredits(DT);
// Crèdits seqüencials — dispara notificacions TOP_CENTER_DROP una darrere l'altra.
if (credits_phase != CreditsPhase::IDLE) {
credits_timer += dt;
switch (credits_phase) {
case CreditsPhase::DELAY:
if (credits_timer >= CREDITS_DELAY) {
showNotification(
{std::string(Locale::get("credits.port_role")),
std::string(Locale::get("credits.port_name"))},
CREDITS_HOLD,
NotifPosition::TOP_CENTER_DROP,
NotifStyle::OUTLINE,
CREDITS_BG,
CREDITS_FG);
credits_phase = CreditsPhase::PLAYING_1;
credits_timer = 0.0F;
}
break;
case CreditsPhase::PLAYING_1:
if (notifications.empty()) {
credits_phase = CreditsPhase::GAP;
credits_timer = 0.0F;
}
break;
case CreditsPhase::GAP:
if (credits_timer >= CREDITS_GAP) {
showNotification(
{std::string(Locale::get("credits.modern_role")),
std::string(Locale::get("credits.modern_name"))},
CREDITS_HOLD,
NotifPosition::TOP_CENTER_DROP,
NotifStyle::OUTLINE,
CREDITS_BG,
CREDITS_FG);
credits_phase = CreditsPhase::PLAYING_2;
credits_timer = 0.0F;
}
break;
case CreditsPhase::PLAYING_2:
if (notifications.empty()) {
credits_phase = CreditsPhase::IDLE;
credits_timer = 0.0F;
}
break;
case CreditsPhase::IDLE:
break;
}
}
// Neteja notificacions finalitzades
std::erase_if(notifications, [](const Notification& n) { return n.status == Status::FINISHED; });
// Menú flotant per damunt de tot (isVisible inclou l'animació de tancament)
if (Menu::isVisible()) {
Menu::render(pixel_data);
}
+65 -177
View File
@@ -1,5 +1,6 @@
#include "core/rendering/text.hpp"
#include <algorithm>
#include <cstdio>
#include <cstdlib>
#include <cstring>
@@ -158,60 +159,57 @@ void Text::loadBitmap(const char* gif_file) {
// --- Renderitzat ---
auto Text::resolveGlyph(uint32_t cp) const -> const GlyphInfo* {
auto it = glyphs_.find(cp);
if (it != glyphs_.end()) {
return &it->second;
}
it = glyphs_.find('?');
return (it != glyphs_.end()) ? &it->second : nullptr;
}
void Text::blitGlyph(Uint32* pixel_data, int dst_x, int dst_y, const GlyphInfo& glyph, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const {
const int GY_START = std::max(0, clip_y_min - dst_y);
const int GY_END = std::min(box_height_, clip_y_max - dst_y);
const int GX_START = std::max(0, clip_x_min - dst_x);
const int GX_END = std::min(glyph.w, clip_x_max - dst_x);
for (int gy = GY_START; gy < GY_END; gy++) {
const int SRC_Y = glyph.y + gy;
if (SRC_Y >= bitmap_height_) {
continue;
}
const int DST_ROW = dst_y + gy;
for (int gx = GX_START; gx < GX_END; gx++) {
const int SRC_X = glyph.x + gx;
if (SRC_X >= bitmap_width_) {
continue;
}
const Uint8 PIXEL = bitmap_[SRC_X + (SRC_Y * bitmap_width_)];
if (PIXEL != 0) {
pixel_data[(dst_x + gx) + (DST_ROW * SCREEN_WIDTH)] = color;
}
}
}
}
void Text::draw(Uint32* pixel_data, int x, int y, const char* text, Uint32 color) const {
if (bitmap_.empty() || (pixel_data == nullptr)) {
return;
}
const char* ptr = text;
int cursor_x = x;
while (*ptr != 0) {
uint32_t cp = nextCodepoint(ptr);
if (cp == 0) {
break;
}
auto it = glyphs_.find(cp);
if (it == glyphs_.end()) {
it = glyphs_.find('?');
if (it == glyphs_.end()) {
cursor_x += box_width_;
continue;
}
const GlyphInfo* glyph = resolveGlyph(cp);
if (glyph == nullptr) {
cursor_x += box_width_;
continue;
}
const auto& glyph = it->second;
// Pinta glifo pixel a pixel
for (int gy = 0; gy < box_height_; gy++) {
int dst_y = y + gy;
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
continue;
}
for (int gx = 0; gx < glyph.w; gx++) {
int dst_x = cursor_x + gx;
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
continue;
}
int src_x = glyph.x + gx;
int src_y = glyph.y + gy;
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
continue;
}
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
// Píxel no transparent (índex 0 és fons típicament)
if (pixel != 0) {
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
}
}
}
cursor_x += glyph.w + 1; // +1 kerning
blitGlyph(pixel_data, cursor_x, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
cursor_x += glyph->w + 1;
}
}
@@ -225,70 +223,29 @@ void Text::drawClipped(Uint32* pixel_data, int x, int y, const char* text, Uint3
if (bitmap_.empty() || (pixel_data == nullptr)) {
return;
}
// Descart ràpid si el glifo sencer cau fora verticalment
if (y + box_height_ <= clip_y_min || y >= clip_y_max) {
return;
}
const int X_MIN = std::max(0, clip_x_min);
const int X_MAX = std::min(SCREEN_WIDTH, clip_x_max);
const int Y_MIN = std::max(0, clip_y_min);
const int Y_MAX = std::min(SCREEN_HEIGHT, clip_y_max);
const char* ptr = text;
int cursor_x = x;
while (*ptr != 0) {
uint32_t cp = nextCodepoint(ptr);
if (cp == 0) {
break;
}
auto it = glyphs_.find(cp);
if (it == glyphs_.end()) {
it = glyphs_.find('?');
if (it == glyphs_.end()) {
cursor_x += box_width_;
continue;
}
}
const auto& glyph = it->second;
// Si el glifo està completament fora del clip horitzontal, salta
if (cursor_x + glyph.w <= clip_x_min || cursor_x >= clip_x_max) {
cursor_x += glyph.w + 1;
const GlyphInfo* glyph = resolveGlyph(cp);
if (glyph == nullptr) {
cursor_x += box_width_;
continue;
}
for (int gy = 0; gy < box_height_; gy++) {
int dst_y = y + gy;
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
continue;
}
if (dst_y < clip_y_min || dst_y >= clip_y_max) {
continue;
}
for (int gx = 0; gx < glyph.w; gx++) {
int dst_x = cursor_x + gx;
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
continue;
}
if (dst_x < clip_x_min || dst_x >= clip_x_max) {
continue;
}
int src_x = glyph.x + gx;
int src_y = glyph.y + gy;
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
continue;
}
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
if (pixel != 0) {
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
}
}
if (cursor_x + glyph->w > X_MIN && cursor_x < X_MAX) {
blitGlyph(pixel_data, cursor_x, y, *glyph, color, X_MIN, X_MAX, Y_MIN, Y_MAX);
}
cursor_x += glyph.w + 1;
cursor_x += glyph->w + 1;
}
}
@@ -296,54 +253,20 @@ void Text::drawMono(Uint32* pixel_data, int x, int y, const char* text, Uint32 c
if (bitmap_.empty() || (pixel_data == nullptr)) {
return;
}
const char* ptr = text;
int cursor_x = x;
while (*ptr != 0) {
uint32_t cp = nextCodepoint(ptr);
if (cp == 0) {
break;
}
auto it = glyphs_.find(cp);
if (it == glyphs_.end()) {
it = glyphs_.find('?');
if (it == glyphs_.end()) {
cursor_x += cell_w;
continue;
}
const GlyphInfo* glyph = resolveGlyph(cp);
if (glyph == nullptr) {
cursor_x += cell_w;
continue;
}
const auto& glyph = it->second;
// Centra el glif dins la cel·la
int glyph_x = cursor_x + ((cell_w - glyph.w) / 2);
for (int gy = 0; gy < box_height_; gy++) {
int dst_y = y + gy;
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
continue;
}
for (int gx = 0; gx < glyph.w; gx++) {
int dst_x = glyph_x + gx;
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
continue;
}
int src_x = glyph.x + gx;
int src_y = glyph.y + gy;
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
continue;
}
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
if (pixel != 0) {
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
}
}
}
const int GLYPH_X = cursor_x + ((cell_w - glyph->w) / 2);
blitGlyph(pixel_data, GLYPH_X, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
cursor_x += cell_w;
}
}
@@ -352,62 +275,27 @@ void Text::drawMonoDigits(Uint32* pixel_data, int x, int y, const char* text, Ui
if (bitmap_.empty() || (pixel_data == nullptr)) {
return;
}
const char* ptr = text;
int cursor_x = x;
bool first = true;
while (*ptr != 0) {
uint32_t cp = nextCodepoint(ptr);
if (cp == 0) {
break;
}
auto it = glyphs_.find(cp);
if (it == glyphs_.end()) {
it = glyphs_.find('?');
if (it == glyphs_.end()) {
if (!first) {
cursor_x += 1;
}
cursor_x += box_width_;
first = false;
continue;
}
}
const auto& glyph = it->second;
bool is_digit = (cp >= '0' && cp <= '9');
if (!first) {
cursor_x += 1; // kerning
}
int glyph_x = is_digit ? cursor_x + ((digit_cell_w - glyph.w) / 2) : cursor_x;
for (int gy = 0; gy < box_height_; gy++) {
int dst_y = y + gy;
if (dst_y < 0 || dst_y >= SCREEN_HEIGHT) {
continue;
}
for (int gx = 0; gx < glyph.w; gx++) {
int dst_x = glyph_x + gx;
if (dst_x < 0 || dst_x >= SCREEN_WIDTH) {
continue;
}
int src_x = glyph.x + gx;
int src_y = glyph.y + gy;
if (src_x >= bitmap_width_ || src_y >= bitmap_height_) {
continue;
}
Uint8 pixel = bitmap_[src_x + (src_y * bitmap_width_)];
if (pixel != 0) {
pixel_data[dst_x + (dst_y * SCREEN_WIDTH)] = color;
}
}
const GlyphInfo* glyph = resolveGlyph(cp);
if (glyph == nullptr) {
cursor_x += box_width_;
first = false;
continue;
}
cursor_x += is_digit ? digit_cell_w : glyph.w;
const bool IS_DIGIT = (cp >= '0' && cp <= '9');
const int GLYPH_X = IS_DIGIT ? cursor_x + ((digit_cell_w - glyph->w) / 2) : cursor_x;
blitGlyph(pixel_data, GLYPH_X, y, *glyph, color, 0, SCREEN_WIDTH, 0, SCREEN_HEIGHT);
cursor_x += IS_DIGIT ? digit_cell_w : glyph->w;
first = false;
}
}
+7
View File
@@ -57,6 +57,13 @@ class Text {
void loadFont(const char* fnt_file);
void loadBitmap(const char* gif_file);
// Resolt un codepoint al GlyphInfo corresponent o al fallback '?'.
// Retorna nullptr si ni el codepoint ni el fallback existeixen.
[[nodiscard]] auto resolveGlyph(uint32_t cp) const -> const GlyphInfo*;
// Pinta un glif a (dst_x, dst_y) amb clipping per finestra.
// Si la finestra és tota la pantalla, passar clip_x_min=0, clip_x_max=SCREEN_WIDTH, idem y.
void blitGlyph(Uint32* pixel_data, int dst_x, int dst_y, const GlyphInfo& glyph, Uint32 color, int clip_x_min, int clip_x_max, int clip_y_min, int clip_y_max) const;
static constexpr int SCREEN_WIDTH = 320;
static constexpr int SCREEN_HEIGHT = 200;
};