From b6403e8de5637426660096c2bf96a8d89bafdc06 Mon Sep 17 00:00:00 2001 From: Sergio Valor Date: Thu, 14 May 2026 10:11:50 +0200 Subject: [PATCH] =?UTF-8?q?feat(ui):=20outline=20per=20slot=20al=20text=20?= =?UTF-8?q?del=20men=C3=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/config/ui.yaml | 36 +++++++++++------- source/core/input/define_buttons.cpp | 24 ++++++++---- source/core/ui/ui_config.cpp | 8 ++++ source/core/ui/ui_config.hpp | 8 ++++ source/game/ui/service_menu.cpp | 55 +++++++++++++++++++++------- source/utils/defaults.hpp | 8 ++++ 6 files changed, 103 insertions(+), 36 deletions(-) diff --git a/data/config/ui.yaml b/data/config/ui.yaml index 42ef72a..e1a5919 100644 --- a/data/config/ui.yaml +++ b/data/config/ui.yaml @@ -35,25 +35,33 @@ menu_themes: # Tema històric (defaults compilats). Cian + verd llima + magenta — # paleta arcade clàssica, freda i amb molt contrast. classic: - bg: 2 # marró-vermell fosc (#480000) - border: 232 # cian brillant (#00B6FF) - title: 237 # lavanda (#B6B6FF) - label: 58 # verd llima (#48FF00) - value: 232 # cian brillant (#00B6FF) — coincideix amb el border - cursor: 71 # magenta-vermell (#FF0055) - disabled: 16 # verd fosc (#004800) + bg: 2 # marró-vermell fosc (#480000) + border: 232 # cian brillant (#00B6FF) + title: 237 # lavanda (#B6B6FF) + title_outline: 64 # navy fosc (#0000AA) — halo fred del títol + label: 58 # verd llima (#48FF00) + label_outline: 17 # verd molt fosc (#244800) — bord same-family del label + value: 232 # cian brillant (#00B6FF) — coincideix amb el border + value_outline: 64 # navy fosc (#0000AA) — halo cian + cursor: 71 # magenta-vermell (#FF0055) + cursor_outline: 1 # granat fosc (#240000) — halo càlid del cursor + disabled: 16 # verd fosc (#004800) # Tema "egípci": família or/ambre/marró coherent amb el HUD i el # rètol de transició de sala (room_text). El bg comparteix to amb # `room_text_outline` i `hud_center.shadow` perquè el menú "creixi" # del mateix món visual que el joc. egyptian: - bg: 10 # marró cremat (#482400) - border: 55 # or brillant (#FFDA00) - title: 127 # groc llimona viu (#FFFF55) - label: 47 # ambre (#FFB600) - value: 191 # crema clar (#FFFFAA) - cursor: 23 # vermilló (#FF4800) - disabled: 19 # oliva fosc (#6D4800) + bg: 10 # marró cremat (#482400) + border: 55 # or brillant (#FFDA00) + title: 127 # groc llimona viu (#FFFF55) + title_outline: 19 # oliva fosc (#6D4800) — halo càlid del groc + label: 47 # ambre (#FFB600) + label_outline: 11 # marró (#6D2400) — bord càlid del label + value: 191 # crema clar (#FFFFAA) + value_outline: 19 # oliva fosc (#6D4800) — recolza el crema + cursor: 23 # vermilló (#FF4800) + cursor_outline: 9 # marró molt fosc (#240000) — halo profund del cursor + disabled: 19 # oliva fosc (#6D4800) timings: notification_ms: 4000 diff --git a/source/core/input/define_buttons.cpp b/source/core/input/define_buttons.cpp index e00625a..95e65c2 100644 --- a/source/core/input/define_buttons.cpp +++ b/source/core/input/define_buttons.cpp @@ -198,42 +198,50 @@ void DefineButtons::render() const { // Títol: nom del mando. const std::string TITLE = pad_name_; - title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, + title_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG, CENTER_X, cursor_y, TITLE, TITLE_KERNING, - UiConfig::get().menuTitle()); + UiConfig::get().menuTitle(), + 1, + UiConfig::get().menuTitleOutline()); cursor_y += TITLE_H + LINE_GAP; if (message_shown_) { // Pantalla final: missatge de configuració completa. - items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, + items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG, CENTER_X, cursor_y, Locale::get().text("menu.values.define_buttons_complete"), ITEM_KERNING, - UiConfig::get().menuValue()); + UiConfig::get().menuValue(), + 1, + UiConfig::get().menuValueOutline()); return; } // Instrucció + acció pendent. Replica visualment el WindowMessage de // coffee_crisis_arcade_edition. - items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, + items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG, CENTER_X, cursor_y, Locale::get().text("menu.values.press_button"), ITEM_KERNING, - UiConfig::get().menuLabel()); + UiConfig::get().menuLabel(), + 1, + UiConfig::get().menuLabelOutline()); cursor_y += ITEM_H + LINE_GAP; if (index_ < sequence_.size()) { const auto& current = sequence_[index_]; - items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, + items_font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG, CENTER_X, cursor_y, Locale::get().text(current.label_key), ITEM_KERNING, - UiConfig::get().menuCursor()); + UiConfig::get().menuCursor(), + 1, + UiConfig::get().menuCursorOutline()); } } diff --git a/source/core/ui/ui_config.cpp b/source/core/ui/ui_config.cpp index b158274..50d293e 100644 --- a/source/core/ui/ui_config.cpp +++ b/source/core/ui/ui_config.cpp @@ -81,6 +81,10 @@ UiConfig::UiConfig() menu_value_(Defaults::UI::MENU_VALUE), menu_cursor_(Defaults::UI::MENU_CURSOR), menu_disabled_(Defaults::UI::MENU_DISABLED), + menu_title_outline_(Defaults::UI::MENU_TITLE_OUTLINE), + menu_label_outline_(Defaults::UI::MENU_LABEL_OUTLINE), + menu_value_outline_(Defaults::UI::MENU_VALUE_OUTLINE), + menu_cursor_outline_(Defaults::UI::MENU_CURSOR_OUTLINE), hud_bg_(Defaults::UI::HUD_BG), hud_p1_label_(Defaults::UI::HUD_P1_LABEL), hud_p1_value_(Defaults::UI::HUD_P1_VALUE), @@ -238,6 +242,10 @@ void UiConfig::load(const std::string& file_path) { readColorIndex(t, "value", menu_value_); readColorIndex(t, "cursor", menu_cursor_); readColorIndex(t, "disabled", menu_disabled_); + readColorIndex(t, "title_outline", menu_title_outline_); + readColorIndex(t, "label_outline", menu_label_outline_); + readColorIndex(t, "value_outline", menu_value_outline_); + readColorIndex(t, "cursor_outline", menu_cursor_outline_); } else { Log::warn("UiConfig", "menu_theme '{}' no existeix a menu_themes — s'usen defaults", THEME_NAME); } diff --git a/source/core/ui/ui_config.hpp b/source/core/ui/ui_config.hpp index d4a0bc0..ea58670 100644 --- a/source/core/ui/ui_config.hpp +++ b/source/core/ui/ui_config.hpp @@ -65,6 +65,10 @@ class UiConfig { [[nodiscard]] auto menuValue() const -> Uint8 { return menu_value_; } [[nodiscard]] auto menuCursor() const -> Uint8 { return menu_cursor_; } [[nodiscard]] auto menuDisabled() const -> Uint8 { return menu_disabled_; } + [[nodiscard]] auto menuTitleOutline() const -> Uint8 { return menu_title_outline_; } + [[nodiscard]] auto menuLabelOutline() const -> Uint8 { return menu_label_outline_; } + [[nodiscard]] auto menuValueOutline() const -> Uint8 { return menu_value_outline_; } + [[nodiscard]] auto menuCursorOutline() const -> Uint8 { return menu_cursor_outline_; } [[nodiscard]] auto hudBg() const -> Uint8 { return hud_bg_; } // Famílies de color del HUD per bloc (LEFT/CENTER/RIGHT). Cada // família té label (etiqueta apagada), value (valor destacat) i @@ -214,6 +218,10 @@ class UiConfig { Uint8 menu_value_; Uint8 menu_cursor_; Uint8 menu_disabled_; + Uint8 menu_title_outline_; + Uint8 menu_label_outline_; + Uint8 menu_value_outline_; + Uint8 menu_cursor_outline_; Uint8 hud_bg_; Uint8 hud_p1_label_; Uint8 hud_p1_value_; diff --git a/source/game/ui/service_menu.cpp b/source/game/ui/service_menu.cpp index 4972197..e435a15 100644 --- a/source/game/ui/service_menu.cpp +++ b/source/game/ui/service_menu.cpp @@ -996,23 +996,38 @@ namespace { return out; } + // Parell (fill, outline) per a un text del menú. Extret per a + // collir-los en un sol pas i no inflar la complexitat cognitiva + // de `renderPage` amb dos ternaris en paral·lel. + struct MenuColorPair { + Uint8 fill; + Uint8 outline; + }; + + auto pickValueColors(bool selected) -> MenuColorPair { + if (selected) { return {.fill = UiConfig::get().menuCursor(), .outline = UiConfig::get().menuCursorOutline()}; } + return {.fill = UiConfig::get().menuValue(), .outline = UiConfig::get().menuValueOutline()}; + } + // 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) { + 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, Uint8 outline) { 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, + font.writeDX(Text::COLOR_FLAG | Text::STROKE_FLAG, box_x + box_w - PAD_X - VALUE_W, cursor_y, DISPLAY, kerning, - color); + color, + 1, + outline); } // Mandos visibles al cycle d'un jugador: tots els connectats @@ -1305,21 +1320,25 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w) const int CURSOR_WIDTH = items_font_->length("> ", ITEM_KERNING); const int LABEL_X = box_x + PAD_X + CURSOR_WIDTH; - font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, + font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG, CENTER_X, cursor_y, Locale::get().text(page.title_key), TITLE_KERNING, - UiConfig::get().menuTitle()); + UiConfig::get().menuTitle(), + 1, + UiConfig::get().menuTitleOutline()); cursor_y += TITLE_H + GAP_Y - TITLE_DESCENDER_TRIM; if (!page.subtitle.empty()) { - font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG, + font_->writeDX(Text::CENTER_FLAG | Text::COLOR_FLAG | Text::STROKE_FLAG, CENTER_X, cursor_y, page.subtitle, TITLE_KERNING, - UiConfig::get().menuTitle()); + UiConfig::get().menuTitle(), + 1, + UiConfig::get().menuTitleOutline()); cursor_y += TITLE_H + GAP_Y - TITLE_DESCENDER_TRIM; } @@ -1344,13 +1363,17 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w) : formatValue(item); Uint8 label_color = UiConfig::get().menuLabel(); + Uint8 label_outline = UiConfig::get().menuLabelOutline(); if (item.kind == Item::Kind::LABEL) { // Mateix to que el títol de la pàgina: les LABELs actuen com // a subtítol-divisor (p.ex. "Jugador 1" a Teclat, "ver.X.Y" a - // Sistema) i han de tenir la mateixa presència visual. + // Sistema) i han de tenir la mateixa presència visual — fill + // i outline van aparellats. label_color = UiConfig::get().menuTitle(); + label_outline = UiConfig::get().menuTitleOutline(); } else if (SELECTED) { label_color = UiConfig::get().menuCursor(); + label_outline = UiConfig::get().menuCursorOutline(); } int label_x = LABEL_X; @@ -1362,27 +1385,31 @@ void ServiceMenu::renderPage(const Page& page, int box_x, int box_y, int box_w) } if (SELECTED) { - items_font_->writeDX(Text::COLOR_FLAG, + items_font_->writeDX(Text::COLOR_FLAG | Text::STROKE_FLAG, label_x - CURSOR_WIDTH, cursor_y, "> ", ITEM_KERNING, - UiConfig::get().menuCursor()); + UiConfig::get().menuCursor(), + 1, + UiConfig::get().menuCursorOutline()); } - items_font_->writeDX(Text::COLOR_FLAG, + items_font_->writeDX(Text::COLOR_FLAG | Text::STROKE_FLAG, label_x, cursor_y, LABEL_TEXT, ITEM_KERNING, - label_color); + label_color, + 1, + label_outline); // Pàgines centrades no dibuixen columna de valor (s'assumeix que // 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 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); + const auto VALUE_COLORS = pickValueColors(SELECTED); + drawItemValue(*items_font_, box_x, box_w, cursor_y, CURSOR_WIDTH, LABEL_TEXT, VALUE, ITEM_KERNING, VALUE_COLORS.fill, VALUE_COLORS.outline); } cursor_y += ITEM_H; diff --git a/source/utils/defaults.hpp b/source/utils/defaults.hpp index 194d096..ec00f16 100644 --- a/source/utils/defaults.hpp +++ b/source/utils/defaults.hpp @@ -253,6 +253,14 @@ namespace Defaults { constexpr Uint8 MENU_VALUE = 232; // Valor actual. constexpr Uint8 MENU_CURSOR = 71; // Item seleccionat. constexpr Uint8 MENU_DISABLED = 16; // Label en gris quan no aplica. + // Outlines del text del menú (8 veïns). Un per slot perquè + // cada to de fill demana un halo diferent: el contorn no és + // un negre uniform sinó una versió fosca de la família del + // fill (efecte "embossing"). + constexpr Uint8 MENU_TITLE_OUTLINE = 0; + constexpr Uint8 MENU_LABEL_OUTLINE = 0; + constexpr Uint8 MENU_VALUE_OUTLINE = 0; + constexpr Uint8 MENU_CURSOR_OUTLINE = 0; // Índexs de paleta per al marcador de joc (HUD de la GameScene). // Tres "famílies" de color — un parell (label, value) + sombra per