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
parent 06afbee4f8
commit 0de44569b7
11 changed files with 165 additions and 15 deletions

Binary file not shown.

View File

@@ -949,6 +949,9 @@
"mining_syncing_tooltip": "Blockchain synchronisiert...",
"mining_tag": " · Mining",
"mining_threads": "Mining-Threads",
"mining_threads_input_tooltip": "Genaue Thread-Anzahl eingeben (Enter zum Übernehmen)",
"mining_threads_minus_tooltip": "Weniger Threads",
"mining_threads_plus_tooltip": "Mehr Threads",
"mining_to_save": "zum Speichern",
"mining_today": "Heute",
"mining_uptime": "Laufzeit",
@@ -1785,6 +1788,7 @@
"xmrig_loading_releases": "Releases werden geladen…",
"xmrig_none": "keiner",
"xmrig_reinstall": "Neu installieren",
"xmrig_releases": "xmrig-Releases",
"xmrig_stop_mining_first": "Stoppen Sie das Mining, bevor Sie den Miner aktualisieren.",
"xmrig_unavailable_body": "Für diese Plattform ist kein Miner-Build verfügbar.",
"xmrig_unavailable_title": "Miner-Updates nicht verfügbar",

View File

@@ -949,6 +949,9 @@
"mining_syncing_tooltip": "El blockchain está sincronizando...",
"mining_tag": " · Minería",
"mining_threads": "Hilos de Minería",
"mining_threads_input_tooltip": "Escribe un número exacto de hilos (pulsa Enter para aplicar)",
"mining_threads_minus_tooltip": "Menos hilos",
"mining_threads_plus_tooltip": "Más hilos",
"mining_to_save": "para guardar",
"mining_today": "Hoy",
"mining_uptime": "Tiempo activo",
@@ -1785,6 +1788,7 @@
"xmrig_loading_releases": "Cargando versiones…",
"xmrig_none": "ninguno",
"xmrig_reinstall": "Reinstalar",
"xmrig_releases": "versiones de xmrig",
"xmrig_stop_mining_first": "Detén la minería antes de actualizar el minero.",
"xmrig_unavailable_body": "No hay ninguna versión del minero disponible para esta plataforma.",
"xmrig_unavailable_title": "Actualizaciones del minero no disponibles",

View File

@@ -949,6 +949,9 @@
"mining_syncing_tooltip": "La blockchain se synchronise...",
"mining_tag": " · Minage",
"mining_threads": "Threads de minage",
"mining_threads_input_tooltip": "Saisissez un nombre exact de threads (Entrée pour appliquer)",
"mining_threads_minus_tooltip": "Moins de threads",
"mining_threads_plus_tooltip": "Plus de threads",
"mining_to_save": "pour enregistrer",
"mining_today": "Aujourd'hui",
"mining_uptime": "Temps de fonctionnement",
@@ -1785,6 +1788,7 @@
"xmrig_loading_releases": "Chargement des versions…",
"xmrig_none": "aucun",
"xmrig_reinstall": "Réinstaller",
"xmrig_releases": "versions de xmrig",
"xmrig_stop_mining_first": "Arrêtez le minage avant de mettre à jour le mineur.",
"xmrig_unavailable_body": "Aucune version du mineur n'est disponible pour cette plateforme.",
"xmrig_unavailable_title": "Mises à jour du mineur indisponibles",

View File

@@ -949,6 +949,9 @@
"mining_syncing_tooltip": "ブロックチェーン同期中...",
"mining_tag": " · マイニング",
"mining_threads": "マイニングスレッド",
"mining_threads_input_tooltip": "正確なスレッド数を入力Enter で適用)",
"mining_threads_minus_tooltip": "スレッドを減らす",
"mining_threads_plus_tooltip": "スレッドを増やす",
"mining_to_save": "保存する",
"mining_today": "今日",
"mining_uptime": "稼働時間",
@@ -1782,6 +1785,7 @@
"xmrig_loading_releases": "リリースを読み込み中…",
"xmrig_none": "なし",
"xmrig_reinstall": "再インストール",
"xmrig_releases": "xmrig リリース",
"xmrig_stop_mining_first": "マイナーを更新する前にマイニングを停止してください。",
"xmrig_unavailable_body": "このプラットフォーム向けのマイナービルドは利用できません。",
"xmrig_unavailable_title": "マイナーの更新は利用できません",

View File

