diff --git a/CMakeLists.txt b/CMakeLists.txt index 3dfe823..b992e78 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -444,6 +444,7 @@ set(APP_SOURCES src/ui/windows/mining_tab.cpp src/ui/windows/mining_earnings.cpp src/ui/windows/mining_stats.cpp + src/ui/windows/mining_controls.cpp src/ui/windows/mining_benchmark.cpp src/ui/windows/mining_pool_panel.cpp src/ui/windows/mining_tab_helpers.cpp diff --git a/src/ui/windows/mining_controls.cpp b/src/ui/windows/mining_controls.cpp new file mode 100644 index 0000000..692d2dc --- /dev/null +++ b/src/ui/windows/mining_controls.cpp @@ -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 +#include +#include +#include +#include + +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 " 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 diff --git a/src/ui/windows/mining_controls.h b/src/ui/windows/mining_controls.h new file mode 100644 index 0000000..30e48ed --- /dev/null +++ b/src/ui/windows/mining_controls.h @@ -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 + +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 diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index d25ad5f..b48adae 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -8,6 +8,7 @@ #include "mining_pool_panel.h" #include "mining_earnings.h" #include "mining_stats.h" +#include "mining_controls.h" #include "xmrig_download_dialog.h" #include "../../util/xmrig_updater.h" #include "../../app.h" @@ -792,852 +793,10 @@ static void RenderMiningTabContent(App* app) // ================================================================ // CONTROLS — Glass card with CPU core grid (no heading) // ================================================================ - { - // Mining button beside the controls card - 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 " 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)); - } + RenderMiningControls(app, state, mining, dl, capFont, sub1, ovFont, dp, hs, vs, gap, pad, + availWidth, glassSpec, controlsBudgetH, max_threads, isMiningActive, + s_pool_mode, s_pool_worker, s_xmrig_latest_tag, s_benchmark, + s_selected_threads, s_drag_active, s_drag_anchor_thread); // (The miner download/update control lives in the mining-control header row, next to the // benchmark button — see the "Miner update" block above.)