// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "mining_stats.h" #include "mining_tab_helpers.h" // FormatHashrate, EstimateHoursToBlock #include "../../util/i18n.h" #include "../schema/ui_schema.h" #include "../material/type.h" #include "../material/draw_helpers.h" #include "../material/colors.h" #include "../layout.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include #include #include #include #include namespace dragonx { namespace ui { using namespace material; // Chart/log view toggles (false = chart, true = live log). Moved here with the card they belong to. static bool s_show_pool_log = false; static bool s_show_solo_log = false; void RenderMiningStats(const WalletState& state, const MiningInfo& mining, ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont, float dp, float vs, float gap, float pad, float availWidth, const GlassPanelSpec& glassSpec, float chartBudgetH, bool poolMode) { const bool s_pool_mode = poolMode; // alias: keep the moved body's identifier verbatim 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)); } } // namespace ui } // namespace dragonx