// 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 "../../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 // Earnings filter: 0 = All, 1 = Solo, 2 = Pool static int s_earnings_filter = 0; static ThreadBenchmark s_benchmark; bool IsMiningBenchmarkActive() { return s_benchmark.active(); } // 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 bool s_show_pool_log = false; // Toggle: false=chart, true=log static bool s_show_solo_log = false; // Toggle: false=chart, true=log (solo mode) 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; } // 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 // ================================================================ { float toggleW = schema::UI().drawElement("tabs.mining", "mode-toggle-width").size * hs; float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size; float toggleRnd = schema::UI().drawElement("tabs.mining", "mode-toggle-rounding").size; float totalW = toggleW * 2; ImVec2 tMin = ImGui::GetCursorScreenPos(); ImVec2 tMax(tMin.x + totalW, tMin.y + toggleH); // Glass background for the segmented control dl->AddRectFilled(tMin, tMax, WithAlpha(OnSurface(), 15), toggleRnd); dl->AddRect(tMin, tMax, WithAlpha(OnSurface(), 40), toggleRnd); // SOLO button (left half) ImVec2 soloMin = tMin; ImVec2 soloMax(tMin.x + toggleW, tMax.y); bool soloHov = material::IsRectHovered(soloMin, soloMax); if (!s_pool_mode) { dl->AddRectFilled(soloMin, soloMax, WithAlpha(Primary(), 180), toggleRnd); } else if (soloHov) { dl->AddRectFilled(soloMin, soloMax, WithAlpha(OnSurface(), 20), toggleRnd); } { const char* label = TR("mining_solo"); ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label); float lx = soloMin.x + (toggleW - sz.x) * 0.5f; float ly = soloMin.y + (toggleH - sz.y) * 0.5f; ImU32 col = !s_pool_mode ? IM_COL32(255, 255, 255, 230) : OnSurfaceMedium(); dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), col, label); } // POOL button (right half) — disabled when solo mining is active bool soloMiningActive = mining.generate; ImVec2 poolMin(tMin.x + toggleW, tMin.y); ImVec2 poolMax = tMax; bool poolHov = material::IsRectHovered(poolMin, poolMax); if (s_pool_mode) { dl->AddRectFilled(poolMin, poolMax, WithAlpha(Primary(), 180), toggleRnd); } else if (soloMiningActive) { // Dimmed — solo mining blocks pool mode } else if (poolHov) { dl->AddRectFilled(poolMin, poolMax, WithAlpha(OnSurface(), 20), toggleRnd); } { const char* label = TR("mining_pool"); ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label); float lx = poolMin.x + (toggleW - sz.x) * 0.5f; float ly = poolMin.y + (toggleH - sz.y) * 0.5f; ImU32 col = s_pool_mode ? IM_COL32(255, 255, 255, 230) : (soloMiningActive ? OnSurfaceDisabled() : OnSurfaceMedium()); dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), col, label); } // Invisible buttons for click targets ImGui::SetCursorScreenPos(soloMin); ImGui::InvisibleButton("##SoloMode", ImVec2(toggleW, toggleH)); if (ImGui::IsItemClicked() && s_pool_mode) { s_pool_mode = false; app->settings()->setPoolMode(false); app->settings()->save(); app->stopPoolMining(); } if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetCursorScreenPos(poolMin); ImGui::InvisibleButton("##PoolMode", ImVec2(toggleW, toggleH)); if (ImGui::IsItemClicked() && !s_pool_mode && !soloMiningActive) { s_pool_mode = true; app->settings()->setPoolMode(true); app->settings()->save(); // Note: soloMiningActive is already false (checked above), // so no need to call stopMining() — it would just set the // toggle-in-progress flag and make the button show "STARTING". } if (poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (poolHov && soloMiningActive && !s_pool_mode) { ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool")); } ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y)); ImGui::Dummy(ImVec2(totalW, 0)); // Pool URL + Worker inputs inline next to toggle (pool mode only) if (s_pool_mode && soloMiningActive) { // Solo mining is active — show disabled message instead of inputs float inputFrameH = ImGui::GetFrameHeight(); float vertOff = (toggleH - inputFrameH) * 0.5f; ImGui::SetCursorScreenPos(ImVec2(tMax.x + Layout::spacingLg(), tMin.y + vertOff)); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning())); ImGui::AlignTextToFramePadding(); ImGui::PushFont(Type().iconSmall()); ImGui::TextUnformatted(ICON_MD_INFO); ImGui::PopFont(); ImGui::SameLine(0, Layout::spacingSm()); ImGui::PushFont(capFont); ImGui::TextUnformatted(TR("mining_stop_solo_for_pool_settings")); ImGui::PopFont(); ImGui::PopStyleColor(); } else if (s_pool_mode) { // Position inputs to the right of the toggle float inputFrameH = ImGui::GetFrameHeight(); float vertOff = (toggleH - inputFrameH) * 0.5f; float inputsStartX = tMax.x + Layout::spacingLg(); ImGui::SetCursorScreenPos(ImVec2(inputsStartX, tMin.y + vertOff)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8 * dp, 4 * dp)); float inputFrameH2 = ImGui::GetFrameHeight(); float iconBtnW = inputFrameH2; float resetBtnW = iconBtnW; float contentEndX = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x; // Each input group: [input][▼][bookmark] // Layout: [URL group] [spacing] [Worker group] [spacing] [reset] float perGroupExtra = iconBtnW * 2; // dropdown + bookmark float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW - Layout::spacingSm() - perGroupExtra * 2; float urlW = std::max(60.0f, remainW * 0.30f); float wrkW = std::max(40.0f, remainW * 0.70f); // Track positions for popup alignment float urlGroupStartX = ImGui::GetCursorScreenPos().x; float urlGroupStartY = ImGui::GetCursorScreenPos().y; float urlGroupW = urlW + perGroupExtra; // === Pool URL input === ImGui::SetNextItemWidth(urlW); if (ImGui::InputTextWithHint("##PoolURL", TR("mining_pool_url"), s_pool_url, sizeof(s_pool_url))) { s_pool_settings_dirty = true; } // --- URL: Dropdown arrow button --- ImGui::SameLine(0, 0); { ImVec2 btnPos = ImGui::GetCursorScreenPos(); ImVec2 btnSize(iconBtnW, inputFrameH2); ImGui::InvisibleButton("##PoolDropdown", btnSize); bool btnHov = ImGui::IsItemHovered(); bool btnClk = ImGui::IsItemClicked(); ImDrawList* dl2 = ImGui::GetWindowDrawList(); ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); if (btnHov) { dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), StateHover(), 4.0f * dp); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("%s", TR("mining_saved_pools")); } ImFont* icoFont = Type().iconSmall(); const char* dropIcon = ICON_MD_ARROW_DROP_DOWN; ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon); dl2->AddText(icoFont, icoFont->LegacySize, ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f), OnSurfaceMedium(), dropIcon); if (btnClk) { ImGui::OpenPopup("##SavedPoolsPopup"); } } // --- URL: Bookmark button --- ImGui::SameLine(0, 0); { ImVec2 btnPos = ImGui::GetCursorScreenPos(); ImVec2 btnSize(iconBtnW, inputFrameH2); ImGui::InvisibleButton("##SavePoolUrl", btnSize); bool btnHov = ImGui::IsItemHovered(); bool btnClk = ImGui::IsItemClicked(); ImDrawList* dl2 = ImGui::GetWindowDrawList(); ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); std::string currentUrl(s_pool_url); bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolUrls(), currentUrl); if (btnHov) { dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), StateHover(), 4.0f * dp); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url")); } ImFont* icoFont = Type().iconSmall(); const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER; ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon); dl2->AddText(icoFont, icoFont->LegacySize, ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f), alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon); if (btnClk && !currentUrl.empty() && !alreadySaved) { app->settings()->addSavedPoolUrl(currentUrl); app->settings()->save(); } } // --- URL: Popup positioned below the input group --- // Match popup width to input group; zero horizontal padding so // item highlights are flush with the popup container edges. ImGui::SetNextWindowPos(ImVec2(urlGroupStartX, urlGroupStartY + inputFrameH2)); ImGui::SetNextWindowSize(ImVec2(urlGroupW, 0)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); if (ImGui::BeginPopup("##SavedPoolsPopup")) { const auto& savedUrls = app->settings()->getSavedPoolUrls(); if (savedUrls.empty()) { ImGui::SetCursorPosX(8 * dp); ImGui::PushFont(Type().caption()); ImGui::TextDisabled("%s", TR("mining_no_saved_pools")); ImGui::PopFont(); ImGui::SetCursorPosX(8 * dp); ImGui::PushFont(Type().caption()); ImGui::TextDisabled("%s", TR("mining_click")); ImGui::PopFont(); ImGui::SameLine(0, 2 * dp); ImGui::PushFont(Type().iconSmall()); ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER); ImGui::PopFont(); ImGui::SameLine(0, 2 * dp); ImGui::PushFont(Type().caption()); ImGui::TextDisabled("%s", TR("mining_to_save")); ImGui::PopFont(); } else { std::string urlToRemove; float popupInnerW = ImGui::GetContentRegionAvail().x; float xZoneW = ImGui::GetFrameHeight(); float textPadX = 8 * dp; ImFont* rowFont = ImGui::GetFont(); float rowFontSz = ImGui::GetFontSize(); float rowH = ImGui::GetFrameHeight(); for (const auto& url : savedUrls) { ImGui::PushID(url.c_str()); bool isCurrent = (std::string(s_pool_url) == url); ImVec2 rowMin = ImGui::GetCursorScreenPos(); ImVec2 rowMax(rowMin.x + popupInnerW, rowMin.y + rowH); ImGui::InvisibleButton("##row", ImVec2(popupInnerW, rowH)); bool rowHov = ImGui::IsItemHovered(); bool rowClk = ImGui::IsItemClicked(); ImDrawList* pdl = ImGui::GetWindowDrawList(); bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - xZoneW; // Row background — flush with popup edges if (isCurrent) pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10)); if (rowHov && !inXZone) pdl->AddRectFilled(rowMin, rowMax, StateHover()); // Item text with internal padding float textY = rowMin.y + (rowH - rowFontSz) * 0.5f; pdl->AddText(rowFont, rowFontSz, ImVec2(rowMin.x + textPadX, textY), isCurrent ? Primary() : OnSurface(), url.c_str()); // X button — flush with right edge, icon centered { ImVec2 xMin(rowMax.x - xZoneW, rowMin.y); ImVec2 xMax(rowMax.x, rowMax.y); if (inXZone) { pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30)); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("%s", TR("mining_remove")); } else if (rowHov) { // Show faint X when row is hovered ImFont* icoF = Type().iconSmall(); const char* xIcon = ICON_MD_CLOSE; ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); pdl->AddText(icoF, icoF->LegacySize, ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), OnSurfaceDisabled(), xIcon); } // Always draw icon when hovering X zone if (inXZone) { ImFont* icoF = Type().iconSmall(); const char* xIcon = ICON_MD_CLOSE; ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); pdl->AddText(icoF, icoF->LegacySize, ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), Error(), xIcon); } } // Click handling if (rowClk) { if (inXZone) { urlToRemove = url; } else { strncpy(s_pool_url, url.c_str(), sizeof(s_pool_url) - 1); s_pool_url[sizeof(s_pool_url) - 1] = '\0'; s_pool_settings_dirty = true; ImGui::CloseCurrentPopup(); } } if (rowHov && !inXZone) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::PopID(); } if (!urlToRemove.empty()) { app->settings()->removeSavedPoolUrl(urlToRemove); app->settings()->save(); } } ImGui::EndPopup(); } ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for URL popup ImGui::SameLine(0, Layout::spacingSm()); float wrkGroupStartX = ImGui::GetCursorScreenPos().x; float wrkGroupStartY = ImGui::GetCursorScreenPos().y; float wrkGroupW = wrkW + perGroupExtra; ImGui::SetNextItemWidth(wrkW); if (ImGui::InputTextWithHint("##PoolWorker", TR("mining_payout_address"), s_pool_worker, sizeof(s_pool_worker))) { s_pool_settings_dirty = true; } if (ImGui::IsItemHovered()) { std::string currentWorkerStr(s_pool_worker); if (currentWorkerStr.empty()) { ImGui::SetTooltip("%s", TR("mining_generate_z_address_hint")); } else { ImGui::SetTooltip("%s", TR("mining_payout_tooltip")); } } // --- Worker: Dropdown arrow button --- ImGui::SameLine(0, 0); { ImVec2 btnPos = ImGui::GetCursorScreenPos(); ImVec2 btnSize(iconBtnW, inputFrameH2); ImGui::InvisibleButton("##WorkerDropdown", btnSize); bool btnHov = ImGui::IsItemHovered(); bool btnClk = ImGui::IsItemClicked(); ImDrawList* dl2 = ImGui::GetWindowDrawList(); ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); if (btnHov) { dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), StateHover(), 4.0f * dp); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("%s", TR("mining_saved_addresses")); } ImFont* icoFont = Type().iconSmall(); const char* dropIcon = ICON_MD_ARROW_DROP_DOWN; ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon); dl2->AddText(icoFont, icoFont->LegacySize, ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f), OnSurfaceMedium(), dropIcon); if (btnClk) { ImGui::OpenPopup("##SavedWorkersPopup"); } } // --- Worker: Bookmark button --- ImGui::SameLine(0, 0); { ImVec2 btnPos = ImGui::GetCursorScreenPos(); ImVec2 btnSize(iconBtnW, inputFrameH2); ImGui::InvisibleButton("##SaveWorkerAddr", btnSize); bool btnHov = ImGui::IsItemHovered(); bool btnClk = ImGui::IsItemClicked(); ImDrawList* dl2 = ImGui::GetWindowDrawList(); ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); std::string currentWorker(s_pool_worker); bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolWorkers(), currentWorker); if (btnHov) { dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), StateHover(), 4.0f * dp); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address")); } ImFont* icoFont = Type().iconSmall(); const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER; ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon); dl2->AddText(icoFont, icoFont->LegacySize, ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f), alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon); if (btnClk && !currentWorker.empty() && currentWorker != "x" && !alreadySaved) { app->settings()->addSavedPoolWorker(currentWorker); app->settings()->save(); } } // --- Worker: Popup positioned below the input group --- // Popup sized to fit full z-addresses without truncation; // zero horizontal padding so item highlights are flush with edges. float addrPopupW = std::max(wrkGroupW, availWidth * 0.55f); ImGui::SetNextWindowPos(ImVec2(wrkGroupStartX, wrkGroupStartY + inputFrameH2)); ImGui::SetNextWindowSize(ImVec2(addrPopupW, 0)); ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); if (ImGui::BeginPopup("##SavedWorkersPopup")) { const auto& savedWorkers = app->settings()->getSavedPoolWorkers(); if (savedWorkers.empty()) { ImGui::SetCursorPosX(8 * dp); ImGui::PushFont(Type().caption()); ImGui::TextDisabled("%s", TR("mining_no_saved_addresses")); ImGui::PopFont(); ImGui::SetCursorPosX(8 * dp); ImGui::PushFont(Type().caption()); ImGui::TextDisabled("%s", TR("mining_click")); ImGui::PopFont(); ImGui::SameLine(0, 2 * dp); ImGui::PushFont(Type().iconSmall()); ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER); ImGui::PopFont(); ImGui::SameLine(0, 2 * dp); ImGui::PushFont(Type().caption()); ImGui::TextDisabled("%s", TR("mining_to_save")); ImGui::PopFont(); } else { std::string addrToRemove; float wPopupInnerW = ImGui::GetContentRegionAvail().x; float wXZoneW = ImGui::GetFrameHeight(); float wTextPadX = 8 * dp; ImFont* wRowFont = ImGui::GetFont(); float wRowFontSz = ImGui::GetFontSize(); float wRowH = ImGui::GetFrameHeight(); for (const auto& addr : savedWorkers) { ImGui::PushID(addr.c_str()); bool isCurrent = (std::string(s_pool_worker) == addr); ImVec2 rowMin = ImGui::GetCursorScreenPos(); ImVec2 rowMax(rowMin.x + wPopupInnerW, rowMin.y + wRowH); ImGui::InvisibleButton("##row", ImVec2(wPopupInnerW, wRowH)); bool rowHov = ImGui::IsItemHovered(); bool rowClk = ImGui::IsItemClicked(); ImDrawList* pdl = ImGui::GetWindowDrawList(); bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - wXZoneW; // Row background — flush with popup edges if (isCurrent) pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10)); if (rowHov && !inXZone) pdl->AddRectFilled(rowMin, rowMax, StateHover()); // Full address text with internal padding float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f; pdl->AddText(wRowFont, wRowFontSz, ImVec2(rowMin.x + wTextPadX, textY), isCurrent ? Primary() : OnSurface(), addr.c_str()); // Tooltip for long addresses if (rowHov && !inXZone) ImGui::SetTooltip("%s", addr.c_str()); // X button — flush with right edge, icon centered { ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y); ImVec2 xMax(rowMax.x, rowMax.y); if (inXZone) { pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30)); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("%s", TR("mining_remove")); } else if (rowHov) { ImFont* icoF = Type().iconSmall(); const char* xIcon = ICON_MD_CLOSE; ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); pdl->AddText(icoF, icoF->LegacySize, ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), OnSurfaceDisabled(), xIcon); } if (inXZone) { ImFont* icoF = Type().iconSmall(); const char* xIcon = ICON_MD_CLOSE; ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon); ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f); pdl->AddText(icoF, icoF->LegacySize, ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f), Error(), xIcon); } } // Click handling if (rowClk) { if (inXZone) { addrToRemove = addr; } else { strncpy(s_pool_worker, addr.c_str(), sizeof(s_pool_worker) - 1); s_pool_worker[sizeof(s_pool_worker) - 1] = '\0'; s_pool_settings_dirty = true; ImGui::CloseCurrentPopup(); } } if (rowHov && !inXZone) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::PopID(); } if (!addrToRemove.empty()) { app->settings()->removeSavedPoolWorker(addrToRemove); app->settings()->save(); } } ImGui::EndPopup(); } ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for Worker popup // === Reset to defaults button === ImGui::SameLine(0, Layout::spacingSm()); { ImVec2 btnPos = ImGui::GetCursorScreenPos(); ImVec2 btnSize(inputFrameH2, inputFrameH2); ImGui::InvisibleButton("##ResetPoolDefaults", btnSize); bool btnHov = ImGui::IsItemHovered(); bool btnClk = ImGui::IsItemClicked(); ImDrawList* dl2 = ImGui::GetWindowDrawList(); ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f); // Hover highlight if (btnHov) { dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y), StateHover(), 4.0f * dp); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("%s", TR("mining_reset_defaults")); } // Icon ImFont* iconFont = Type().iconSmall(); const char* resetIcon = ICON_MD_REFRESH; ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, resetIcon); dl2->AddText(iconFont, iconFont->LegacySize, ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f), OnSurfaceMedium(), resetIcon); if (btnClk) { strncpy(s_pool_url, defaultPoolUrl(), sizeof(s_pool_url) - 1); // Default to user's first shielded (z) address for pool payouts. // Leave blank if no z-address exists yet. std::string defaultAddr = defaultPoolWorkerAddress(state.addresses); 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; } } ImGui::PopStyleVar(2); } // Ensure cursor Y is at toggle bottom regardless of pool input widgets, // so the cards below stay at the same position in both solo and pool modes. ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y)); ImGui::Dummy(ImVec2(0, gap * 0.5f)); } // ================================================================ // 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; } 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)); } // ================================================================ // HASHRATE + STATS — Combined glass card: stat values on top, chart below // (Or full-card log view when toggled in pool mode) // ================================================================ { ImU32 greenCol = Success(); // Determine view mode first bool showLogView = s_pool_mode ? (s_show_pool_log && !state.pool_mining.log_lines.empty()) : (s_show_solo_log && !mining.log_lines.empty()); bool hasLogContent = s_pool_mode ? !state.pool_mining.log_lines.empty() : !mining.log_lines.empty(); // Use pool hashrate history when in pool mode, solo otherwise const std::vector& chartHistory = s_pool_mode ? state.pool_mining.hashrate_history : mining.hashrate_history; bool hasChartContent = chartHistory.size() >= 2; // Stat row height (single line: overline + value) float statRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + Layout::spacingSm(); float totalCardH = statRowH + chartBudgetH + pad; ImVec2 cardMin = ImGui::GetCursorScreenPos(); ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + totalCardH); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); bool& showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log; if (showLogView) { // --- Full-card log view (selectable + copyable) --- const std::vector& logLines = s_pool_mode ? state.pool_mining.log_lines : mining.log_lines; // Build a single string buffer for InputTextMultiline static std::string s_log_buf; s_log_buf.clear(); for (const auto& line : logLines) { if (!line.empty()) { s_log_buf += line; s_log_buf += '\n'; } } float logPad = pad * 0.5f; float logW = availWidth - logPad * 2; float logH = totalCardH - logPad * 2; ImGui::SetCursorScreenPos(ImVec2(cardMin.x + logPad, cardMin.y + logPad)); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface())); ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0)); ImFont* monoFont = Type().body2(); ImGui::PushFont(monoFont); const char* inputId = s_pool_mode ? "##PoolLogText" : "##SoloLogText"; ImGui::InputTextMultiline(inputId, const_cast(s_log_buf.c_str()), s_log_buf.size() + 1, ImVec2(logW, logH), ImGuiInputTextFlags_ReadOnly); ImGui::PopFont(); ImGui::PopStyleColor(2); // Reset cursor to end of card after the input ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); // Register position with layout system } else { // --- Stats + Chart view --- // Pool vs Solo stats — different columns std::string col1Str, col2Str, col3Str, col4Str; const char* col1Label; const char* col2Label; const char* col3Label; const char* col4Label = nullptr; ImU32 col1Col, col2Col, col3Col, col4Col = OnSurface(); int numStats = 3; if (s_pool_mode) { col1Label = TR("mining_local_hashrate"); col1Str = FormatHashrate(state.pool_mining.hashrate_10s); col1Col = state.pool_mining.xmrig_running ? greenCol : OnSurfaceDisabled(); col2Label = TR("mining_pool_hashrate"); col2Str = FormatHashrate(state.pool_mining.pool_hashrate); col2Col = state.pool_mining.pool_hashrate > 0 ? OnSurface() : OnSurfaceDisabled(); col3Label = TR("mining_shares"); char sharesBuf[64]; snprintf(sharesBuf, sizeof(sharesBuf), "%lld / %lld", (long long)state.pool_mining.accepted, (long long)state.pool_mining.rejected); col3Str = sharesBuf; col3Col = OnSurface(); col4Label = TR("mining_uptime"); int64_t up = state.pool_mining.uptime_sec; char uptBuf[64]; if (up <= 0) snprintf(uptBuf, sizeof(uptBuf), "N/A"); else if (up < 3600) snprintf(uptBuf, sizeof(uptBuf), "%lldm %llds", (long long)(up / 60), (long long)(up % 60)); else snprintf(uptBuf, sizeof(uptBuf), "%lldh %lldm", (long long)(up / 3600), (long long)((up % 3600) / 60)); col4Str = uptBuf; col4Col = OnSurface(); numStats = 4; } else { double est_hours = EstimateHoursToBlock(mining.localHashrate, mining.networkHashrate, mining.difficulty); col1Label = TR("mining_local_hashrate"); col1Str = FormatHashrate(mining.localHashrate); col1Col = mining.generate ? greenCol : OnSurfaceDisabled(); col2Label = TR("mining_network"); col2Str = FormatHashrate(mining.networkHashrate); col2Col = OnSurface(); col3Label = TR("mining_est_block"); col3Str = FormatEstTime(est_hours); col3Col = OnSurface(); } // Draw stat values as inline columns at top of card { float statColW = (availWidth - pad * 2) / (float)numStats; float sy = cardMin.y + pad * 0.5f; struct StatEntry { const char* label; const char* value; ImU32 col; }; char c1Buf[64], c2Buf[64], c3Buf[64], c4Buf[64]; snprintf(c1Buf, sizeof(c1Buf), "%s", col1Str.c_str()); snprintf(c2Buf, sizeof(c2Buf), "%s", col2Str.c_str()); snprintf(c3Buf, sizeof(c3Buf), "%s", col3Str.c_str()); if (numStats > 3) snprintf(c4Buf, sizeof(c4Buf), "%s", col4Str.c_str()); StatEntry stats[] = { { col1Label, c1Buf, col1Col }, { col2Label, c2Buf, col2Col }, { col3Label, c3Buf, col3Col }, { col4Label ? col4Label : "", c4Buf, col4Col }, }; for (int si = 0; si < numStats; si++) { float sx = cardMin.x + pad + si * statColW; float centerX = sx + statColW * 0.5f; ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, stats[si].label); dl->AddText(ovFont, ovFont->LegacySize, ImVec2(centerX - lblSz.x * 0.5f, sy), OnSurfaceMedium(), stats[si].label); ImVec2 valSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, stats[si].value); float valY = sy + ovFont->LegacySize + Layout::spacingXs(); dl->AddText(sub1, sub1->LegacySize, ImVec2(centerX - valSz.x * 0.5f, valY), stats[si].col, stats[si].value); // Trend arrow for hashrate (first column only) if (si == 0 && chartHistory.size() >= 6) { size_t hn = chartHistory.size(); double recent = (chartHistory[hn-1] + chartHistory[hn-2] + chartHistory[hn-3]) / 3.0; double older = (chartHistory[hn-4] + chartHistory[hn-5] + chartHistory[hn-6]) / 3.0; ImFont* iconFont = Type().iconSmall(); float arrowX = centerX + valSz.x * 0.5f + Layout::spacingSm(); if (recent > older * 1.02) { const char* icon = ICON_MD_TRENDING_UP; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(arrowX, valY + sub1->LegacySize * 0.5f - iSz.y * 0.5f), WithAlpha(Success(), 220), icon); } else if (recent < older * 0.98) { const char* icon = ICON_MD_TRENDING_DOWN; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(arrowX, valY + sub1->LegacySize * 0.5f - iSz.y * 0.5f), WithAlpha(Error(), 220), icon); } } } } // Sparkline chart below stats if (hasChartContent) { float chartTop = cardMin.y + statRowH; float chartBot = cardMax.y; // Compute Y range double yMin = *std::min_element(chartHistory.begin(), chartHistory.end()); double yMax = *std::max_element(chartHistory.begin(), chartHistory.end()); if (yMax <= yMin) { yMax = yMin + 1.0; } double yRange = yMax - yMin; double yPad2 = yRange * 0.1; yMin -= yPad2; yMax += yPad2; float plotLeft = cardMin.x + pad; float plotRight = cardMax.x - pad; float plotTop = chartTop + capFont->LegacySize + 4 * dp; float plotBottom = chartBot - capFont->LegacySize * 2 - 16 * dp; float plotW = plotRight - plotLeft; float plotH = std::max(1.0f, plotBottom - plotTop); // Build raw data points — evenly spaced across the plot. // No smooth-scroll animation: the chart updates in-place // when new data arrives without any interim compression. size_t n = chartHistory.size(); float stepW = (n > 1) ? plotW / (float)(n - 1) : plotW; std::vector rawPts(n); for (size_t i = 0; i < n; i++) { float x = plotLeft + (float)i * stepW; float y = plotBottom - (float)((chartHistory[i] - yMin) / (yMax - yMin)) * plotH; rawPts[i] = ImVec2(x, y); } // Catmull-Rom spline interpolation for smooth curve // Subdivisions are adaptive: more when points are far apart, // none when points are already sub-2px apart. std::vector points; if (n <= 2) { points = rawPts; } else { points.reserve(n * 4); // conservative estimate for (size_t i = 0; i + 1 < n; i++) { ImVec2 p0 = rawPts[i > 0 ? i - 1 : 0]; ImVec2 p1 = rawPts[i]; ImVec2 p2 = rawPts[i + 1]; ImVec2 p3 = rawPts[i + 2 < n ? i + 2 : n - 1]; // Adaptive subdivision: ~1 segment per 3px of distance float dx = p2.x - p1.x, dy = p2.y - p1.y; float dist = sqrtf(dx * dx + dy * dy); int subdivs = std::clamp((int)(dist / 3.0f), 1, 16); for (int s = 0; s < subdivs; s++) { float t = (float)s / (float)subdivs; float t2 = t * t; float t3 = t2 * t; float q0 = -t3 + 2.0f * t2 - t; float q1 = 3.0f * t3 - 5.0f * t2 + 2.0f; float q2 = -3.0f * t3 + 4.0f * t2 + t; float q3 = t3 - t2; float sx = 0.5f * (p0.x * q0 + p1.x * q1 + p2.x * q2 + p3.x * q3); float sy = 0.5f * (p0.y * q0 + p1.y * q1 + p2.y * q2 + p3.y * q3); // Clamp Y to plot bounds to prevent Catmull-Rom overshoot sy = std::clamp(sy, plotTop, plotBottom); points.push_back(ImVec2(sx, sy)); } } points.push_back(rawPts[n - 1]); // final point } // Fill under curve (single concave polygon to avoid AA seam shimmer) if (points.size() >= 2) { for (size_t i = 0; i < points.size(); i++) dl->PathLineTo(points[i]); dl->PathLineTo(ImVec2(points.back().x, plotBottom)); dl->PathLineTo(ImVec2(points.front().x, plotBottom)); dl->PathFillConcave(WithAlpha(Success(), 25)); } // Green line dl->AddPolyline(points.data(), (int)points.size(), WithAlpha(Success(), 200), ImDrawFlags_None, schema::UI().drawElement("tabs.mining", "chart-line-thickness").size); // Y-axis labels std::string yMaxStr = FormatHashrate(yMax); std::string yMinStr = FormatHashrate(yMin); dl->AddText(capFont, capFont->LegacySize, ImVec2(plotLeft + 2 * dp, plotTop - capFont->LegacySize - 2 * dp), OnSurfaceDisabled(), yMaxStr.c_str()); dl->AddText(capFont, capFont->LegacySize, ImVec2(plotLeft + 2 * dp, plotBottom + 4 * dp), OnSurfaceDisabled(), yMinStr.c_str()); // X-axis labels dl->AddText(capFont, capFont->LegacySize, ImVec2(plotLeft, chartBot - capFont->LegacySize - 2 * dp), OnSurfaceDisabled(), chartHistory.size() >= 300 ? TR("mining_chart_5m_ago") : chartHistory.size() >= 60 ? TR("mining_chart_1m_ago") : TR("mining_chart_start")); std::string nowLbl = TR("mining_chart_now"); ImVec2 nowSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, nowLbl.c_str()); dl->AddText(capFont, capFont->LegacySize, ImVec2(plotRight - nowSz.x, chartBot - capFont->LegacySize - 2 * dp), OnSurfaceDisabled(), nowLbl.c_str()); } // Advance cursor past the card (stats/chart view only) ImGui::Dummy(ImVec2(availWidth, totalCardH)); } // --- Toggle button in top-right corner --- // Rendered after content so the Hand cursor takes priority over // the InputTextMultiline text-cursor when hovering the button. if (hasLogContent || hasChartContent) { ImFont* iconFont = Type().iconSmall(); const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE; const char* toggleTip = showLogFlag ? TR("mining_show_chart") : TR("mining_show_log"); ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon); float btnSize = iconSz.y + 8 * dp; float btnX = cardMax.x - pad - btnSize; float btnY = cardMin.y + pad * 0.5f; ImVec2 btnMin(btnX, btnY); ImVec2 btnMax(btnX + btnSize, btnY + btnSize); ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f); bool hov = IsRectHovered(btnMin, btnMax); if (hov) { dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover()); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("%s", toggleTip); } dl->AddText(iconFont, iconFont->LegacySize, ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f), OnSurfaceMedium(), toggleIcon); if (hov && ImGui::IsMouseClicked(0)) { showLogFlag = !showLogFlag; } } ImGui::Dummy(ImVec2(0, gap)); } // ================================================================ // EARNINGS — Horizontal row card (Today | Yesterday | All Time | Est. Daily) // ================================================================ { // Gather mining transactions from state double minedToday = 0.0, minedYesterday = 0.0, minedAllTime = 0.0; int minedTodayCount = 0, minedYesterdayCount = 0, minedAllTimeCount = 0; int64_t now = (int64_t)std::time(nullptr); // Calendar-day boundaries (local time) time_t nowT = (time_t)now; struct tm local; #ifdef _WIN32 localtime_s(&local, &nowT); #else localtime_r(&nowT, &local); #endif local.tm_hour = 0; local.tm_min = 0; local.tm_sec = 0; int64_t todayStart = (int64_t)mktime(&local); int64_t yesterdayStart = todayStart - 86400; struct MinedTx { int64_t timestamp; double amount; int confirmations; bool mature; std::string txid; bool isPoolPayout; }; std::vector recentMined; for (const auto& tx : state.transactions) { bool isSoloMined = (tx.type == "generate" || tx.type == "immature" || tx.type == "mined"); bool isPoolPayout = (tx.type == "receive" && !tx.memo.empty() && tx.memo.find("Mining Pool payout") != std::string::npos); if (isSoloMined || isPoolPayout) { // Apply earnings filter if (s_earnings_filter == 1 && !isSoloMined) continue; if (s_earnings_filter == 2 && !isPoolPayout) continue; double amt = std::abs(tx.amount); minedAllTime += amt; minedAllTimeCount++; if (tx.timestamp >= todayStart) { minedToday += amt; minedTodayCount++; } else if (tx.timestamp >= yesterdayStart) { minedYesterday += amt; minedYesterdayCount++; } // Separate solo blocks from pool payouts based on current mode bool showInCurrentMode = s_pool_mode ? isPoolPayout : isSoloMined; if (showInCurrentMode && recentMined.size() < 4) { recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100, tx.txid, isPoolPayout}); } } } // Use pool hashrate for EST. DAILY when in pool mode double estHashrate = s_pool_mode ? state.pool_mining.hashrate_10s : mining.localHashrate; double est_hours_2 = EstimateHoursToBlock(estHashrate, mining.networkHashrate, mining.difficulty); double estDailyBlocks = (est_hours_2 > 0) ? (24.0 / est_hours_2) : 0.0; double blockReward = schema::UI().drawElement("business", "block-reward").size; double estDaily = estDailyBlocks * blockReward; bool estActive = isMiningActive && estDaily > 0; ImU32 greenCol2 = Success(); // --- Combined Earnings + Details card --- float earningsRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + pad * 1.5f; float detailsContentH = pad * 0.5f + capFont->LegacySize + pad * 0.5f; float barH_est = capFont->LegacySize + Layout::spacingMd() * 2.0f; float combinedCardH = earningsRowH + detailsContentH + barH_est + pad; combinedCardH = std::max(combinedCardH, schema::UI().drawElement("tabs.mining", "details-card-min-height").size + earningsRowH); ImVec2 cardMin = ImGui::GetCursorScreenPos(); ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + combinedCardH); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); // === Earnings filter toggle button (top-right of card) === { const char* filterLabels[] = { TR("mining_filter_all"), TR("mining_solo"), TR("mining_pool") }; const char* filterIcons[] = { ICON_MD_FUNCTIONS, ICON_MD_MEMORY, ICON_MD_CLOUD }; const char* curIcon = filterIcons[s_earnings_filter]; const char* curLabel = filterLabels[s_earnings_filter]; ImFont* icoFont = Type().iconSmall(); float icoH = icoFont->LegacySize; ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, curIcon); ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, curLabel); float padH = Layout::spacingSm(); float btnW = padH + icoSz.x + Layout::spacingXs() + lblSz.x + padH; float btnH = icoH + 8.0f * dp; float btnX = cardMax.x - pad - btnW; float btnY = cardMin.y + (earningsRowH - btnH) * 0.5f; ImVec2 bMin(btnX, btnY), bMax(btnX + btnW, btnY + btnH); bool hov = material::IsRectHovered(bMin, bMax); // Pill background ImU32 pillBg = s_earnings_filter != 0 ? WithAlpha(Primary(), 60) : WithAlpha(OnSurface(), hov ? 25 : 12); dl->AddRectFilled(bMin, bMax, pillBg, btnH * 0.5f); // Icon ImU32 icoCol = s_earnings_filter != 0 ? Primary() : (hov ? OnSurface() : OnSurfaceDisabled()); float cx = bMin.x + padH; float cy = bMin.y + (btnH - icoSz.y) * 0.5f; dl->AddText(icoFont, icoFont->LegacySize, ImVec2(cx, cy), icoCol, curIcon); // Label ImU32 lblCol = s_earnings_filter != 0 ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()); float lx = cx + icoSz.x + Layout::spacingXs(); float ly = bMin.y + (btnH - lblSz.y) * 0.5f; dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), lblCol, curLabel); // Click target ImVec2 savedCur = ImGui::GetCursorScreenPos(); ImGui::SetCursorScreenPos(bMin); ImGui::InvisibleButton("##EarningsFilter", ImVec2(btnW, btnH)); if (ImGui::IsItemClicked()) { s_earnings_filter = (s_earnings_filter + 1) % 3; } if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); const char* tips[] = { TR("mining_filter_tip_all"), TR("mining_filter_tip_solo"), TR("mining_filter_tip_pool") }; ImGui::SetTooltip("%s", tips[s_earnings_filter]); } ImGui::SetCursorScreenPos(savedCur); } // === Earnings section (top of combined card) === { const int numCols = 4; float colW = (availWidth - pad * 2) / (float)numCols; float ey = cardMin.y + pad * 0.5f; char valBuf[64], subBuf2[64]; struct EarningsEntry { const char* label; const char* value; const char* sub; ImU32 col; }; snprintf(valBuf, sizeof(valBuf), "+%.4f", minedToday); snprintf(subBuf2, sizeof(subBuf2), "(%d txn)", minedTodayCount); char todayVal[64], todaySub[64]; strncpy(todayVal, valBuf, sizeof(todayVal)); strncpy(todaySub, subBuf2, sizeof(todaySub)); char yesterdayVal[64], yesterdaySub[64]; snprintf(yesterdayVal, sizeof(yesterdayVal), "+%.4f", minedYesterday); snprintf(yesterdaySub, sizeof(yesterdaySub), "(%d txn)", minedYesterdayCount); char allVal[64], allSub[64]; snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime); snprintf(allSub, sizeof(allSub), "(%d txn)", minedAllTimeCount); char estVal[64]; if (estActive) snprintf(estVal, sizeof(estVal), "~%.4f", estDaily); else snprintf(estVal, sizeof(estVal), "N/A"); EarningsEntry entries[] = { { TR("mining_today"), todayVal, todaySub, greenCol2 }, { TR("mining_yesterday"), yesterdayVal, yesterdaySub, OnSurface() }, { TR("mining_all_time"), allVal, allSub, OnSurface() }, { TR("mining_est_daily"), estVal, nullptr, estActive ? greenCol2 : OnSurfaceDisabled() }, }; for (int ei = 0; ei < numCols; ei++) { float sx = cardMin.x + pad + ei * colW; float centerX = sx + colW * 0.5f; // Overline label (centered) ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, entries[ei].label); dl->AddText(ovFont, ovFont->LegacySize, ImVec2(centerX - lblSz.x * 0.5f, ey), OnSurfaceMedium(), entries[ei].label); // Value (centered) float valY = ey + ovFont->LegacySize + Layout::spacingXs(); ImVec2 valSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, entries[ei].value); if (entries[ei].sub) { // Value + sub annotation side by side, centered together ImVec2 subSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, entries[ei].sub); float totalW = valSz.x + 4 * dp + subSz.x; float startX = centerX - totalW * 0.5f; dl->AddText(sub1, sub1->LegacySize, ImVec2(startX, valY), entries[ei].col, entries[ei].value); dl->AddText(capFont, capFont->LegacySize, ImVec2(startX + valSz.x + 4 * dp, valY + (sub1->LegacySize - capFont->LegacySize) * 0.5f), OnSurfaceDisabled(), entries[ei].sub); } else { dl->AddText(sub1, sub1->LegacySize, ImVec2(centerX - valSz.x * 0.5f, valY), entries[ei].col, entries[ei].value); } } } // === Separator between earnings & details === float earningsSepY = cardMin.y + earningsRowH; { float rnd = glassSpec.rounding; dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, earningsSepY), ImVec2(cardMax.x - rnd * 0.5f, earningsSepY), WithAlpha(OnSurface(), 15), 1.0f * dp); } // === Details section (below separator) === { float cx = cardMin.x + pad; float cy = earningsSepY + pad * 0.5f; // Three equal columns: Difficulty | Block | Mining Address float colW = availWidth / 3.0f; float valOffX = availWidth * schema::UI().drawElement("tabs.mining", "stats-col1-value-x-ratio").size; float col1X = cx; float col2X = cx + colW; float col3X = cx + colW * 2.0f; // -- Difficulty -- dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X, cy), OnSurfaceMedium(), TR("difficulty")); if (mining.difficulty > 0) { snprintf(buf, sizeof(buf), "%.4f", mining.difficulty); dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X + valOffX, cy), OnSurface(), buf); ImVec2 diffSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); ImGui::SetCursorScreenPos(ImVec2(col1X + valOffX, cy)); ImGui::InvisibleButton("##DiffCopy", ImVec2(diffSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp)); if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("%s", TR("mining_click_copy_difficulty")); dl->AddLine(ImVec2(col1X + valOffX, cy + capFont->LegacySize + 1 * dp), ImVec2(col1X + valOffX + diffSz.x, cy + capFont->LegacySize + 1 * dp), WithAlpha(OnSurface(), 60), 1.0f * dp); } if (ImGui::IsItemClicked()) { ImGui::SetClipboardText(buf); Notifications::instance().info(TR("mining_difficulty_copied")); } } // -- Block -- dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X, cy), OnSurfaceMedium(), TR("block")); if (mining.blocks > 0) { snprintf(buf, sizeof(buf), "%d", mining.blocks); dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X + valOffX, cy), OnSurface(), buf); ImVec2 blkSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); ImGui::SetCursorScreenPos(ImVec2(col2X + valOffX, cy)); ImGui::InvisibleButton("##BlockCopy", ImVec2(blkSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp)); if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("%s", TR("mining_click_copy_block")); dl->AddLine(ImVec2(col2X + valOffX, cy + capFont->LegacySize + 1 * dp), ImVec2(col2X + valOffX + blkSz.x, cy + capFont->LegacySize + 1 * dp), WithAlpha(OnSurface(), 60), 1.0f * dp); } if (ImGui::IsItemClicked()) { ImGui::SetClipboardText(buf); Notifications::instance().info(TR("mining_block_copied")); } } // -- Mining Address -- dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), TR("mining_mining_addr")); std::string mining_address = ""; for (const auto& addr : state.addresses) { if (addr.type == "transparent" && !addr.address.empty()) { mining_address = addr.address; break; } } if (!mining_address.empty()) { float addrAvailW = colW - pad - valOffX; float charW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, "M").x; if (charW <= 0.0f) charW = 8.0f; int maxChars = std::max(8, (int)(addrAvailW / charW)); std::string truncAddr = mining_address; if ((int)truncAddr.length() > maxChars && maxChars > 5) { int half = (maxChars - 3) / 2; if (half > 0 && (size_t)half < truncAddr.length()) truncAddr = truncAddr.substr(0, half) + "..." + truncAddr.substr(truncAddr.length() - half); } dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X + valOffX, cy), OnSurface(), truncAddr.c_str()); ImVec2 addrTextSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, truncAddr.c_str()); ImGui::SetCursorScreenPos(ImVec2(col3X + valOffX, cy)); ImGui::InvisibleButton("##MiningAddrCopy", ImVec2(addrTextSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp)); if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("%s", TR("mining_click_copy_address")); dl->AddLine(ImVec2(col3X + valOffX, cy + capFont->LegacySize + 1 * dp), ImVec2(col3X + valOffX + addrTextSz.x, cy + capFont->LegacySize + 1 * dp), WithAlpha(OnSurface(), 60), 1.0f * dp); } if (ImGui::IsItemClicked()) { ImGui::SetClipboardText(mining_address.c_str()); Notifications::instance().info(TR("mining_address_copied")); } } } // ---- Memory bar — centered in remaining space below details ---- { double totalRAM = dragonx::util::Platform::getTotalSystemRAM_MB(); double usedRAM = dragonx::util::Platform::getUsedSystemRAM_MB(); double selfRAM = dragonx::util::Platform::getSelfMemoryUsageMB(); double daemonRAM = app->getDaemonMemoryUsageMB(); // Include xmrig memory when pool mining double xmrigRAM = state.pool_mining.memory_used / (1024.0 * 1024.0); // bytes -> MB double appRAM = selfRAM + daemonRAM + xmrigRAM; // App + daemon + xmrig combined // Fixed bar height (text + padding) float barH = capFont->LegacySize + Layout::spacingMd() * 2.0f; float barRnd = barH * 0.5f; // fully rounded corners // Details content ends here float detailsEndY = earningsSepY + detailsContentH; // Subtle top separator at the boundary float rnd = glassSpec.rounding; float stripY = detailsEndY; dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, stripY), ImVec2(cardMax.x - rnd * 0.5f, stripY), WithAlpha(OnSurface(), 15), 1.0f * dp); float remainingH = cardMax.y - stripY; float barX = cardMin.x + pad; float barW = cardMax.x - pad - barX; float barY = stripY + (remainingH - barH) * 0.5f; float textY = barY + (barH - capFont->LegacySize) * 0.5f; float textPadX = Layout::spacingMd(); // Background track dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH), WithAlpha(OnSurface(), 20), barRnd); float sysFrac = 0.0f, appFrac = 0.0f; float sysFillW = 0.0f, appFillW = 0.0f; // Helper: draw a fill bar that perfectly matches the track's rounded // left edge regardless of fill width. We draw a full-width rounded // rect (same shape as the track) but clip it to just the fill portion. auto drawFillBar = [&](float fillW, ImU32 col) { if (fillW <= 1.0f) return; dl->PushClipRect(ImVec2(barX, barY), ImVec2(barX + fillW, barY + barH), true); dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH), col, barRnd); dl->PopClipRect(); }; if (usedRAM > 0 && totalRAM > 0) { sysFrac = std::clamp((float)(usedRAM / totalRAM), 0.0f, 1.0f); sysFillW = barW * sysFrac; // System memory bar (subtle fill) ImU32 ramBarCol = schema::UI().resolveColor("var(--ram-bar-system)", IsDarkTheme() ? IM_COL32(255, 255, 255, 46) : IM_COL32(0, 0, 0, 46)); drawFillBar(sysFillW, ramBarCol); // App+daemon memory bar (saturated accent) if (appRAM > 0) { appFrac = std::clamp((float)(appRAM / totalRAM), 0.0f, sysFrac); appFillW = barW * appFrac; ImU32 appBarCol = schema::UI().resolveColor("var(--ram-bar-app)", ImGui::ColorConvertFloat4ToU32(ImGui::GetStyle().Colors[ImGuiCol_ButtonActive])); drawFillBar(appFillW, appBarCol); } } // --- Text overlaying the bar --- float accentEdge = barX + appFillW; float whiteEdge = barX + sysFillW; // Determine text colors for bar segments // On dark themes: white text on empty, dark on filled bars (both system and app) // On light themes: dark text on empty/system, white on app accent bar ImU32 barTextOnEmpty = IsDarkTheme() ? IM_COL32(255, 255, 255, 230) : IM_COL32(30, 30, 30, 230); ImU32 barTextOnFill = IsDarkTheme() ? IM_COL32(30, 30, 30, 230) : IM_COL32(255, 255, 255, 230); ImU32 barTextOnAccent = IsDarkTheme() ? IM_COL32(0, 0, 0, 210) : IM_COL32(255, 255, 255, 230); // App usage on the left char appBuf[64] = {}; if (appRAM > 0) { if (appRAM >= 1024.0) snprintf(appBuf, sizeof(appBuf), "%.1f GB", appRAM / 1024.0); else snprintf(appBuf, sizeof(appBuf), "%.0f MB", appRAM); float appTextX = barX + textPadX; float appTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, appBuf).x; float appTextEnd = appTextX + appTextW; // Inside accent bar: white | inside white bar: dark | outside: white if (appTextEnd <= accentEdge) { dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY), barTextOnAccent, appBuf); } else if (appTextX >= whiteEdge) { dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY), barTextOnEmpty, appBuf); } else { // Part in accent bar: white if (accentEdge > appTextX) { dl->PushClipRect(ImVec2(appTextX, barY), ImVec2(accentEdge, barY + barH)); dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY), barTextOnAccent, appBuf); dl->PopClipRect(); } // Part in white bar (past accent): dark float dkS = std::max(appTextX, accentEdge); float dkE = std::min(appTextEnd, whiteEdge); if (dkE > dkS) { dl->PushClipRect(ImVec2(dkS, barY), ImVec2(dkE, barY + barH)); dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY), barTextOnFill, appBuf); dl->PopClipRect(); } // Part outside white bar: white if (appTextEnd > whiteEdge) { dl->PushClipRect(ImVec2(whiteEdge, barY), ImVec2(appTextEnd + 1, barY + barH)); dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY), barTextOnEmpty, appBuf); dl->PopClipRect(); } } } // System usage on the right char sysBuf[64] = {}; if (usedRAM > 0 && totalRAM > 0) snprintf(sysBuf, sizeof(sysBuf), "%.1f / %.0f GB", usedRAM / 1024.0, totalRAM / 1024.0); else if (totalRAM > 0) snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0); else snprintf(sysBuf, sizeof(sysBuf), "N/A"); float sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x; float sysTextX = barX + barW - textPadX - sysTextW; if (sysTextX >= whiteEdge) { dl->AddText(capFont, capFont->LegacySize, ImVec2(sysTextX, textY), barTextOnEmpty, sysBuf); } else if (sysTextX + sysTextW <= whiteEdge) { dl->AddText(capFont, capFont->LegacySize, ImVec2(sysTextX, textY), barTextOnFill, sysBuf); } else { dl->PushClipRect(ImVec2(sysTextX, barY), ImVec2(whiteEdge, barY + barH)); dl->AddText(capFont, capFont->LegacySize, ImVec2(sysTextX, textY), barTextOnFill, sysBuf); dl->PopClipRect(); dl->PushClipRect(ImVec2(whiteEdge, barY), ImVec2(sysTextX + sysTextW + 1, barY + barH)); dl->AddText(capFont, capFont->LegacySize, ImVec2(sysTextX, textY), barTextOnEmpty, sysBuf); dl->PopClipRect(); } // Invisible button over the bar for tooltip interaction ImGui::SetCursorScreenPos(ImVec2(barX, barY)); ImGui::InvisibleButton("##rambar", ImVec2(barW, barH)); if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); if (selfRAM >= 1024.0) ImGui::Text(TR("ram_wallet_gb"), selfRAM / 1024.0); else ImGui::Text(TR("ram_wallet_mb"), selfRAM); if (daemonRAM >= 1024.0) ImGui::Text(TR("ram_daemon_gb"), daemonRAM / 1024.0, app->getDaemonMemDiag().c_str()); else ImGui::Text(TR("ram_daemon_mb"), daemonRAM, app->getDaemonMemDiag().c_str()); ImGui::Separator(); ImGui::Text(TR("ram_system_gb"), usedRAM / 1024.0, totalRAM / 1024.0); ImGui::EndTooltip(); } } ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); ImGui::Dummy(ImVec2(availWidth, 0)); ImGui::Dummy(ImVec2(0, gap)); // ============================================================ // RECENT BLOCKS — last 4 mined blocks (always shown in pool mode) // ============================================================ if (!recentMined.empty() || s_pool_mode) { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), s_pool_mode ? TR("mining_recent_payouts") : TR("mining_recent_blocks")); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); float rowH_blocks = std::max(schema::UI().drawElement("tabs.mining", "recent-row-min-height").size, schema::UI().drawElement("tabs.mining", "recent-row-height").size * vs); // Size to remaining space — proportional budget ensures fit float recentAvailH = ImGui::GetContentRegionAvail().y - sHdr - gapOver; float minRows = recentMined.empty() ? 2.0f : (float)recentMined.size(); float contentH_blocks = rowH_blocks * minRows + pad * 2.5f; float recentH = std::clamp(contentH_blocks, 30.0f * dp, std::max(30.0f * dp, recentAvailH)); // Glass panel wrapping the list + scroll-edge mask state ImVec2 recentPanelMin = ImGui::GetCursorScreenPos(); ImVec2 recentPanelMax(recentPanelMin.x + availWidth, recentPanelMin.y + recentH); GlassPanelSpec recentGlass; recentGlass.rounding = Layout::glassRounding(); DrawGlassPanel(dl, recentPanelMin, recentPanelMax, recentGlass); float miningScrollY = 0.0f, miningScrollMaxY = 0.0f; int miningParentVtx = dl->VtxBuffer.Size; ImGui::BeginChild("##RecentBlocks", ImVec2(availWidth, recentH), false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar); ImDrawList* miningChildDL = ImGui::GetWindowDrawList(); int miningChildVtx = miningChildDL->VtxBuffer.Size; miningScrollY = ImGui::GetScrollY(); miningScrollMaxY = ImGui::GetScrollMaxY(); // Top padding inside glass card ImGui::Dummy(ImVec2(0, pad * 0.5f)); if (recentMined.empty()) { // Empty state — card is visible but no rows yet float emptyY = ImGui::GetCursorScreenPos().y; float emptyX = ImGui::GetCursorScreenPos().x; float centerX = emptyX + availWidth * 0.5f; ImFont* icoFont = Type().iconMed(); const char* emptyIcon = ICON_MD_HOURGLASS_EMPTY; ImVec2 iSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, emptyIcon); miningChildDL->AddText(icoFont, icoFont->LegacySize, ImVec2(centerX - iSz.x * 0.5f, emptyY), OnSurfaceDisabled(), emptyIcon); const char* emptyMsg = s_pool_mode ? TR("mining_no_payouts_yet") : TR("mining_no_blocks_yet"); ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, emptyMsg); miningChildDL->AddText(capFont, capFont->LegacySize, ImVec2(centerX - msgSz.x * 0.5f, emptyY + iSz.y + Layout::spacingXs()), OnSurfaceDisabled(), emptyMsg); } for (size_t mi = 0; mi < recentMined.size(); mi++) { const auto& mtx = recentMined[mi]; ImVec2 rMin = ImGui::GetCursorScreenPos(); float rH = std::max(schema::UI().drawElement("tabs.mining", "recent-row-min-height").size, schema::UI().drawElement("tabs.mining", "recent-row-height").size * vs); ImVec2 rMax(rMin.x + availWidth, rMin.y + rH); // Subtle background on hover (inset from card edges) bool hovered = material::IsRectHovered(rMin, rMax); bool isClickable = !mtx.txid.empty(); if (hovered) { dl->AddRectFilled(ImVec2(rMin.x + pad * 0.5f, rMin.y), ImVec2(rMax.x - pad * 0.5f, rMax.y), IM_COL32(255, 255, 255, 8), 3.0f * dp); if (isClickable) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } } float rx = rMin.x + pad; float ry = rMin.y + Layout::spacingXs(); // Mining icon — Material Design ImFont* iconFont = Type().iconSmall(); const char* mIcon = ICON_MD_CONSTRUCTION; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, mIcon); float iconX = rx + 2 * dp, iconY = ry + 2 * dp; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(iconX, iconY), WithAlpha(Warning(), 200), mIcon); // Time int64_t diff = now - mtx.timestamp; if (diff < 60) snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff); else if (diff < 3600) snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); else if (diff < 86400) snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); else snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400)); dl->AddText(capFont, capFont->LegacySize, ImVec2(rx + iSz.x + 8 * dp, ry), OnSurfaceDisabled(), buf); // Amount snprintf(buf, sizeof(buf), "+%.8f %s", mtx.amount, DRAGONX_TICKER); float amtX = rMin.x + pad + (availWidth - pad * 2) * 0.35f; dl->AddText(capFont, capFont->LegacySize, ImVec2(amtX, ry), greenCol2, buf); // Maturity badge — inset from right edge float badgeX = rMax.x - pad - Layout::spacingXl() * 3.5f; if (mtx.mature) { dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry), WithAlpha(Success(), 180), TR("mature")); } else { snprintf(buf, sizeof(buf), TR("conf_count"), mtx.confirmations); dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry), WithAlpha(Warning(), 200), buf); } // Click to open in block explorer ImGui::SetCursorScreenPos(rMin); char blockBtnId[32]; snprintf(blockBtnId, sizeof(blockBtnId), "##RecentBlock%zu", mi); ImGui::InvisibleButton(blockBtnId, ImVec2(availWidth, rH)); if (ImGui::IsItemClicked() && !mtx.txid.empty()) { std::string url = app->settings()->getTxExplorerUrl() + mtx.txid; dragonx::util::Platform::openUrl(url); } if (ImGui::IsItemHovered() && !mtx.txid.empty()) { ImGui::SetTooltip("%s", TR("mining_open_in_explorer")); } } ImGui::EndChild(); // ##RecentBlocks // CSS-style clipping mask { float fadeZone = std::min(capFont->LegacySize * 3.0f, recentH * 0.18f); ApplyScrollEdgeMask(dl, miningParentVtx, miningChildDL, miningChildVtx, recentPanelMin.y, recentPanelMax.y, fadeZone, miningScrollY, miningScrollMaxY); } ImGui::Dummy(ImVec2(0, gap)); } } // ================================================================ // 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