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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
353
src/ui/windows/mining_stats.cpp
Normal file
353
src/ui/windows/mining_stats.cpp
Normal file
@@ -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 <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
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<double>& 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<std::string>& 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<char*>(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<ImVec2> 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<ImVec2> 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
|
||||
24
src/ui/windows/mining_stats.h
Normal file
24
src/ui/windows/mining_stats.h
Normal file
@@ -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
|
||||
@@ -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<double>& 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<std::string>& 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<char*>(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<ImVec2> 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<ImVec2> 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)
|
||||
|
||||
Reference in New Issue
Block a user