From 29fe2b4396781f126e239e51936d5150b893d4e1 Mon Sep 17 00:00:00 2001 From: chrg6 Date: Sat, 7 Nov 2026 06:44:54 -0500 Subject: [PATCH] Restyle the settings menu with the game's own interface fonts and textures, and give the window a gold pointer. runtime/menu_assets.c reads them from the user's RomFS at runtime (nothing is shipped): .zs files with the dictionaries of Pack/ZsDic.pack.zs (streamed when a frame does not say its size), the fonts of Font2/Scalable behind their 8-byte header and per-word XOR key, and BNTX textures (Switch 2 archives say "Ounc" where Switch ones say "NX ") through the runtime's block-linear copy and BC4 and ASTC decoders. ui/menu_art.cpp decodes the shared interface archive on a worker at the first present and uploads each texture for Dear ImGui at its first use. The menu now uses ClarimoUDPE for its text (with a sixth more line height) and CetraDisplayPro for its title and tabs, Roboto merged behind both; umber, brown and gold colours; the game's ornamented frame along the window's edge (the art holds two sides and is drawn again turned for the other two); its cursor glow and line around the selected row (edges only, so the label stays readable); and its control-guide bar under Continue and Quit. Without the files it keeps Roboto and plain shapes. The window's pointer is an original gold arrowhead drawn at start for the system DPI. Co-Authored-By: Claude Opus 5.5 --- runtime/gfx_window_win.c | 61 +++++++++- runtime/menu_assets.c | 235 +++++++++++++++++++++++++++++++++++++ runtime/menu_assets.h | 19 +++ runtime/ui/menu.cpp | 113 +++++++++++++++--- runtime/ui/menu_art.cpp | 183 +++++++++++++++++++++++++++++ runtime/ui/menu_art.h | 26 ++++ runtime/ui/option_rows.cpp | 20 +++- 7 files changed, 634 insertions(+), 23 deletions(-) create mode 100644 runtime/menu_assets.c create mode 100644 runtime/menu_assets.h create mode 100644 runtime/ui/menu_art.cpp create mode 100644 runtime/ui/menu_art.h diff --git a/runtime/gfx_window_win.c b/runtime/gfx_window_win.c index 9a0c98f1..b2e71362 100644 --- a/runtime/gfx_window_win.c +++ b/runtime/gfx_window_win.c @@ -299,6 +299,65 @@ static void raise_window(HWND h) { } } +// The window's pointer: a gold arrowhead in the menu's colours, its tip the hotspot at the top left, the half towards +// the tip's right edge a lighter facet, with a dark outline; sized for the system DPI (32 px at 96 DPI) and drawn with +// 4x4 samples per pixel into a 32-bit cursor with alpha. The arrow when anything fails. +static int in_tri(float px, float py, float ax, float ay, float bx, float by, float cx, float cy) { + float d1 = (px - bx) * (ay - by) - (ax - bx) * (py - by), d2 = (px - cx) * (by - cy) - (bx - cx) * (py - cy), + d3 = (px - ax) * (cy - ay) - (cx - ax) * (py - ay); + int neg = d1 < 0 || d2 < 0 || d3 < 0, pos = d1 > 0 || d2 > 0 || d3 > 0; + return !(neg && pos); +} + +static HCURSOR pointer_cursor(void) { + UINT dpi = GetDpiForSystem(); + int n = (int)(32 * (dpi ? dpi : 96) / 96); + if (n < 24) n = 24; + if (n > 96) n = 96; + BITMAPV5HEADER bh = {.bV5Size = sizeof bh, .bV5Width = n, .bV5Height = -n, .bV5Planes = 1, .bV5BitCount = 32, + .bV5Compression = BI_BITFIELDS, .bV5RedMask = 0x00ff0000, .bV5GreenMask = 0x0000ff00, + .bV5BlueMask = 0x000000ff, .bV5AlphaMask = 0xff000000}; + HDC dc = GetDC(NULL); + uint32_t *px = NULL; + HBITMAP color = CreateDIBSection(dc, (BITMAPINFO *)&bh, DIB_RGB_COLORS, (void **)&px, NULL, 0); + ReleaseDC(NULL, dc); + HBITMAP mask = CreateBitmap(n, n, 1, 1, NULL); + if (!color || !mask || !px) { + if (color) DeleteObject(color); + if (mask) DeleteObject(mask); + return LoadCursorW(NULL, (LPCWSTR)IDC_ARROW); + } + // the arrowhead: tip (ax, ay), the right wing (bx, by), the lower wing (cx, cy), the notch (mx, my) between them + float o = n * 0.07f, ax = o, ay = o, bx = o + n * 0.70f, by = o + n * 0.46f, cx = o + n * 0.30f, cy = o + n * 0.80f; + float mx = o + n * 0.36f, my = o + n * 0.50f, e = n * 0.055f; + for (int y = 0; y < n; y++) + for (int x = 0; x < n; x++) { + int light = 0, deep = 0, edge = 0; + for (int sy = 0; sy < 4; sy++) + for (int sx = 0; sx < 4; sx++) { + float fx = x + (sx + 0.5f) / 4, fy = y + (sy + 0.5f) / 4; + int l = in_tri(fx, fy, ax, ay, bx, by, mx, my), d = !l && in_tri(fx, fy, ax, ay, mx, my, cx, cy); + int out = in_tri(fx, fy, ax - e, ay - e * 1.3f, bx + e * 1.4f, by + e * 0.2f, mx, my + e * 1.2f) || + in_tri(fx, fy, ax - e * 1.3f, ay - e, mx + e * 1.2f, my, cx + e * 0.2f, cy + e * 1.4f); + light += l, deep += d, edge += out && !l && !d; + } + int cov = light + deep + edge; + uint32_t a = (uint32_t)(cov * 255 / 16), r = 0, g = 0, b = 0; + if (cov) { + r = (uint32_t)((255 * light + 214 * deep + 44 * edge) / cov); + g = (uint32_t)((226 * light + 158 * deep + 28 * edge) / cov); + b = (uint32_t)((138 * light + 58 * deep + 10 * edge) / cov); + } + // premultiplied by alpha, as 32-bit cursors want + px[y * n + x] = a << 24 | (r * a / 255) << 16 | (g * a / 255) << 8 | (b * a / 255); + } + ICONINFO ii = {.fIcon = FALSE, .xHotspot = (DWORD)o, .yHotspot = (DWORD)o, .hbmMask = mask, .hbmColor = color}; + HCURSOR cur = CreateIconIndirect(&ii); + DeleteObject(color); + DeleteObject(mask); + return cur ? cur : LoadCursorW(NULL, (LPCWSTR)IDC_ARROW); +} + static void window_main(void *arg) { (void)arg; HINSTANCE inst = GetModuleHandleW(NULL); @@ -308,7 +367,7 @@ static void window_main(void *arg) { GetSystemMetrics(SM_CYICON), LR_SHARED), .hIconSm = LoadImageW(inst, MAKEINTRESOURCEW(1), IMAGE_ICON, GetSystemMetrics(SM_CXSMICON), GetSystemMetrics(SM_CYSMICON), LR_SHARED), - .hCursor = LoadCursorW(NULL, (LPCWSTR)IDC_ARROW), .lpszClassName = L"NX2naviRecompWindow"}; + .hCursor = pointer_cursor(), .lpszClassName = L"NX2naviRecompWindow"}; RegisterClassExW(&wc); int cw, ch, ow, oh; cfg_window_size(&cw, &ch); diff --git a/runtime/menu_assets.c b/runtime/menu_assets.c new file mode 100644 index 00000000..d4036a89 --- /dev/null +++ b/runtime/menu_assets.c @@ -0,0 +1,235 @@ +// The settings menu's look from the game's own files in the user's RomFS, read at the first use (menu_assets.h): its +// interface fonts (Font2/Scalable) and the shared interface textures (UI/Layout2Archive/Common...bntx). Nothing is +// shipped; a RomFS without them leaves the menu as it was. +// +// - .zs files are zstd frames, most with one of the dictionaries of Pack/ZsDic.pack.zs (a SARC compressed without one; +// the frame names its dictionary by id). +// - A .bfotf / .bfttf font is an OpenType / TrueType file behind an 8-byte header, every big-endian 32-bit word XORed +// with one key: the first word is 0x7f9a0218 under it, the second the payload size. +// - BNTX: the platform block at 0x20 ("NX ", or "Ounc" on Switch 2) counts the textures (+4) and points at their BRTI records (+8); a BRTI holds the +// format (+0x1c), width and height (+0x24, +0x28), the block height of the block-linear layout as log2 of GOBs +// (+0x34), the level size (+0x50), the name (+0x60, a u16-length string) and the level offsets (+0x70). The levels +// are block linear in the Switch 2 GOB layout (tex_deswizzle.c tex_bl_copy). BC4 masks become white with the mask as +// alpha (the menu tints them), ASTC 4x4 goes through the runtime's decoder. +#include +#include + +#include + +#include "fs.h" +#include "host.h" +#include "menu_assets.h" +#include "tex.h" + +#define MAX_RAW (512u << 20) + +static uint32_t le32(const uint8_t *p) { return (uint32_t)p[0] | (uint32_t)p[1] << 8 | (uint32_t)p[2] << 16 | (uint32_t)p[3] << 24; } +static uint16_t le16(const uint8_t *p) { return (uint16_t)(p[0] | p[1] << 8); } +static uint64_t le64(const uint8_t *p) { return (uint64_t)le32(p) | (uint64_t)le32(p + 4) << 32; } +static uint32_t be32(const uint8_t *p) { return (uint32_t)p[0] << 24 | (uint32_t)p[1] << 16 | (uint32_t)p[2] << 8 | p[3]; } + +// SARC member `name`: start and size inside p[0..n); 0 when absent or out of range. +static int sarc_member(const uint8_t *p, size_t n, const char *name, size_t *at, size_t *size) { + if (n < 0x14 || memcmp(p, "SARC", 4)) return 0; + size_t hl = le16(p + 4), data = le32(p + 0xc); + if (hl + 0xc > n || memcmp(p + hl, "SFAT", 4)) return 0; + size_t count = le16(p + hl + 6), ents = hl + 0xc, names = ents + 16 * count + 8; + if (names > n || memcmp(p + ents + 16 * count, "SFNT", 4)) return 0; + for (size_t i = 0; i < count; i++) { + const uint8_t *e = p + ents + 16 * i; + size_t no = names + (size_t)(le32(e + 4) & 0xffff) * 4, s = le32(e + 8), end = le32(e + 12); + if (no >= n || data + end > n || s > end) continue; + size_t len = strnlen((const char *)p + no, n - no); + if (len == strlen(name) && !memcmp(p + no, name, len)) { + *at = data + s, *size = end - s; + return 1; + } + } + return 0; +} + +static ZSTD_DDict *dicts[8]; +static unsigned dict_ids[8]; +static int ndicts = -1; + +static void dicts_load(void) { + ndicts = 0; + size_t zn = 0; + uint64_t zn64 = 0; + uint8_t *z = romfs_load("Pack/ZsDic.pack.zs", &zn64); + zn = (size_t)zn64; + if (!z) return; + unsigned long long rn = ZSTD_getFrameContentSize(z, zn); + uint8_t *raw = rn && rn <= MAX_RAW ? malloc((size_t)rn) : NULL; + if (raw && ZSTD_decompress(raw, (size_t)rn, z, zn) == rn) { + static const char *const names[] = {"zs.zsdic", "pack.zsdic", "bcett.byml.zsdic", "bkprb.zsdic"}; + for (int i = 0; i < 4 && ndicts < 8; i++) { + size_t at, size; + if (!sarc_member(raw, (size_t)rn, names[i], &at, &size)) continue; + ZSTD_DDict *d = ZSTD_createDDict(raw + at, size); + if (!d) continue; + dict_ids[ndicts] = ZSTD_getDictID_fromDDict(d); + dicts[ndicts++] = d; + } + } + free(raw); + free(z); +} + +uint8_t *menu_asset_load(const char *rel, size_t *n_out) { + uint64_t n = 0; + uint8_t *b = romfs_load(rel, &n); + size_t len = strlen(rel); + if (!b || len < 3 || strcmp(rel + len - 3, ".zs")) { + if (b && n_out) *n_out = (size_t)n; + return b; + } + unsigned long long rn = ZSTD_getFrameContentSize(b, (size_t)n); + unsigned id = ZSTD_getDictID_fromFrame(b, (size_t)n); + if (rn == ZSTD_CONTENTSIZE_UNKNOWN) { // a frame that does not say its size: streamed into a growing buffer + ZSTD_DDict *d = NULL; + if (id) { + if (ndicts < 0) dicts_load(); + for (int i = 0; i < ndicts; i++) + if (dict_ids[i] == id) d = dicts[i]; + if (!d) { free(b); return NULL; } + } + ZSTD_DCtx *dc = ZSTD_createDCtx(); + if (d && dc) ZSTD_DCtx_refDDict(dc, d); + size_t cap = (size_t)n * 4 + (1u << 20), len = 0; + uint8_t *out = dc ? malloc(cap) : NULL; + ZSTD_inBuffer in = {b, (size_t)n, 0}; + size_t r = 1; + while (out && r != 0) { + if (len == cap) { + if (cap >= MAX_RAW) { free(out); out = NULL; break; } + cap = cap * 2 > MAX_RAW ? MAX_RAW : cap * 2; + uint8_t *grown = realloc(out, cap); + if (!grown) { free(out); out = NULL; break; } + out = grown; + } + ZSTD_outBuffer o = {out, cap, len}; + r = ZSTD_decompressStream(dc, &o, &in); + len = o.pos; + if (ZSTD_isError(r) || (r && in.pos == in.size && o.pos < o.size)) { free(out); out = NULL; break; } + } + ZSTD_freeDCtx(dc); + free(b); + if (out && n_out) *n_out = len; + return out; + } + uint8_t *raw = rn && rn != ZSTD_CONTENTSIZE_ERROR && rn <= MAX_RAW ? malloc((size_t)rn) : NULL; + size_t got = 0; + if (raw) { + if (id) { + if (ndicts < 0) dicts_load(); + ZSTD_DDict *d = NULL; + for (int i = 0; i < ndicts; i++) + if (dict_ids[i] == id) d = dicts[i]; + ZSTD_DCtx *dc = d ? ZSTD_createDCtx() : NULL; + got = dc ? ZSTD_decompress_usingDDict(dc, raw, (size_t)rn, b, (size_t)n, d) : 0; + ZSTD_freeDCtx(dc); + } else { + got = ZSTD_decompress(raw, (size_t)rn, b, (size_t)n); + } + } + free(b); + if (!raw || ZSTD_isError(got) || got != rn) { + free(raw); + return NULL; + } + if (n_out) *n_out = (size_t)rn; + return raw; +} + +uint8_t *menu_asset_font(const char *rel, size_t *n_out) { + size_t n = 0; + uint8_t *b = menu_asset_load(rel, &n); + if (!b || n < 12) { free(b); return NULL; } + uint32_t key = be32(b) ^ 0x7f9a0218u, size = be32(b + 4) ^ key; + if (size > n - 8 || size < 12) { free(b); return NULL; } + uint8_t *out = malloc(size); + if (!out) { free(b); return NULL; } + for (uint32_t i = 0; i < size; i += 4) { + uint8_t w[4] = {0}; + uint32_t k = size - i < 4 ? size - i : 4; + memcpy(w, b + 8 + i, k); + uint32_t v = be32(w) ^ key; + uint8_t o[4] = {(uint8_t)(v >> 24), (uint8_t)(v >> 16), (uint8_t)(v >> 8), (uint8_t)v}; + memcpy(out + i, o, k); + } + free(b); + // OpenType with CFF ("OTTO") or TrueType outlines (0x00010000) + if (memcmp(out, "OTTO", 4) && be32(out) != 0x00010000u) { free(out); return NULL; } + if (n_out) *n_out = size; + return out; +} + +static void bc4_decode(const uint8_t *blocks, uint32_t wb, uint32_t hb, uint32_t w, uint32_t h, uint8_t *rgba) { + for (uint32_t by = 0; by < hb; by++) + for (uint32_t bx = 0; bx < wb; bx++) { + const uint8_t *b = blocks + ((size_t)by * wb + bx) * 8; + uint8_t pal[8] = {b[0], b[1]}; + if (b[0] > b[1]) + for (int i = 0; i < 6; i++) pal[2 + i] = (uint8_t)(((6 - i) * b[0] + (i + 1) * b[1]) / 7); + else { + for (int i = 0; i < 4; i++) pal[2 + i] = (uint8_t)(((4 - i) * b[0] + (i + 1) * b[1]) / 5); + pal[6] = 0, pal[7] = 255; + } + uint64_t bits = 0; + for (int i = 0; i < 6; i++) bits |= (uint64_t)b[2 + i] << (8 * i); + for (int i = 0; i < 16; i++) { + uint32_t x = bx * 4 + i % 4, y = by * 4 + i / 4; + if (x >= w || y >= h) continue; + uint8_t *p = rgba + ((size_t)y * w + x) * 4; + p[0] = p[1] = p[2] = 255; + p[3] = pal[bits >> (3 * i) & 7]; + } + } +} + +int menu_asset_texture(const uint8_t *bntx, size_t n, const char *name, uint8_t **rgba_out, uint32_t *w_out, uint32_t *h_out) { + // the platform block at 0x20 is "NX " on Switch, "Ounc" in Switch 2 archives; its layout is the same + if (!bntx || n < 0x40 || memcmp(bntx, "BNTX", 4)) return 0; + uint32_t count = le32(bntx + 0x24); + if (!count || count > 65536) return 0; + uint64_t arr = le64(bntx + 0x28); + if (arr + 8ull * count > n) return 0; + for (uint32_t i = 0; i < count; i++) { + uint64_t p = le64(bntx + arr + 8ull * i); + if (p + 0x78 > n || memcmp(bntx + p, "BRTI", 4)) continue; + uint64_t no = le64(bntx + p + 0x60); + if (no + 2 > n) continue; + uint16_t nl = le16(bntx + no); + if (no + 2 + nl > n || nl != strlen(name) || memcmp(bntx + no + 2, name, nl)) continue; + uint32_t fmt = le32(bntx + p + 0x1c), w = le32(bntx + p + 0x24), h = le32(bntx + p + 0x28); + uint32_t th = le32(bntx + p + 0x34), size = le32(bntx + p + 0x50); + uint64_t mips = le64(bntx + p + 0x70); + if (mips + 8 > n || !w || !h || w > 8192 || h > 8192 || th > 5) return 0; + uint64_t data = le64(bntx + mips); + if (data + size > n) return 0; + uint32_t kind = fmt >> 8, bpb, bw = 4, bh = 4; + if (kind == 0x1d) bpb = 8; // BC4 + else if (kind == 0x2d) bpb = 16; // ASTC 4x4 + else if (kind == 0x0b) bpb = 4, bw = bh = 1; // R8G8B8A8 + else { + rt_log("menu: interface texture %s has format %#x, not read", name, fmt); + return 0; + } + uint32_t wb = (w + bw - 1) / bw, hb = (h + bh - 1) / bh, row = wb * bpb; + // The block-linear source padded out to whole GOB columns and block-height units (tex_bl_copy reads them). + uint64_t gob_cols = (row + 63) / 64, unit_rows = 8ull << th, units = (hb + unit_rows - 1) / unit_rows; + uint64_t need = gob_cols * units * (512ull << th); + uint8_t *bl = calloc(1, need > size ? need : size), *lin = malloc((size_t)row * hb), *rgba = malloc((size_t)w * h * 4); + if (!bl || !lin || !rgba) { free(bl), free(lin), free(rgba); return 0; } + memcpy(bl, bntx + data, size); + tex_bl_copy(bl, lin, row, row, hb, 1, th, 0, 1); + if (kind == 0x1d) bc4_decode(lin, wb, hb, w, h, rgba); + else if (kind == 0x2d) astc_decode_ldr(lin, w, h, 4, 4, (fmt & 0xff) == 6, rgba); + else memcpy(rgba, lin, (size_t)w * h * 4); + free(bl), free(lin); + *rgba_out = rgba, *w_out = w, *h_out = h; + return 1; + } + return 0; +} diff --git a/runtime/menu_assets.h b/runtime/menu_assets.h new file mode 100644 index 00000000..315bf5e6 --- /dev/null +++ b/runtime/menu_assets.h @@ -0,0 +1,19 @@ +// The game's own interface fonts and textures for the settings menu, read from the user's RomFS (menu_assets.c). +#pragma once +#include +#include + +#ifdef __cplusplus +extern "C" { +#endif + +// A RomFS file, a .zs one decompressed with the game's dictionaries; malloc'd, NULL when absent or unreadable. +uint8_t *menu_asset_load(const char *rel, size_t *n); +// A .bfotf / .bfttf font as plain OpenType / TrueType bytes; malloc'd, NULL when absent or not a font. +uint8_t *menu_asset_font(const char *rel, size_t *n); +// Texture `name` of a BNTX archive as RGBA8 (BC4 masks: white, the mask in alpha); 1 with a malloc'd *rgba. +int menu_asset_texture(const uint8_t *bntx, size_t n, const char *name, uint8_t **rgba, uint32_t *w, uint32_t *h); + +#ifdef __cplusplus +} +#endif diff --git a/runtime/ui/menu.cpp b/runtime/ui/menu.cpp index 14c94c8d..eff5dcf5 100644 --- a/runtime/ui/menu.cpp +++ b/runtime/ui/menu.cpp @@ -30,6 +30,8 @@ static int g_font_bad; #pragma clang diagnostic ignored "-Wunused-function" #include "imstb_truetype.h" #include "menu_sound.h" +#include "menu_assets.h" +#include "ui/menu_art.h" #pragma clang diagnostic pop #include "ui/amiibo_page.h" #include "ui/binds_page.h" @@ -204,41 +206,42 @@ static float menu_scale(uint32_t h) { return ((float)h >= least ? (float)h : least) / 1080.0f; } -// Ocarina of Time colours: a night-sky navy panel, Kokiri green for what is selected, Triforce gold for accents and -// the cursor, the Ocarina's blue for the active control. -static const ImVec4 kGold(0.95f, 0.78f, 0.30f, 1), kGreen(0.16f, 0.42f, 0.26f, 1), kNavy(0.05f, 0.07f, 0.13f, 0.96f), - kOcarina(0.26f, 0.48f, 0.82f, 1); +// The game's menu colours: a dark umber panel, warm browns for frames and what is selected, gold for the ornaments, +// accents and the cursor, parchment text. kGreen and kOcarina keep their names for the selected and active controls. +static const ImVec4 kGold(0.93f, 0.76f, 0.38f, 1), kGreen(0.36f, 0.26f, 0.13f, 1), kNavy(0.075f, 0.058f, 0.042f, 0.95f), + kOcarina(0.62f, 0.47f, 0.22f, 1); static void style_init(ImGuiStyle &s) { ImGui::StyleColorsDark(&s); s.WindowRounding = 10; s.FrameRounding = 6; s.GrabRounding = 6; s.TabRounding = 6; s.ChildRounding = 8; - s.WindowPadding = ImVec2(30, 22); s.FramePadding = ImVec2(14, 8); s.ItemSpacing = ImVec2(14, 12); + s.WindowPadding = ImVec2(40, 30); s.FramePadding = ImVec2(14, 8); s.ItemSpacing = ImVec2(14, 12); s.WindowBorderSize = 2; s.ScrollbarSize = 16; ImVec4 *c = s.Colors; c[ImGuiCol_WindowBg] = kNavy; c[ImGuiCol_Border] = ImVec4(kGold.x, kGold.y, kGold.z, 0.55f); c[ImGuiCol_ChildBg] = ImVec4(0, 0, 0, 0); c[ImGuiCol_PopupBg] = kNavy; - c[ImGuiCol_Text] = ImVec4(0.97f, 0.95f, 0.88f, 1); - c[ImGuiCol_TextDisabled] = ImVec4(0.60f, 0.64f, 0.70f, 1); - c[ImGuiCol_FrameBg] = ImVec4(0.10f, 0.14f, 0.22f, 1); - c[ImGuiCol_FrameBgHovered] = ImVec4(0.13f, 0.24f, 0.22f, 1); + c[ImGuiCol_Text] = ImVec4(0.96f, 0.92f, 0.82f, 1); + c[ImGuiCol_TextDisabled] = ImVec4(0.62f, 0.56f, 0.46f, 1); + c[ImGuiCol_FrameBg] = ImVec4(0.15f, 0.115f, 0.08f, 1); + c[ImGuiCol_FrameBgHovered] = ImVec4(0.24f, 0.18f, 0.10f, 1); c[ImGuiCol_FrameBgActive] = kGreen; c[ImGuiCol_SliderGrab] = kOcarina; c[ImGuiCol_SliderGrabActive] = kGold; c[ImGuiCol_CheckMark] = kGold; - c[ImGuiCol_Button] = ImVec4(0.12f, 0.18f, 0.28f, 1); + c[ImGuiCol_Button] = ImVec4(0.19f, 0.145f, 0.09f, 1); c[ImGuiCol_ButtonHovered] = kGreen; c[ImGuiCol_ButtonActive] = kOcarina; - c[ImGuiCol_Header] = ImVec4(0.13f, 0.24f, 0.22f, 1); + c[ImGuiCol_Header] = ImVec4(0.24f, 0.18f, 0.10f, 1); c[ImGuiCol_HeaderHovered] = kGreen; c[ImGuiCol_HeaderActive] = kOcarina; - c[ImGuiCol_Tab] = ImVec4(0.09f, 0.13f, 0.21f, 1); + c[ImGuiCol_Tab] = ImVec4(0.13f, 0.10f, 0.07f, 1); c[ImGuiCol_TabHovered] = kGreen; - c[ImGuiCol_TabSelected] = ImVec4(0.14f, 0.36f, 0.24f, 1); + c[ImGuiCol_TabSelected] = ImVec4(0.42f, 0.31f, 0.15f, 1); c[ImGuiCol_TabSelectedOverline] = kGold; c[ImGuiCol_NavCursor] = kGold; c[ImGuiCol_Separator] = ImVec4(kGold.x, kGold.y, kGold.z, 0.30f); - c[ImGuiCol_ScrollbarGrab] = ImVec4(0.20f, 0.28f, 0.40f, 1); + c[ImGuiCol_ScrollbarGrab] = ImVec4(0.36f, 0.28f, 0.16f, 1); + c[ImGuiCol_ScrollbarBg] = ImVec4(0, 0, 0, 0.2f); } // The Triforce, its top at (x, y), h high, drawn with the title. @@ -264,6 +267,7 @@ static void make_context(void) { static void vk_down(void) { if (!g_vk_ok) return; + menu_art_release(); ImGui_ImplVulkan_Shutdown(); g_vk_ok = 0; g_font_scale = 0; @@ -351,13 +355,42 @@ static void add_row_glyphs(void *ctx, const char *text) { static_cast(ctx)->AddText(text); } +// The game's own interface fonts from the user's RomFS (menu_assets.c), read once: ClarimoUDPE for the menu's text, +// CetraDisplayPro for its title and tabs. Roboto stays merged behind them for the glyphs they lack; without them the +// menu keeps Roboto alone. +static uint8_t *g_game_font, *g_title_font_data; +static size_t g_game_font_n, g_title_font_n; +static ImFont *g_title_font; + +static void game_fonts_read(void) { + static int done; + if (done) return; + done = 1; + g_game_font = menu_asset_font("Font2/Scalable/ClarimoUDPE-Regular.Nin_Ounce_NVN2.bfotf.zs", &g_game_font_n); + g_title_font_data = menu_asset_font("Font2/Scalable/CetraDisplayPro-Medium.Nin_Ounce_NVN2.bfotf.zs", &g_title_font_n); + if (g_game_font && !font_usable(g_game_font, g_game_font_n)) free(g_game_font), g_game_font = NULL; + if (g_title_font_data && !font_usable(g_title_font_data, g_title_font_n)) free(g_title_font_data), g_title_font_data = NULL; + rt_log("menu: the game's interface fonts: text %s, titles %s", g_game_font ? "ClarimoUDPE" : "not found (Roboto)", + g_title_font_data ? "CetraDisplayPro" : "not found"); +} + static int fonts_up(float scale) { enum { N = sizeof kCjk / sizeof *kCjk }; ImFontAtlas *fa = ImGui::GetIO().Fonts; fa->Clear(); + game_fonts_read(); + g_title_font = nullptr; ImFontConfig fc; fc.FontDataOwnedByAtlas = false; - fa->AddFontFromMemoryTTF((void *)roboto_ttf, (int)sizeof roboto_ttf, 26.0f * scale, &fc, kRanges); + if (g_game_font) { + fa->AddFontFromMemoryTTF(g_game_font, (int)g_game_font_n, 25.5f * scale, &fc, kRanges); + ImFontConfig rc; + rc.MergeMode = true; + rc.FontDataOwnedByAtlas = false; + fa->AddFontFromMemoryTTF((void *)roboto_ttf, (int)sizeof roboto_ttf, 26.0f * scale, &rc, kRanges); + } else { + fa->AddFontFromMemoryTTF((void *)roboto_ttf, (int)sizeof roboto_ttf, 26.0f * scale, &fc, kRanges); + } uint8_t *data[N] = {}; ImVector ranges[N]; int merged = 0; @@ -388,8 +421,20 @@ static int fonts_up(float scale) { mc.FontDataOwnedByAtlas = false; fa->AddFontFromMemoryTTF(data[i], (int)size, 26.0f * scale, &mc, ranges[i].Data); } + if (g_title_font_data) { // after the text font's merges, which go to the font added last + ImFontConfig tc; + tc.FontDataOwnedByAtlas = false; + g_title_font = fa->AddFontFromMemoryTTF(g_title_font_data, (int)g_title_font_n, 31.0f * scale, &tc, kRanges); + ImFontConfig rc; + rc.MergeMode = true; + rc.FontDataOwnedByAtlas = false; + fa->AddFontFromMemoryTTF((void *)roboto_ttf, (int)sizeof roboto_ttf, 30.0f * scale, &rc, kRanges); + } vk_queue_lock(); // the upload submits on the queue and waits for it int ok = ImGui_ImplVulkan_CreateFontsTexture(); + // ClarimoUDPE's line (ascent to descent, which is all Dear ImGui spaces lines by) leaves wrapped lines touching: the + // line height grows by a sixth over the glyphs' size, the glyphs stay as rasterized. + if (ok && g_game_font && fa->Fonts.Size) fa->Fonts[0]->FontSize *= 1.16f; vk_queue_unlock(); fa->ClearInputData(); // the glyphs are in the texture: the font files and the pixels are not needed again fa->ClearTexData(); @@ -889,12 +934,33 @@ static void menu_sounds_frame(void) { } static void layout_menu(uint32_t w, uint32_t h, float scale) { - ImGui::GetBackgroundDrawList()->AddRectFilled(ImVec2(0, 0), ImVec2((float)w, (float)h), IM_COL32(4, 8, 20, 140)); + ImGui::GetBackgroundDrawList()->AddRectFilled(ImVec2(0, 0), ImVec2((float)w, (float)h), IM_COL32(10, 6, 2, 150)); float ww = fminf(w * 0.88f, 1400 * scale), wh = fminf(h * 0.86f, 820 * scale); ImGui::SetNextWindowPos(ImVec2(w * 0.5f, h * 0.5f), ImGuiCond_Always, ImVec2(0.5f, 0.5f)); ImGui::SetNextWindowSize(ImVec2(ww, wh), ImGuiCond_Always); + ImTextureID frame_tex; + const bool framed = menu_art(MENU_ART_FRAME_DECO, &frame_tex, nullptr, nullptr); + if (framed) ImGui::PushStyleVar(ImGuiStyleVar_WindowBorderSize, 0); // the game's frame is the border ImGui::Begin("##menu", nullptr, ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoMove | ImGuiWindowFlags_NoSavedSettings); + if (framed) ImGui::PopStyleVar(); + const ImVec2 win_p = ImGui::GetWindowPos(), win_s = ImGui::GetWindowSize(); + const float inset = 7 * scale; + // The game's ornamented frame (ui/menu_art.cpp; nothing without the RomFS's), in gold. Its lines lie inside the + // texture (about 4 % of the width, 7 % of the height in), so the frame is drawn that far outside the window and its + // lines run along the window's edge, clear of the title and the rows; unclipped, it may reach past the window. + if (framed) { + ImDrawList *fdl = ImGui::GetWindowDrawList(); + const float ox = 15 * scale, oy = 30 * scale; + fdl->PushClipRectFullScreen(); + // the art holds the top and left edges with their corners; turned half a turn it gives the bottom and right ones + for (int turned = 0; turned < 2; turned++) + menu_art_draw9_turned(fdl, MENU_ART_FRAME_DECO, ImVec2(win_p.x - ox, win_p.y - oy), + ImVec2(win_p.x + win_s.x + ox, win_p.y + win_s.y + oy), 96 * scale, 0.2f, + IM_COL32(236, 196, 112, 235), 0, turned); + fdl->PopClipRect(); + } + if (g_title_font) ImGui::PushFont(g_title_font); { // the Triforce, then "Ocarina of Time" and the page title ImVec2 at = ImGui::GetCursorScreenPos(); float th = ImGui::GetTextLineHeight(); @@ -906,6 +972,7 @@ static void layout_menu(uint32_t w, uint32_t h, float scale) { ImGui::SameLine(); ImGui::TextUnformatted(menu_text(UI_MENU_TITLE)); } + if (g_title_font) ImGui::PopFont(); // the port version and the game update, right-aligned on the title line: what a bug report needs first char game[16], version[96]; snprintf(game, sizeof game, "%d.%d.%d", rt_game_version / 100, rt_game_version / 10 % 10, rt_game_version % 10); @@ -927,7 +994,10 @@ static void layout_menu(uint32_t w, uint32_t h, float scale) { for (int k = 0; k < TAB_COUNT; k++) { char label[128]; snprintf(label, sizeof label, " %s ###tab%d", menu_text(kTabNames[k]), k); // the id stays per language - if (!ImGui::BeginTabItem(label, nullptr, g_tab_request == k ? ImGuiTabItemFlags_SetSelected : 0)) continue; + if (g_title_font) ImGui::PushFont(g_title_font); // the tab labels in the title face + bool shown = ImGui::BeginTabItem(label, nullptr, g_tab_request == k ? ImGuiTabItemFlags_SetSelected : 0); + if (g_title_font) ImGui::PopFont(); + if (!shown) continue; g_tab = k; if (g_focus_page == k) { opt_focus_first(); @@ -950,9 +1020,15 @@ static void layout_menu(uint32_t w, uint32_t h, float scale) { ImGui::EndTabBar(); } - // bottom row: the save status on the left, the two buttons on the right + // bottom row: the save status on the left, the two buttons on the right, on the game's control-guide bar ImGui::Spacing(); float y = ImGui::GetCursorPosY(); + { + float sy = win_p.y + y, fh = ImGui::GetFrameHeight(); + // within the bottom row's own height: above it the page's last row may still show + menu_art_draw(ImGui::GetWindowDrawList(), MENU_ART_GUIDE_BAR, ImVec2(win_p.x + st.WindowPadding.x, sy), + ImVec2(win_p.x + win_s.x - st.WindowPadding.x, sy + fh), IM_COL32(96, 70, 36, 170)); + } if (!cfg_last_save_ok()) { // stays until a save succeeds; the settings hold for the session ImGui::SetCursorPosY(y + st.FramePadding.y); ImGui::TextColored(ImVec4(0.95f, 0.45f, 0.35f, 1), "%s", menu_text(UI_MENU_SAVE_FAILED)); @@ -1247,6 +1323,7 @@ static void perf_draw(const MenuTarget *t, float scale) { extern "C" int menu_overlay_frame(uint64_t frame, const MenuTarget *t, const char **shot) { *shot = NULL; + menu_art_start(); // the game's interface textures decode on a worker from the first present on cheats_frame(frame); // the data cheats and the buttons of the cheats that need them held if (!g_test.parsed) test_parse(); if (g_test.next_set < g_test.n_sets && frame >= g_test.sets[g_test.next_set].frame) test_apply_sets(frame); diff --git a/runtime/ui/menu_art.cpp b/runtime/ui/menu_art.cpp new file mode 100644 index 00000000..0745b363 --- /dev/null +++ b/runtime/ui/menu_art.cpp @@ -0,0 +1,183 @@ +// The settings menu's art from the game's own interface textures (menu_assets.c reads them from the user's RomFS): +// the cursor's glow and line, the frame's corner ornaments and the control guide's bar. A worker decodes them into +// RGBA once at start (the archive unpacks to about 240 MB and takes a moment); the window thread uploads each to the GPU +// the first time the menu asks for it, and registers it with Dear ImGui's Vulkan back end. Without the files the menu +// draws its plain shapes. +#include +#include +#include +#include + +#include "imgui.h" +#include "backends/imgui_impl_vulkan.h" + +extern "C" { +#include "host.h" +#include "vk.h" +#include "platform/platform.h" +} +#include "menu_assets.h" +#include "ui/menu_art.h" + +#define COMMON_BNTX "UI/Layout2Archive/Common.Product.100.Nin_Ounce_NVN2.bntx.zs" + +static const char *const kNames[MENU_ART_COUNT] = { + "CmnBtnCursorGlow_00", "CmnBtnCursorLine_00", "LocationDeco_00", "CtrlGuideBG_00", "MessageWindowCommonBase_00", +}; + +static struct Art { + uint8_t *rgba; + uint32_t w, h; + VkImage img; + VmaAllocation alloc; + VkImageView view; + VkDescriptorSet ds; + int failed; +} g_art[MENU_ART_COUNT]; +static std::atomic g_state; // 0 not started, 1 decoding, 2 decoded, 3 nothing to show +static VkSampler g_sampler; + +static void decode_all(void *) { + size_t n = 0; + uint8_t *bntx = menu_asset_load(COMMON_BNTX, &n); + int got = 0; + if (!bntx) rt_log("menu: %s could not be read from the RomFS", COMMON_BNTX); + for (int i = 0; bntx && i < MENU_ART_COUNT; i++) { + Art &a = g_art[i]; + if (menu_asset_texture(bntx, n, kNames[i], &a.rgba, &a.w, &a.h)) got++; + else rt_log("menu: interface texture %s not decoded (archive of %llu bytes)", kNames[i], (unsigned long long)n); + } + free(bntx); + rt_log("menu: %d of %d interface textures from the RomFS", got, (int)MENU_ART_COUNT); + g_state.store(got ? 2 : 3, std::memory_order_release); +} + +extern "C" void menu_art_start(void) { + int expect = 0; + if (!g_state.compare_exchange_strong(expect, 1)) return; + if (!plat_thread_spawn(decode_all, nullptr, "menu-art", 0)) decode_all(nullptr); +} + +static int upload(Art &a) { + VkCtx *c = vk_ctx(); + if (!c || !a.rgba) return 0; + if (!g_sampler) { + VkSamplerCreateInfo si = {VK_STRUCTURE_TYPE_SAMPLER_CREATE_INFO}; + si.magFilter = si.minFilter = VK_FILTER_LINEAR; + si.mipmapMode = VK_SAMPLER_MIPMAP_MODE_LINEAR; + si.addressModeU = si.addressModeV = si.addressModeW = VK_SAMPLER_ADDRESS_MODE_CLAMP_TO_EDGE; + si.maxLod = 1; + if (vkCreateSampler(c->device, &si, nullptr, &g_sampler) != VK_SUCCESS) return 0; + } + VkImageCreateInfo ii = {VK_STRUCTURE_TYPE_IMAGE_CREATE_INFO}; + ii.imageType = VK_IMAGE_TYPE_2D; + ii.format = VK_FORMAT_R8G8B8A8_UNORM; + ii.extent = {a.w, a.h, 1}; + ii.mipLevels = ii.arrayLayers = 1; + ii.samples = VK_SAMPLE_COUNT_1_BIT; + ii.tiling = VK_IMAGE_TILING_OPTIMAL; + ii.usage = VK_IMAGE_USAGE_SAMPLED_BIT | VK_IMAGE_USAGE_TRANSFER_DST_BIT; + VmaAllocationCreateInfo ai = {}; + ai.usage = VMA_MEMORY_USAGE_AUTO_PREFER_DEVICE; + if (vmaCreateImage(c->vma, &ii, &ai, &a.img, &a.alloc, nullptr) != VK_SUCCESS) return 0; + VkDeviceSize bytes = (VkDeviceSize)a.w * a.h * 4; + VkBuffer sb; + VmaAllocation sa; + void *map = vk_staging(bytes, &sb, &sa); + if (!map) return 0; + memcpy(map, a.rgba, (size_t)bytes); + VkCommandBuffer cb = vk_upload_begin(); + VkImageMemoryBarrier2 b = {VK_STRUCTURE_TYPE_IMAGE_MEMORY_BARRIER_2}; + b.srcStageMask = VK_PIPELINE_STAGE_2_NONE; + b.dstStageMask = VK_PIPELINE_STAGE_2_COPY_BIT; + b.dstAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + b.oldLayout = VK_IMAGE_LAYOUT_UNDEFINED; + b.newLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + b.srcQueueFamilyIndex = b.dstQueueFamilyIndex = VK_QUEUE_FAMILY_IGNORED; + b.image = a.img; + b.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + VkDependencyInfo di = {VK_STRUCTURE_TYPE_DEPENDENCY_INFO}; + di.imageMemoryBarrierCount = 1; + di.pImageMemoryBarriers = &b; + vkCmdPipelineBarrier2(cb, &di); + VkBufferImageCopy r = {}; + r.imageSubresource = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 0, 1}; + r.imageExtent = {a.w, a.h, 1}; + vkCmdCopyBufferToImage(cb, sb, a.img, VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL, 1, &r); + b.srcStageMask = VK_PIPELINE_STAGE_2_COPY_BIT; + b.srcAccessMask = VK_ACCESS_2_TRANSFER_WRITE_BIT; + b.dstStageMask = VK_PIPELINE_STAGE_2_FRAGMENT_SHADER_BIT; + b.dstAccessMask = VK_ACCESS_2_SHADER_SAMPLED_READ_BIT; + b.oldLayout = VK_IMAGE_LAYOUT_TRANSFER_DST_OPTIMAL; + b.newLayout = VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL; + vkCmdPipelineBarrier2(cb, &di); + vk_upload_end(); + vk_upload_flush(1); + vmaDestroyBuffer(c->vma, sb, sa); + VkImageViewCreateInfo vi = {VK_STRUCTURE_TYPE_IMAGE_VIEW_CREATE_INFO}; + vi.image = a.img; + vi.viewType = VK_IMAGE_VIEW_TYPE_2D; + vi.format = VK_FORMAT_R8G8B8A8_UNORM; + vi.subresourceRange = {VK_IMAGE_ASPECT_COLOR_BIT, 0, 1, 0, 1}; + if (vkCreateImageView(c->device, &vi, nullptr, &a.view) != VK_SUCCESS) return 0; + a.ds = ImGui_ImplVulkan_AddTexture(g_sampler, a.view, VK_IMAGE_LAYOUT_SHADER_READ_ONLY_OPTIMAL); + return a.ds != VK_NULL_HANDLE; // the pixels stay (a few MB): the back end may start again on another format +} + +extern "C" int menu_art(MenuArt id, ImTextureID *tex, float *w, float *h) { + if ((unsigned)id >= MENU_ART_COUNT || g_state.load(std::memory_order_acquire) != 2) return 0; + Art &a = g_art[id]; + if (!a.ds && !a.failed && !upload(a)) a.failed = 1; + if (!a.ds) return 0; + *tex = (ImTextureID)a.ds; + if (w) *w = (float)a.w; + if (h) *h = (float)a.h; + return 1; +} + +extern "C" void menu_art_release(void) { + VkCtx *c = vk_ctx(); + if (!c) return; + for (Art &a : g_art) { + if (a.ds) ImGui_ImplVulkan_RemoveTexture(a.ds); + if (a.view) vkDestroyImageView(c->device, a.view, nullptr); + if (a.img) vmaDestroyImage(c->vma, a.img, a.alloc); + a.ds = VK_NULL_HANDLE, a.view = VK_NULL_HANDLE, a.img = VK_NULL_HANDLE, a.alloc = nullptr; + a.failed = 0; // uploaded again at the next use + } + if (g_sampler) vkDestroySampler(c->device, g_sampler, nullptr), g_sampler = VK_NULL_HANDLE; +} + +extern "C" int menu_art_draw(ImDrawList *dl, MenuArt id, ImVec2 a, ImVec2 b, ImU32 tint) { + ImTextureID t; + if (!menu_art(id, &t, nullptr, nullptr)) return 0; + dl->AddImage(t, a, b, ImVec2(0, 0), ImVec2(1, 1), tint); + return 1; +} + +// Nine pieces: the corners at `corner` pixels on screen from the texture's outer `frac` of each side, the edges +// stretched between them, the middle left out (frames) or stretched (fill). +extern "C" int menu_art_draw9_turned(ImDrawList *dl, MenuArt id, ImVec2 a, ImVec2 b, float corner, float frac, ImU32 tint, int fill, + int turned); +extern "C" int menu_art_draw9(ImDrawList *dl, MenuArt id, ImVec2 a, ImVec2 b, float corner, float frac, ImU32 tint, int fill) { + return menu_art_draw9_turned(dl, id, a, b, corner, frac, tint, fill, 0); +} + +// The same, the texture turned half a turn when turned: its top-left corner piece at the bottom right. +extern "C" int menu_art_draw9_turned(ImDrawList *dl, MenuArt id, ImVec2 a, ImVec2 b, float corner, float frac, ImU32 tint, int fill, + int turned) { + ImTextureID t; + if (!menu_art(id, &t, nullptr, nullptr)) return 0; + float c = corner, maxc = fminf((b.x - a.x) / 2, (b.y - a.y) / 2); + if (c > maxc) c = maxc; + const float xs[4] = {a.x, a.x + c, b.x - c, b.x}, ys[4] = {a.y, a.y + c, b.y - c, b.y}; + float us[4] = {0, frac, 1 - frac, 1}, vs[4] = {0, frac, 1 - frac, 1}; + if (turned) + for (int k = 0; k < 4; k++) us[k] = 1 - us[k], vs[k] = 1 - vs[k]; + for (int j = 0; j < 3; j++) + for (int i = 0; i < 3; i++) { + if (i == 1 && j == 1 && !fill) continue; + dl->AddImage(t, ImVec2(xs[i], ys[j]), ImVec2(xs[i + 1], ys[j + 1]), ImVec2(us[i], vs[j]), ImVec2(us[i + 1], vs[j + 1]), tint); + } + return 1; +} diff --git a/runtime/ui/menu_art.h b/runtime/ui/menu_art.h new file mode 100644 index 00000000..3b7e650e --- /dev/null +++ b/runtime/ui/menu_art.h @@ -0,0 +1,26 @@ +// The settings menu's art from the game's own interface textures (ui/menu_art.cpp). +#pragma once +#include "imgui.h" + +typedef enum { + MENU_ART_CURSOR_GLOW, // the glow of the game's button cursor (white mask; tinted by the caller) + MENU_ART_CURSOR_LINE, // its outline + MENU_ART_FRAME_DECO, // a frame with ornamented corners + MENU_ART_GUIDE_BAR, // the control guide's bar + MENU_ART_WINDOW_BASE, // a rounded window base + MENU_ART_COUNT +} MenuArt; + +extern "C" void menu_art_start(void); // decodes the textures on a worker, once +// The texture for the menu's draw lists and its size in pixels; 0 while it is not there (still decoding, or absent). +// Window thread: the first call for an art uploads it. +extern "C" int menu_art(MenuArt id, ImTextureID *tex, float *w, float *h); +extern "C" void menu_art_release(void); // before ImGui's Vulkan back end shuts down +// Draws an art stretched over [a, b] with a tint; 0 (nothing drawn) while it is not there. +extern "C" int menu_art_draw(ImDrawList *dl, MenuArt id, ImVec2 a, ImVec2 b, ImU32 tint); +// Draws an art as nine pieces: corners `corner` pixels on screen from the outer `frac` of the texture, edges stretched, +// the middle too when fill. +extern "C" int menu_art_draw9(ImDrawList *dl, MenuArt id, ImVec2 a, ImVec2 b, float corner, float frac, ImU32 tint, int fill); +// The same with the texture turned half a turn (turned 1): a top-left corner piece drawn at the bottom right. +extern "C" int menu_art_draw9_turned(ImDrawList *dl, MenuArt id, ImVec2 a, ImVec2 b, float corner, float frac, ImU32 tint, int fill, + int turned); diff --git a/runtime/ui/option_rows.cpp b/runtime/ui/option_rows.cpp index a7efacd3..c5fac488 100644 --- a/runtime/ui/option_rows.cpp +++ b/runtime/ui/option_rows.cpp @@ -18,6 +18,7 @@ extern "C" { #include "cfg.h" #include "platform/platform.h" #include "ui_text.h" +#include "ui/menu_art.h" #include "menu_sound.h" // host.h pulls in cpu.h, which is C only: the one function needed from it, as declared there. void rt_log(const char *fmt, ...) __attribute__((format(printf, 1, 2))); @@ -164,10 +165,21 @@ static int row_text(const char *key, const char *label, const char *desc, const ImDrawList *dl = ImGui::GetWindowDrawList(); ImFont *font = ImGui::GetFont(); float r = 9 * g_scale, pad = 16 * g_scale, small = fs * 0.72f; - dl->AddRectFilled(p, ImVec2(p.x + w, p.y + h), hov || foc ? IM_COL32(255, 255, 255, 22) : IM_COL32(0, 0, 0, 40), r); - dl->AddRect(p, ImVec2(p.x + w, p.y + h), - hov || foc ? (off ? IM_COL32(236, 194, 84, 120) : kGold) : IM_COL32(255, 255, 255, off ? 18 : 38), r, 0, - hov || foc ? 2.0f : 1.0f); + dl->AddRectFilled(p, ImVec2(p.x + w, p.y + h), hov || foc ? IM_COL32(255, 230, 170, 18) : IM_COL32(0, 0, 0, 46), r); + // the selected row in the game's own cursor (glow, then its line; ui/menu_art.cpp), else a gold border + bool art = false; + if (hov || foc) { // a halo around the row (the glow's middle left out, so the label stays readable), then the line + float gx = h * 0.22f, gy = h * 0.16f; + art = menu_art_draw9(dl, MENU_ART_CURSOR_GLOW, ImVec2(p.x - gx, p.y - gy), ImVec2(p.x + w + gx, p.y + h + gy), h * 0.5f, 0.3f, + off ? IM_COL32(236, 194, 84, 50) : IM_COL32(244, 200, 96, 120), 0) && + menu_art_draw9(dl, MENU_ART_CURSOR_LINE, ImVec2(p.x - gx * 0.3f, p.y - gy * 0.3f), + ImVec2(p.x + w + gx * 0.3f, p.y + h + gy * 0.3f), h * 0.12f, 0.1f, + off ? IM_COL32(255, 236, 190, 80) : IM_COL32(255, 236, 180, 150), 0); // edges only: the art is a filled bar + } + if (!art) + dl->AddRect(p, ImVec2(p.x + w, p.y + h), + hov || foc ? (off ? IM_COL32(236, 194, 84, 120) : kGold) : IM_COL32(255, 236, 200, off ? 16 : 34), r, 0, + hov || foc ? 2.0f : 1.0f); float ty = p.y + fs * 0.25f, ny = p.y + fs * 1.3f; // the label line and the note line under it float right = p.x + w - pad; // the value's right edge float value_left = right;