From e21f7bf8aa7c8b20e8f069f0097c21ae3f1639cb Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 10 Jun 2026 16:03:51 -0500 Subject: [PATCH] refactor(mining): extract the Hashrate+Stats card into mining_stats.{h,cpp} (audit #10, slice 2) Second slice of decomposing mining_tab.cpp. The ~313-line "Hashrate + Stats" card (stat values + hashrate chart / live-log view) is moved verbatim into RenderMiningStats(); mining_tab.cpp is now 1680 lines (was 1992 after slice 1, 2628 originally). Body byte-identical apart from a s_pool_mode alias; the chart/log toggle statics (s_show_pool_log/s_show_solo_log) moved with the card, and the log buffer was already a function-local static. No App dependency in this section. Verified: full-node + Windows + lite build, tests, hygiene, clean smoke start. Pending hands-on visual check before the next slice. Co-Authored-By: Claude Opus 4.8 --- CMakeLists.txt | 1 + src/ui/windows/mining_stats.cpp | 353 ++++++++++++++++++++++++++++++++ src/ui/windows/mining_stats.h | 24 +++ src/ui/windows/mining_tab.cpp | 319 +---------------------------- 4 files changed, 381 insertions(+), 316 deletions(-) create mode 100644 src/ui/windows/mining_stats.cpp create mode 100644 src/ui/windows/mining_stats.h diff --git a/CMakeLists.txt b/CMakeLists.txt index fa3c166..3dfe823 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -443,6 +443,7 @@ set(APP_SOURCES src/ui/windows/transactions_tab.cpp src/ui/windows/mining_tab.cpp src/ui/windows/mining_earnings.cpp + src/ui/windows/mining_stats.cpp src/ui/windows/mining_benchmark.cpp src/ui/windows/mining_pool_panel.cpp src/ui/windows/mining_tab_helpers.cpp diff --git a/src/ui/windows/mining_stats.cpp b/src/ui/windows/mining_stats.cpp new file mode 100644 index 0000000..3f92520 --- /dev/null +++ b/src/ui/windows/mining_stats.cpp @@ -0,0 +1,353 @@ +// 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 diff --git a/src/ui/windows/mining_stats.h b/src/ui/windows/mining_stats.h new file mode 100644 index 0000000..8bc4137 --- /dev/null +++ b/src/ui/windows/mining_stats.h @@ -0,0 +1,24 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 + +#pragma once + +#include "../../data/wallet_state.h" +#include "../material/draw_helpers.h" // GlassPanelSpec +#include "imgui.h" + +namespace dragonx { +namespace ui { + +// Renders the mining "Hashrate + Stats" card (stat values on top, hashrate chart or live log +// below). Extracted verbatim from the monolithic mining tab; the immediate-mode layout context is +// passed in so the rendering flows in sequence exactly as before. `poolMode` is the parent's +// solo/pool toggle. +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 material::GlassPanelSpec& glassSpec, float chartBudgetH, bool poolMode); + +} // namespace ui +} // namespace dragonx diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index 4649e16..d25ad5f 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -7,6 +7,7 @@ #include "mining_tab_helpers.h" #include "mining_pool_panel.h" #include "mining_earnings.h" +#include "mining_stats.h" #include "xmrig_download_dialog.h" #include "../../util/xmrig_updater.h" #include "../../app.h" @@ -66,8 +67,6 @@ 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); @@ -1647,320 +1646,8 @@ static void RenderMiningTabContent(App* app) // 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)); - } + RenderMiningStats(state, mining, dl, capFont, sub1, ovFont, dp, vs, gap, pad, + availWidth, glassSpec, chartBudgetH, s_pool_mode); // ================================================================ // EARNINGS — Horizontal row card (Today | Yesterday | All Time | Est. Daily)