refactor(mining): extract the Controls/CPU-grid card into mining_controls.{h,cpp} (audit #10, slice 3)
Third and largest slice of decomposing mining_tab.cpp. The ~843-line "Controls" card (CPU-core grid + drag-to-select, mining start/stop button, benchmark + miner-update controls) is moved verbatim into RenderMiningControls(). mining_tab.cpp is now 839 lines (was 2628 originally). The most coupled section, so mutated state is passed BY REFERENCE — the benchmark (ThreadBenchmark&), selected thread count (int&), and drag state (bool&/int&) — with local reference aliases so the body stays byte-identical and interactions (drag, benchmark, start/stop) behave exactly as before. Read-only context is passed by value/const; the compiler verified const-correctness. Local statics inside the block moved with it. Verified: full-node + Windows + lite build, tests, hygiene, no startup crash. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -444,6 +444,7 @@ set(APP_SOURCES
|
|||||||
src/ui/windows/mining_tab.cpp
|
src/ui/windows/mining_tab.cpp
|
||||||
src/ui/windows/mining_earnings.cpp
|
src/ui/windows/mining_earnings.cpp
|
||||||
src/ui/windows/mining_stats.cpp
|
src/ui/windows/mining_stats.cpp
|
||||||
|
src/ui/windows/mining_controls.cpp
|
||||||
src/ui/windows/mining_benchmark.cpp
|
src/ui/windows/mining_benchmark.cpp
|
||||||
src/ui/windows/mining_pool_panel.cpp
|
src/ui/windows/mining_pool_panel.cpp
|
||||||
src/ui/windows/mining_tab_helpers.cpp
|
src/ui/windows/mining_tab_helpers.cpp
|
||||||
|
|||||||
902
src/ui/windows/mining_controls.cpp
Normal file
902
src/ui/windows/mining_controls.cpp
Normal file
@@ -0,0 +1,902 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#include "mining_controls.h"
|
||||||
|
#include "mining_tab_helpers.h"
|
||||||
|
#include "mining_benchmark.h"
|
||||||
|
#include "xmrig_download_dialog.h"
|
||||||
|
|
||||||
|
#include "../../app.h"
|
||||||
|
#include "../../config/version.h"
|
||||||
|
#include "../../config/settings.h"
|
||||||
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/platform.h"
|
||||||
|
#include "../schema/ui_schema.h"
|
||||||
|
#include "../material/type.h"
|
||||||
|
#include "../material/draw_helpers.h"
|
||||||
|
#include "../material/colors.h"
|
||||||
|
#include "../material/components/buttons.h"
|
||||||
|
#include "../layout.h"
|
||||||
|
#include "../notifications.h"
|
||||||
|
#include "../../embedded/IconsMaterialDesign.h"
|
||||||
|
#include "imgui.h"
|
||||||
|
#include "imgui_internal.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
using namespace material;
|
||||||
|
|
||||||
|
void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& mining,
|
||||||
|
ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
|
||||||
|
float dp, float hs, float vs, float gap, float pad, float availWidth,
|
||||||
|
const GlassPanelSpec& glassSpec, float controlsBudgetH,
|
||||||
|
int max_threads, bool isMiningActive, bool poolMode,
|
||||||
|
const char* poolWorker, const std::string& xmrigLatestTag,
|
||||||
|
ThreadBenchmark& benchmark, int& selectedThreads,
|
||||||
|
bool& dragActive, int& dragAnchorThread)
|
||||||
|
{
|
||||||
|
// Aliases so the moved body keeps its original identifiers verbatim. Mutated state is a
|
||||||
|
// reference to the real static; read-only state is a const copy/ref.
|
||||||
|
const bool s_pool_mode = poolMode;
|
||||||
|
const char* const s_pool_worker = poolWorker;
|
||||||
|
const std::string& s_xmrig_latest_tag = xmrigLatestTag;
|
||||||
|
ThreadBenchmark& s_benchmark = benchmark;
|
||||||
|
int& s_selected_threads = selectedThreads;
|
||||||
|
bool& s_drag_active = dragActive;
|
||||||
|
int& s_drag_anchor_thread = dragAnchorThread;
|
||||||
|
char buf[128];
|
||||||
|
float miningBtnGap = gap;
|
||||||
|
float miningBtnMaxW = availWidth * schema::UI().drawElement("tabs.mining", "btn-max-width-ratio").size;
|
||||||
|
|
||||||
|
// --- Compute thread grid layout based on controls card width ---
|
||||||
|
// Estimate controlsW first to compute cols correctly
|
||||||
|
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size, miningBtnMaxW) - miningBtnGap;
|
||||||
|
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;
|
||||||
|
float gridW = cols * cellSz + (cols - 1) * cellGap;
|
||||||
|
float gridH = rows * cellSz + (rows - 1) * cellGap;
|
||||||
|
|
||||||
|
// Card sections: header(label+info+RAM inline) | grid
|
||||||
|
float headerH = capFont->LegacySize + Layout::spacingXs();
|
||||||
|
float secGap = Layout::spacingLg();
|
||||||
|
|
||||||
|
// Card height from actual content, capped by proportional budget
|
||||||
|
float cardH = pad + headerH + secGap + gridH + pad;
|
||||||
|
cardH = std::clamp(cardH, schema::UI().drawElement("tabs.mining", "control-card-min-height").size, controlsBudgetH);
|
||||||
|
|
||||||
|
// Mining button — sized to match card height (square)
|
||||||
|
float miningBtnSz = cardH;
|
||||||
|
if (miningBtnSz + miningBtnGap > miningBtnMaxW)
|
||||||
|
miningBtnSz = miningBtnMaxW;
|
||||||
|
float controlsW = availWidth - miningBtnSz - miningBtnGap;
|
||||||
|
|
||||||
|
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||||
|
ImVec2 cardMax(cardMin.x + controlsW, cardMin.y + cardH);
|
||||||
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||||
|
|
||||||
|
float curY = cardMin.y + pad;
|
||||||
|
|
||||||
|
// --- Header row: "THREADS 4 / 16" + RAM est + active indicator ---
|
||||||
|
{
|
||||||
|
ImVec2 labelPos(cardMin.x + pad, curY);
|
||||||
|
dl->AddText(ovFont, ovFont->LegacySize, labelPos, OnSurfaceMedium(), TR("mining_threads"));
|
||||||
|
|
||||||
|
float labelW = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, TR("mining_threads")).x;
|
||||||
|
snprintf(buf, sizeof(buf), " %d / %d", s_selected_threads, max_threads);
|
||||||
|
ImVec2 countPos(labelPos.x + labelW, curY);
|
||||||
|
dl->AddText(sub1, sub1->LegacySize, countPos, OnSurface(), buf);
|
||||||
|
|
||||||
|
// RAM estimate inline (after thread count)
|
||||||
|
// Model matches DragonX RandomX: shared ~2080MB dataset + ~256MB cache (allocated once),
|
||||||
|
// plus ~2MB scratchpad per mining thread VM.
|
||||||
|
{
|
||||||
|
float countW = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf).x;
|
||||||
|
double ram_estimate_mb = schema::UI().drawElement("business", "ram-base-mb").size
|
||||||
|
+ schema::UI().drawElement("business", "ram-dataset-mb").size
|
||||||
|
+ schema::UI().drawElement("business", "ram-cache-mb").size
|
||||||
|
+ s_selected_threads * schema::UI().drawElement("business", "ram-per-thread-mb").size;
|
||||||
|
double ram_estimate_gb = ram_estimate_mb / 1024.0;
|
||||||
|
snprintf(buf, sizeof(buf), " RAM ~%.1fGB", ram_estimate_gb);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(countPos.x + countW, curY + (sub1->LegacySize - capFont->LegacySize) * 0.5f),
|
||||||
|
OnSurfaceDisabled(), buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle mining toggle (top-right corner of card)
|
||||||
|
float idleRightEdge = cardMax.x - pad;
|
||||||
|
{
|
||||||
|
bool idleOn = app->settings()->getMineWhenIdle();
|
||||||
|
bool threadScaling = app->settings()->getIdleThreadScaling();
|
||||||
|
ImFont* icoFont = Type().iconSmall();
|
||||||
|
const char* idleIcon = ICON_MD_SCHEDULE;
|
||||||
|
float icoH = icoFont->LegacySize;
|
||||||
|
float btnSz = icoH + 8.0f * dp;
|
||||||
|
float btnX = idleRightEdge - btnSz;
|
||||||
|
float btnY = curY + (headerH - btnSz) * 0.5f;
|
||||||
|
|
||||||
|
// Pill background when active
|
||||||
|
if (idleOn) {
|
||||||
|
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
|
||||||
|
WithAlpha(Primary(), 60), btnSz * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Icon centered in button
|
||||||
|
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, idleIcon);
|
||||||
|
ImU32 icoCol = idleOn ? Primary() : OnSurfaceDisabled();
|
||||||
|
dl->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(btnX + (btnSz - icoSz.x) * 0.5f, btnY + (btnSz - icoSz.y) * 0.5f),
|
||||||
|
icoCol, idleIcon);
|
||||||
|
|
||||||
|
// Click target (save/restore cursor so layout is unaffected)
|
||||||
|
ImVec2 savedCur = ImGui::GetCursorScreenPos();
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
|
||||||
|
ImGui::InvisibleButton("##IdleMining", ImVec2(btnSz, btnSz));
|
||||||
|
if (ImGui::IsItemClicked()) {
|
||||||
|
app->settings()->setMineWhenIdle(!idleOn);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip"));
|
||||||
|
}
|
||||||
|
|
||||||
|
idleRightEdge = btnX - 4.0f * dp;
|
||||||
|
|
||||||
|
// Thread scaling mode toggle (to the left of idle icon, shown when idle is on)
|
||||||
|
if (idleOn) {
|
||||||
|
const char* scaleIcon = threadScaling ? ICON_MD_TUNE : ICON_MD_POWER_SETTINGS_NEW;
|
||||||
|
float sBtnX = idleRightEdge - btnSz;
|
||||||
|
float sBtnY = btnY;
|
||||||
|
|
||||||
|
if (threadScaling) {
|
||||||
|
dl->AddRectFilled(ImVec2(sBtnX, sBtnY), ImVec2(sBtnX + btnSz, sBtnY + btnSz),
|
||||||
|
WithAlpha(Primary(), 40), btnSz * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec2 sIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, scaleIcon);
|
||||||
|
ImU32 sIcoCol = threadScaling ? Primary() : OnSurfaceMedium();
|
||||||
|
dl->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(sBtnX + (btnSz - sIcoSz.x) * 0.5f, sBtnY + (btnSz - sIcoSz.y) * 0.5f),
|
||||||
|
sIcoCol, scaleIcon);
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(sBtnX, sBtnY));
|
||||||
|
ImGui::InvisibleButton("##IdleScaleMode", ImVec2(btnSz, btnSz));
|
||||||
|
if (ImGui::IsItemClicked()) {
|
||||||
|
app->settings()->setIdleThreadScaling(!threadScaling);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", threadScaling
|
||||||
|
? TR("mining_idle_scale_on_tooltip")
|
||||||
|
: TR("mining_idle_scale_off_tooltip"));
|
||||||
|
}
|
||||||
|
idleRightEdge = sBtnX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// GPU-aware idle toggle (to the left, when idle is on)
|
||||||
|
// When ON (default): GPU utilization >= 10% counts as "not idle"
|
||||||
|
// When OFF: unrestricted mode, only keyboard/mouse input matters
|
||||||
|
if (idleOn) {
|
||||||
|
bool gpuAware = app->settings()->getIdleGpuAware();
|
||||||
|
const char* gpuIcon = gpuAware ? ICON_MD_MONITOR : ICON_MD_MONITOR;
|
||||||
|
float gBtnX = idleRightEdge - btnSz;
|
||||||
|
float gBtnY = btnY;
|
||||||
|
|
||||||
|
if (gpuAware) {
|
||||||
|
dl->AddRectFilled(ImVec2(gBtnX, gBtnY), ImVec2(gBtnX + btnSz, gBtnY + btnSz),
|
||||||
|
WithAlpha(Primary(), 40), btnSz * 0.5f);
|
||||||
|
}
|
||||||
|
|
||||||
|
ImVec2 gIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, gpuIcon);
|
||||||
|
ImU32 gIcoCol = gpuAware ? Primary() : OnSurfaceDisabled();
|
||||||
|
dl->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(gBtnX + (btnSz - gIcoSz.x) * 0.5f, gBtnY + (btnSz - gIcoSz.y) * 0.5f),
|
||||||
|
gIcoCol, gpuIcon);
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(gBtnX, gBtnY));
|
||||||
|
ImGui::InvisibleButton("##IdleGpuAware", ImVec2(btnSz, btnSz));
|
||||||
|
if (ImGui::IsItemClicked()) {
|
||||||
|
app->settings()->setIdleGpuAware(!gpuAware);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", gpuAware
|
||||||
|
? TR("mining_idle_gpu_on_tooltip")
|
||||||
|
: TR("mining_idle_gpu_off_tooltip"));
|
||||||
|
}
|
||||||
|
idleRightEdge = gBtnX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode)
|
||||||
|
if (idleOn && !threadScaling) {
|
||||||
|
struct DelayOption { int seconds; const char* label; };
|
||||||
|
static const DelayOption delays[] = {
|
||||||
|
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
|
||||||
|
};
|
||||||
|
int curDelay = app->settings()->getMineIdleDelay();
|
||||||
|
const char* previewLabel = "2m";
|
||||||
|
for (const auto& d : delays) {
|
||||||
|
if (d.seconds == curDelay) { previewLabel = d.label; break; }
|
||||||
|
}
|
||||||
|
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
||||||
|
float comboX = idleRightEdge - comboW;
|
||||||
|
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
||||||
|
ImGui::SetNextItemWidth(comboW);
|
||||||
|
if (ImGui::BeginCombo("##IdleDelay", previewLabel, ImGuiComboFlags_NoArrowButton)) {
|
||||||
|
for (const auto& d : delays) {
|
||||||
|
bool selected = (d.seconds == curDelay);
|
||||||
|
if (ImGui::Selectable(d.label, selected)) {
|
||||||
|
app->settings()->setMineIdleDelay(d.seconds);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (selected) ImGui::SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
|
||||||
|
idleRightEdge = comboX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread scaling controls: idle delay + active threads / idle threads combos
|
||||||
|
if (idleOn && threadScaling) {
|
||||||
|
int hwThreads = std::max(1, (int)std::thread::hardware_concurrency());
|
||||||
|
|
||||||
|
// Idle delay combo
|
||||||
|
{
|
||||||
|
struct DelayOption { int seconds; const char* label; };
|
||||||
|
static const DelayOption delays[] = {
|
||||||
|
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
|
||||||
|
};
|
||||||
|
int curDelay = app->settings()->getMineIdleDelay();
|
||||||
|
const char* previewLabel = "2m";
|
||||||
|
for (const auto& d : delays) {
|
||||||
|
if (d.seconds == curDelay) { previewLabel = d.label; break; }
|
||||||
|
}
|
||||||
|
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
||||||
|
float comboX = idleRightEdge - comboW;
|
||||||
|
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
||||||
|
ImGui::SetNextItemWidth(comboW);
|
||||||
|
if (ImGui::BeginCombo("##IdleDelayScale", previewLabel, ImGuiComboFlags_NoArrowButton)) {
|
||||||
|
for (const auto& d : delays) {
|
||||||
|
bool selected = (d.seconds == curDelay);
|
||||||
|
if (ImGui::Selectable(d.label, selected)) {
|
||||||
|
app->settings()->setMineIdleDelay(d.seconds);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (selected) ImGui::SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
|
||||||
|
idleRightEdge = comboX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Idle threads combo (threads when system is idle)
|
||||||
|
{
|
||||||
|
int curVal = app->settings()->getIdleThreadsIdle();
|
||||||
|
if (curVal <= 0) curVal = hwThreads;
|
||||||
|
char previewBuf[16];
|
||||||
|
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
|
||||||
|
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
||||||
|
float comboX = idleRightEdge - comboW;
|
||||||
|
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
||||||
|
ImGui::SetNextItemWidth(comboW);
|
||||||
|
if (ImGui::BeginCombo("##IdleThreadsIdle", previewBuf, ImGuiComboFlags_NoArrowButton)) {
|
||||||
|
for (int t = 1; t <= hwThreads; t++) {
|
||||||
|
char lbl[16];
|
||||||
|
snprintf(lbl, sizeof(lbl), "%d", t);
|
||||||
|
bool selected = (t == curVal);
|
||||||
|
if (ImGui::Selectable(lbl, selected)) {
|
||||||
|
app->settings()->setIdleThreadsIdle(t);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (selected) ImGui::SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_idle_threads_idle_tooltip"));
|
||||||
|
idleRightEdge = comboX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Separator arrow icon
|
||||||
|
{
|
||||||
|
const char* arrowIcon = ICON_MD_ARROW_BACK;
|
||||||
|
ImVec2 arrSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, arrowIcon);
|
||||||
|
float arrX = idleRightEdge - arrSz.x;
|
||||||
|
float arrY = curY + (headerH - arrSz.y) * 0.5f;
|
||||||
|
dl->AddText(icoFont, icoFont->LegacySize, ImVec2(arrX, arrY), OnSurfaceDisabled(), arrowIcon);
|
||||||
|
idleRightEdge = arrX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active threads combo (threads when user is active)
|
||||||
|
{
|
||||||
|
int curVal = app->settings()->getIdleThreadsActive();
|
||||||
|
if (curVal <= 0) curVal = std::max(1, hwThreads / 2);
|
||||||
|
char previewBuf[16];
|
||||||
|
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
|
||||||
|
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
||||||
|
float comboX = idleRightEdge - comboW;
|
||||||
|
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
||||||
|
ImGui::SetNextItemWidth(comboW);
|
||||||
|
if (ImGui::BeginCombo("##IdleThreadsActive", previewBuf, ImGuiComboFlags_NoArrowButton)) {
|
||||||
|
for (int t = 1; t <= hwThreads; t++) {
|
||||||
|
char lbl[16];
|
||||||
|
snprintf(lbl, sizeof(lbl), "%d", t);
|
||||||
|
bool selected = (t == curVal);
|
||||||
|
if (ImGui::Selectable(lbl, selected)) {
|
||||||
|
app->settings()->setIdleThreadsActive(t);
|
||||||
|
app->settings()->save();
|
||||||
|
}
|
||||||
|
if (selected) ImGui::SetItemDefaultFocus();
|
||||||
|
}
|
||||||
|
ImGui::EndCombo();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered())
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_idle_threads_active_tooltip"));
|
||||||
|
idleRightEdge = comboX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(savedCur);
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Thread Benchmark button / progress (left of idle toggle) ---
|
||||||
|
{
|
||||||
|
ImVec2 benchSavedCur = ImGui::GetCursorScreenPos();
|
||||||
|
bool benchRunning = s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
|
||||||
|
s_benchmark.phase != ThreadBenchmark::Phase::Done;
|
||||||
|
bool benchDone = s_benchmark.phase == ThreadBenchmark::Phase::Done;
|
||||||
|
ImFont* icoFont = Type().iconSmall();
|
||||||
|
|
||||||
|
if (benchRunning) {
|
||||||
|
// Show progress bar + current test info
|
||||||
|
float barW = std::min(180.0f * hs, idleRightEdge - (cardMin.x + pad) - 10.0f * dp);
|
||||||
|
float barH = 4.0f * dp;
|
||||||
|
float barX = idleRightEdge - barW;
|
||||||
|
float barY = curY + headerH - barH - 2.0f * dp;
|
||||||
|
|
||||||
|
// Progress bar track
|
||||||
|
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
|
||||||
|
WithAlpha(OnSurface(), 30), barH * 0.5f);
|
||||||
|
// Progress bar fill
|
||||||
|
float pct = s_benchmark.progress();
|
||||||
|
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW * pct, barY + barH),
|
||||||
|
Primary(), barH * 0.5f);
|
||||||
|
|
||||||
|
// Status text above bar
|
||||||
|
int ct = s_benchmark.current_index < (int)s_benchmark.candidates.size()
|
||||||
|
? s_benchmark.candidates[s_benchmark.current_index] : 0;
|
||||||
|
// Estimated remaining time (uses observed warmup for better accuracy)
|
||||||
|
int remaining_tests = (int)s_benchmark.candidates.size() - s_benchmark.current_index;
|
||||||
|
float elapsed_in_phase = s_benchmark.phase_timer;
|
||||||
|
float phase_total;
|
||||||
|
if (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp)
|
||||||
|
phase_total = s_benchmark.avgWarmupSecs(); // adaptive estimate
|
||||||
|
else if (s_benchmark.phase == ThreadBenchmark::Phase::CoolingDown)
|
||||||
|
phase_total = ThreadBenchmark::COOLDOWN_SECS;
|
||||||
|
else
|
||||||
|
phase_total = ThreadBenchmark::MEASURE_SECS;
|
||||||
|
float remaining_in_current = std::max(0.0f, phase_total - elapsed_in_phase);
|
||||||
|
// Remaining tests after current each need warmup + measure + cooldown
|
||||||
|
float est_secs = remaining_in_current
|
||||||
|
+ (remaining_tests - 1) * (s_benchmark.avgWarmupSecs() + ThreadBenchmark::MEASURE_SECS + ThreadBenchmark::COOLDOWN_SECS);
|
||||||
|
int est_min = (int)(est_secs / 60.0f);
|
||||||
|
int est_sec = (int)est_secs % 60;
|
||||||
|
const char* phase_label;
|
||||||
|
if (s_benchmark.phase == ThreadBenchmark::Phase::CoolingDown)
|
||||||
|
phase_label = TR("mining_benchmark_cooling");
|
||||||
|
else if (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp
|
||||||
|
&& s_benchmark.phase_timer >= ThreadBenchmark::MIN_WARMUP_SECS)
|
||||||
|
phase_label = TR("mining_benchmark_stabilizing");
|
||||||
|
else
|
||||||
|
phase_label = TR("mining_benchmark_testing");
|
||||||
|
if (est_min > 0)
|
||||||
|
snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%dm%ds",
|
||||||
|
phase_label,
|
||||||
|
s_benchmark.current_index + 1,
|
||||||
|
(int)s_benchmark.candidates.size(), ct, est_min, est_sec);
|
||||||
|
else
|
||||||
|
snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%ds",
|
||||||
|
phase_label,
|
||||||
|
s_benchmark.current_index + 1,
|
||||||
|
(int)s_benchmark.candidates.size(), ct, est_sec);
|
||||||
|
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(barX + (barW - txtSz.x) * 0.5f, barY - txtSz.y - 2.0f * dp),
|
||||||
|
OnSurfaceMedium(), buf);
|
||||||
|
|
||||||
|
// Cancel button (small X)
|
||||||
|
float cancelSz = icoFont->LegacySize + 4.0f * dp;
|
||||||
|
float cancelX = barX - cancelSz - 4.0f * dp;
|
||||||
|
float cancelY = curY + (headerH - cancelSz) * 0.5f;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(cancelX, cancelY));
|
||||||
|
ImGui::InvisibleButton("##BenchCancel", ImVec2(cancelSz, cancelSz));
|
||||||
|
if (ImGui::IsItemClicked()) {
|
||||||
|
app->stopPoolMining();
|
||||||
|
if (s_benchmark.was_pool_running)
|
||||||
|
app->startPoolMining(s_benchmark.prev_threads);
|
||||||
|
s_benchmark.reset();
|
||||||
|
}
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_benchmark_cancel"));
|
||||||
|
}
|
||||||
|
const char* cancelIcon = ICON_MD_CLOSE;
|
||||||
|
ImVec2 cIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, cancelIcon);
|
||||||
|
dl->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(cancelX + (cancelSz - cIcoSz.x) * 0.5f,
|
||||||
|
cancelY + (cancelSz - cIcoSz.y) * 0.5f),
|
||||||
|
OnSurfaceMedium(), cancelIcon);
|
||||||
|
|
||||||
|
idleRightEdge = cancelX - 4.0f * dp;
|
||||||
|
|
||||||
|
} else if (benchDone && s_benchmark.optimal_threads > 0) {
|
||||||
|
// Show result briefly, then reset on next click
|
||||||
|
snprintf(buf, sizeof(buf), "%s: %dt (%.1f H/s)",
|
||||||
|
TR("mining_benchmark_result"),
|
||||||
|
s_benchmark.optimal_threads, s_benchmark.optimal_hashrate);
|
||||||
|
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||||
|
float txtX = idleRightEdge - txtSz.x;
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(txtX, curY + (headerH - txtSz.y) * 0.5f),
|
||||||
|
WithAlpha(Success(), 220), buf);
|
||||||
|
|
||||||
|
// Dismiss button
|
||||||
|
float dismissSz = icoFont->LegacySize + 4.0f * dp;
|
||||||
|
float dismissX = txtX - dismissSz - 4.0f * dp;
|
||||||
|
float dismissY = curY + (headerH - dismissSz) * 0.5f;
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(dismissX, dismissY));
|
||||||
|
ImGui::InvisibleButton("##BenchDismiss", ImVec2(dismissSz, dismissSz));
|
||||||
|
if (ImGui::IsItemClicked())
|
||||||
|
s_benchmark.reset();
|
||||||
|
if (ImGui::IsItemHovered()) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_benchmark_dismiss"));
|
||||||
|
}
|
||||||
|
const char* okIcon = ICON_MD_CHECK;
|
||||||
|
ImVec2 oIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, okIcon);
|
||||||
|
dl->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(dismissX + (dismissSz - oIcoSz.x) * 0.5f,
|
||||||
|
dismissY + (dismissSz - oIcoSz.y) * 0.5f),
|
||||||
|
WithAlpha(Success(), 200), okIcon);
|
||||||
|
|
||||||
|
idleRightEdge = dismissX - 4.0f * dp;
|
||||||
|
|
||||||
|
} else if (s_pool_mode) {
|
||||||
|
// Show benchmark button (only in pool mode)
|
||||||
|
float btnSz = icoFont->LegacySize + 8.0f * dp;
|
||||||
|
float btnX = idleRightEdge - btnSz;
|
||||||
|
float btnY = curY + (headerH - btnSz) * 0.5f;
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
|
||||||
|
ImGui::InvisibleButton("##BenchStart", ImVec2(btnSz, btnSz));
|
||||||
|
bool benchHovered = ImGui::IsItemHovered();
|
||||||
|
bool benchClicked = ImGui::IsItemClicked();
|
||||||
|
|
||||||
|
// Hover highlight
|
||||||
|
if (benchHovered) {
|
||||||
|
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
|
||||||
|
StateHover(), btnSz * 0.5f);
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_benchmark_tooltip"));
|
||||||
|
}
|
||||||
|
|
||||||
|
const char* benchIcon = ICON_MD_SPEED;
|
||||||
|
ImVec2 bIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, benchIcon);
|
||||||
|
dl->AddText(icoFont, icoFont->LegacySize,
|
||||||
|
ImVec2(btnX + (btnSz - bIcoSz.x) * 0.5f,
|
||||||
|
btnY + (btnSz - bIcoSz.y) * 0.5f),
|
||||||
|
OnSurfaceMedium(), benchIcon);
|
||||||
|
|
||||||
|
if (benchClicked) {
|
||||||
|
// Require a wallet address for pool mining
|
||||||
|
std::string worker(s_pool_worker);
|
||||||
|
if (!worker.empty()) {
|
||||||
|
s_benchmark.reset();
|
||||||
|
s_benchmark.was_pool_running = state.pool_mining.xmrig_running;
|
||||||
|
s_benchmark.prev_threads = s_selected_threads;
|
||||||
|
s_benchmark.buildCandidates(max_threads);
|
||||||
|
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
|
||||||
|
// Stop any active solo mining first
|
||||||
|
if (mining.generate)
|
||||||
|
app->stopMining();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
idleRightEdge = btnX - 4.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Miner update: "Update <latest>" button + current version (left of benchmark) ---
|
||||||
|
if (s_pool_mode && !benchRunning) {
|
||||||
|
const std::string curVer = app->settings()->getXmrigVersion();
|
||||||
|
char xbtn[64];
|
||||||
|
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"));
|
||||||
|
|
||||||
|
const bool minerBusy = state.pool_mining.xmrig_running;
|
||||||
|
ImVec2 xlblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, xbtn);
|
||||||
|
float xpadX = 8.0f * dp;
|
||||||
|
float xbtnW = xlblSz.x + xpadX * 2.0f;
|
||||||
|
float xbtnH = capFont->LegacySize + 6.0f * dp;
|
||||||
|
float xbtnX = idleRightEdge - xbtnW;
|
||||||
|
float xbtnY = curY + (headerH - xbtnH) * 0.5f;
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(xbtnX, xbtnY));
|
||||||
|
ImGui::InvisibleButton("##XmrigUpdateBtn", ImVec2(xbtnW, xbtnH));
|
||||||
|
bool xhov = ImGui::IsItemHovered();
|
||||||
|
bool xclk = ImGui::IsItemClicked();
|
||||||
|
ImU32 xpill = minerBusy ? WithAlpha(OnSurface(), 12)
|
||||||
|
: (xhov ? StateHover() : WithAlpha(OnSurface(), 28));
|
||||||
|
dl->AddRectFilled(ImVec2(xbtnX, xbtnY), ImVec2(xbtnX + xbtnW, xbtnY + xbtnH),
|
||||||
|
xpill, xbtnH * 0.3f);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(xbtnX + xpadX, xbtnY + (xbtnH - xlblSz.y) * 0.5f),
|
||||||
|
minerBusy ? OnSurfaceDisabled() : OnSurfaceMedium(), xbtn);
|
||||||
|
if (xhov) {
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
ImGui::SetTooltip("%s", minerBusy ? TR("xmrig_stop_mining_first")
|
||||||
|
: TR("xmrig_update_title"));
|
||||||
|
}
|
||||||
|
if (xclk && !minerBusy) XmrigDownloadDialog::show(app);
|
||||||
|
|
||||||
|
// Current installed version, as text to the LEFT of the button.
|
||||||
|
char xcur[64];
|
||||||
|
snprintf(xcur, sizeof(xcur), "%s %s", TR("xmrig_current"),
|
||||||
|
curVer.empty() ? TR("xmrig_none") : curVer.c_str());
|
||||||
|
ImVec2 xcurSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, xcur);
|
||||||
|
float xcurX = xbtnX - 6.0f * dp - xcurSz.x;
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(xcurX, curY + (headerH - xcurSz.y) * 0.5f),
|
||||||
|
OnSurfaceDisabled(), xcur);
|
||||||
|
idleRightEdge = xcurX - 8.0f * dp;
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(benchSavedCur);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Active mining indicator (left of idle toggle)
|
||||||
|
if (mining.generate) {
|
||||||
|
float pulse = effects::isLowSpecMode()
|
||||||
|
? schema::UI().drawElement("animations", "pulse-base-normal").size
|
||||||
|
: schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size);
|
||||||
|
ImU32 pulseCol = WithAlpha(Success(), (int)(255 * pulse));
|
||||||
|
float dotR = schema::UI().drawElement("tabs.mining", "active-dot-radius").size + 2.0f * hs;
|
||||||
|
dl->AddCircleFilled(ImVec2(idleRightEdge - dotR, curY + dotR + 1 * dp), dotR, pulseCol);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize,
|
||||||
|
ImVec2(idleRightEdge - dotR - dotR - 60 * hs, curY),
|
||||||
|
WithAlpha(Success(), 200), TR("mining_active"));
|
||||||
|
}
|
||||||
|
curY += headerH + secGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Thread Grid (drag-to-select) ---
|
||||||
|
{
|
||||||
|
// Center the grid horizontally within the card
|
||||||
|
float gridX = cardMin.x + pad + (innerW - gridW) * 0.5f;
|
||||||
|
float gridY = curY;
|
||||||
|
|
||||||
|
bool threads_changed = false;
|
||||||
|
|
||||||
|
// 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++) {
|
||||||
|
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;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show pointer cursor when hovering the thread grid
|
||||||
|
bool benchActive = s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
|
||||||
|
s_benchmark.phase != ThreadBenchmark::Phase::Done;
|
||||||
|
if (hovered_thread > 0 && !benchActive)
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
|
||||||
|
// Drag-to-select logic (disabled during benchmark)
|
||||||
|
if (!benchActive && ImGui::IsMouseClicked(0) && hovered_thread > 0) {
|
||||||
|
// Begin drag
|
||||||
|
s_drag_active = true;
|
||||||
|
s_drag_anchor_thread = hovered_thread;
|
||||||
|
s_selected_threads = hovered_thread;
|
||||||
|
threads_changed = true;
|
||||||
|
}
|
||||||
|
if (s_drag_active) {
|
||||||
|
if (ImGui::IsMouseDown(0)) {
|
||||||
|
if (hovered_thread > 0 && hovered_thread != s_selected_threads) {
|
||||||
|
// Drag selects up to the hovered thread in either direction
|
||||||
|
s_selected_threads = hovered_thread;
|
||||||
|
threads_changed = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Mouse released — end drag
|
||||||
|
s_drag_active = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Render cells
|
||||||
|
for (int i = 0; i < max_threads; i++) {
|
||||||
|
int row = i / cols;
|
||||||
|
int col = i % cols;
|
||||||
|
float cx = gridX + col * (cellSz + cellGap);
|
||||||
|
float cy = gridY + row * (cellSz + cellGap);
|
||||||
|
ImVec2 cMin(cx, cy);
|
||||||
|
ImVec2 cMax(cx + cellSz, cy + cellSz);
|
||||||
|
|
||||||
|
int threadNum = i + 1;
|
||||||
|
bool active = threadNum <= s_selected_threads;
|
||||||
|
bool hovered = (threadNum == hovered_thread);
|
||||||
|
|
||||||
|
// Determine visual state
|
||||||
|
float rounding = schema::UI().drawElement("tabs.mining", "cell-rounding").size;
|
||||||
|
if (active && mining.generate) {
|
||||||
|
// Mining + active: animated heat glow using theme colors
|
||||||
|
float t = (float)ImGui::GetTime();
|
||||||
|
float phase = t * schema::UI().drawElement("animations", "heat-glow-speed").size + i * schema::UI().drawElement("animations", "heat-glow-phase-offset").size;
|
||||||
|
float glow = effects::isLowSpecMode() ? 0.5f : 0.5f + 0.5f * (float)std::sin(phase);
|
||||||
|
|
||||||
|
// Base color from theme (--mining-heat-glow) falling back to Primary
|
||||||
|
ImU32 heatBase = schema::UI().resolveColor("var(--mining-heat-glow)", Primary());
|
||||||
|
int baseR = (heatBase >> 0) & 0xFF;
|
||||||
|
int baseG = (heatBase >> 8) & 0xFF;
|
||||||
|
int baseB = (heatBase >> 16) & 0xFF;
|
||||||
|
// Brighten toward white as glow increases
|
||||||
|
int r = std::min(255, (int)(baseR + (255 - baseR) * 0.3f * glow));
|
||||||
|
int g = std::min(255, (int)(baseG + (255 - baseG) * 0.3f * glow));
|
||||||
|
int b = std::min(255, (int)(baseB + (255 - baseB) * 0.3f * glow));
|
||||||
|
int a = (int)(180 + 60 * glow);
|
||||||
|
ImU32 fillCol = IM_COL32(r, g, b, a);
|
||||||
|
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
|
||||||
|
// Bright border
|
||||||
|
dl->AddRect(cMin, cMax, WithAlpha(Primary(), (int)(160 + 60 * glow)), rounding, 0, schema::UI().drawElement("tabs.mining", "active-cell-border-thickness").size);
|
||||||
|
} else if (active) {
|
||||||
|
// Active but not mining: solid primary fill
|
||||||
|
ImU32 pri = Primary();
|
||||||
|
int priR = (pri >> 0) & 0xFF;
|
||||||
|
int priG = (pri >> 8) & 0xFF;
|
||||||
|
int priB = (pri >> 16) & 0xFF;
|
||||||
|
ImU32 fillCol = hovered
|
||||||
|
? IM_COL32(priR, priG, priB, 220)
|
||||||
|
: IM_COL32(priR, priG, priB, 180);
|
||||||
|
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
|
||||||
|
dl->AddRect(cMin, cMax, IM_COL32(priR, priG, priB, 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size);
|
||||||
|
} else {
|
||||||
|
// Inactive: dim outline
|
||||||
|
ImU32 fillCol = hovered
|
||||||
|
? WithAlpha(OnSurface(), 25)
|
||||||
|
: WithAlpha(OnSurface(), 8);
|
||||||
|
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
|
||||||
|
dl->AddRect(cMin, cMax, WithAlpha(OnSurface(), hovered ? 80 : 35), rounding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Thread number label (centered)
|
||||||
|
snprintf(buf, sizeof(buf), "%d", threadNum);
|
||||||
|
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
||||||
|
ImVec2 txtPos(cx + (cellSz - txtSz.x) * 0.5f, cy + (cellSz - txtSz.y) * 0.5f);
|
||||||
|
ImU32 txtCol = active ? schema::UI().resolveColor("var(--on-primary)", IM_COL32(255, 255, 255, 230)) : WithAlpha(OnSurface(), 80);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, txtPos, txtCol, buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (threads_changed) {
|
||||||
|
app->settings()->setPoolThreads(s_selected_threads);
|
||||||
|
app->settings()->save();
|
||||||
|
if (mining.generate) {
|
||||||
|
app->startMining(s_selected_threads);
|
||||||
|
}
|
||||||
|
if (s_pool_mode && state.pool_mining.xmrig_running) {
|
||||||
|
app->stopPoolMining();
|
||||||
|
app->startPoolMining(s_selected_threads);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
curY += gridH + secGap;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ============================================================
|
||||||
|
// Large square mining button — right of the controls card
|
||||||
|
// ============================================================
|
||||||
|
{
|
||||||
|
float btnX = cardMin.x + controlsW + miningBtnGap;
|
||||||
|
float btnY = cardMin.y;
|
||||||
|
ImVec2 bMin(btnX, btnY);
|
||||||
|
ImVec2 bMax(btnX + miningBtnSz, btnY + cardH);
|
||||||
|
|
||||||
|
bool btnHovered = material::IsRectHovered(bMin, bMax);
|
||||||
|
bool btnClicked = btnHovered && ImGui::IsMouseClicked(0);
|
||||||
|
bool isSyncing = state.sync.syncing;
|
||||||
|
bool poolBlockedBySolo = s_pool_mode && mining.generate && !state.pool_mining.xmrig_running;
|
||||||
|
bool isToggling = app->isMiningToggleInProgress();
|
||||||
|
// Pool mining connects to an external pool via xmrig — it does not
|
||||||
|
// need the local blockchain synced or even the daemon connected.
|
||||||
|
// If pool mining is still shutting down after switching to solo,
|
||||||
|
// keep the button enabled so user can stop it.
|
||||||
|
bool poolStillRunning = !s_pool_mode && state.pool_mining.xmrig_running;
|
||||||
|
bool disabled = s_pool_mode
|
||||||
|
? (isToggling || poolBlockedBySolo)
|
||||||
|
: (poolStillRunning ? false : (!app->isConnected() || isToggling || isSyncing));
|
||||||
|
|
||||||
|
// Glass panel background with state-dependent tint
|
||||||
|
GlassPanelSpec btnGlass;
|
||||||
|
btnGlass.rounding = Layout::glassRounding();
|
||||||
|
if (isToggling) {
|
||||||
|
// Toggling: subtle pulsing glow to indicate activity
|
||||||
|
float pulse = effects::isLowSpecMode()
|
||||||
|
? 0.5f
|
||||||
|
: 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 4.0);
|
||||||
|
int glowA = (int)(15 + 25 * pulse);
|
||||||
|
btnGlass.fillAlpha = glowA;
|
||||||
|
} else if (isMiningActive) {
|
||||||
|
// Active mining: warm glow
|
||||||
|
float pulse = effects::isLowSpecMode()
|
||||||
|
? schema::UI().drawElement("animations", "pulse-base-glow").size
|
||||||
|
: schema::UI().drawElement("animations", "pulse-base-glow").size + schema::UI().drawElement("animations", "pulse-amp-glow").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-slow").size);
|
||||||
|
int glowA = (int)(20 + 30 * pulse);
|
||||||
|
btnGlass.fillAlpha = glowA;
|
||||||
|
} else {
|
||||||
|
btnGlass.fillAlpha = btnHovered ? 30 : 18;
|
||||||
|
}
|
||||||
|
DrawGlassPanel(dl, bMin, bMax, btnGlass);
|
||||||
|
|
||||||
|
// Hover highlight
|
||||||
|
if (btnHovered && !disabled) {
|
||||||
|
dl->AddRectFilled(bMin, bMax,
|
||||||
|
isMiningActive ? WithAlpha(Error(), 25) : WithAlpha(Success(), 20),
|
||||||
|
btnGlass.rounding);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw mining icon centered in button — Material Design ICON_MD_CONSTRUCTION
|
||||||
|
{
|
||||||
|
float btnW = bMax.x - bMin.x;
|
||||||
|
float btnH = bMax.y - bMin.y;
|
||||||
|
float cx = bMin.x + btnW * 0.5f;
|
||||||
|
float cy = bMin.y + btnH * schema::UI().drawElement("tabs.mining", "button-icon-y-ratio").size; // shift up to leave room for label
|
||||||
|
|
||||||
|
if (isToggling) {
|
||||||
|
// Draw a spinning arc spinner to indicate progress
|
||||||
|
float spinnerR = std::min(btnW, btnH) * 0.18f;
|
||||||
|
float thickness = std::max(2.5f, spinnerR * 0.15f);
|
||||||
|
float time = (float)ImGui::GetTime();
|
||||||
|
|
||||||
|
// Track circle (faint)
|
||||||
|
ImU32 trackCol = WithAlpha(Primary(), 40);
|
||||||
|
dl->AddCircle(ImVec2(cx, cy), spinnerR, trackCol, 0, thickness);
|
||||||
|
|
||||||
|
// Animated arc
|
||||||
|
float rotation = fmodf(time * 2.0f * IM_PI / 1.4f, IM_PI * 2.0f);
|
||||||
|
float cycleTime = fmodf(time, 1.333f);
|
||||||
|
float arcLength = (cycleTime < 0.666f)
|
||||||
|
? (cycleTime / 0.666f) * 0.75f + 0.1f
|
||||||
|
: ((1.333f - cycleTime) / 0.666f) * 0.75f + 0.1f;
|
||||||
|
|
||||||
|
float startAngle = rotation - IM_PI * 0.5f;
|
||||||
|
float endAngle = startAngle + IM_PI * 2.0f * arcLength;
|
||||||
|
int segments = (int)(32 * arcLength) + 1;
|
||||||
|
float angleStep = (endAngle - startAngle) / segments;
|
||||||
|
ImU32 arcCol = Primary();
|
||||||
|
|
||||||
|
for (int i = 0; i < segments; i++) {
|
||||||
|
float a1 = startAngle + angleStep * i;
|
||||||
|
float a2 = startAngle + angleStep * (i + 1);
|
||||||
|
ImVec2 p1(cx + cosf(a1) * spinnerR, cy + sinf(a1) * spinnerR);
|
||||||
|
ImVec2 p2(cx + cosf(a2) * spinnerR, cy + sinf(a2) * spinnerR);
|
||||||
|
dl->AddLine(p1, p2, arcCol, thickness);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ImU32 iconCol;
|
||||||
|
if (disabled) {
|
||||||
|
iconCol = OnSurfaceDisabled();
|
||||||
|
} else if (isMiningActive) {
|
||||||
|
float pulse = effects::isLowSpecMode()
|
||||||
|
? schema::UI().drawElement("animations", "pulse-base-normal").size
|
||||||
|
: schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size);
|
||||||
|
iconCol = WithAlpha(Error(), (int)(200 + 55 * pulse));
|
||||||
|
} else {
|
||||||
|
iconCol = btnHovered ? WithAlpha(Success(), 255) : OnSurfaceMedium();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use XL icon for the large mining button
|
||||||
|
ImFont* iconFont = Type().iconXL();
|
||||||
|
const char* mineIcon = isMiningActive ? ICON_MD_CLOSE : ICON_MD_CONSTRUCTION;
|
||||||
|
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, mineIcon);
|
||||||
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
|
ImVec2(cx - iconSz.x * 0.5f, cy - iconSz.y * 0.5f), iconCol, mineIcon);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Label below icon
|
||||||
|
{
|
||||||
|
float btnW = bMax.x - bMin.x;
|
||||||
|
float btnH = bMax.y - bMin.y;
|
||||||
|
const char* label;
|
||||||
|
ImU32 lblCol;
|
||||||
|
if (isToggling) {
|
||||||
|
label = isMiningActive ? TR("mining_stopping") : TR("mining_starting");
|
||||||
|
// Animated dots effect via alpha pulse
|
||||||
|
float pulse = effects::isLowSpecMode()
|
||||||
|
? 0.7f
|
||||||
|
: 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 3.0);
|
||||||
|
lblCol = WithAlpha(Primary(), (int)(120 + 135 * pulse));
|
||||||
|
} else if (isMiningActive) {
|
||||||
|
label = TR("mining_stop");
|
||||||
|
lblCol = WithAlpha(Error(), 220);
|
||||||
|
} else if (disabled) {
|
||||||
|
label = TR("mining_mine");
|
||||||
|
lblCol = WithAlpha(OnSurface(), 50);
|
||||||
|
} else {
|
||||||
|
label = TR("mining_mine");
|
||||||
|
lblCol = WithAlpha(OnSurface(), 160);
|
||||||
|
}
|
||||||
|
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
|
||||||
|
float lblX = bMin.x + (btnW - lblSz.x) * 0.5f;
|
||||||
|
float lblY = bMin.y + btnH * schema::UI().drawElement("tabs.mining", "button-label-y-ratio").size;
|
||||||
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lblX, lblY), lblCol, label);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tooltip + pointer cursor
|
||||||
|
if (btnHovered) {
|
||||||
|
if (!disabled)
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
if (isToggling)
|
||||||
|
ImGui::SetTooltip("%s", isMiningActive ? TR("mining_stopping_tooltip") : TR("mining_starting_tooltip"));
|
||||||
|
else if (isSyncing && !s_pool_mode)
|
||||||
|
ImGui::SetTooltip(TR("mining_syncing_tooltip"), state.sync.verification_progress * 100.0);
|
||||||
|
else if (poolBlockedBySolo)
|
||||||
|
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
|
||||||
|
else
|
||||||
|
ImGui::SetTooltip("%s", isMiningActive ? TR("stop_mining") : TR("start_mining"));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Click action — pool or solo
|
||||||
|
if (btnClicked && !disabled) {
|
||||||
|
if (s_pool_mode) {
|
||||||
|
if (state.pool_mining.xmrig_running)
|
||||||
|
app->stopPoolMining();
|
||||||
|
else
|
||||||
|
app->startPoolMining(s_selected_threads);
|
||||||
|
} else {
|
||||||
|
// If pool mining is still running (user just switched from pool to solo),
|
||||||
|
// stop pool mining first
|
||||||
|
if (state.pool_mining.xmrig_running)
|
||||||
|
app->stopPoolMining();
|
||||||
|
else if (mining.generate)
|
||||||
|
app->stopMining();
|
||||||
|
else
|
||||||
|
app->startMining(s_selected_threads);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||||
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||||
|
ImGui::Dummy(ImVec2(0, gap));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
32
src/ui/windows/mining_controls.h
Normal file
32
src/ui/windows/mining_controls.h
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
// DragonX Wallet - ImGui Edition
|
||||||
|
// Copyright 2024-2026 The Hush Developers
|
||||||
|
// Released under the GPLv3
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "../../data/wallet_state.h"
|
||||||
|
#include "../material/draw_helpers.h" // GlassPanelSpec
|
||||||
|
#include "mining_benchmark.h" // ThreadBenchmark
|
||||||
|
#include "imgui.h"
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
|
||||||
|
namespace dragonx {
|
||||||
|
class App;
|
||||||
|
namespace ui {
|
||||||
|
|
||||||
|
// Renders the mining "Controls" card (CPU-core grid + mining start/stop button + benchmark /
|
||||||
|
// miner-update controls). Extracted verbatim from the monolithic mining tab. State the section
|
||||||
|
// MUTATES is passed BY REFERENCE (the benchmark, selected thread count, and drag state) so the
|
||||||
|
// interactions behave exactly as before; read-only context is passed by value/const.
|
||||||
|
void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& mining,
|
||||||
|
ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
|
||||||
|
float dp, float hs, float vs, float gap, float pad, float availWidth,
|
||||||
|
const material::GlassPanelSpec& glassSpec, float controlsBudgetH,
|
||||||
|
int max_threads, bool isMiningActive, bool poolMode,
|
||||||
|
const char* poolWorker, const std::string& xmrigLatestTag,
|
||||||
|
ThreadBenchmark& benchmark, int& selectedThreads,
|
||||||
|
bool& dragActive, int& dragAnchorThread);
|
||||||
|
|
||||||
|
} // namespace ui
|
||||||
|
} // namespace dragonx
|
||||||
@@ -8,6 +8,7 @@
|
|||||||
#include "mining_pool_panel.h"
|
#include "mining_pool_panel.h"
|
||||||
#include "mining_earnings.h"
|
#include "mining_earnings.h"
|
||||||
#include "mining_stats.h"
|
#include "mining_stats.h"
|
||||||
|
#include "mining_controls.h"
|
||||||
#include "xmrig_download_dialog.h"
|
#include "xmrig_download_dialog.h"
|
||||||
#include "../../util/xmrig_updater.h"
|
#include "../../util/xmrig_updater.h"
|
||||||
#include "../../app.h"
|
#include "../../app.h"
|
||||||
@@ -792,852 +793,10 @@ static void RenderMiningTabContent(App* app)
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
// CONTROLS — Glass card with CPU core grid (no heading)
|
// CONTROLS — Glass card with CPU core grid (no heading)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
{
|
RenderMiningControls(app, state, mining, dl, capFont, sub1, ovFont, dp, hs, vs, gap, pad,
|
||||||
// Mining button beside the controls card
|
availWidth, glassSpec, controlsBudgetH, max_threads, isMiningActive,
|
||||||
float miningBtnGap = gap;
|
s_pool_mode, s_pool_worker, s_xmrig_latest_tag, s_benchmark,
|
||||||
float miningBtnMaxW = availWidth * schema::UI().drawElement("tabs.mining", "btn-max-width-ratio").size;
|
s_selected_threads, s_drag_active, s_drag_anchor_thread);
|
||||||
|
|
||||||
// --- Compute thread grid layout based on controls card width ---
|
|
||||||
// Estimate controlsW first to compute cols correctly
|
|
||||||
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size, miningBtnMaxW) - miningBtnGap;
|
|
||||||
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;
|
|
||||||
float gridW = cols * cellSz + (cols - 1) * cellGap;
|
|
||||||
float gridH = rows * cellSz + (rows - 1) * cellGap;
|
|
||||||
|
|
||||||
// Card sections: header(label+info+RAM inline) | grid
|
|
||||||
float headerH = capFont->LegacySize + Layout::spacingXs();
|
|
||||||
float secGap = Layout::spacingLg();
|
|
||||||
|
|
||||||
// Card height from actual content, capped by proportional budget
|
|
||||||
float cardH = pad + headerH + secGap + gridH + pad;
|
|
||||||
cardH = std::clamp(cardH, schema::UI().drawElement("tabs.mining", "control-card-min-height").size, controlsBudgetH);
|
|
||||||
|
|
||||||
// Mining button — sized to match card height (square)
|
|
||||||
float miningBtnSz = cardH;
|
|
||||||
if (miningBtnSz + miningBtnGap > miningBtnMaxW)
|
|
||||||
miningBtnSz = miningBtnMaxW;
|
|
||||||
float controlsW = availWidth - miningBtnSz - miningBtnGap;
|
|
||||||
|
|
||||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
|
||||||
ImVec2 cardMax(cardMin.x + controlsW, cardMin.y + cardH);
|
|
||||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
|
||||||
|
|
||||||
float curY = cardMin.y + pad;
|
|
||||||
|
|
||||||
// --- Header row: "THREADS 4 / 16" + RAM est + active indicator ---
|
|
||||||
{
|
|
||||||
ImVec2 labelPos(cardMin.x + pad, curY);
|
|
||||||
dl->AddText(ovFont, ovFont->LegacySize, labelPos, OnSurfaceMedium(), TR("mining_threads"));
|
|
||||||
|
|
||||||
float labelW = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, TR("mining_threads")).x;
|
|
||||||
snprintf(buf, sizeof(buf), " %d / %d", s_selected_threads, max_threads);
|
|
||||||
ImVec2 countPos(labelPos.x + labelW, curY);
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, countPos, OnSurface(), buf);
|
|
||||||
|
|
||||||
// RAM estimate inline (after thread count)
|
|
||||||
// Model matches DragonX RandomX: shared ~2080MB dataset + ~256MB cache (allocated once),
|
|
||||||
// plus ~2MB scratchpad per mining thread VM.
|
|
||||||
{
|
|
||||||
float countW = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf).x;
|
|
||||||
double ram_estimate_mb = schema::UI().drawElement("business", "ram-base-mb").size
|
|
||||||
+ schema::UI().drawElement("business", "ram-dataset-mb").size
|
|
||||||
+ schema::UI().drawElement("business", "ram-cache-mb").size
|
|
||||||
+ s_selected_threads * schema::UI().drawElement("business", "ram-per-thread-mb").size;
|
|
||||||
double ram_estimate_gb = ram_estimate_mb / 1024.0;
|
|
||||||
snprintf(buf, sizeof(buf), " RAM ~%.1fGB", ram_estimate_gb);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(countPos.x + countW, curY + (sub1->LegacySize - capFont->LegacySize) * 0.5f),
|
|
||||||
OnSurfaceDisabled(), buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Idle mining toggle (top-right corner of card)
|
|
||||||
float idleRightEdge = cardMax.x - pad;
|
|
||||||
{
|
|
||||||
bool idleOn = app->settings()->getMineWhenIdle();
|
|
||||||
bool threadScaling = app->settings()->getIdleThreadScaling();
|
|
||||||
ImFont* icoFont = Type().iconSmall();
|
|
||||||
const char* idleIcon = ICON_MD_SCHEDULE;
|
|
||||||
float icoH = icoFont->LegacySize;
|
|
||||||
float btnSz = icoH + 8.0f * dp;
|
|
||||||
float btnX = idleRightEdge - btnSz;
|
|
||||||
float btnY = curY + (headerH - btnSz) * 0.5f;
|
|
||||||
|
|
||||||
// Pill background when active
|
|
||||||
if (idleOn) {
|
|
||||||
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
|
|
||||||
WithAlpha(Primary(), 60), btnSz * 0.5f);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Icon centered in button
|
|
||||||
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, idleIcon);
|
|
||||||
ImU32 icoCol = idleOn ? Primary() : OnSurfaceDisabled();
|
|
||||||
dl->AddText(icoFont, icoFont->LegacySize,
|
|
||||||
ImVec2(btnX + (btnSz - icoSz.x) * 0.5f, btnY + (btnSz - icoSz.y) * 0.5f),
|
|
||||||
icoCol, idleIcon);
|
|
||||||
|
|
||||||
// Click target (save/restore cursor so layout is unaffected)
|
|
||||||
ImVec2 savedCur = ImGui::GetCursorScreenPos();
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
|
|
||||||
ImGui::InvisibleButton("##IdleMining", ImVec2(btnSz, btnSz));
|
|
||||||
if (ImGui::IsItemClicked()) {
|
|
||||||
app->settings()->setMineWhenIdle(!idleOn);
|
|
||||||
app->settings()->save();
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered()) {
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
ImGui::SetTooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip"));
|
|
||||||
}
|
|
||||||
|
|
||||||
idleRightEdge = btnX - 4.0f * dp;
|
|
||||||
|
|
||||||
// Thread scaling mode toggle (to the left of idle icon, shown when idle is on)
|
|
||||||
if (idleOn) {
|
|
||||||
const char* scaleIcon = threadScaling ? ICON_MD_TUNE : ICON_MD_POWER_SETTINGS_NEW;
|
|
||||||
float sBtnX = idleRightEdge - btnSz;
|
|
||||||
float sBtnY = btnY;
|
|
||||||
|
|
||||||
if (threadScaling) {
|
|
||||||
dl->AddRectFilled(ImVec2(sBtnX, sBtnY), ImVec2(sBtnX + btnSz, sBtnY + btnSz),
|
|
||||||
WithAlpha(Primary(), 40), btnSz * 0.5f);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImVec2 sIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, scaleIcon);
|
|
||||||
ImU32 sIcoCol = threadScaling ? Primary() : OnSurfaceMedium();
|
|
||||||
dl->AddText(icoFont, icoFont->LegacySize,
|
|
||||||
ImVec2(sBtnX + (btnSz - sIcoSz.x) * 0.5f, sBtnY + (btnSz - sIcoSz.y) * 0.5f),
|
|
||||||
sIcoCol, scaleIcon);
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(sBtnX, sBtnY));
|
|
||||||
ImGui::InvisibleButton("##IdleScaleMode", ImVec2(btnSz, btnSz));
|
|
||||||
if (ImGui::IsItemClicked()) {
|
|
||||||
app->settings()->setIdleThreadScaling(!threadScaling);
|
|
||||||
app->settings()->save();
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered()) {
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
ImGui::SetTooltip("%s", threadScaling
|
|
||||||
? TR("mining_idle_scale_on_tooltip")
|
|
||||||
: TR("mining_idle_scale_off_tooltip"));
|
|
||||||
}
|
|
||||||
idleRightEdge = sBtnX - 4.0f * dp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// GPU-aware idle toggle (to the left, when idle is on)
|
|
||||||
// When ON (default): GPU utilization >= 10% counts as "not idle"
|
|
||||||
// When OFF: unrestricted mode, only keyboard/mouse input matters
|
|
||||||
if (idleOn) {
|
|
||||||
bool gpuAware = app->settings()->getIdleGpuAware();
|
|
||||||
const char* gpuIcon = gpuAware ? ICON_MD_MONITOR : ICON_MD_MONITOR;
|
|
||||||
float gBtnX = idleRightEdge - btnSz;
|
|
||||||
float gBtnY = btnY;
|
|
||||||
|
|
||||||
if (gpuAware) {
|
|
||||||
dl->AddRectFilled(ImVec2(gBtnX, gBtnY), ImVec2(gBtnX + btnSz, gBtnY + btnSz),
|
|
||||||
WithAlpha(Primary(), 40), btnSz * 0.5f);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImVec2 gIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, gpuIcon);
|
|
||||||
ImU32 gIcoCol = gpuAware ? Primary() : OnSurfaceDisabled();
|
|
||||||
dl->AddText(icoFont, icoFont->LegacySize,
|
|
||||||
ImVec2(gBtnX + (btnSz - gIcoSz.x) * 0.5f, gBtnY + (btnSz - gIcoSz.y) * 0.5f),
|
|
||||||
gIcoCol, gpuIcon);
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(gBtnX, gBtnY));
|
|
||||||
ImGui::InvisibleButton("##IdleGpuAware", ImVec2(btnSz, btnSz));
|
|
||||||
if (ImGui::IsItemClicked()) {
|
|
||||||
app->settings()->setIdleGpuAware(!gpuAware);
|
|
||||||
app->settings()->save();
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered()) {
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
ImGui::SetTooltip("%s", gpuAware
|
|
||||||
? TR("mining_idle_gpu_on_tooltip")
|
|
||||||
: TR("mining_idle_gpu_off_tooltip"));
|
|
||||||
}
|
|
||||||
idleRightEdge = gBtnX - 4.0f * dp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode)
|
|
||||||
if (idleOn && !threadScaling) {
|
|
||||||
struct DelayOption { int seconds; const char* label; };
|
|
||||||
static const DelayOption delays[] = {
|
|
||||||
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
|
|
||||||
};
|
|
||||||
int curDelay = app->settings()->getMineIdleDelay();
|
|
||||||
const char* previewLabel = "2m";
|
|
||||||
for (const auto& d : delays) {
|
|
||||||
if (d.seconds == curDelay) { previewLabel = d.label; break; }
|
|
||||||
}
|
|
||||||
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
|
||||||
float comboX = idleRightEdge - comboW;
|
|
||||||
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
|
||||||
ImGui::SetNextItemWidth(comboW);
|
|
||||||
if (ImGui::BeginCombo("##IdleDelay", previewLabel, ImGuiComboFlags_NoArrowButton)) {
|
|
||||||
for (const auto& d : delays) {
|
|
||||||
bool selected = (d.seconds == curDelay);
|
|
||||||
if (ImGui::Selectable(d.label, selected)) {
|
|
||||||
app->settings()->setMineIdleDelay(d.seconds);
|
|
||||||
app->settings()->save();
|
|
||||||
}
|
|
||||||
if (selected) ImGui::SetItemDefaultFocus();
|
|
||||||
}
|
|
||||||
ImGui::EndCombo();
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered())
|
|
||||||
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
|
|
||||||
idleRightEdge = comboX - 4.0f * dp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thread scaling controls: idle delay + active threads / idle threads combos
|
|
||||||
if (idleOn && threadScaling) {
|
|
||||||
int hwThreads = std::max(1, (int)std::thread::hardware_concurrency());
|
|
||||||
|
|
||||||
// Idle delay combo
|
|
||||||
{
|
|
||||||
struct DelayOption { int seconds; const char* label; };
|
|
||||||
static const DelayOption delays[] = {
|
|
||||||
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
|
|
||||||
};
|
|
||||||
int curDelay = app->settings()->getMineIdleDelay();
|
|
||||||
const char* previewLabel = "2m";
|
|
||||||
for (const auto& d : delays) {
|
|
||||||
if (d.seconds == curDelay) { previewLabel = d.label; break; }
|
|
||||||
}
|
|
||||||
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
|
||||||
float comboX = idleRightEdge - comboW;
|
|
||||||
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
|
||||||
ImGui::SetNextItemWidth(comboW);
|
|
||||||
if (ImGui::BeginCombo("##IdleDelayScale", previewLabel, ImGuiComboFlags_NoArrowButton)) {
|
|
||||||
for (const auto& d : delays) {
|
|
||||||
bool selected = (d.seconds == curDelay);
|
|
||||||
if (ImGui::Selectable(d.label, selected)) {
|
|
||||||
app->settings()->setMineIdleDelay(d.seconds);
|
|
||||||
app->settings()->save();
|
|
||||||
}
|
|
||||||
if (selected) ImGui::SetItemDefaultFocus();
|
|
||||||
}
|
|
||||||
ImGui::EndCombo();
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered())
|
|
||||||
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
|
|
||||||
idleRightEdge = comboX - 4.0f * dp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Idle threads combo (threads when system is idle)
|
|
||||||
{
|
|
||||||
int curVal = app->settings()->getIdleThreadsIdle();
|
|
||||||
if (curVal <= 0) curVal = hwThreads;
|
|
||||||
char previewBuf[16];
|
|
||||||
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
|
|
||||||
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
|
||||||
float comboX = idleRightEdge - comboW;
|
|
||||||
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
|
||||||
ImGui::SetNextItemWidth(comboW);
|
|
||||||
if (ImGui::BeginCombo("##IdleThreadsIdle", previewBuf, ImGuiComboFlags_NoArrowButton)) {
|
|
||||||
for (int t = 1; t <= hwThreads; t++) {
|
|
||||||
char lbl[16];
|
|
||||||
snprintf(lbl, sizeof(lbl), "%d", t);
|
|
||||||
bool selected = (t == curVal);
|
|
||||||
if (ImGui::Selectable(lbl, selected)) {
|
|
||||||
app->settings()->setIdleThreadsIdle(t);
|
|
||||||
app->settings()->save();
|
|
||||||
}
|
|
||||||
if (selected) ImGui::SetItemDefaultFocus();
|
|
||||||
}
|
|
||||||
ImGui::EndCombo();
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered())
|
|
||||||
ImGui::SetTooltip("%s", TR("mining_idle_threads_idle_tooltip"));
|
|
||||||
idleRightEdge = comboX - 4.0f * dp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Separator arrow icon
|
|
||||||
{
|
|
||||||
const char* arrowIcon = ICON_MD_ARROW_BACK;
|
|
||||||
ImVec2 arrSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, arrowIcon);
|
|
||||||
float arrX = idleRightEdge - arrSz.x;
|
|
||||||
float arrY = curY + (headerH - arrSz.y) * 0.5f;
|
|
||||||
dl->AddText(icoFont, icoFont->LegacySize, ImVec2(arrX, arrY), OnSurfaceDisabled(), arrowIcon);
|
|
||||||
idleRightEdge = arrX - 4.0f * dp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Active threads combo (threads when user is active)
|
|
||||||
{
|
|
||||||
int curVal = app->settings()->getIdleThreadsActive();
|
|
||||||
if (curVal <= 0) curVal = std::max(1, hwThreads / 2);
|
|
||||||
char previewBuf[16];
|
|
||||||
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
|
|
||||||
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
|
|
||||||
float comboX = idleRightEdge - comboW;
|
|
||||||
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
|
|
||||||
ImGui::SetNextItemWidth(comboW);
|
|
||||||
if (ImGui::BeginCombo("##IdleThreadsActive", previewBuf, ImGuiComboFlags_NoArrowButton)) {
|
|
||||||
for (int t = 1; t <= hwThreads; t++) {
|
|
||||||
char lbl[16];
|
|
||||||
snprintf(lbl, sizeof(lbl), "%d", t);
|
|
||||||
bool selected = (t == curVal);
|
|
||||||
if (ImGui::Selectable(lbl, selected)) {
|
|
||||||
app->settings()->setIdleThreadsActive(t);
|
|
||||||
app->settings()->save();
|
|
||||||
}
|
|
||||||
if (selected) ImGui::SetItemDefaultFocus();
|
|
||||||
}
|
|
||||||
ImGui::EndCombo();
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered())
|
|
||||||
ImGui::SetTooltip("%s", TR("mining_idle_threads_active_tooltip"));
|
|
||||||
idleRightEdge = comboX - 4.0f * dp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(savedCur);
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Thread Benchmark button / progress (left of idle toggle) ---
|
|
||||||
{
|
|
||||||
ImVec2 benchSavedCur = ImGui::GetCursorScreenPos();
|
|
||||||
bool benchRunning = s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
|
|
||||||
s_benchmark.phase != ThreadBenchmark::Phase::Done;
|
|
||||||
bool benchDone = s_benchmark.phase == ThreadBenchmark::Phase::Done;
|
|
||||||
ImFont* icoFont = Type().iconSmall();
|
|
||||||
|
|
||||||
if (benchRunning) {
|
|
||||||
// Show progress bar + current test info
|
|
||||||
float barW = std::min(180.0f * hs, idleRightEdge - (cardMin.x + pad) - 10.0f * dp);
|
|
||||||
float barH = 4.0f * dp;
|
|
||||||
float barX = idleRightEdge - barW;
|
|
||||||
float barY = curY + headerH - barH - 2.0f * dp;
|
|
||||||
|
|
||||||
// Progress bar track
|
|
||||||
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
|
|
||||||
WithAlpha(OnSurface(), 30), barH * 0.5f);
|
|
||||||
// Progress bar fill
|
|
||||||
float pct = s_benchmark.progress();
|
|
||||||
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW * pct, barY + barH),
|
|
||||||
Primary(), barH * 0.5f);
|
|
||||||
|
|
||||||
// Status text above bar
|
|
||||||
int ct = s_benchmark.current_index < (int)s_benchmark.candidates.size()
|
|
||||||
? s_benchmark.candidates[s_benchmark.current_index] : 0;
|
|
||||||
// Estimated remaining time (uses observed warmup for better accuracy)
|
|
||||||
int remaining_tests = (int)s_benchmark.candidates.size() - s_benchmark.current_index;
|
|
||||||
float elapsed_in_phase = s_benchmark.phase_timer;
|
|
||||||
float phase_total;
|
|
||||||
if (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp)
|
|
||||||
phase_total = s_benchmark.avgWarmupSecs(); // adaptive estimate
|
|
||||||
else if (s_benchmark.phase == ThreadBenchmark::Phase::CoolingDown)
|
|
||||||
phase_total = ThreadBenchmark::COOLDOWN_SECS;
|
|
||||||
else
|
|
||||||
phase_total = ThreadBenchmark::MEASURE_SECS;
|
|
||||||
float remaining_in_current = std::max(0.0f, phase_total - elapsed_in_phase);
|
|
||||||
// Remaining tests after current each need warmup + measure + cooldown
|
|
||||||
float est_secs = remaining_in_current
|
|
||||||
+ (remaining_tests - 1) * (s_benchmark.avgWarmupSecs() + ThreadBenchmark::MEASURE_SECS + ThreadBenchmark::COOLDOWN_SECS);
|
|
||||||
int est_min = (int)(est_secs / 60.0f);
|
|
||||||
int est_sec = (int)est_secs % 60;
|
|
||||||
const char* phase_label;
|
|
||||||
if (s_benchmark.phase == ThreadBenchmark::Phase::CoolingDown)
|
|
||||||
phase_label = TR("mining_benchmark_cooling");
|
|
||||||
else if (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp
|
|
||||||
&& s_benchmark.phase_timer >= ThreadBenchmark::MIN_WARMUP_SECS)
|
|
||||||
phase_label = TR("mining_benchmark_stabilizing");
|
|
||||||
else
|
|
||||||
phase_label = TR("mining_benchmark_testing");
|
|
||||||
if (est_min > 0)
|
|
||||||
snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%dm%ds",
|
|
||||||
phase_label,
|
|
||||||
s_benchmark.current_index + 1,
|
|
||||||
(int)s_benchmark.candidates.size(), ct, est_min, est_sec);
|
|
||||||
else
|
|
||||||
snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%ds",
|
|
||||||
phase_label,
|
|
||||||
s_benchmark.current_index + 1,
|
|
||||||
(int)s_benchmark.candidates.size(), ct, est_sec);
|
|
||||||
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(barX + (barW - txtSz.x) * 0.5f, barY - txtSz.y - 2.0f * dp),
|
|
||||||
OnSurfaceMedium(), buf);
|
|
||||||
|
|
||||||
// Cancel button (small X)
|
|
||||||
float cancelSz = icoFont->LegacySize + 4.0f * dp;
|
|
||||||
float cancelX = barX - cancelSz - 4.0f * dp;
|
|
||||||
float cancelY = curY + (headerH - cancelSz) * 0.5f;
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cancelX, cancelY));
|
|
||||||
ImGui::InvisibleButton("##BenchCancel", ImVec2(cancelSz, cancelSz));
|
|
||||||
if (ImGui::IsItemClicked()) {
|
|
||||||
app->stopPoolMining();
|
|
||||||
if (s_benchmark.was_pool_running)
|
|
||||||
app->startPoolMining(s_benchmark.prev_threads);
|
|
||||||
s_benchmark.reset();
|
|
||||||
}
|
|
||||||
if (ImGui::IsItemHovered()) {
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
ImGui::SetTooltip("%s", TR("mining_benchmark_cancel"));
|
|
||||||
}
|
|
||||||
const char* cancelIcon = ICON_MD_CLOSE;
|
|
||||||
ImVec2 cIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, cancelIcon);
|
|
||||||
dl->AddText(icoFont, icoFont->LegacySize,
|
|
||||||
ImVec2(cancelX + (cancelSz - cIcoSz.x) * 0.5f,
|
|
||||||
cancelY + (cancelSz - cIcoSz.y) * 0.5f),
|
|
||||||
OnSurfaceMedium(), cancelIcon);
|
|
||||||
|
|
||||||
idleRightEdge = cancelX - 4.0f * dp;
|
|
||||||
|
|
||||||
} else if (benchDone && s_benchmark.optimal_threads > 0) {
|
|
||||||
// Show result briefly, then reset on next click
|
|
||||||
snprintf(buf, sizeof(buf), "%s: %dt (%.1f H/s)",
|
|
||||||
TR("mining_benchmark_result"),
|
|
||||||
s_benchmark.optimal_threads, s_benchmark.optimal_hashrate);
|
|
||||||
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
|
||||||
float txtX = idleRightEdge - txtSz.x;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(txtX, curY + (headerH - txtSz.y) * 0.5f),
|
|
||||||
WithAlpha(Success(), 220), buf);
|
|
||||||
|
|
||||||
// Dismiss button
|
|
||||||
float dismissSz = icoFont->LegacySize + 4.0f * dp;
|
|
||||||
float dismissX = txtX - dismissSz - 4.0f * dp;
|
|
||||||
float dismissY = curY + (headerH - dismissSz) * 0.5f;
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(dismissX, dismissY));
|
|
||||||
ImGui::InvisibleButton("##BenchDismiss", ImVec2(dismissSz, dismissSz));
|
|
||||||
if (ImGui::IsItemClicked())
|
|
||||||
s_benchmark.reset();
|
|
||||||
if (ImGui::IsItemHovered()) {
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
ImGui::SetTooltip("%s", TR("mining_benchmark_dismiss"));
|
|
||||||
}
|
|
||||||
const char* okIcon = ICON_MD_CHECK;
|
|
||||||
ImVec2 oIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, okIcon);
|
|
||||||
dl->AddText(icoFont, icoFont->LegacySize,
|
|
||||||
ImVec2(dismissX + (dismissSz - oIcoSz.x) * 0.5f,
|
|
||||||
dismissY + (dismissSz - oIcoSz.y) * 0.5f),
|
|
||||||
WithAlpha(Success(), 200), okIcon);
|
|
||||||
|
|
||||||
idleRightEdge = dismissX - 4.0f * dp;
|
|
||||||
|
|
||||||
} else if (s_pool_mode) {
|
|
||||||
// Show benchmark button (only in pool mode)
|
|
||||||
float btnSz = icoFont->LegacySize + 8.0f * dp;
|
|
||||||
float btnX = idleRightEdge - btnSz;
|
|
||||||
float btnY = curY + (headerH - btnSz) * 0.5f;
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
|
|
||||||
ImGui::InvisibleButton("##BenchStart", ImVec2(btnSz, btnSz));
|
|
||||||
bool benchHovered = ImGui::IsItemHovered();
|
|
||||||
bool benchClicked = ImGui::IsItemClicked();
|
|
||||||
|
|
||||||
// Hover highlight
|
|
||||||
if (benchHovered) {
|
|
||||||
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
|
|
||||||
StateHover(), btnSz * 0.5f);
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
ImGui::SetTooltip("%s", TR("mining_benchmark_tooltip"));
|
|
||||||
}
|
|
||||||
|
|
||||||
const char* benchIcon = ICON_MD_SPEED;
|
|
||||||
ImVec2 bIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, benchIcon);
|
|
||||||
dl->AddText(icoFont, icoFont->LegacySize,
|
|
||||||
ImVec2(btnX + (btnSz - bIcoSz.x) * 0.5f,
|
|
||||||
btnY + (btnSz - bIcoSz.y) * 0.5f),
|
|
||||||
OnSurfaceMedium(), benchIcon);
|
|
||||||
|
|
||||||
if (benchClicked) {
|
|
||||||
// Require a wallet address for pool mining
|
|
||||||
std::string worker(s_pool_worker);
|
|
||||||
if (!worker.empty()) {
|
|
||||||
s_benchmark.reset();
|
|
||||||
s_benchmark.was_pool_running = state.pool_mining.xmrig_running;
|
|
||||||
s_benchmark.prev_threads = s_selected_threads;
|
|
||||||
s_benchmark.buildCandidates(max_threads);
|
|
||||||
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
|
|
||||||
// Stop any active solo mining first
|
|
||||||
if (mining.generate)
|
|
||||||
app->stopMining();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
idleRightEdge = btnX - 4.0f * dp;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Miner update: "Update <latest>" button + current version (left of benchmark) ---
|
|
||||||
if (s_pool_mode && !benchRunning) {
|
|
||||||
const std::string curVer = app->settings()->getXmrigVersion();
|
|
||||||
char xbtn[64];
|
|
||||||
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"));
|
|
||||||
|
|
||||||
const bool minerBusy = state.pool_mining.xmrig_running;
|
|
||||||
ImVec2 xlblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, xbtn);
|
|
||||||
float xpadX = 8.0f * dp;
|
|
||||||
float xbtnW = xlblSz.x + xpadX * 2.0f;
|
|
||||||
float xbtnH = capFont->LegacySize + 6.0f * dp;
|
|
||||||
float xbtnX = idleRightEdge - xbtnW;
|
|
||||||
float xbtnY = curY + (headerH - xbtnH) * 0.5f;
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(xbtnX, xbtnY));
|
|
||||||
ImGui::InvisibleButton("##XmrigUpdateBtn", ImVec2(xbtnW, xbtnH));
|
|
||||||
bool xhov = ImGui::IsItemHovered();
|
|
||||||
bool xclk = ImGui::IsItemClicked();
|
|
||||||
ImU32 xpill = minerBusy ? WithAlpha(OnSurface(), 12)
|
|
||||||
: (xhov ? StateHover() : WithAlpha(OnSurface(), 28));
|
|
||||||
dl->AddRectFilled(ImVec2(xbtnX, xbtnY), ImVec2(xbtnX + xbtnW, xbtnY + xbtnH),
|
|
||||||
xpill, xbtnH * 0.3f);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(xbtnX + xpadX, xbtnY + (xbtnH - xlblSz.y) * 0.5f),
|
|
||||||
minerBusy ? OnSurfaceDisabled() : OnSurfaceMedium(), xbtn);
|
|
||||||
if (xhov) {
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
ImGui::SetTooltip("%s", minerBusy ? TR("xmrig_stop_mining_first")
|
|
||||||
: TR("xmrig_update_title"));
|
|
||||||
}
|
|
||||||
if (xclk && !minerBusy) XmrigDownloadDialog::show(app);
|
|
||||||
|
|
||||||
// Current installed version, as text to the LEFT of the button.
|
|
||||||
char xcur[64];
|
|
||||||
snprintf(xcur, sizeof(xcur), "%s %s", TR("xmrig_current"),
|
|
||||||
curVer.empty() ? TR("xmrig_none") : curVer.c_str());
|
|
||||||
ImVec2 xcurSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, xcur);
|
|
||||||
float xcurX = xbtnX - 6.0f * dp - xcurSz.x;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(xcurX, curY + (headerH - xcurSz.y) * 0.5f),
|
|
||||||
OnSurfaceDisabled(), xcur);
|
|
||||||
idleRightEdge = xcurX - 8.0f * dp;
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(benchSavedCur);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Active mining indicator (left of idle toggle)
|
|
||||||
if (mining.generate) {
|
|
||||||
float pulse = effects::isLowSpecMode()
|
|
||||||
? schema::UI().drawElement("animations", "pulse-base-normal").size
|
|
||||||
: schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size);
|
|
||||||
ImU32 pulseCol = WithAlpha(Success(), (int)(255 * pulse));
|
|
||||||
float dotR = schema::UI().drawElement("tabs.mining", "active-dot-radius").size + 2.0f * hs;
|
|
||||||
dl->AddCircleFilled(ImVec2(idleRightEdge - dotR, curY + dotR + 1 * dp), dotR, pulseCol);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(idleRightEdge - dotR - dotR - 60 * hs, curY),
|
|
||||||
WithAlpha(Success(), 200), TR("mining_active"));
|
|
||||||
}
|
|
||||||
curY += headerH + secGap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Thread Grid (drag-to-select) ---
|
|
||||||
{
|
|
||||||
// Center the grid horizontally within the card
|
|
||||||
float gridX = cardMin.x + pad + (innerW - gridW) * 0.5f;
|
|
||||||
float gridY = curY;
|
|
||||||
|
|
||||||
bool threads_changed = false;
|
|
||||||
|
|
||||||
// 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++) {
|
|
||||||
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;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Show pointer cursor when hovering the thread grid
|
|
||||||
bool benchActive = s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
|
|
||||||
s_benchmark.phase != ThreadBenchmark::Phase::Done;
|
|
||||||
if (hovered_thread > 0 && !benchActive)
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
|
|
||||||
// Drag-to-select logic (disabled during benchmark)
|
|
||||||
if (!benchActive && ImGui::IsMouseClicked(0) && hovered_thread > 0) {
|
|
||||||
// Begin drag
|
|
||||||
s_drag_active = true;
|
|
||||||
s_drag_anchor_thread = hovered_thread;
|
|
||||||
s_selected_threads = hovered_thread;
|
|
||||||
threads_changed = true;
|
|
||||||
}
|
|
||||||
if (s_drag_active) {
|
|
||||||
if (ImGui::IsMouseDown(0)) {
|
|
||||||
if (hovered_thread > 0 && hovered_thread != s_selected_threads) {
|
|
||||||
// Drag selects up to the hovered thread in either direction
|
|
||||||
s_selected_threads = hovered_thread;
|
|
||||||
threads_changed = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Mouse released — end drag
|
|
||||||
s_drag_active = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Render cells
|
|
||||||
for (int i = 0; i < max_threads; i++) {
|
|
||||||
int row = i / cols;
|
|
||||||
int col = i % cols;
|
|
||||||
float cx = gridX + col * (cellSz + cellGap);
|
|
||||||
float cy = gridY + row * (cellSz + cellGap);
|
|
||||||
ImVec2 cMin(cx, cy);
|
|
||||||
ImVec2 cMax(cx + cellSz, cy + cellSz);
|
|
||||||
|
|
||||||
int threadNum = i + 1;
|
|
||||||
bool active = threadNum <= s_selected_threads;
|
|
||||||
bool hovered = (threadNum == hovered_thread);
|
|
||||||
|
|
||||||
// Determine visual state
|
|
||||||
float rounding = schema::UI().drawElement("tabs.mining", "cell-rounding").size;
|
|
||||||
if (active && mining.generate) {
|
|
||||||
// Mining + active: animated heat glow using theme colors
|
|
||||||
float t = (float)ImGui::GetTime();
|
|
||||||
float phase = t * schema::UI().drawElement("animations", "heat-glow-speed").size + i * schema::UI().drawElement("animations", "heat-glow-phase-offset").size;
|
|
||||||
float glow = effects::isLowSpecMode() ? 0.5f : 0.5f + 0.5f * (float)std::sin(phase);
|
|
||||||
|
|
||||||
// Base color from theme (--mining-heat-glow) falling back to Primary
|
|
||||||
ImU32 heatBase = schema::UI().resolveColor("var(--mining-heat-glow)", Primary());
|
|
||||||
int baseR = (heatBase >> 0) & 0xFF;
|
|
||||||
int baseG = (heatBase >> 8) & 0xFF;
|
|
||||||
int baseB = (heatBase >> 16) & 0xFF;
|
|
||||||
// Brighten toward white as glow increases
|
|
||||||
int r = std::min(255, (int)(baseR + (255 - baseR) * 0.3f * glow));
|
|
||||||
int g = std::min(255, (int)(baseG + (255 - baseG) * 0.3f * glow));
|
|
||||||
int b = std::min(255, (int)(baseB + (255 - baseB) * 0.3f * glow));
|
|
||||||
int a = (int)(180 + 60 * glow);
|
|
||||||
ImU32 fillCol = IM_COL32(r, g, b, a);
|
|
||||||
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
|
|
||||||
// Bright border
|
|
||||||
dl->AddRect(cMin, cMax, WithAlpha(Primary(), (int)(160 + 60 * glow)), rounding, 0, schema::UI().drawElement("tabs.mining", "active-cell-border-thickness").size);
|
|
||||||
} else if (active) {
|
|
||||||
// Active but not mining: solid primary fill
|
|
||||||
ImU32 pri = Primary();
|
|
||||||
int priR = (pri >> 0) & 0xFF;
|
|
||||||
int priG = (pri >> 8) & 0xFF;
|
|
||||||
int priB = (pri >> 16) & 0xFF;
|
|
||||||
ImU32 fillCol = hovered
|
|
||||||
? IM_COL32(priR, priG, priB, 220)
|
|
||||||
: IM_COL32(priR, priG, priB, 180);
|
|
||||||
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
|
|
||||||
dl->AddRect(cMin, cMax, IM_COL32(priR, priG, priB, 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size);
|
|
||||||
} else {
|
|
||||||
// Inactive: dim outline
|
|
||||||
ImU32 fillCol = hovered
|
|
||||||
? WithAlpha(OnSurface(), 25)
|
|
||||||
: WithAlpha(OnSurface(), 8);
|
|
||||||
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
|
|
||||||
dl->AddRect(cMin, cMax, WithAlpha(OnSurface(), hovered ? 80 : 35), rounding);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Thread number label (centered)
|
|
||||||
snprintf(buf, sizeof(buf), "%d", threadNum);
|
|
||||||
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
|
|
||||||
ImVec2 txtPos(cx + (cellSz - txtSz.x) * 0.5f, cy + (cellSz - txtSz.y) * 0.5f);
|
|
||||||
ImU32 txtCol = active ? schema::UI().resolveColor("var(--on-primary)", IM_COL32(255, 255, 255, 230)) : WithAlpha(OnSurface(), 80);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, txtPos, txtCol, buf);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (threads_changed) {
|
|
||||||
app->settings()->setPoolThreads(s_selected_threads);
|
|
||||||
app->settings()->save();
|
|
||||||
if (mining.generate) {
|
|
||||||
app->startMining(s_selected_threads);
|
|
||||||
}
|
|
||||||
if (s_pool_mode && state.pool_mining.xmrig_running) {
|
|
||||||
app->stopPoolMining();
|
|
||||||
app->startPoolMining(s_selected_threads);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
curY += gridH + secGap;
|
|
||||||
}
|
|
||||||
|
|
||||||
// ============================================================
|
|
||||||
// Large square mining button — right of the controls card
|
|
||||||
// ============================================================
|
|
||||||
{
|
|
||||||
float btnX = cardMin.x + controlsW + miningBtnGap;
|
|
||||||
float btnY = cardMin.y;
|
|
||||||
ImVec2 bMin(btnX, btnY);
|
|
||||||
ImVec2 bMax(btnX + miningBtnSz, btnY + cardH);
|
|
||||||
|
|
||||||
bool btnHovered = material::IsRectHovered(bMin, bMax);
|
|
||||||
bool btnClicked = btnHovered && ImGui::IsMouseClicked(0);
|
|
||||||
bool isSyncing = state.sync.syncing;
|
|
||||||
bool poolBlockedBySolo = s_pool_mode && mining.generate && !state.pool_mining.xmrig_running;
|
|
||||||
bool isToggling = app->isMiningToggleInProgress();
|
|
||||||
// Pool mining connects to an external pool via xmrig — it does not
|
|
||||||
// need the local blockchain synced or even the daemon connected.
|
|
||||||
// If pool mining is still shutting down after switching to solo,
|
|
||||||
// keep the button enabled so user can stop it.
|
|
||||||
bool poolStillRunning = !s_pool_mode && state.pool_mining.xmrig_running;
|
|
||||||
bool disabled = s_pool_mode
|
|
||||||
? (isToggling || poolBlockedBySolo)
|
|
||||||
: (poolStillRunning ? false : (!app->isConnected() || isToggling || isSyncing));
|
|
||||||
|
|
||||||
// Glass panel background with state-dependent tint
|
|
||||||
GlassPanelSpec btnGlass;
|
|
||||||
btnGlass.rounding = Layout::glassRounding();
|
|
||||||
if (isToggling) {
|
|
||||||
// Toggling: subtle pulsing glow to indicate activity
|
|
||||||
float pulse = effects::isLowSpecMode()
|
|
||||||
? 0.5f
|
|
||||||
: 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 4.0);
|
|
||||||
int glowA = (int)(15 + 25 * pulse);
|
|
||||||
btnGlass.fillAlpha = glowA;
|
|
||||||
} else if (isMiningActive) {
|
|
||||||
// Active mining: warm glow
|
|
||||||
float pulse = effects::isLowSpecMode()
|
|
||||||
? schema::UI().drawElement("animations", "pulse-base-glow").size
|
|
||||||
: schema::UI().drawElement("animations", "pulse-base-glow").size + schema::UI().drawElement("animations", "pulse-amp-glow").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-slow").size);
|
|
||||||
int glowA = (int)(20 + 30 * pulse);
|
|
||||||
btnGlass.fillAlpha = glowA;
|
|
||||||
} else {
|
|
||||||
btnGlass.fillAlpha = btnHovered ? 30 : 18;
|
|
||||||
}
|
|
||||||
DrawGlassPanel(dl, bMin, bMax, btnGlass);
|
|
||||||
|
|
||||||
// Hover highlight
|
|
||||||
if (btnHovered && !disabled) {
|
|
||||||
dl->AddRectFilled(bMin, bMax,
|
|
||||||
isMiningActive ? WithAlpha(Error(), 25) : WithAlpha(Success(), 20),
|
|
||||||
btnGlass.rounding);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw mining icon centered in button — Material Design ICON_MD_CONSTRUCTION
|
|
||||||
{
|
|
||||||
float btnW = bMax.x - bMin.x;
|
|
||||||
float btnH = bMax.y - bMin.y;
|
|
||||||
float cx = bMin.x + btnW * 0.5f;
|
|
||||||
float cy = bMin.y + btnH * schema::UI().drawElement("tabs.mining", "button-icon-y-ratio").size; // shift up to leave room for label
|
|
||||||
|
|
||||||
if (isToggling) {
|
|
||||||
// Draw a spinning arc spinner to indicate progress
|
|
||||||
float spinnerR = std::min(btnW, btnH) * 0.18f;
|
|
||||||
float thickness = std::max(2.5f, spinnerR * 0.15f);
|
|
||||||
float time = (float)ImGui::GetTime();
|
|
||||||
|
|
||||||
// Track circle (faint)
|
|
||||||
ImU32 trackCol = WithAlpha(Primary(), 40);
|
|
||||||
dl->AddCircle(ImVec2(cx, cy), spinnerR, trackCol, 0, thickness);
|
|
||||||
|
|
||||||
// Animated arc
|
|
||||||
float rotation = fmodf(time * 2.0f * IM_PI / 1.4f, IM_PI * 2.0f);
|
|
||||||
float cycleTime = fmodf(time, 1.333f);
|
|
||||||
float arcLength = (cycleTime < 0.666f)
|
|
||||||
? (cycleTime / 0.666f) * 0.75f + 0.1f
|
|
||||||
: ((1.333f - cycleTime) / 0.666f) * 0.75f + 0.1f;
|
|
||||||
|
|
||||||
float startAngle = rotation - IM_PI * 0.5f;
|
|
||||||
float endAngle = startAngle + IM_PI * 2.0f * arcLength;
|
|
||||||
int segments = (int)(32 * arcLength) + 1;
|
|
||||||
float angleStep = (endAngle - startAngle) / segments;
|
|
||||||
ImU32 arcCol = Primary();
|
|
||||||
|
|
||||||
for (int i = 0; i < segments; i++) {
|
|
||||||
float a1 = startAngle + angleStep * i;
|
|
||||||
float a2 = startAngle + angleStep * (i + 1);
|
|
||||||
ImVec2 p1(cx + cosf(a1) * spinnerR, cy + sinf(a1) * spinnerR);
|
|
||||||
ImVec2 p2(cx + cosf(a2) * spinnerR, cy + sinf(a2) * spinnerR);
|
|
||||||
dl->AddLine(p1, p2, arcCol, thickness);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ImU32 iconCol;
|
|
||||||
if (disabled) {
|
|
||||||
iconCol = OnSurfaceDisabled();
|
|
||||||
} else if (isMiningActive) {
|
|
||||||
float pulse = effects::isLowSpecMode()
|
|
||||||
? schema::UI().drawElement("animations", "pulse-base-normal").size
|
|
||||||
: schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size);
|
|
||||||
iconCol = WithAlpha(Error(), (int)(200 + 55 * pulse));
|
|
||||||
} else {
|
|
||||||
iconCol = btnHovered ? WithAlpha(Success(), 255) : OnSurfaceMedium();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use XL icon for the large mining button
|
|
||||||
ImFont* iconFont = Type().iconXL();
|
|
||||||
const char* mineIcon = isMiningActive ? ICON_MD_CLOSE : ICON_MD_CONSTRUCTION;
|
|
||||||
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, mineIcon);
|
|
||||||
dl->AddText(iconFont, iconFont->LegacySize,
|
|
||||||
ImVec2(cx - iconSz.x * 0.5f, cy - iconSz.y * 0.5f), iconCol, mineIcon);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Label below icon
|
|
||||||
{
|
|
||||||
float btnW = bMax.x - bMin.x;
|
|
||||||
float btnH = bMax.y - bMin.y;
|
|
||||||
const char* label;
|
|
||||||
ImU32 lblCol;
|
|
||||||
if (isToggling) {
|
|
||||||
label = isMiningActive ? TR("mining_stopping") : TR("mining_starting");
|
|
||||||
// Animated dots effect via alpha pulse
|
|
||||||
float pulse = effects::isLowSpecMode()
|
|
||||||
? 0.7f
|
|
||||||
: 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 3.0);
|
|
||||||
lblCol = WithAlpha(Primary(), (int)(120 + 135 * pulse));
|
|
||||||
} else if (isMiningActive) {
|
|
||||||
label = TR("mining_stop");
|
|
||||||
lblCol = WithAlpha(Error(), 220);
|
|
||||||
} else if (disabled) {
|
|
||||||
label = TR("mining_mine");
|
|
||||||
lblCol = WithAlpha(OnSurface(), 50);
|
|
||||||
} else {
|
|
||||||
label = TR("mining_mine");
|
|
||||||
lblCol = WithAlpha(OnSurface(), 160);
|
|
||||||
}
|
|
||||||
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
|
|
||||||
float lblX = bMin.x + (btnW - lblSz.x) * 0.5f;
|
|
||||||
float lblY = bMin.y + btnH * schema::UI().drawElement("tabs.mining", "button-label-y-ratio").size;
|
|
||||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lblX, lblY), lblCol, label);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tooltip + pointer cursor
|
|
||||||
if (btnHovered) {
|
|
||||||
if (!disabled)
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
if (isToggling)
|
|
||||||
ImGui::SetTooltip("%s", isMiningActive ? TR("mining_stopping_tooltip") : TR("mining_starting_tooltip"));
|
|
||||||
else if (isSyncing && !s_pool_mode)
|
|
||||||
ImGui::SetTooltip(TR("mining_syncing_tooltip"), state.sync.verification_progress * 100.0);
|
|
||||||
else if (poolBlockedBySolo)
|
|
||||||
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
|
|
||||||
else
|
|
||||||
ImGui::SetTooltip("%s", isMiningActive ? TR("stop_mining") : TR("start_mining"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click action — pool or solo
|
|
||||||
if (btnClicked && !disabled) {
|
|
||||||
if (s_pool_mode) {
|
|
||||||
if (state.pool_mining.xmrig_running)
|
|
||||||
app->stopPoolMining();
|
|
||||||
else
|
|
||||||
app->startPoolMining(s_selected_threads);
|
|
||||||
} else {
|
|
||||||
// If pool mining is still running (user just switched from pool to solo),
|
|
||||||
// stop pool mining first
|
|
||||||
if (state.pool_mining.xmrig_running)
|
|
||||||
app->stopPoolMining();
|
|
||||||
else if (mining.generate)
|
|
||||||
app->stopMining();
|
|
||||||
else
|
|
||||||
app->startMining(s_selected_threads);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
|
||||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
|
||||||
ImGui::Dummy(ImVec2(0, gap));
|
|
||||||
}
|
|
||||||
|
|
||||||
// (The miner download/update control lives in the mining-control header row, next to the
|
// (The miner download/update control lives in the mining-control header row, next to the
|
||||||
// benchmark button — see the "Miner update" block above.)
|
// benchmark button — see the "Miner update" block above.)
|
||||||
|
|||||||
Reference in New Issue
Block a user