mirror of
https://github.com/tangerineturtle1/NX2navi.git
synced 2026-11-15 19:19:58 -04:00
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 <noreply@anthropic.com>
490 lines
24 KiB
C++
490 lines
24 KiB
C++
// Two-pane settings pages (option_rows.h): rows on the left, the selected or hovered row's description on the right.
|
|
//
|
|
// Contains code Copyright (c) 2026 Revivedx and the Savage Wind Recomp contributors, Copyright (c) 2026 Peiikai (MIT; see
|
|
// THIRD_PARTY_NOTICES.md).
|
|
// The rows read and write cfg keys, take their text from the menu catalog (menu_text), and show the default, "after
|
|
// restart" and environment notes.
|
|
#include <math.h>
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
|
|
#include "imgui.h"
|
|
#include "imgui_internal.h" // SetKeyOwner, SetFocusID, NavCursorVisible
|
|
|
|
#include "ui/option_rows.h"
|
|
|
|
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)));
|
|
}
|
|
|
|
static const ImU32 kGold = IM_COL32(236, 194, 84, 255);
|
|
static const ImU32 kDim = IM_COL32(150, 150, 140, 255);
|
|
static const ImU32 kGrey = IM_COL32(110, 110, 102, 255);
|
|
|
|
static float g_scale = 1;
|
|
static bool g_focus_req, g_take_first; // opt_focus_first: requested / this page's first row takes it
|
|
static char g_focus_key[64]; // opt_focus_key: the row that takes the cursor when it is next drawn
|
|
static bool g_held; // the last row's left / right has been held a while (steppers go faster)
|
|
static float g_bottom; // opt_reserve_bottom: kept free under the panes
|
|
static bool g_disable_next; // opt_disable_next: the next row is greyed out with g_disable_note
|
|
static const char *g_disable_note;
|
|
|
|
// The description pane: this frame's hovered and selected rows, the page's first row, and the last one shown.
|
|
struct Desc { char text[UI_DESC_BYTES]; };
|
|
static Desc g_hover, g_focus, g_first, g_shown;
|
|
|
|
// disabled: the note of a row opt_disable_next greyed out, else NULL.
|
|
static void desc_set(Desc &d, const char *desc, const char *key, const CfgMeta *m, const char *disabled) {
|
|
int n = snprintf(d.text, sizeof d.text, "%s", desc);
|
|
if (n < 0 || n >= (int)sizeof d.text) return;
|
|
if (disabled)
|
|
snprintf(d.text + n, sizeof d.text - n, "\n\n%s", disabled);
|
|
else if (cfg_from_env(key))
|
|
snprintf(d.text + n, sizeof d.text - n, "\n\n%s %s", menu_text(UI_MENU_SET_BY_ENV), key);
|
|
else if (m && !m->live)
|
|
snprintf(d.text + n, sizeof d.text - n, "\n\n%s", menu_text(UI_MENU_AFTER_RESTART));
|
|
}
|
|
|
|
void opt_focus_first(void) { g_focus_req = true; }
|
|
|
|
void opt_focus_key(const char *key) { snprintf(g_focus_key, sizeof g_focus_key, "%s", key); }
|
|
|
|
void opt_reserve_bottom(float h) { g_bottom = h; }
|
|
float opt_bottom(void) { return g_bottom; }
|
|
|
|
void opt_disable_next(UiTextId note) { opt_disable_next_text(menu_text(note)); }
|
|
|
|
void opt_disable_next_text(const char *note) {
|
|
g_disable_next = true;
|
|
g_disable_note = note;
|
|
}
|
|
|
|
void opt_begin(const char *id, float scale) {
|
|
g_scale = scale;
|
|
g_take_first = g_focus_req;
|
|
g_focus_req = false;
|
|
if (g_take_first) ImGui::NavMoveRequestCancel(); // this frame's move goes to the first row instead
|
|
g_hover.text[0] = g_focus.text[0] = g_first.text[0] = 0;
|
|
ImVec2 avail = ImGui::GetContentRegionAvail();
|
|
avail.y -= g_bottom;
|
|
ImGui::PushID(id);
|
|
ImGui::BeginChild("##rows", ImVec2(avail.x * 0.58f, avail.y), ImGuiChildFlags_NavFlattened);
|
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(ImGui::GetStyle().ItemSpacing.x, 5 * scale));
|
|
}
|
|
|
|
void opt_end(void) {
|
|
ImGui::PopStyleVar();
|
|
ImGui::EndChild();
|
|
// the hovered row (mouse) wins, else the selected one (controller), else the last shown, else the first row
|
|
if (g_hover.text[0]) g_shown = g_hover;
|
|
else if (g_focus.text[0]) g_shown = g_focus;
|
|
else if (!g_shown.text[0]) g_shown = g_first;
|
|
ImGui::SameLine(0, 0);
|
|
ImVec2 p = ImGui::GetCursorScreenPos(), avail = ImGui::GetContentRegionAvail();
|
|
avail.y -= g_bottom;
|
|
float gap = 24 * g_scale;
|
|
ImGui::GetWindowDrawList()->AddLine(ImVec2(p.x + gap * 0.5f, p.y), ImVec2(p.x + gap * 0.5f, p.y + avail.y),
|
|
IM_COL32(236, 194, 84, 110), 1.0f);
|
|
ImGui::SetCursorScreenPos(ImVec2(p.x + gap, p.y));
|
|
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(14 * g_scale, 6 * g_scale));
|
|
ImGui::BeginChild("##desc", ImVec2(0, avail.y), ImGuiChildFlags_AlwaysUseWindowPadding, ImGuiWindowFlags_NoNav);
|
|
ImGui::PopStyleVar();
|
|
ImGui::PushTextWrapPos(0);
|
|
ImGui::TextUnformatted(g_shown.text);
|
|
ImGui::PopTextWrapPos();
|
|
ImGui::EndChild();
|
|
ImGui::PopID();
|
|
}
|
|
|
|
// What a row shows besides its label.
|
|
struct RowView {
|
|
const char *value; // right side; "" for none
|
|
bool is_default; // the value is the setting's default: the "default" mark, the value not highlighted
|
|
bool arrows; // a stepper: < value >, the arrows click too
|
|
const char *note; // under the value instead of the "default" mark; NULL for none
|
|
};
|
|
|
|
// One row: draws it, handles A / click and left / right while selected. Returns 2 (A / click), -1 / +1 (left / right),
|
|
// 3 / 4 (move up / down, movable rows only) or 0; always 0 for a row the environment fixes (greyed out, still selectable
|
|
// so its description shows). movable: the top / left face buttons, Ctrl+Up / Ctrl+Down and two drawn arrows move it.
|
|
static int row_text(const char *key, const char *label, const char *desc, const RowView &v, bool movable) {
|
|
const CfgMeta *m = cfg_meta(key);
|
|
const char *disabled = g_disable_next ? g_disable_note : NULL;
|
|
g_disable_next = false;
|
|
bool off = disabled || cfg_from_env(key) != 0;
|
|
ImGui::PushID(key);
|
|
ImVec2 p = ImGui::GetCursorScreenPos();
|
|
// h whole pixels: ImGui truncates cursor positions, which rounds a fractional height the other way above the window's
|
|
// top (negative y), so the page grew a pixel per row scrolled out and a row scrolled to the bottom was cut off
|
|
float fs = ImGui::GetFontSize(), w = ImGui::GetContentRegionAvail().x, h = floorf(fs * 2.2f);
|
|
ImGui::PushStyleColor(ImGuiCol_NavCursor, ImVec4(0, 0, 0, 0)); // the row's own border shows the selection
|
|
bool clicked = ImGui::InvisibleButton("##row", ImVec2(w, h), ImGuiButtonFlags_EnableNav);
|
|
ImGui::PopStyleColor();
|
|
bool to_key = g_focus_key[0] && !strcmp(key, g_focus_key);
|
|
if (g_take_first || to_key) {
|
|
ImGui::SetFocusID(ImGui::GetItemID(), ImGui::GetCurrentWindow());
|
|
ImGui::SetNavCursorVisible(true);
|
|
if (g_take_first) ImGui::SetScrollY(0); // the page's top: text and a heading above the first row show
|
|
else ImGui::ScrollToItem(ImGuiScrollFlags_KeepVisibleEdgeY); // as a cursor move would (the last row whole)
|
|
g_take_first = false;
|
|
if (to_key) g_focus_key[0] = 0;
|
|
}
|
|
bool hov = ImGui::IsItemHovered(), foc = ImGui::IsItemFocused() && ImGui::GetCurrentContext()->NavCursorVisible;
|
|
int step = clicked && !off ? 2 : 0;
|
|
g_held = false;
|
|
if (foc && !off) {
|
|
// the left stick arrives as the D-pad (menu.cpp pad_events)
|
|
if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadLeft) || ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) step = -1;
|
|
if (ImGui::IsKeyPressed(ImGuiKey_GamepadDpadRight) || ImGui::IsKeyPressed(ImGuiKey_RightArrow)) step = 1;
|
|
static const ImGuiKey steps[] = {ImGuiKey_GamepadDpadLeft, ImGuiKey_GamepadDpadRight, ImGuiKey_LeftArrow,
|
|
ImGuiKey_RightArrow};
|
|
for (ImGuiKey k : steps) {
|
|
if (ImGui::GetKeyData(k)->DownDuration > 1.0f) g_held = true;
|
|
// left and right belong to the row (navigation would otherwise move sideways into the description pane)
|
|
ImGui::SetKeyOwner(k, ImGui::GetItemID());
|
|
}
|
|
if (movable) {
|
|
bool ctrl = ImGui::GetIO().KeyCtrl;
|
|
if (ImGui::IsKeyPressed(ImGuiKey_GamepadFaceUp, false) || (ctrl && ImGui::IsKeyPressed(ImGuiKey_UpArrow)))
|
|
step = 3;
|
|
if (ImGui::IsKeyPressed(ImGuiKey_GamepadFaceLeft, false) || (ctrl && ImGui::IsKeyPressed(ImGuiKey_DownArrow)))
|
|
step = 4;
|
|
}
|
|
}
|
|
if (foc) desc_set(g_focus, desc, key, m, disabled);
|
|
if (hov) desc_set(g_hover, desc, key, m, disabled);
|
|
if (!g_first.text[0]) desc_set(g_first, desc, key, m, disabled);
|
|
|
|
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, 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;
|
|
if (v.arrows && v.value[0]) { // < value >, drawn (the font has no arrow glyphs); a click on one steps
|
|
float aw = fs * 0.42f, gap = fs * 0.55f, cy = ty + fs * 0.52f;
|
|
float rx = right - aw, lx = rx - gap - ImGui::CalcTextSize(v.value).x - gap - aw;
|
|
ImU32 ac = off ? kGrey : hov || foc ? kGold : IM_COL32(170, 170, 160, 255);
|
|
dl->AddTriangleFilled(ImVec2(lx, cy), ImVec2(lx + aw, cy - aw * 0.75f), ImVec2(lx + aw, cy + aw * 0.75f), ac);
|
|
dl->AddTriangleFilled(ImVec2(rx + aw, cy), ImVec2(rx, cy - aw * 0.75f), ImVec2(rx, cy + aw * 0.75f), ac);
|
|
if (clicked && !off) {
|
|
float mx = ImGui::GetIO().MousePos.x;
|
|
if (mx >= lx - gap && mx <= lx + aw + gap) step = -1;
|
|
else if (mx >= rx - gap && mx <= rx + aw + gap) step = 1;
|
|
}
|
|
right = rx - gap;
|
|
value_left = lx;
|
|
}
|
|
if (movable) { // up and down arrows left of the value; a click on one moves the row
|
|
// a fixed value column (On / Off differ in width) keeps the arrows of the rows in one line
|
|
float aw = fs * 0.42f, gap = fs * 0.55f;
|
|
float cx = right - fmaxf(ImGui::CalcTextSize(v.value).x, fs * 2.8f) - gap * 2 - aw;
|
|
float uy = ty + fs * 0.12f, dy = ty + fs * 0.92f;
|
|
ImU32 ac = off ? kGrey : hov || foc ? kGold : IM_COL32(170, 170, 160, 255);
|
|
dl->AddTriangleFilled(ImVec2(cx, uy + aw * 0.7f), ImVec2(cx + aw, uy + aw * 0.7f), ImVec2(cx + aw * 0.5f, uy), ac);
|
|
dl->AddTriangleFilled(ImVec2(cx, dy - aw * 0.7f), ImVec2(cx + aw, dy - aw * 0.7f), ImVec2(cx + aw * 0.5f, dy), ac);
|
|
if (clicked && !off) {
|
|
ImVec2 mp = ImGui::GetIO().MousePos;
|
|
if (mp.x >= cx - gap * 0.5f && mp.x <= cx + aw + gap * 0.5f) step = mp.y < ty + fs * 0.52f ? 3 : 4;
|
|
}
|
|
value_left = cx - gap;
|
|
}
|
|
if (v.value[0]) {
|
|
float vx = right - ImGui::CalcTextSize(v.value).x;
|
|
dl->AddText(ImVec2(vx, ty), off ? kGrey : v.is_default ? IM_COL32(205, 205, 195, 255) : IM_COL32(255, 255, 255, 255),
|
|
v.value);
|
|
if (!v.arrows && !movable) value_left = vx;
|
|
}
|
|
if (v.note || v.is_default) { // under the value, right-aligned with it
|
|
const char *t = v.note ? v.note : menu_text(UI_MENU_DEFAULT_MARK);
|
|
float tw = font->CalcTextSizeA(small, FLT_MAX, 0, t).x;
|
|
dl->AddText(font, small, ImVec2(right - tw, ny), v.note ? IM_COL32(220, 170, 100, 255) : kDim, t);
|
|
}
|
|
// the label and its note, clipped before the value
|
|
ImVec4 clip(p.x, p.y, value_left - fs * 0.6f, p.y + h);
|
|
dl->AddText(font, fs, ImVec2(p.x + pad, ty), off ? kGrey : IM_COL32(240, 236, 224, 255), label, NULL, 0, &clip);
|
|
const char *note = disabled ? disabled
|
|
: off ? menu_text(UI_MENU_SET_BY_ENV)
|
|
: m && !m->live ? menu_text(UI_MENU_AFTER_RESTART)
|
|
: NULL;
|
|
if (note) dl->AddText(font, small, ImVec2(p.x + pad, ny), off ? IM_COL32(200, 150, 90, 255) : kDim, note);
|
|
ImGui::PopID();
|
|
return off ? 0 : step;
|
|
}
|
|
|
|
static int row(const char *key, UiTextId label, UiTextId desc, const RowView &v) {
|
|
return row_text(key, menu_text(label), menu_text(desc), v, false);
|
|
}
|
|
|
|
int opt_text_row(const char *id, const char *label, const char *value, const char *note, const char *desc,
|
|
bool movable) {
|
|
RowView v = {value ? value : "", false, false, note};
|
|
return row_text(id, label, desc, v, movable);
|
|
}
|
|
|
|
// ---- writing a setting: at once in memory (live settings follow), to nx2navi.ini a moment later (opt_save_tick)
|
|
static uint64_t g_save_at; // plat_now_ns deadline of the pending write; 0 none
|
|
static bool g_save_logged;
|
|
|
|
static void apply(const char *key, const char *value) {
|
|
if (*value) {
|
|
cfg_set(key, value);
|
|
} else { // the automatic choice: the key unset, its line removed from nx2navi.ini
|
|
const char *k = key;
|
|
cfg_reset(&k, 1);
|
|
}
|
|
opt_save_soon();
|
|
menu_sound_play(MENU_SOUND_CURSOR);
|
|
}
|
|
|
|
void opt_save_soon(void) { g_save_at = plat_now_ns() + 400000000ull; }
|
|
|
|
int opt_save_tick(int force) {
|
|
if (!g_save_at || (!force && plat_now_ns() < g_save_at)) return g_save_at != 0;
|
|
g_save_at = 0;
|
|
if (!cfg_save() && !g_save_logged) {
|
|
g_save_logged = true;
|
|
rt_log("menu: could not write nx2navi.ini; the changed settings hold for this session");
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// ---- rows
|
|
|
|
bool opt_toggle(const char *key, UiTextId label, UiTextId desc, bool inverted) {
|
|
const CfgMeta *m = cfg_meta(key);
|
|
bool set = cfg_bool(key) != 0, def = m && strcmp(m->def, "0") != 0;
|
|
bool on = set != inverted; // what the row shows
|
|
RowView v = {menu_text(on ? UI_MENU_ON : UI_MENU_OFF), set == def, false, NULL};
|
|
if (!row(key, label, desc, v)) return false; // any step flips it
|
|
apply(key, set ? "0" : "1");
|
|
return true;
|
|
}
|
|
|
|
bool opt_button(const char *id, UiTextId label, UiTextId desc, const char *value) {
|
|
RowView v = {value ? value : "", false, false, NULL};
|
|
return row(id, label, desc, v) == 2; // A or a click; left and right do nothing
|
|
}
|
|
|
|
bool opt_toggle_text(const char *id, const char *label, const char *desc, bool on, const char *value) {
|
|
// off is the plain state: its value drawn dim, without the "default" mark
|
|
RowView v = {value ? value : menu_text(on ? UI_MENU_ON : UI_MENU_OFF), !on, false, on ? NULL : ""};
|
|
return row_text(id, label, desc, v, false) != 0; // any step flips it
|
|
}
|
|
|
|
void opt_group_text(const char *title) {
|
|
ImGui::Dummy(ImVec2(0, 6 * g_scale));
|
|
ImGui::PushTextWrapPos(0);
|
|
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1), "%s", title);
|
|
ImGui::PopTextWrapPos();
|
|
}
|
|
|
|
void opt_text_str(const char *text, bool warn) {
|
|
ImGui::PushTextWrapPos(0);
|
|
if (warn) ImGui::TextColored(ImVec4(0.95f, 0.66f, 0.40f, 1), "%s", text);
|
|
else ImGui::TextUnformatted(text);
|
|
ImGui::PopTextWrapPos();
|
|
}
|
|
|
|
void opt_group(UiTextId title) {
|
|
ImGui::Dummy(ImVec2(0, 6 * g_scale));
|
|
ImGui::TextColored(ImVec4(0.93f, 0.76f, 0.33f, 1), "%s", menu_text(title));
|
|
}
|
|
|
|
void opt_text(UiTextId text, bool warn) {
|
|
ImGui::PushTextWrapPos(0);
|
|
if (warn) ImGui::TextColored(ImVec4(0.95f, 0.66f, 0.40f, 1), "%s", menu_text(text));
|
|
else ImGui::TextUnformatted(menu_text(text));
|
|
ImGui::PopTextWrapPos();
|
|
}
|
|
|
|
bool opt_choice_list(const char *key, UiTextId label, const char *const *values, const char *const *names, int n,
|
|
UiTextId desc, int cur, const char *note) {
|
|
const CfgMeta *m = cfg_meta(key);
|
|
const char *now = cfg_value(key);
|
|
if (cur < 0)
|
|
for (int i = 0; i < n; i++)
|
|
if (!strcmp(values[i], now)) cur = i;
|
|
int def = -1;
|
|
for (int i = 0; m && i < n; i++)
|
|
if (!strcmp(values[i], m->def)) def = i;
|
|
RowView v = {cur >= 0 ? names[cur] : now, cur == def, true, note};
|
|
int step = row(key, label, desc, v);
|
|
if (!step || n <= 0) return false;
|
|
int nv;
|
|
if (cur < 0) nv = 0;
|
|
else if (step == 2) nv = (cur + 1) % n; // A / click: the next value, round to the first
|
|
else nv = cur + step < 0 ? 0 : cur + step >= n ? n - 1 : cur + step; // left / right stop at the ends
|
|
if (nv == cur) return false;
|
|
apply(key, values[nv]);
|
|
return true;
|
|
}
|
|
|
|
bool opt_choice(const char *key, UiTextId label, const UiTextId *names, int n, UiTextId desc, const char *note) {
|
|
enum { MAX = 32 };
|
|
const CfgMeta *m = cfg_meta(key);
|
|
const char *values[MAX], *shown[MAX];
|
|
char buf[256];
|
|
int k = 0;
|
|
if (m && !m->def[0]) values[k++] = ""; // the automatic choice
|
|
snprintf(buf, sizeof buf, "%s", m && m->values ? m->values : "");
|
|
for (char *s = buf, *e; *s && k < MAX; s = e) {
|
|
e = strchr(s, '|');
|
|
if (e) *e++ = 0;
|
|
else e = s + strlen(s);
|
|
values[k++] = s;
|
|
}
|
|
if (n > k) n = k;
|
|
for (int i = 0; i < n; i++) shown[i] = menu_text(names[i]);
|
|
return opt_choice_list(key, label, values, shown, n, desc, -1, note);
|
|
}
|
|
|
|
bool opt_slider(const char *key, UiTextId label, float lo, float hi, UiTextId desc, float step, const char *fmt,
|
|
const char *note, const char *fallback) {
|
|
const char *set = cfg_get(key);
|
|
const char *src = fallback && !(set && *set) ? fallback : key; // unset: show the value in effect, the fallback's
|
|
const CfgMeta *m = cfg_meta(src);
|
|
// A key without a table default of its own (the per-stick dead zones) defaults to its fallback: the "default" mark
|
|
// follows the fallback's default, not 0, even once the key is set.
|
|
if (fallback && m && !m->def[0]) m = cfg_meta(fallback);
|
|
double v = cfg_double(src), def = m && m->def[0] ? strtod(m->def, NULL) : lo;
|
|
char shown[48];
|
|
snprintf(shown, sizeof shown, fmt, v);
|
|
RowView rv = {shown, fabs(v - def) < step * 0.01, true, note};
|
|
int s = row(key, label, desc, rv);
|
|
if (!s) return false;
|
|
int steps = g_held && (hi - lo) > 40 * step ? 10 : 1; // held: ten steps at a time over a long range
|
|
double n = round(v / step) * step + (s == 2 ? step : s * step * steps);
|
|
if (s == 2 && n > hi + step * 0.01) n = lo; // A / click past the top goes round; left / right stop at the ends
|
|
n = n < lo ? lo : n > hi ? hi : n;
|
|
if (fabs(n - v) < step * 0.01) return false;
|
|
char out[32];
|
|
if (step >= 1) snprintf(out, sizeof out, "%d", (int)lround(n));
|
|
else snprintf(out, sizeof out, "%g", round(n / step) * step);
|
|
// Landing on the fallback's value unsets the key (its line leaves nx2navi.ini), so the row follows the fallback again.
|
|
apply(key, fallback && fabs(n - cfg_double(fallback)) < step * 0.01 ? "" : out);
|
|
return true;
|
|
}
|
|
|
|
// ---- binding rows (the remapping rows of the Controls tab): a label and three cells, each its own nav item
|
|
|
|
// Text centred (left-aligned when centred is false) in [x0, x1] on a row from y to y + h, at the font size or smaller so
|
|
// a long name ("Нажатие правого стика", "Стрелка вправо") fits; under 65 % it is clipped instead.
|
|
static void fit_text(ImDrawList *dl, float x0, float x1, float y, float h, ImU32 col, const char *t, bool centred) {
|
|
ImFont *font = ImGui::GetFont();
|
|
float fs = ImGui::GetFontSize(), room = x1 - x0, tw = font->CalcTextSizeA(fs, FLT_MAX, 0, t).x;
|
|
if (tw > room) {
|
|
fs = fmaxf(fs * room / tw, fs * 0.65f);
|
|
tw = font->CalcTextSizeA(fs, FLT_MAX, 0, t).x;
|
|
}
|
|
float x = centred && tw < room ? x0 + (room - tw) * 0.5f : x0;
|
|
ImVec4 clip(x0, y, x1, y + h);
|
|
dl->AddText(font, fs, ImVec2(x, y + (h - fs) * 0.5f), col, t, NULL, 0, &clip);
|
|
}
|
|
|
|
// The label column and the three cells of a binding row of width w.
|
|
static void bind_columns(float x, float w, float cx[3], float *cw) {
|
|
float lw = floorf(w * 0.34f), gap = floorf(8 * g_scale);
|
|
*cw = floorf((w - lw - gap * 3) / 3);
|
|
for (int c = 0; c < 3; c++) cx[c] = x + lw + gap + c * (*cw + gap);
|
|
}
|
|
|
|
void opt_bind_header(UiTextId c0, UiTextId c1, UiTextId c2) {
|
|
ImVec2 p = ImGui::GetCursorScreenPos();
|
|
float w = ImGui::GetContentRegionAvail().x, cx[3], cw, fs = ImGui::GetFontSize() * 0.8f;
|
|
bind_columns(p.x, w, cx, &cw);
|
|
const UiTextId cols[3] = {c0, c1, c2};
|
|
ImFont *font = ImGui::GetFont();
|
|
for (int c = 0; c < 3; c++) {
|
|
const char *t = menu_text(cols[c]);
|
|
float tw = font->CalcTextSizeA(fs, FLT_MAX, 0, t).x;
|
|
ImGui::GetWindowDrawList()->AddText(font, fs, ImVec2(cx[c] + (cw - tw) * 0.5f, p.y), kDim, t);
|
|
}
|
|
ImGui::Dummy(ImVec2(w, fs * 1.2f));
|
|
}
|
|
|
|
int opt_bind_row(const char *key, const char *label, UiTextId desc, const char *const cells[3], const bool enabled[3],
|
|
const char *note) {
|
|
ImGui::PushID(key);
|
|
ImVec2 p = ImGui::GetCursorScreenPos();
|
|
float fs = ImGui::GetFontSize(), w = ImGui::GetContentRegionAvail().x, h = floorf(fs * 1.9f), cx[3], cw;
|
|
bind_columns(p.x, w, cx, &cw);
|
|
ImDrawList *dl = ImGui::GetWindowDrawList();
|
|
float r = 8 * g_scale, pad = 6 * g_scale;
|
|
fit_text(dl, p.x + 16 * g_scale, cx[0] - fs * 0.3f, p.y, h, IM_COL32(240, 236, 224, 255), label, false);
|
|
int result = -1;
|
|
bool row_foc = false, row_hov = false;
|
|
for (int c = 0; c < 3; c++) {
|
|
ImVec2 a(cx[c], p.y), b(cx[c] + cw, p.y + h);
|
|
const char *t = cells[c] && cells[c][0] ? cells[c] : "-";
|
|
if (!enabled[c]) { // not a nav item: the cursor skips it
|
|
dl->AddRect(a, b, IM_COL32(255, 255, 255, 14), r);
|
|
fit_text(dl, a.x + pad, b.x - pad, a.y, h, kGrey, t, true);
|
|
continue;
|
|
}
|
|
ImGui::SetCursorScreenPos(a);
|
|
ImGui::PushID(c);
|
|
ImGui::PushStyleColor(ImGuiCol_NavCursor, ImVec4(0, 0, 0, 0)); // the cell's own border shows the selection
|
|
bool clicked = ImGui::InvisibleButton("##cell", ImVec2(cw, h), ImGuiButtonFlags_EnableNav);
|
|
ImGui::PopStyleColor();
|
|
char id[80];
|
|
snprintf(id, sizeof id, "%s#%d", key, c);
|
|
bool to_key = g_focus_key[0] && !strcmp(id, g_focus_key);
|
|
if (g_take_first || to_key) {
|
|
ImGui::SetFocusID(ImGui::GetItemID(), ImGui::GetCurrentWindow());
|
|
ImGui::SetNavCursorVisible(true);
|
|
if (g_take_first) ImGui::SetScrollY(0);
|
|
else ImGui::ScrollToItem(ImGuiScrollFlags_KeepVisibleEdgeY);
|
|
g_take_first = false;
|
|
if (to_key) g_focus_key[0] = 0;
|
|
}
|
|
bool hov = ImGui::IsItemHovered(), foc = ImGui::IsItemFocused() && ImGui::GetCurrentContext()->NavCursorVisible;
|
|
row_foc |= foc;
|
|
row_hov |= hov;
|
|
if (clicked) result = c;
|
|
if (foc && (ImGui::IsKeyPressed(ImGuiKey_Delete, false) || ImGui::IsKeyPressed(ImGuiKey_Backspace, false) ||
|
|
ImGui::IsKeyPressed(ImGuiKey_GamepadFaceUp, false)))
|
|
result = 10 + c;
|
|
dl->AddRectFilled(a, b, hov || foc ? IM_COL32(255, 255, 255, 22) : IM_COL32(0, 0, 0, 40), r);
|
|
dl->AddRect(a, b, hov || foc ? kGold : IM_COL32(255, 255, 255, 38), r, 0, hov || foc ? 2.0f : 1.0f);
|
|
fit_text(dl, a.x + pad, b.x - pad, a.y, h, IM_COL32(255, 255, 255, 255), t, true);
|
|
ImGui::PopID();
|
|
}
|
|
char k[64];
|
|
snprintf(k, sizeof k, "NX2NAVI_%s", key);
|
|
if (row_foc) desc_set(g_focus, menu_text(desc), k, NULL, NULL);
|
|
if (row_hov) desc_set(g_hover, menu_text(desc), k, NULL, NULL);
|
|
if (!g_first.text[0]) desc_set(g_first, menu_text(desc), k, NULL, NULL);
|
|
if (note && (row_foc || row_hov)) { // e.g. what moved here, after the description
|
|
Desc &d = row_hov ? g_hover : g_focus;
|
|
size_t n = strlen(d.text);
|
|
snprintf(d.text + n, sizeof d.text - n, "\n\n%s", note);
|
|
}
|
|
ImGui::SetCursorScreenPos(p);
|
|
ImGui::Dummy(ImVec2(w, h));
|
|
ImGui::PopID();
|
|
return result;
|
|
}
|