// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "mining_tab.h" #include "mining_benchmark.h" #include "mining_tab_helpers.h" #include "mining_pool_panel.h" #include "mining_earnings.h" #include "mining_stats.h" #include "mining_controls.h" #include "mining_mode_toggle.h" #include "xmrig_download_dialog.h" #include "../../util/xmrig_updater.h" #include "../../app.h" #include "../../util/i18n.h" #include "../../config/version.h" #include "../../data/wallet_state.h" #include "../../config/settings.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 "../effects/low_spec.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; // Local UI state for thread grid static int s_selected_threads = 0; static bool s_threads_initialized = false; // Drag-to-select state static bool s_drag_active = false; static int s_drag_anchor_thread = 0; // thread# where drag started static ThreadBenchmark s_benchmark; bool IsMiningBenchmarkActive() { return s_benchmark.active(); } // Miner-update version check (one shot per session): fetches the latest DRG-XMRig release tag in // the background so the "Update" button can show it. Network call to the project Gitea, started // the first time the pool section is shown. static util::XmrigUpdater s_xmrig_version_check; static bool s_xmrig_check_started = false; static std::string s_xmrig_latest_tag; // Pool mode state static bool s_pool_mode = false; static char s_pool_url[256] = "pool.dragonx.is:3433"; static char s_pool_worker[256] = "x"; static bool s_pool_settings_dirty = false; static bool s_pool_state_loaded = false; static void RenderMiningTabContent(App* app); void RenderMiningTab(App* app) { // Scrollable child to contain all content within available space ImVec2 miningAvail = ImGui::GetContentRegionAvail(); ImGui::BeginChild("##MiningScroll", miningAvail, false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar); ImGuiErrorRecoveryState erState; ImGui::ErrorRecoveryStoreState(&erState); try { RenderMiningTabContent(app); } catch (const std::exception& e) { DEBUG_LOGF("[MiningTab] Exception: %s\n", e.what()); ImGui::ErrorRecoveryTryToRecoverState(&erState); } catch (...) { DEBUG_LOGF("[MiningTab] Unknown exception\n"); ImGui::ErrorRecoveryTryToRecoverState(&erState); } ImGui::EndChild(); // ##MiningScroll } static void RenderMiningTabContent(App* app) { auto& S = schema::UI(); ImVec2 miningAvail = ImGui::GetContentRegionAvail(); auto sliderInput = S.input("tabs.mining", "thread-slider"); auto startBtn = S.button("tabs.mining", "start-button"); auto lbl = S.label("tabs.mining", "label-column"); const auto& state = app->getWalletState(); const auto& mining = state.mining; // Responsive: scale factors per frame float availWidth = ImGui::GetContentRegionAvail().x; float hs = Layout::hScale(availWidth); float vs = Layout::vScale(miningAvail.y); float pad = Layout::cardInnerPadding(); float gap = Layout::cardGap(); const float dp = Layout::dpiScale(); auto tier = Layout::currentTier(availWidth, miningAvail.y); (void)tier; int max_threads = GetMaxMiningThreads(); if (!s_threads_initialized) { int saved = app->settings()->getPoolThreads(); if (mining.generate) s_selected_threads = ClampMiningThreads(mining.genproclimit, max_threads); else if (saved > 0) s_selected_threads = ClampMiningThreads(saved, max_threads); else s_selected_threads = 1; s_threads_initialized = true; } // Sync thread grid with actual count when idle thread scaling adjusts threads // Skip during benchmark — the benchmark controls thread counts directly if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active && !IsMiningBenchmarkActive()) { if (s_pool_mode && state.pool_mining.xmrig_running) { // Use the requested thread count (available immediately) rather // than threads_active from the xmrig API which lags during restarts. int reqThreads = app->getXmrigRequestedThreads(); if (reqThreads > 0) s_selected_threads = ClampMiningThreads(reqThreads, max_threads); else if (state.pool_mining.threads_active > 0) s_selected_threads = ClampMiningThreads(state.pool_mining.threads_active, max_threads); } else if (mining.generate && mining.genproclimit > 0) { s_selected_threads = ClampMiningThreads(mining.genproclimit, max_threads); } } ImDrawList* dl = ImGui::GetWindowDrawList(); GlassPanelSpec glassSpec; glassSpec.rounding = Layout::glassRounding(); ImFont* ovFont = Type().overline(); ImFont* capFont = Type().caption(); ImFont* sub1 = Type().subtitle1(); if (!ovFont || !capFont || !sub1) { return; } char buf[128]; // Load pool state from settings on first frame if (!s_pool_state_loaded) { s_pool_mode = app->settings()->getPoolMode(); strncpy(s_pool_url, app->settings()->getPoolUrl().c_str(), sizeof(s_pool_url) - 1); strncpy(s_pool_worker, app->settings()->getPoolWorker().c_str(), sizeof(s_pool_worker) - 1); s_pool_state_loaded = true; } // Kick off a one-shot miner-version check the first time the pool section is shown, then cache // the latest tag once it arrives (so the "Update" button can display it). Network call. if (s_pool_mode && !s_xmrig_check_started) { s_xmrig_version_check.startCheck(app->settings()->getXmrigVersion()); s_xmrig_check_started = true; } if (s_xmrig_check_started && s_xmrig_latest_tag.empty()) { const auto _vp = s_xmrig_version_check.getProgress(); if (!_vp.latest_tag.empty()) s_xmrig_latest_tag = _vp.latest_tag; } if (!app->supportsSoloMining() && !s_pool_mode) { s_pool_mode = true; app->settings()->setPoolMode(true); app->settings()->save(); } // Default pool worker to user's first shielded (z) address once available. // For new wallets without a z-address, leave the field blank so the user // is prompted to generate one before mining. { static bool s_pool_worker_defaulted = false; std::string workerStr(s_pool_worker); if (shouldDefaultPoolWorker(workerStr, s_pool_worker_defaulted) && !state.addresses.empty()) { std::string defaultAddr = defaultPoolWorkerAddress(state.addresses); if (!defaultAddr.empty()) { strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1); s_pool_worker[sizeof(s_pool_worker) - 1] = '\0'; s_pool_settings_dirty = true; } else { // No z-address yet — clear the placeholder "x" so field shows empty s_pool_worker[0] = '\0'; s_pool_settings_dirty = true; } s_pool_worker_defaulted = true; } } // Persist pool settings when dirty and no field is active if (s_pool_settings_dirty && !ImGui::IsAnyItemActive()) { app->settings()->setPoolUrl(s_pool_url); app->settings()->setPoolWorker(s_pool_worker); app->settings()->save(); s_pool_settings_dirty = false; // Auto-restart pool miner if it is currently running so the new // URL / worker address takes effect immediately. if (state.pool_mining.xmrig_running) { app->stopPoolMining(); app->startPoolMining(s_selected_threads); } } // Determine active mining state for UI // Include pool mining running state even when user just switched to solo, // so the button shows STOP/STOPPING while xmrig shuts down. bool isMiningActive = IsPoolMiningActive(s_pool_mode, state.pool_mining.xmrig_running, mining.generate); // ================================================================ // Thread Benchmark state machine — runs pool mining at each candidate // thread count to find the optimal setting for this CPU. // ================================================================ if (s_benchmark.active()) { auto benchmarkUpdate = AdvanceThreadBenchmark( s_benchmark, ImGui::GetIO().DeltaTime, state.pool_mining.hashrate_10s); if (benchmarkUpdate.stopPoolMining) { app->stopPoolMining(); } if (benchmarkUpdate.saveOptimalThreads) { s_selected_threads = benchmarkUpdate.optimalThreads; app->settings()->setPoolThreads(s_selected_threads); app->settings()->save(); } if (benchmarkUpdate.startPoolMining) { app->startPoolMining(benchmarkUpdate.startThreads); } } // ================================================================ // Proportional section budget — ensures all content fits without // scrolling at the minimum window size (1024×775). // ================================================================ float sHdr = ovFont->LegacySize + Layout::spacingXs() + ImGui::GetStyle().ItemSpacing.y * 2.0f; float gapOver = gap + ImGui::GetStyle().ItemSpacing.y; // 3 sections with headers (CHART+STATS, DETAILS+EARNINGS, BLOCKS) float totalOverhead = 3.0f * (sHdr + gapOver) + 1.0f * gapOver; float cardBudget = std::max(schema::UI().drawElement("tabs.mining", "card-budget-min").size, miningAvail.y - totalOverhead); Layout::SectionBudget cb(cardBudget); float controlsBudgetH = cb.allocate(0.26f, 80.0f * dp); float chartBudgetH = cb.allocate(0.22f, 60.0f * dp); (void)cb; // remaining budget used by combined earnings+details card // ================================================================ // MODE TOGGLE — SOLO | POOL segmented control // ================================================================ RenderMiningModeToggle(app, state, mining, dl, capFont, ovFont, dp, hs, gap, availWidth, s_pool_mode, s_pool_url, s_pool_worker, s_pool_settings_dirty); // ================================================================ // CONTROLS — Glass card with CPU core grid (no heading) // ================================================================ 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.) // ================================================================ // HASHRATE + STATS — Combined glass card: stat values on top, chart below // (Or full-card log view when toggled in pool mode) // ================================================================ RenderMiningStats(state, mining, dl, capFont, sub1, ovFont, dp, vs, gap, pad, availWidth, glassSpec, chartBudgetH, s_pool_mode); // ================================================================ // EARNINGS — Horizontal row card (Today | Yesterday | All Time | Est. Daily) // ================================================================ RenderMiningEarnings(app, state, mining, dl, capFont, sub1, ovFont, dp, vs, gap, pad, isMiningActive, s_pool_mode, availWidth, glassSpec, sHdr, gapOver); // ================================================================ // POOL CONNECTION STATUS — inline indicator (pool mode, no log) // ================================================================ if (s_pool_mode && state.pool_mining.log_lines.empty() && state.pool_mining.xmrig_running) { ImFont* iconFont = Type().iconSmall(); const char* dotIcon = ICON_MD_CIRCLE; ImU32 dotCol = state.pool_mining.connected ? WithAlpha(Success(), 200) : WithAlpha(Error(), 200); const char* statusText = state.pool_mining.connected ? (state.pool_mining.pool_url.empty() ? TR("mining_connected") : state.pool_mining.pool_url.c_str()) : TR("mining_connecting"); ImVec2 pos = ImGui::GetCursorScreenPos(); ImVec2 dotSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, dotIcon); dl->AddText(iconFont, iconFont->LegacySize, pos, dotCol, dotIcon); dl->AddText(capFont, capFont->LegacySize, ImVec2(pos.x + dotSz.x + 4 * dp, pos.y + (dotSz.y - capFont->LegacySize) * 0.5f), OnSurfaceMedium(), statusText); ImGui::Dummy(ImVec2(availWidth, capFont->LegacySize + 4 * dp)); } } } // namespace ui } // namespace dragonx