@@ -948,6 +948,9 @@
"mining_syncing_tooltip": "블록체인 동기화 중...",
"mining_tag": " · 채굴",
"mining_threads": "채굴 스레드",
"mining_threads_input_tooltip": "정확한 스레드 수 입력 (Enter로 적용)",
"mining_threads_minus_tooltip": "스레드 줄이기",
"mining_threads_plus_tooltip": "스레드 늘리기",
"mining_to_save": "저장하려면",
"mining_today": "오늘",
"mining_uptime": "가동 시간",
@@ -1784,6 +1787,7 @@
"xmrig_loading_releases": "릴리스를 불러오는 중…",
"xmrig_none": "없음",
"xmrig_reinstall": "재설치",
"xmrig_releases": "xmrig 릴리스",
"xmrig_stop_mining_first": "채굴기를 업데이트하기 전에 채굴을 중지하세요.",
"xmrig_unavailable_body": "이 플랫폼에서 사용 가능한 채굴기 빌드가 없습니다.",
"xmrig_unavailable_title": "채굴기 업데이트를 사용할 수 없습니다",

View File

@@ -949,6 +949,9 @@
"mining_syncing_tooltip": "Blockchain está sincronizando...",
"mining_tag": " · Mineração",
"mining_threads": "Threads de Mineração",
"mining_threads_input_tooltip": "Digite um número exato de threads (Enter para aplicar)",
"mining_threads_minus_tooltip": "Menos threads",
"mining_threads_plus_tooltip": "Mais threads",
"mining_to_save": "para salvar",
"mining_today": "Hoje",
"mining_uptime": "Tempo Ativo",
@@ -1785,6 +1788,7 @@
"xmrig_loading_releases": "Carregando versões…",
"xmrig_none": "nenhum",
"xmrig_reinstall": "Reinstalar",
"xmrig_releases": "versões do xmrig",
"xmrig_stop_mining_first": "Pare a mineração antes de atualizar o minerador.",
"xmrig_unavailable_body": "Nenhuma versão do minerador está disponível para esta plataforma.",
"xmrig_unavailable_title": "Atualizações do minerador indisponíveis",

View File

@@ -949,6 +949,9 @@
"mining_syncing_tooltip": "Блокчейн синхронизируется...",
"mining_tag": " · Майнинг",
"mining_threads": "Потоки майнинга",
"mining_threads_input_tooltip": "Введите точное число потоков (Enter для применения)",
"mining_threads_minus_tooltip": "Меньше потоков",
"mining_threads_plus_tooltip": "Больше потоков",
"mining_to_save": "для сохранения",
"mining_today": "Сегодня",
"mining_uptime": "Время работы",
@@ -1785,6 +1788,7 @@
"xmrig_loading_releases": "Загрузка релизов…",
"xmrig_none": "нет",
"xmrig_reinstall": "Переустановить",
"xmrig_releases": "релизы xmrig",
"xmrig_stop_mining_first": "Остановите майнинг перед обновлением майнера.",
"xmrig_unavailable_body": "Для этой платформы нет доступной сборки майнера.",
"xmrig_unavailable_title": "Обновления майнера недоступны",

View File

@@ -948,6 +948,9 @@
"mining_syncing_tooltip": "区块链同步中...",
"mining_tag": " · 挖矿",
"mining_threads": "挖矿线程",
"mining_threads_input_tooltip": "输入精确的线程数(按 Enter 应用)",
"mining_threads_minus_tooltip": "减少线程",
"mining_threads_plus_tooltip": "增加线程",
"mining_to_save": "保存",
"mining_today": "今天",
"mining_uptime": "运行时间",
@@ -1783,6 +1786,7 @@
"xmrig_loading_releases": "正在加载发行版…",
"xmrig_none": "无",
"xmrig_reinstall": "重新安装",
"xmrig_releases": "xmrig 版本",
"xmrig_stop_mining_first": "更新矿工程序前请先停止挖矿。",
"xmrig_unavailable_body": "此平台没有可用的矿工构建版本。",
"xmrig_unavailable_title": "矿工更新不可用",

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;

View File

@@ -1296,6 +1296,9 @@ void I18n::loadBuiltinEnglish()
strings_["start_mining"] = "Start Mining";
strings_["stop_mining"] = "Stop Mining";
strings_["mining_threads"] = "Mining Threads";
strings_["mining_threads_input_tooltip"] = "Type an exact thread count (press Enter to apply)";
strings_["mining_threads_minus_tooltip"] = "Fewer threads";
strings_["mining_threads_plus_tooltip"] = "More threads";
strings_["mining_statistics"] = "Mining Statistics";
strings_["local_hashrate"] = "Local Hashrate";
strings_["network_hashrate"] = "Network Hashrate";
@@ -1933,6 +1936,7 @@ void I18n::loadBuiltinEnglish()
// --- Miner (xmrig) updater ---
strings_["xmrig_update_button"] = "Update miner…";
strings_["xmrig_update_short"] = "Update";
strings_["xmrig_releases"] = "xmrig releases";
strings_["xmrig_current"] = "Current:";
strings_["xmrig_none"] = "none";
strings_["xmrig_update_title"] = "Update Miner";