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;