feat(mining): thread-count stepper, adaptive tiles, dropdown click-through fix, xmrig version state

Mining-tab UI improvements:
- Thread selector: a centered [-] N [+] stepper (top-aligned in the header) to pick an exact
  thread count — number centered, -/+ step by one (clamped to [1, cores]), still typeable
  (commits on Enter so it doesn't restart the miner mid-typing).
- Thread tiles now render at an adaptive step (1/2/4/8 by core count) plus 1 and the max, so a
  high-core CPU (e.g. a 192-thread EPYC) shows ~25 tiles instead of one-per-thread and no longer
  overflows the card. Unchanged for <=24-core machines.
- Fix: clicking the X (or a row) in an open saved-pools / payout-address dropdown no longer bleeds
  through to the thread tiles / Mine button — the custom drawlist hit-tests now gate on
  IsPopupOpen(AnyPopup).
- xmrig update button: shows "xmrig releases" when installed >= latest (numeric version compare),
  else "Update <latest>"; the "Current: <ver>" text and the button are subtle green when up to date
  and subtle orange when an update is available (neutral when either version is unknown).

New i18n keys back-filled across all 8 languages; CJK subset font rebuilt for the new glyphs.
Verified across full-node, lite, and Windows builds; the stepper layout confirmed via the UI sweep.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-18 15:39:43 -05:00
co-authored by Claude Opus 4.8
parent 06afbee4f8
commit 0de44569b7
11 changed files with 165 additions and 15 deletions
+129 -15
View File
@@ -28,6 +28,7 @@
#include <cstdio>
#include <string>
#include <vector>
#include <cstdlib> // std::atoi (xmrig version compare)
namespace dragonx {
namespace ui {
@@ -56,6 +57,25 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
float miningBtnGap = gap;
float miningBtnMaxW = availWidth * schema::UI().drawElement("tabs.mining", "btn-max-width-ratio").size;
// Thread-count tiles at an adaptive step (multiples of 1/2/4/8, chosen by core count) plus 1 and the
// max, so the tile row stays bounded (~<=24 tiles) instead of one tile per thread — a 192-thread
// EPYC would otherwise overflow the card. Exact in-between counts are settable via the input box.
int tileStep = 1;
if (max_threads > 96) tileStep = 8;
else if (max_threads > 48) tileStep = 4;
else if (max_threads > 24) tileStep = 2;
std::vector<int> threadOptions;
threadOptions.push_back(1);
for (int v = tileStep; v < max_threads; v += tileStep)
if (v > 1) threadOptions.push_back(v);
if (max_threads > 1 && threadOptions.back() != max_threads) threadOptions.push_back(max_threads);
const int nOpts = (int)threadOptions.size();
// Custom drawlist hit-tests (the thread tiles + the Mine button) bypass ImGui's popup input capture,
// so a click on an open dropdown (saved pools / payout addresses) would ALSO fire them. Gate on this
// so clicking the dropdown's X (or a row) doesn't bleed through to the tiles/Mine button.
const bool anyPopupOpen = ImGui::IsPopupOpen("", ImGuiPopupFlags_AnyPopupId | ImGuiPopupFlags_AnyPopupLevel);
// --- Compute thread grid layout based on controls card width ---
// Estimate controlsW first to compute cols correctly
// The Mine button is square (= card height, which scales with DPI), so the width we
@@ -65,8 +85,8 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
float innerW = estControlsW - pad * 2;
float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f));
float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size);
int cols = std::max(1, std::min(max_threads, (int)(innerW / (cellSz + cellGap))));
int rows = (max_threads + cols - 1) / cols;
int cols = std::max(1, std::min(nOpts, (int)(innerW / (cellSz + cellGap))));
int rows = (nOpts + cols - 1) / cols;
float gridW = cols * cellSz + (cols - 1) * cellGap;
float gridH = rows * cellSz + (rows - 1) * cellGap;
@@ -116,6 +136,67 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
OnSurfaceDisabled(), buf);
}
// Centered, top-aligned thread-count stepper: [-] centered editable number [+]. Lets a
// high-core CPU pick an EXACT count that isn't one of the tiles. The input commits on Enter
// (not per keystroke) so it doesn't restart the miner while the user is still typing.
{
auto applyThreads = [&](int tc) {
tc = std::clamp(tc, 1, max_threads);
if (tc == s_selected_threads) return;
s_selected_threads = tc;
app->settings()->setPoolThreads(tc);
app->settings()->save();
if (mining.generate) app->startMining(tc);
if (s_pool_mode && state.pool_mining.xmrig_running) {
app->stopPoolMining();
app->startPoolMining(tc);
}
};
ImFont* stepFont = Type().iconSmall();
const float fieldH = capFont->LegacySize + 6.0f * dp;
const float sideW = fieldH; // square -/+ buttons
const float fieldW = 46.0f * dp;
const float g = 2.0f * dp;
const float totalW = sideW + g + fieldW + g + sideW;
float sx = cardMin.x + (controlsW - totalW) * 0.5f;
const float sy = curY;
ImVec2 savedCur = ImGui::GetCursorScreenPos();
material::IconButtonStyle sideStyle;
sideStyle.color = OnSurfaceMedium();
sideStyle.hoverBg = StateHover();
// "-" button
sideStyle.tooltip = TR("mining_threads_minus_tooltip");
ImGui::SetCursorScreenPos(ImVec2(sx, sy));
if (material::IconButton("##ThreadMinus", ICON_MD_REMOVE, stepFont, ImVec2(sideW, fieldH), sideStyle))
applyThreads(s_selected_threads - 1);
sx += sideW + g;
// Centered editable number: symmetric FramePadding pushes the digits to the field centre.
char tb[8]; snprintf(tb, sizeof(tb), "%d", s_selected_threads);
float basePad = ImGui::GetStyle().FramePadding.x;
float txtW = ImGui::CalcTextSize(tb).x;
float centerPad = std::max(basePad, (fieldW - txtW) * 0.5f);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(centerPad, ImGui::GetStyle().FramePadding.y));
ImGui::SetCursorScreenPos(ImVec2(sx, sy));
ImGui::SetNextItemWidth(fieldW);
int tc = s_selected_threads;
if (ImGui::InputInt("##ThreadCountInput", &tc, 0, 0, ImGuiInputTextFlags_EnterReturnsTrue))
applyThreads(tc);
if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("mining_threads_input_tooltip"));
ImGui::PopStyleVar();
sx += fieldW + g;
// "+" button
sideStyle.tooltip = TR("mining_threads_plus_tooltip");
ImGui::SetCursorScreenPos(ImVec2(sx, sy));
if (material::IconButton("##ThreadPlus", ICON_MD_ADD, stepFont, ImVec2(sideW, fieldH), sideStyle))
applyThreads(s_selected_threads + 1);
ImGui::SetCursorScreenPos(savedCur);
}
// Idle mining toggle (top-right corner of card)
float idleRightEdge = cardMax.x - pad;
{
@@ -475,8 +556,39 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
std::string curVer = s_live_miner_ver;
if (curVer.empty()) curVer = app->poolMiningInstalledVersion();
if (curVer.empty()) curVer = app->settings()->getXmrigVersion();
// Version state: subtle GREEN + "xmrig releases" when up to date, subtle ORANGE +
// "Update <latest>" when an update is available, neutral when either version is unknown.
auto verNum = [](std::string s) -> std::vector<int> {
if (!s.empty() && (s[0] == 'v' || s[0] == 'V')) s.erase(0, 1);
s = s.substr(0, s.find('-')); // drop any -build suffix
std::vector<int> v; size_t p = 0;
while (p <= s.size()) {
size_t q = s.find('.', p);
v.push_back(std::atoi(s.substr(p, q == std::string::npos ? std::string::npos : q - p).c_str()));
if (q == std::string::npos) break;
p = q + 1;
}
return v;
};
const bool verKnown = !curVer.empty() && !s_xmrig_latest_tag.empty();
bool upToDate = false;
if (verKnown) {
auto a = verNum(curVer), b = verNum(s_xmrig_latest_tag);
int cmp = 0;
for (size_t i = 0; i < std::max(a.size(), b.size()) && cmp == 0; ++i) {
int x = i < a.size() ? a[i] : 0, y = i < b.size() ? b[i] : 0;
if (x != y) cmp = x < y ? -1 : 1;
}
upToDate = (cmp >= 0); // installed >= latest
}
const bool outdated = verKnown && !upToDate;
const ImU32 subtleGreen = IM_COL32(120, 190, 130, 255);
const ImU32 subtleOrange = IM_COL32(214, 158, 74, 255);
char xbtn[64];
if (!s_xmrig_latest_tag.empty())
if (upToDate)
snprintf(xbtn, sizeof(xbtn), "%s", TR("xmrig_releases"));
else if (!s_xmrig_latest_tag.empty())
snprintf(xbtn, sizeof(xbtn), "%s %s", TR("xmrig_update_short"), s_xmrig_latest_tag.c_str());
else
snprintf(xbtn, sizeof(xbtn), "%s", TR("xmrig_update_button"));
@@ -499,7 +611,9 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
xpill, xbtnH * 0.3f);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(xbtnX + xpadX, xbtnY + (xbtnH - xlblSz.y) * 0.5f),
minerBusy ? OnSurfaceDisabled() : OnSurfaceMedium(), xbtn);
minerBusy ? OnSurfaceDisabled()
: (upToDate ? subtleGreen : outdated ? subtleOrange : OnSurfaceMedium()),
xbtn);
if (xhov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", minerBusy ? TR("xmrig_stop_mining_first")
@@ -515,7 +629,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
float xcurX = xbtnX - 6.0f * dp - xcurSz.x;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(xcurX, curY + (headerH - xcurSz.y) * 0.5f),
OnSurfaceDisabled(), xcur);
upToDate ? subtleGreen : outdated ? subtleOrange : OnSurfaceDisabled(), xcur);
idleRightEdge = xcurX - 8.0f * dp;
}
@@ -548,14 +662,14 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
// Track which thread the mouse is currently over (-1 = none)
int hovered_thread = -1;
// First pass: hit-test all cells to find hovered thread
for (int i = 0; i < max_threads; i++) {
// First pass: hit-test all cells to find the hovered thread-count option (its value, not index)
for (int i = 0; i < nOpts; i++) {
int row = i / cols;
int col = i % cols;
float cx = gridX + col * (cellSz + cellGap);
float cy = gridY + row * (cellSz + cellGap);
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + cellSz, cy + cellSz))) {
hovered_thread = i + 1;
hovered_thread = threadOptions[i];
break;
}
}
@@ -566,8 +680,8 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
if (hovered_thread > 0 && !benchActive)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Drag-to-select logic (disabled during benchmark)
if (!benchActive && ImGui::IsMouseClicked(0) && hovered_thread > 0) {
// Drag-to-select logic (disabled during benchmark; ignored while a dropdown popup is open)
if (!benchActive && !anyPopupOpen && ImGui::IsMouseClicked(0) && hovered_thread > 0) {
// Begin drag
s_drag_active = true;
s_drag_anchor_thread = hovered_thread;
@@ -587,8 +701,8 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
}
}
// Render cells
for (int i = 0; i < max_threads; i++) {
// Render cells (one per discrete thread-count option)
for (int i = 0; i < nOpts; i++) {
int row = i / cols;
int col = i % cols;
float cx = gridX + col * (cellSz + cellGap);
@@ -596,8 +710,8 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
ImVec2 cMin(cx, cy);
ImVec2 cMax(cx + cellSz, cy + cellSz);
int threadNum = i + 1;
bool active = threadNum <= s_selected_threads;
int threadNum = threadOptions[i];
bool active = threadNum <= s_selected_threads; // fill-up-to-selected heat metaphor
bool hovered = (threadNum == hovered_thread);
// Determine visual state
@@ -674,7 +788,7 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
ImVec2 bMin(btnX, btnY);
ImVec2 bMax(btnX + miningBtnSz, btnY + cardH);
bool btnHovered = material::IsRectHovered(bMin, bMax);
bool btnHovered = !anyPopupOpen && material::IsRectHovered(bMin, bMax); // don't bleed through an open dropdown
bool btnClicked = btnHovered && ImGui::IsMouseClicked(0);
bool isSyncing = state.sync.syncing;
bool poolBlockedBySolo = s_pool_mode && mining.generate && !state.pool_mining.xmrig_running;