diff --git a/source/game/ui/service_menu.cpp b/source/game/ui/service_menu.cpp index aa5931f..9ee4d91 100644 --- a/source/game/ui/service_menu.cpp +++ b/source/game/ui/service_menu.cpp @@ -35,6 +35,10 @@ namespace { constexpr int PAD_X = 10; constexpr int GAP_Y = 6; constexpr int ITEM_GAP_Y = 2; + // Mínim buit entre la label (a l'esquerra) i el value (a la dreta). + // Si amb el value sencer no queda aquest gap, truncam amb "..." des + // del final del value. + constexpr int MIN_LABEL_VALUE_GAP = 6; // Les fonts display reserven ~2 px de descendent al fons de la seua // caixa (per a `g`, `p`, `q`...). Quan el títol no porta cap d'aquests // glifs, eixos 2 px queden buits i el gap visual sota el títol sembla @@ -755,6 +759,48 @@ auto ServiceMenu::buildKeyboardControls() -> Page { namespace { + // Trunca `text` amb "..." perquè la seua amplada renderitzada no + // supere `max_w` píxels amb la font i el kerning donats. Port del + // helper de coffee_crisis_arcade_edition: estimació inicial a partir + // de l'amplada mitjana per caràcter i ajust iteratiu mesurant amb + // `Text::length`, per a fonts d'amplada variable on l'estimació + // només és aproximada. + auto truncateToFit(const Text& font, const std::string& text, int max_w, int kerning) -> std::string { + if (font.length(text, kerning) <= max_w) { return text; } + static constexpr const char* ELLIPSIS = "..."; + const int ELLIPSIS_W = font.length(ELLIPSIS, kerning); + if (max_w <= ELLIPSIS_W || text.empty()) { return ELLIPSIS; } + const auto TOTAL_W = static_cast(font.length(text, kerning)); + const auto AVG_CHAR_W = TOTAL_W / static_cast(text.size()); + auto keep = static_cast(static_cast(max_w - ELLIPSIS_W) / AVG_CHAR_W); + keep = std::min(keep, text.size()); + std::string out = text.substr(0, keep) + ELLIPSIS; + while (keep > 0 && font.length(out, kerning) > max_w) { + --keep; + out = text.substr(0, keep) + ELLIPSIS; + } + return out; + } + + // Dibuixa el value d'un item a la dreta del box, truncant-lo amb + // "..." si no cap a l'esquerra de la label. Extret de `renderPage` + // per a mantenir la complexitat cognitiva sota el llindar de + // clang-tidy. + void drawItemValue(Text& font, int box_x, int box_w, int cursor_y, int cursor_width, const std::string& label, const std::string& value, int kerning, Uint8 color) { + const int LABEL_W = font.length(label, kerning); + const int AVAILABLE_W = box_w - (2 * PAD_X) - cursor_width - LABEL_W - MIN_LABEL_VALUE_GAP; + if (AVAILABLE_W <= 0) { return; } + const std::string DISPLAY = truncateToFit(font, value, AVAILABLE_W, kerning); + if (DISPLAY.empty()) { return; } + const int VALUE_W = font.length(DISPLAY, kerning); + font.writeDX(Text::COLOR_FLAG, + box_x + box_w - PAD_X - VALUE_W, + cursor_y, + DISPLAY, + kerning, + color); + } + // Mandos visibles al cycle d'un jugador: tots els connectats // EXCEPTE els reclamats per l'altre jugador. Així l'usuari mai pot // assignar el mateix mando a dos jugadors des de la UI — per @@ -1025,13 +1071,8 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w) // només tenen ACTION / LABEL); els Toggle/Cycle viuen en pàgines // d'opcions amb layout esquerra-dreta. if (!page.center_items && !VALUE.empty()) { - const int VALUE_W = items_font_->length(VALUE, ITEM_KERNING); - items_font_->writeDX(Text::COLOR_FLAG, - box_x + box_w - PAD_X - VALUE_W, - cursor_y, - VALUE, - ITEM_KERNING, - SELECTED ? UiConfig::get().menuCursor() : UiConfig::get().menuValue()); + const Uint8 VALUE_COLOR = SELECTED ? UiConfig::get().menuCursor() : UiConfig::get().menuValue(); + drawItemValue(*items_font_, box_x, box_w, cursor_y, CURSOR_WIDTH, LABEL_TEXT, VALUE, ITEM_KERNING, VALUE_COLOR); } cursor_y += ITEM_H;