Files
ObsidianDragon/src/ui/windows/mining_stats.cpp
DanS d27f387d6d feat(ui): in-app FAQ, DPI-scaling audit fixes, mining/stratum polish + localize strings
Bundles the session's UI work (the touched files carry several of these
changes together, so they are committed as one coherent UI batch):

- FAQ: new RenderFaqDialog + data-driven faq_content, opened from a
  status-bar "?" (and the Windows title bar), styled like the Wallets
  modal with search, Wallet/Daemon tabs, and smooth scroll.
- DPI/font-scale audit: multiply hand-drawn absolute geometry by
  Layout::dpiScale() across ~30 files so nothing renders native-size at
  HiDPI / font_scale 1.5 (verified with a full sweep at 1.5x).
- Mining: chart now fills the horizontal space; thread stepper +/- buttons
  match the input-box height; move the stratum-host toggle into
  Node & Security (v1.3.0+).
- Settings: fix the auto-shield status text overlapping the grid.
- Sidebar: drop the peer-count badge on the Network button.
- i18n: wrap 193 hardcoded literals with TR() (keys/translations added in
  the preceding i18n commit), so the security/PIN/lock flow, seed-backup
  wizard, and witness-rebuild dialog localize.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-09-01 19:37:16 -05:00

584 lines
29 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;
// The pool list = official pools user-saved favorites the current custom pool.
const auto effective = util::effectivePools(app->settings()->getPoolUrl(),
app->settings()->getSavedPoolUrls());
// --- POOLS (N) header + Refresh ---
{
char hdr[48];
snprintf(hdr, sizeof(hdr), "%s (%d)", TR("mining_pools_header"),
(int)effective.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::findPoolByUrl(effective, app->settings()->getPoolUrl());
const float childW = ImGui::GetContentRegionAvail().x;
const float listRowH = capFont->LegacySize + 10 * dp;
for (const auto& kp : effective) {
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;
// Build the right-side "<hashrate> N% fee" run first so we know its width
// and can bound (and ellipsis-truncate) the left host label to avoid a
// collision — both text runs grow ~1.5x at font_scale 1.5 while the card
// width barely does.
char right[64];
std::string hrStr = haveHr ? FormatHashrate(it->second.hashrateHs) : std::string("");
// Prefer the live fee the pool reports; fall back to the compile-time
// KnownPool.feePercent. A synthetic user pool has an unknown (<0) fee, so
// we show just its hashrate placeholder for it.
double feePct = (it != snap.byId.end() && it->second.feePercent >= 0.0)
? it->second.feePercent
: kp.feePercent;
if (feePct >= 0.0)
snprintf(right, sizeof(right), TR("grpc_hashrate_fee"), hrStr.c_str(),
FormatFeePercent(feePct).c_str());
else
snprintf(right, sizeof(right), "%s", hrStr.c_str());
ImVec2 rSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, right);
// Show the short host label (the narrow card can't fit host:port); the
// full stratum URL is in the hover tooltip. Truncate it to the gap left
// of the right-side run so a long hostname can't overlap the hashrate.
const float labelX = rMin.x + 16 * dp;
const float labelMaxW = (rMax.x - rSz.x - 6 * dp - gap) - labelX;
std::string label = TruncateToWidth(kp.label, capFont, capFont->LegacySize, labelMaxW);
cdl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, textY),
isCurrent ? Primary() : OnSurface(), label.c_str());
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.
// Ratio ceiling (0.45) never binds on a wide window, so give leftW an
// absolute cap too — the pool card doesn't need to be enormous.
float leftW = std::clamp(availWidth * 0.30f, 210.0f * dp, 440.0f * dp);
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);
// rightW is uncapped, so the chart/hint band it draws would sprawl the
// full panel on a wide window. Constrain just the drawn content to a
// centered ~1000dp band inside the panel's padded content region; the
// glass panel itself still fills rightW, leftover -> side margin.
const float rightContentW = rightW - pad * 2.0f;
// Fill the panel's content width so the chart extends across the whole panel on a wide window.
// (Previously capped at ~1000dp and centered, which left large empty margins either side.)
const float chartBandW = rightContentW;
const float chartBandX = rightMin.x + pad;
// 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(chartBandX, rightMin.y + statRowH * 0.5f),
ImVec2(chartBandX + chartBandW, 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(chartBandX + (chartBandW - 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