Files
ObsidianDragon/src/ui/windows/mining_stats.cpp
DanS 24bb32eb61 feat(mining): label the pool fee inline + widen the auto-balance card
Each pool row showed "<hashrate>  N%" with nothing saying the % is the pool fee.
Make it "<hashrate>  N% fee" and widen the left auto-balance card (25%/180dp ->
30%/210dp min) so the added text fits.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 12:53:46 -05:00

548 lines
27 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 "../../app.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../../util/pool_registry.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 <cstring>
#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;
// ---------------------------------------------------------------------------
// Sparkline chart of a hashrate history, drawn inside [regionMin, regionMax]
// (the plot area within a card, already inset by padding). Extracted so both
// the solo full-width card and the pool split card share one implementation.
// ---------------------------------------------------------------------------
static void DrawHashrateSparkline(ImDrawList* dl, ImVec2 regionMin, ImVec2 regionMax,
const std::vector<double>& chartHistory,
ImFont* capFont, float dp)
{
if (chartHistory.size() < 2) return;
const float chartBot = regionMax.y;
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;
const float plotLeft = regionMin.x;
const float plotRight = regionMax.x;
const float plotTop = regionMin.y + capFont->LegacySize + 4 * dp;
const float plotBottom = chartBot - capFont->LegacySize * 2 - 16 * dp;
const float plotW = plotRight - plotLeft;
const float plotH = std::max(1.0f, plotBottom - plotTop);
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 a smooth curve (adaptive subdivision).
std::vector<ImVec2> points;
if (n <= 2) {
points = rawPts;
} else {
points.reserve(n * 4);
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];
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);
sy = std::clamp(sy, plotTop, plotBottom);
points.push_back(ImVec2(sx, sy));
}
}
points.push_back(rawPts[n - 1]);
}
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));
}
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());
}
// ---------------------------------------------------------------------------
// Read-only, selectable/copyable log view filling [regionMin, size].
// ---------------------------------------------------------------------------
static void DrawMiningLogView(ImVec2 regionMin, float regionW, float regionH,
const std::vector<std::string>& logLines, const char* inputId)
{
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'; }
ImGui::SetCursorScreenPos(regionMin);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface()));
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0));
ImGui::PushFont(Type().body2());
ImGui::InputTextMultiline(inputId, const_cast<char*>(s_log_buf.c_str()), s_log_buf.size() + 1,
ImVec2(regionW, regionH), ImGuiInputTextFlags_ReadOnly);
ImGui::PopFont();
ImGui::PopStyleColor(2);
}
// ---------------------------------------------------------------------------
// The left pool card (pool mode only): Manual/Auto toggle, a metric stack, and
// a fixed-height scrolling pool list. Pool rows are clickable to select in
// Manual mode (routed through the parent's dirty flag → save + restart) and
// show a "mining here" dot in Auto mode. The list scrolls, so any number of
// pools keeps the card — and everything below it — at a constant height.
// ---------------------------------------------------------------------------
static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* dl,
ImFont* capFont, ImFont* sub1, ImFont* ovFont,
float dp, float gap, float pad,
ImVec2 cardMin, ImVec2 cardMax,
char (&s_pool_url)[256], bool& s_pool_settings_dirty)
{
const bool autoMode = app->settings()->getPoolSelectMode()
== config::Settings::PoolSelectMode::AutoBalance;
const float x = cardMin.x + pad;
const float w = (cardMax.x - cardMin.x) - pad * 2;
float y = cardMin.y + pad * 0.75f;
const float toggleH = sub1->LegacySize + 10 * dp;
const float rowH = sub1->LegacySize + 7 * dp;
// --- Manual | Auto-balance toggle ---
{
ImFont* segFont = Type().caption();
const char* segA = TR("mining_select_manual");
const char* segB = TR("mining_select_auto");
const float rnd = 4 * dp;
const float half = w * 0.5f;
const ImVec2 aMin(x, y), aMax(x + half, y + toggleH);
const ImVec2 bMin(x + half, y), bMax(x + w, y + toggleH);
dl->AddRectFilled(aMin, bMax, WithAlpha(OnSurface(), 15), rnd);
dl->AddRect(aMin, bMax, WithAlpha(OnSurface(), 40), rnd);
auto seg = [&](const ImVec2& mn, const ImVec2& mx, const char* label, bool selected) {
if (selected) dl->AddRectFilled(mn, mx, WithAlpha(Primary(), 180), rnd);
else if (material::IsRectHovered(mn, mx)) dl->AddRectFilled(mn, mx, WithAlpha(OnSurface(), 20), rnd);
ImVec2 sz = segFont->CalcTextSizeA(segFont->LegacySize, FLT_MAX, 0, label);
dl->AddText(segFont, segFont->LegacySize,
ImVec2(mn.x + (mx.x - mn.x - sz.x) * 0.5f, mn.y + (mx.y - mn.y - sz.y) * 0.5f),
selected ? IM_COL32(255, 255, 255, 230) : OnSurfaceMedium(), label);
};
seg(aMin, aMax, segA, !autoMode);
seg(bMin, bMax, segB, autoMode);
ImGui::SetCursorScreenPos(aMin);
ImGui::InvisibleButton("##CardSelManual", ImVec2(half, toggleH));
if (ImGui::IsItemClicked() && autoMode) {
app->settings()->setPoolSelectMode(config::Settings::PoolSelectMode::Manual);
app->settings()->save();
}
if (material::IsRectHovered(aMin, aMax)) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_manual_desc"));
}
ImGui::SetCursorScreenPos(bMin);
ImGui::InvisibleButton("##CardSelAuto", ImVec2(w - half, toggleH));
if (ImGui::IsItemClicked() && !autoMode) {
app->settings()->setPoolSelectMode(config::Settings::PoolSelectMode::AutoBalance);
app->settings()->save();
app->requestPoolBalanceRefresh();
}
if (material::IsRectHovered(bMin, bMax)) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", TR("mining_auto_balance_desc"));
}
}
y += toggleH + gap;
// --- Metric stack (label left, value right) ---
auto metricRow = [&](const char* label, const std::string& val, ImU32 valCol) {
dl->AddText(ovFont, ovFont->LegacySize,
ImVec2(x, y + (rowH - ovFont->LegacySize) * 0.5f), OnSurfaceMedium(), label);
ImVec2 vSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, val.c_str());
dl->AddText(sub1, sub1->LegacySize,
ImVec2(x + w - vSz.x, y + (rowH - sub1->LegacySize) * 0.5f), valCol, val.c_str());
y += rowH;
};
const auto& pm = state.pool_mining;
metricRow(TR("mining_local_hashrate"), FormatHashrate(pm.hashrate_10s),
pm.xmrig_running ? Success() : OnSurfaceDisabled());
metricRow(TR("mining_pool_hashrate"), FormatHashrate(pm.pool_hashrate),
pm.pool_hashrate > 0 ? OnSurface() : OnSurfaceDisabled());
{
char sharesBuf[64];
snprintf(sharesBuf, sizeof(sharesBuf), "%lld / %lld",
(long long)pm.accepted, (long long)pm.rejected);
metricRow(TR("mining_shares"), sharesBuf, OnSurface());
}
{
char uptBuf[64];
int64_t up = pm.uptime_sec;
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));
metricRow(TR("mining_uptime"), uptBuf, OnSurface());
}
y += gap * 0.5f;
// --- POOLS (N) header + Refresh ---
{
char hdr[48];
snprintf(hdr, sizeof(hdr), "%s (%d)", TR("mining_pools_header"),
(int)util::knownPools().size());
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(x, y), OnSurfaceMedium(), hdr);
float btnS = ovFont->LegacySize + 6 * dp;
ImVec2 rbMin(x + w - btnS, y - 2 * dp);
material::IconButtonStyle st;
st.color = OnSurfaceMedium();
st.hoverBg = StateHover();
st.bgRounding = 3 * dp;
st.tooltip = TR("mining_refresh");
ImGui::SetCursorScreenPos(rbMin);
if (material::IconButton("##PoolRefresh", ICON_MD_REFRESH, Type().iconSmall(), ImVec2(btnS, btnS), st))
app->requestPoolBalanceRefresh();
}
y += ovFont->LegacySize + 4 * dp;
// --- Scrollable pool list (fixed height → constant card height for any N) ---
const float listH = std::max(rowH, cardMax.y - y - pad * 0.75f);
ImGui::SetCursorScreenPos(ImVec2(x, y));
ImGui::BeginChild("##PoolList", ImVec2(w, listH), false, ImGuiWindowFlags_NoBackground);
{
ImDrawList* cdl = ImGui::GetWindowDrawList();
const auto snap = app->poolStatsSnapshot();
const util::KnownPool* current = util::findKnownPoolByUrl(app->settings()->getPoolUrl());
const float childW = ImGui::GetContentRegionAvail().x;
const float listRowH = capFont->LegacySize + 10 * dp;
for (const auto& kp : util::knownPools()) {
ImGui::PushID(kp.id.c_str());
const bool isCurrent = current && current->id == kp.id;
const auto it = snap.byId.find(kp.id);
const bool haveHr = (it != snap.byId.end() && it->second.ok);
ImVec2 rMin = ImGui::GetCursorScreenPos();
ImVec2 rMax(rMin.x + childW, rMin.y + listRowH);
bool hov = false, clk = false;
if (!autoMode) {
ImGui::InvisibleButton("##prow", ImVec2(childW, listRowH));
hov = ImGui::IsItemHovered();
clk = ImGui::IsItemClicked();
} else {
ImGui::Dummy(ImVec2(childW, listRowH));
}
if (isCurrent) cdl->AddRectFilled(rMin, rMax, WithAlpha(Primary(), 30), 3 * dp);
else if (hov) cdl->AddRectFilled(rMin, rMax, StateHover(), 3 * dp);
const float cy = rMin.y + listRowH * 0.5f;
cdl->AddCircleFilled(ImVec2(rMin.x + 6 * dp, cy), 3.0f * dp,
isCurrent ? Success() : OnSurfaceDisabled());
const float textY = rMin.y + (listRowH - capFont->LegacySize) * 0.5f;
// Show the short host label (the narrow card can't fit host:port); the
// full stratum URL is in the hover tooltip.
cdl->AddText(capFont, capFont->LegacySize, ImVec2(rMin.x + 16 * dp, textY),
isCurrent ? Primary() : OnSurface(), kp.label.c_str());
char right[64];
std::string hrStr = haveHr ? FormatHashrate(it->second.hashrateHs) : std::string("");
snprintf(right, sizeof(right), "%s %.0f%% fee", hrStr.c_str(), kp.feePercent);
ImVec2 rSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, right);
cdl->AddText(capFont, capFont->LegacySize,
ImVec2(rMax.x - rSz.x - 6 * dp, textY), OnSurfaceMedium(), right);
if (!autoMode && hov) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s", kp.stratum.c_str());
}
if (clk) {
strncpy(s_pool_url, kp.stratum.c_str(), sizeof(s_pool_url) - 1);
s_pool_url[sizeof(s_pool_url) - 1] = '\0';
s_pool_settings_dirty = true;
}
ImGui::PopID();
}
}
ImGui::EndChild();
}
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,
App* app, char (&s_pool_url)[256], bool& s_pool_settings_dirty)
{
(void)vs;
const bool s_pool_mode = poolMode;
ImU32 greenCol = Success();
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();
const std::vector<double>& chartHistory = s_pool_mode
? state.pool_mining.hashrate_history
: mining.hashrate_history;
bool hasChartContent = chartHistory.size() >= 2;
float statRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + Layout::spacingSm();
bool& showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log;
// ================================================================
// POOL MODE — split card: [ left pool card ][ chart / log ]
// ================================================================
if (s_pool_mode) {
// Left card height driven by its content so the pool list always fits;
// the whole row is at least as tall as the old stats+chart card.
const float toggleH = sub1->LegacySize + 10 * dp;
const float mRowH = sub1->LegacySize + 7 * dp;
const float listMinH = (capFont->LegacySize + 10 * dp) * 2.6f; // ~23 rows before scroll
float leftContentH = pad * 0.75f + toggleH + gap
+ mRowH * 4.0f + gap * 0.5f
+ (ovFont->LegacySize + 4 * dp) + listMinH + pad * 0.75f;
float cardH = std::max(leftContentH, statRowH + chartBudgetH + pad);
ImVec2 cardMin = ImGui::GetCursorScreenPos();
// A bit wider than the old 25%/180dp so the pool rows fit the "<hashrate> N% fee" text.
float leftW = std::clamp(availWidth * 0.30f, 210.0f * dp, availWidth * 0.45f);
float colGap = gap;
float rightW = availWidth - leftW - colGap;
ImVec2 leftMin = cardMin;
ImVec2 leftMax(cardMin.x + leftW, cardMin.y + cardH);
ImVec2 rightMin(leftMax.x + colGap, cardMin.y);
ImVec2 rightMax(cardMin.x + availWidth, cardMin.y + cardH);
DrawGlassPanel(dl, leftMin, leftMax, glassSpec);
DrawGlassPanel(dl, rightMin, rightMax, glassSpec);
RenderLeftPoolCard(app, state, dl, capFont, sub1, ovFont, dp, gap, pad,
leftMin, leftMax, s_pool_url, s_pool_settings_dirty);
// Right panel: live log (if toggled + available) else the sparkline.
if (showLogView) {
float logPad = pad * 0.5f;
DrawMiningLogView(ImVec2(rightMin.x + logPad, rightMin.y + logPad),
rightW - logPad * 2, cardH - logPad * 2,
state.pool_mining.log_lines, "##PoolLogText");
} else if (hasChartContent) {
DrawHashrateSparkline(dl,
ImVec2(rightMin.x + pad, rightMin.y + statRowH * 0.5f),
ImVec2(rightMax.x - pad, rightMax.y),
chartHistory, capFont, dp);
} else {
const char* hint = TR("mining_chart_start");
ImVec2 hs = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, hint);
dl->AddText(capFont, capFont->LegacySize,
ImVec2((rightMin.x + rightMax.x - hs.x) * 0.5f,
(rightMin.y + rightMax.y - hs.y) * 0.5f),
OnSurfaceDisabled(), hint);
}
// Chart/log toggle button, top-right of the RIGHT panel.
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;
ImVec2 btnMin(rightMax.x - pad - btnSize, rightMin.y + pad * 0.5f);
ImVec2 btnMax(btnMin.x + btnSize, btnMin.y + 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);
material::Tooltip("%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::SetCursorScreenPos(cardMin);
ImGui::Dummy(ImVec2(availWidth, cardH));
ImGui::Dummy(ImVec2(0, gap));
return;
}
// ================================================================
// SOLO MODE — full-width card: stat columns on top, chart/log below
// ================================================================
float totalCardH = statRowH + chartBudgetH + pad;
ImVec2 cardMin = ImGui::GetCursorScreenPos();
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + totalCardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
if (showLogView) {
float logPad = pad * 0.5f;
DrawMiningLogView(ImVec2(cardMin.x + logPad, cardMin.y + logPad),
availWidth - logPad * 2, totalCardH - logPad * 2,
mining.log_lines, "##SoloLogText");
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
} else {
double est_hours = EstimateHoursToBlock(mining.localHashrate, mining.networkHashrate, mining.difficulty);
const char* col1Label = TR("mining_local_hashrate");
std::string col1Str = FormatHashrate(mining.localHashrate);
ImU32 col1Col = mining.generate ? greenCol : OnSurfaceDisabled();
const char* col2Label = TR("mining_network");
std::string col2Str = FormatHashrate(mining.networkHashrate);
const char* col3Label = TR("mining_est_block");
std::string col3Str = FormatEstTime(est_hours);
int numStats = 3;
float statColW = (availWidth - pad * 2) / (float)numStats;
float sy = cardMin.y + pad * 0.5f;
struct StatEntry { const char* label; std::string value; ImU32 col; };
StatEntry stats[] = {
{ col1Label, col1Str, col1Col },
{ col2Label, col2Str, OnSurface() },
{ col3Label, col3Str, OnSurface() },
};
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.c_str());
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.c_str());
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);
}
}
}
if (hasChartContent) {
DrawHashrateSparkline(dl,
ImVec2(cardMin.x + pad, cardMin.y + statRowH),
ImVec2(cardMax.x - pad, cardMax.y),
chartHistory, capFont, dp);
}
ImGui::Dummy(ImVec2(availWidth, totalCardH));
}
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);
material::Tooltip("%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