// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "mining_tab.h" #include "../../app.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 #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 // Pool mode state static bool s_pool_mode = false; static char s_pool_url[256] = "pool.dragonx.is"; 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 // Chart smooth-scroll state static size_t s_chart_last_n = 0; static double s_chart_last_newest = -1.0; static double s_chart_update_time = 0.0; static float s_chart_interval = 1.0f; // measured seconds between data updates // Get max threads based on hardware static int GetMaxMiningThreads() { int hw_threads = std::thread::hardware_concurrency(); return std::max(1, hw_threads); } // Format hashrate with appropriate units static std::string FormatHashrate(double hashrate) { char buf[64]; if (hashrate >= 1e12) { snprintf(buf, sizeof(buf), "%.2f TH/s", hashrate / 1e12); } else if (hashrate >= 1e9) { snprintf(buf, sizeof(buf), "%.2f GH/s", hashrate / 1e9); } else if (hashrate >= 1e6) { snprintf(buf, sizeof(buf), "%.2f MH/s", hashrate / 1e6); } else if (hashrate >= 1e3) { snprintf(buf, sizeof(buf), "%.2f KH/s", hashrate / 1e3); } else { snprintf(buf, sizeof(buf), "%.2f H/s", hashrate); } return std::string(buf); } // Calculate estimated hours to find a block static double EstimateHoursToBlock(double localHashrate, double networkHashrate, double difficulty) { if (localHashrate <= 0 || networkHashrate <= 0) return 0; double blocksPerHour = 3600.0 / 75.0; double yourShare = localHashrate / networkHashrate; if (yourShare <= 0) return 0; return 1.0 / (blocksPerHour * yourShare); } // Format estimated time static std::string FormatEstTime(double est_hours) { char buf[64]; if (est_hours <= 0) { return "N/A"; } else if (est_hours < 1.0) { snprintf(buf, sizeof(buf), "~%.0f min", est_hours * 60.0); } else if (est_hours < 24.0) { snprintf(buf, sizeof(buf), "~%.1f hrs", est_hours); } else if (est_hours < 168.0) { snprintf(buf, sizeof(buf), "~%.1f days", est_hours / 24.0); } else { snprintf(buf, sizeof(buf), "~%.1f weeks", est_hours / 168.0); } return std::string(buf); } void RenderMiningTab(App* app) { auto& S = schema::UI(); 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; // Scrollable child to contain all content within available space ImVec2 miningAvail = ImGui::GetContentRegionAvail(); ImGui::BeginChild("##MiningScroll", miningAvail, false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar); // 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) { s_selected_threads = mining.generate ? std::max(1, mining.genproclimit) : 1; s_threads_initialized = true; } ImDrawList* dl = ImGui::GetWindowDrawList(); GlassPanelSpec glassSpec; glassSpec.rounding = Layout::glassRounding(); ImFont* ovFont = Type().overline(); ImFont* capFont = Type().caption(); ImFont* sub1 = Type().subtitle1(); 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); // If pool worker is empty or placeholder, default to user's first address std::string workerStr(s_pool_worker); if (workerStr.empty() || workerStr == "x") { std::string defaultAddr; for (const auto& addr : state.addresses) { if (addr.type == "shielded" && !addr.address.empty()) { defaultAddr = addr.address; break; } } if (defaultAddr.empty()) { for (const auto& addr : state.addresses) { if (addr.type == "transparent" && !addr.address.empty()) { defaultAddr = addr.address; break; } } } 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_state_loaded = 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; } // Determine active mining state for UI bool isMiningActive = s_pool_mode ? state.pool_mining.xmrig_running : mining.generate; // ================================================================ // 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 = "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 = "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(); app->stopMining(); } if (poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (poolHov && soloMiningActive && !s_pool_mode) { ImGui::SetTooltip("Stop solo mining to use pool mining"); } 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("Stop solo mining to configure 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)); // Calculate remaining width from inputs start to end of content region float inputFrameH2 = ImGui::GetFrameHeight(); float resetBtnW = inputFrameH2; // Square button matching input height float contentEndX = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x; float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW - Layout::spacingSm(); float urlW = std::max(60.0f, remainW * 0.30f); float wrkW = std::max(40.0f, remainW * 0.70f); ImGui::SetNextItemWidth(urlW); if (ImGui::InputTextWithHint("##PoolURL", "Pool URL", s_pool_url, sizeof(s_pool_url))) { s_pool_settings_dirty = true; } ImGui::SameLine(0, Layout::spacingSm()); ImGui::SetNextItemWidth(wrkW); if (ImGui::InputTextWithHint("##PoolWorker", "Payout Address", s_pool_worker, sizeof(s_pool_worker))) { s_pool_settings_dirty = true; } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("Your DRAGONX address for receiving pool payouts"); } // Reset to defaults button (matching input height) 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("Reset to 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, "pool.dragonx.is", sizeof(s_pool_url) - 1); // Default to user's first shielded address for pool payouts std::string defaultAddr; for (const auto& addr : state.addresses) { if (addr.type == "shielded" && !addr.address.empty()) { defaultAddr = addr.address; break; } } if (defaultAddr.empty()) { // Fallback to transparent if no shielded available for (const auto& addr : state.addresses) { if (addr.type == "transparent" && !addr.address.empty()) { defaultAddr = addr.address; break; } } } 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(), "THREADS"); float labelW = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, "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 hush3 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); } // Active mining indicator (top-right) 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(cardMax.x - pad - dotR * 2, curY + dotR + 1 * dp), dotR, pulseCol); dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMax.x - pad - dotR * 2 - 60 * hs, curY), WithAlpha(Success(), 200), "Mining"); } 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 if (hovered_thread > 0) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); // Drag-to-select logic if (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 && mining.generate) { app->startMining(s_selected_threads); } if (threads_changed && 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(); bool disabled = !app->isConnected() || isToggling || isSyncing || poolBlockedBySolo; // 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 ? "STOPPING" : "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 = "STOP"; lblCol = WithAlpha(Error(), 220); } else if (disabled) { label = "MINE"; lblCol = WithAlpha(OnSurface(), 50); } else { label = "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(isMiningActive ? "Stopping miner..." : "Starting miner..."); else if (isSyncing) ImGui::SetTooltip("Syncing blockchain... (%.1f%%)", state.sync.verification_progress * 100.0); else if (poolBlockedBySolo) ImGui::SetTooltip("Stop solo mining before starting pool mining"); else ImGui::SetTooltip(isMiningActive ? "Stop Mining" : "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 (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(); bool hasLogContent = s_pool_mode && !state.pool_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); // --- Toggle button in top-right corner (pool mode only) --- if (s_pool_mode && (hasLogContent || hasChartContent)) { ImFont* iconFont = Type().iconSmall(); const char* toggleIcon = s_show_pool_log ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE; const char* toggleTip = s_show_pool_log ? "Show hashrate chart" : "Show miner 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)) { s_show_pool_log = !s_show_pool_log; } } if (showLogView) { // --- Full-card log view --- float logPad = pad * 0.5f; ImGui::SetCursorScreenPos(ImVec2(cardMin.x + logPad, cardMin.y + logPad)); ImGui::BeginChild("##PoolLogInCard", ImVec2(availWidth - logPad * 2, totalCardH - logPad * 2), false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_HorizontalScrollbar); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface())); ImFont* monoFont = Type().body2(); ImGui::PushFont(monoFont); for (const auto& line : state.pool_mining.log_lines) { if (!line.empty()) ImGui::TextUnformatted(line.c_str()); } ImGui::PopFont(); ImGui::PopStyleColor(); // Auto-scroll to bottom only if user is already near the bottom // This allows manual scrolling up to read history float scrollY = ImGui::GetScrollY(); float scrollMaxY = ImGui::GetScrollMaxY(); if (scrollMaxY <= 0.0f || scrollY >= scrollMaxY - 20.0f * dp) ImGui::SetScrollHereY(1.0f); ImGui::EndChild(); // Reset cursor to end of card after the child window 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 = "POOL HASHRATE"; col1Str = FormatHashrate(state.pool_mining.hashrate_10s); col1Col = state.pool_mining.xmrig_running ? greenCol : OnSurfaceDisabled(); col2Label = "THREADS / MEM"; { char buf[64]; int64_t memMB = state.pool_mining.memory_used / (1024 * 1024); if (memMB > 0) snprintf(buf, sizeof(buf), "%d / %lld MB", state.pool_mining.threads_active, (long long)memMB); else snprintf(buf, sizeof(buf), "%d / --", state.pool_mining.threads_active); col2Str = buf; } col2Col = OnSurface(); col3Label = "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 = "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 = "LOCAL HASHRATE"; col1Str = FormatHashrate(mining.localHashrate); col1Col = mining.generate ? greenCol : OnSurfaceDisabled(); col2Label = "NETWORK"; col2Str = FormatHashrate(mining.networkHashrate); col2Col = OnSurface(); col3Label = "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); // --- Smooth scroll: detect new data and measure interval --- size_t n = chartHistory.size(); double newestVal = chartHistory.back(); double nowTime = ImGui::GetTime(); bool dataChanged = (n != s_chart_last_n) || (newestVal != s_chart_last_newest); if (dataChanged) { float dt = (float)(nowTime - s_chart_update_time); if (dt > 0.3f && dt < 10.0f) s_chart_interval = s_chart_interval * 0.6f + dt * 0.4f; // smoothed s_chart_last_n = n; s_chart_last_newest = newestVal; s_chart_update_time = nowTime; } float elapsed = (float)(nowTime - s_chart_update_time); float scrollFrac = std::clamp(elapsed / s_chart_interval, 0.0f, 1.0f); // Build raw data points with smooth scroll offset. // Newest point is anchored at plotRight; as scrollFrac grows // the spacing compresses by one virtual slot so the next // incoming point will appear seamlessly at plotRight. float virtualSlots = (float)(n - 1) + scrollFrac; if (virtualSlots < 1.0f) virtualSlots = 1.0f; float stepW = plotW / virtualSlots; std::vector rawPts(n); for (size_t i = 0; i < n; i++) { float x = plotRight - (float)(n - 1 - i) * stepW; float y = plotBottom - (float)((chartHistory[i] - yMin) / (yMax - yMin)) * plotH; rawPts[i] = ImVec2(x, y); } // Catmull-Rom spline interpolation for smooth curve std::vector points; if (n <= 2) { points = rawPts; } else { const int subdivs = 8; // segments between each pair of data points points.reserve((n - 1) * subdivs + 1); for (size_t i = 0; i + 1 < n; i++) { // Four control points: p0, p1, p2, p3 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]; for (int s = 0; s < subdivs; s++) { float t = (float)s / (float)subdivs; float t2 = t * t; float t3 = t2 * t; // Catmull-Rom basis 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); points.push_back(ImVec2(sx, sy)); } } points.push_back(rawPts[n - 1]); // final point } // Fill under curve for (size_t i = 0; i + 1 < points.size(); i++) { ImVec2 quad[4] = { points[i], points[i + 1], ImVec2(points[i + 1].x, plotBottom), ImVec2(points[i].x, plotBottom) }; dl->AddConvexPolyFilled(quad, 4, 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 ? "5m ago" : chartHistory.size() >= 60 ? "1m ago" : "start"); std::string nowLbl = "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)); } 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::vector recentMined; for (const auto& tx : state.transactions) { if (tx.type == "generate" || tx.type == "immature" || tx.type == "mined") { double amt = std::abs(tx.amount); minedAllTime += amt; minedAllTimeCount++; if (tx.timestamp >= todayStart) { minedToday += amt; minedTodayCount++; } else if (tx.timestamp >= yesterdayStart) { minedYesterday += amt; minedYesterdayCount++; } if (recentMined.size() < 4) { recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100}); } } } // 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 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 blk)", 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 blk)", minedYesterdayCount); char allVal[64], allSub[64]; snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime); snprintf(allSub, sizeof(allSub), "(%d blk)", minedAllTimeCount); char estVal[64]; if (estActive) snprintf(estVal, sizeof(estVal), "~%.4f", estDaily); else snprintf(estVal, sizeof(estVal), "N/A"); EarningsEntry entries[] = { { "TODAY", todayVal, todaySub, greenCol2 }, { "YESTERDAY", yesterdayVal, yesterdaySub, OnSurface() }, { "ALL TIME", allVal, allSub, OnSurface() }, { "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(), "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("Click to 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("Difficulty copied"); } } // -- Block -- dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X, cy), OnSurfaceMedium(), "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("Click to copy block height"); 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("Block height copied"); } } // -- Mining Address -- dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), "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; int maxChars = std::max(8, (int)(addrAvailW / charW)); std::string truncAddr = mining_address; if ((int)truncAddr.length() > maxChars) { int half = (maxChars - 3) / 2; 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("Click to copy mining 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("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("Wallet: %.1f GB", selfRAM / 1024.0); else ImGui::Text("Wallet: %.0f MB", selfRAM); if (daemonRAM >= 1024.0) ImGui::Text("Daemon: %.1f GB (%s)", daemonRAM / 1024.0, app->getDaemonMemDiag().c_str()); else ImGui::Text("Daemon: %.0f MB (%s)", daemonRAM, app->getDaemonMemDiag().c_str()); ImGui::Separator(); ImGui::Text("System: %.1f / %.0f 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 // ============================================================ if (!recentMined.empty()) { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "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 contentH_blocks = rowH_blocks * (float)recentMined.size() + 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)); 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); 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); } 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), "%llds ago", (long long)diff); else if (diff < 3600) snprintf(buf, sizeof(buf), "%lldm ago", (long long)(diff / 60)); else if (diff < 86400) snprintf(buf, sizeof(buf), "%lldh ago", (long long)(diff / 3600)); else snprintf(buf, sizeof(buf), "%lldd 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), "Mature"); } else { snprintf(buf, sizeof(buf), "%d conf", mtx.confirmations); dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry), WithAlpha(Warning(), 200), buf); } ImGui::Dummy(ImVec2(availWidth, rH)); } 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() ? "Connected" : state.pool_mining.pool_url.c_str()) : "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)); } ImGui::EndChild(); // ##MiningScroll } } // namespace ui } // namespace dragonx