Files
totk-pc-recomp/runtime/ui/option_rows.cpp
T
Valiantsin DzerakhandClaude Opus 5.5 89cc960f09 Menu navigation with the left stick and back from the bottom buttons; AMD sensors and the picture lines in the performance overlay
The left stick now moves the menu cursor as the D-pad does (ImGui only scrolled with it). The page's rows are a
direct NavFlattened child of the menu window: behind a second child level ImGui did not clip them, so up from
Continue went to the tabs and down from the tabs to Continue.

The full overlay reads temperature, board power, clock and GPU load from ADL's PM log on AMD GPUs (matched by PCI
bus), and shows the window, game image and 3D scene sizes, the scaling to the window (FSR and its sharpness), the
frame limit and the presentation mode in use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:21:41 +03:00

316 lines
15 KiB
C++

// Two-pane settings pages (option_rows.h): rows on the left, the selected or hovered row's description on the right.
//
// Adapted from SavageWindRecomp's runtime/ui/option_rows.mm (https://github.com/Revivedx/SavageWindRecomp @
// 575ad88e02f9a89a9a885d90039d48af33c982f4, MIT, Copyright (c) 2026 Revivedx and the Savage Wind Recomp contributors,
// Copyright (c) 2026 Peiikai): the row look, the two panes, the left / right stepping and the hold speed-up. The rows
// read and write cfg keys instead of a settings struct, take their text from the menu catalog (menu_text), and show
// the default, "after restart" and environment notes; the text field, radio and on-screen keyboard rows are not ported.
#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"
// 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 UiTextId 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[512]; };
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, UiTextId desc, const char *key, const CfgMeta *m, const char *disabled) {
int n = snprintf(d.text, sizeof d.text, "%s", menu_text(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; }
void opt_disable_next(UiTextId 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) or
// 0; always 0 for a row the environment fixes (greyed out, still selectable so its description shows).
static int row(const char *key, UiTextId label, UiTextId desc, const RowView &v) {
const CfgMeta *m = cfg_meta(key);
const char *disabled = g_disable_next ? menu_text(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 (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, 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);
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 (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) 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), menu_text(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;
}
// ---- writing a setting: at once in memory (live settings follow), to totk.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 totk.ini
const char *k = key;
cfg_reset(&k, 1);
}
opt_save_soon();
}
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 totk.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
}
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 CfgMeta *m = cfg_meta(key);
double v = cfg_double(key), def = m ? strtod(m->def, NULL) : lo;
char shown[48];
snprintf(shown, sizeof shown, fmt, v);
RowView rv = {shown, fabs(v - def) < step * 0.01, true, NULL};
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);
apply(key, out);
return true;
}