Files
ObsidianDragon/src/ui/windows/mining_tab.cpp
dan_s b4e09cdf75 Reduce redundant RPC calls in periodic refresh cycle
- Remove getinfo from refreshMiningInfo slow path; daemon_version,
  protocol_version, and p2p_port are static per connection (set in
  onConnected). Move longestchain/notarized into refreshBalance's
  existing getblockchaininfo callback.

- Remove refreshMiningInfo from refreshData(); it already runs on
  the 1-second fast_refresh_timer_ independently.

- Make refreshAddresses demand-driven via addresses_dirty_ flag;
  only re-fetch when a new address is created or a send completes,
  not unconditionally every 5 seconds.

- Gate refreshPeerInfo to only run when the Peers tab is active.

- Skip duplicate getwalletinfo on connect; onConnected() already
  prefetches it for immediate lock-screen display, so suppress the
  redundant call in the first refreshData() cycle.

Steady-state savings: ~8 fewer RPC calls per 5-second cycle
(from ~12+ down to ~4-5 in the common case).
2026-02-27 02:28:29 -06:00

1644 lines
80 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_tab.h"
#include "../../app.h"
#include "../../config/version.h"
#include "../../data/wallet_state.h"
#include "../../config/settings.h"
#include "../../util/platform.h"
#include "../schema/ui_schema.h"
#include "../material/type.h"
#include "../material/draw_helpers.h"
#include "../material/colors.h"
#include "../material/components/buttons.h"
#include "../effects/low_spec.h"
#include "../layout.h"
#include "../notifications.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
#include <thread>
#include <algorithm>
#include <cmath>
#include <ctime>
#include <vector>
namespace dragonx {
namespace ui {
using namespace material;
// Local UI state for thread grid
static int s_selected_threads = 0;
static bool s_threads_initialized = false;
// Drag-to-select state
static bool s_drag_active = false;
static int s_drag_anchor_thread = 0; // thread# where drag started
// Pool mode state
static bool s_pool_mode = false;
static char s_pool_url[256] = "pool.dragonx.is";
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
// Chart smooth-scroll state
static size_t s_chart_last_n = 0;
static double s_chart_last_newest = -1.0;
static double s_chart_update_time = 0.0;
static float s_chart_interval = 1.0f; // measured seconds between data updates
// Get max threads based on hardware
static int GetMaxMiningThreads()
{
int hw_threads = std::thread::hardware_concurrency();
return std::max(1, hw_threads);
}
// Format hashrate with appropriate units
static std::string FormatHashrate(double hashrate)
{
char buf[64];
if (hashrate >= 1e12) {
snprintf(buf, sizeof(buf), "%.2f TH/s", hashrate / 1e12);
} else if (hashrate >= 1e9) {
snprintf(buf, sizeof(buf), "%.2f GH/s", hashrate / 1e9);
} else if (hashrate >= 1e6) {
snprintf(buf, sizeof(buf), "%.2f MH/s", hashrate / 1e6);
} else if (hashrate >= 1e3) {
snprintf(buf, sizeof(buf), "%.2f KH/s", hashrate / 1e3);
} else {
snprintf(buf, sizeof(buf), "%.2f H/s", hashrate);
}
return std::string(buf);
}
// Calculate estimated hours to find a block
static double EstimateHoursToBlock(double localHashrate, double networkHashrate, double difficulty)
{
if (localHashrate <= 0 || networkHashrate <= 0) return 0;
double blocksPerHour = 3600.0 / 75.0;
double yourShare = localHashrate / networkHashrate;
if (yourShare <= 0) return 0;
return 1.0 / (blocksPerHour * yourShare);
}
// Format estimated time
static std::string FormatEstTime(double est_hours)
{
char buf[64];
if (est_hours <= 0) {
return "N/A";
} else if (est_hours < 1.0) {
snprintf(buf, sizeof(buf), "~%.0f min", est_hours * 60.0);
} else if (est_hours < 24.0) {
snprintf(buf, sizeof(buf), "~%.1f hrs", est_hours);
} else if (est_hours < 168.0) {
snprintf(buf, sizeof(buf), "~%.1f days", est_hours / 24.0);
} else {
snprintf(buf, sizeof(buf), "~%.1f weeks", est_hours / 168.0);
}
return std::string(buf);
}
void RenderMiningTab(App* app)
{
auto& S = schema::UI();
auto sliderInput = S.input("tabs.mining", "thread-slider");
auto startBtn = S.button("tabs.mining", "start-button");
auto lbl = S.label("tabs.mining", "label-column");
const auto& state = app->getWalletState();
const auto& mining = state.mining;
// Scrollable child to contain all content within available space
ImVec2 miningAvail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##MiningScroll", miningAvail, false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
// Responsive: scale factors per frame
float availWidth = ImGui::GetContentRegionAvail().x;
float hs = Layout::hScale(availWidth);
float vs = Layout::vScale(miningAvail.y);
float pad = Layout::cardInnerPadding();
float gap = Layout::cardGap();
const float dp = Layout::dpiScale();
auto tier = Layout::currentTier(availWidth, miningAvail.y);
(void)tier;
int max_threads = GetMaxMiningThreads();
if (!s_threads_initialized) {
s_selected_threads = mining.generate ? std::max(1, mining.genproclimit) : 1;
s_threads_initialized = true;
}
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
glassSpec.rounding = Layout::glassRounding();
ImFont* ovFont = Type().overline();
ImFont* capFont = Type().caption();
ImFont* sub1 = Type().subtitle1();
char buf[128];
// Load pool state from settings on first frame
if (!s_pool_state_loaded) {
s_pool_mode = app->settings()->getPoolMode();
strncpy(s_pool_url, app->settings()->getPoolUrl().c_str(), sizeof(s_pool_url) - 1);
strncpy(s_pool_worker, app->settings()->getPoolWorker().c_str(), sizeof(s_pool_worker) - 1);
// If pool worker is empty or placeholder, default to user's first address
std::string workerStr(s_pool_worker);
if (workerStr.empty() || workerStr == "x") {
std::string defaultAddr;
for (const auto& addr : state.addresses) {
if (addr.type == "shielded" && !addr.address.empty()) {
defaultAddr = addr.address;
break;
}
}
if (defaultAddr.empty()) {
for (const auto& addr : state.addresses) {
if (addr.type == "transparent" && !addr.address.empty()) {
defaultAddr = addr.address;
break;
}
}
}
if (!defaultAddr.empty()) {
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
}
}
s_pool_state_loaded = true;
}
// Persist pool settings when dirty and no field is active
if (s_pool_settings_dirty && !ImGui::IsAnyItemActive()) {
app->settings()->setPoolUrl(s_pool_url);
app->settings()->setPoolWorker(s_pool_worker);
app->settings()->save();
s_pool_settings_dirty = false;
}
// Determine active mining state for UI
bool isMiningActive = s_pool_mode
? state.pool_mining.xmrig_running
: mining.generate;
// ================================================================
// Proportional section budget — ensures all content fits without
// scrolling at the minimum window size (1024×775).
// ================================================================
float sHdr = ovFont->LegacySize + Layout::spacingXs()
+ ImGui::GetStyle().ItemSpacing.y * 2.0f;
float gapOver = gap + ImGui::GetStyle().ItemSpacing.y;
// 3 sections with headers (CHART+STATS, DETAILS+EARNINGS, BLOCKS)
float totalOverhead = 3.0f * (sHdr + gapOver) + 1.0f * gapOver;
float cardBudget = std::max(schema::UI().drawElement("tabs.mining", "card-budget-min").size, miningAvail.y - totalOverhead);
Layout::SectionBudget cb(cardBudget);
float controlsBudgetH = cb.allocate(0.26f, 80.0f * dp);
float chartBudgetH = cb.allocate(0.22f, 60.0f * dp);
(void)cb; // remaining budget used by combined earnings+details card
// ================================================================
// MODE TOGGLE — SOLO | POOL segmented control
// ================================================================
{
float toggleW = schema::UI().drawElement("tabs.mining", "mode-toggle-width").size * hs;
float toggleH = schema::UI().drawElement("tabs.mining", "mode-toggle-height").size;
float toggleRnd = schema::UI().drawElement("tabs.mining", "mode-toggle-rounding").size;
float totalW = toggleW * 2;
ImVec2 tMin = ImGui::GetCursorScreenPos();
ImVec2 tMax(tMin.x + totalW, tMin.y + toggleH);
// Glass background for the segmented control
dl->AddRectFilled(tMin, tMax, WithAlpha(OnSurface(), 15), toggleRnd);
dl->AddRect(tMin, tMax, WithAlpha(OnSurface(), 40), toggleRnd);
// SOLO button (left half)
ImVec2 soloMin = tMin;
ImVec2 soloMax(tMin.x + toggleW, tMax.y);
bool soloHov = material::IsRectHovered(soloMin, soloMax);
if (!s_pool_mode) {
dl->AddRectFilled(soloMin, soloMax, WithAlpha(Primary(), 180), toggleRnd);
} else if (soloHov) {
dl->AddRectFilled(soloMin, soloMax, WithAlpha(OnSurface(), 20), toggleRnd);
}
{
const char* label = "SOLO";
ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
float lx = soloMin.x + (toggleW - sz.x) * 0.5f;
float ly = soloMin.y + (toggleH - sz.y) * 0.5f;
ImU32 col = !s_pool_mode ? IM_COL32(255, 255, 255, 230) : OnSurfaceMedium();
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), col, label);
}
// POOL button (right half) — disabled when solo mining is active
bool soloMiningActive = mining.generate;
ImVec2 poolMin(tMin.x + toggleW, tMin.y);
ImVec2 poolMax = tMax;
bool poolHov = material::IsRectHovered(poolMin, poolMax);
if (s_pool_mode) {
dl->AddRectFilled(poolMin, poolMax, WithAlpha(Primary(), 180), toggleRnd);
} else if (soloMiningActive) {
// Dimmed — solo mining blocks pool mode
} else if (poolHov) {
dl->AddRectFilled(poolMin, poolMax, WithAlpha(OnSurface(), 20), toggleRnd);
}
{
const char* label = "POOL";
ImVec2 sz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
float lx = poolMin.x + (toggleW - sz.x) * 0.5f;
float ly = poolMin.y + (toggleH - sz.y) * 0.5f;
ImU32 col = s_pool_mode ? IM_COL32(255, 255, 255, 230)
: (soloMiningActive ? OnSurfaceDisabled() : OnSurfaceMedium());
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), col, label);
}
// Invisible buttons for click targets
ImGui::SetCursorScreenPos(soloMin);
ImGui::InvisibleButton("##SoloMode", ImVec2(toggleW, toggleH));
if (ImGui::IsItemClicked() && s_pool_mode) {
s_pool_mode = false;
app->settings()->setPoolMode(false);
app->settings()->save();
app->stopPoolMining();
}
if (soloHov) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetCursorScreenPos(poolMin);
ImGui::InvisibleButton("##PoolMode", ImVec2(toggleW, toggleH));
if (ImGui::IsItemClicked() && !s_pool_mode && !soloMiningActive) {
s_pool_mode = true;
app->settings()->setPoolMode(true);
app->settings()->save();
app->stopMining();
}
if (poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (poolHov && soloMiningActive && !s_pool_mode) {
ImGui::SetTooltip("Stop solo mining to use pool mining");
}
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
ImGui::Dummy(ImVec2(totalW, 0));
// Pool URL + Worker inputs inline next to toggle (pool mode only)
if (s_pool_mode && soloMiningActive) {
// Solo mining is active — show disabled message instead of inputs
float inputFrameH = ImGui::GetFrameHeight();
float vertOff = (toggleH - inputFrameH) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(tMax.x + Layout::spacingLg(), tMin.y + vertOff));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
ImGui::AlignTextToFramePadding();
ImGui::PushFont(Type().iconSmall());
ImGui::TextUnformatted(ICON_MD_INFO);
ImGui::PopFont();
ImGui::SameLine(0, Layout::spacingSm());
ImGui::PushFont(capFont);
ImGui::TextUnformatted("Stop solo mining to configure pool settings");
ImGui::PopFont();
ImGui::PopStyleColor();
} else if (s_pool_mode) {
// Position inputs to the right of the toggle
float inputFrameH = ImGui::GetFrameHeight();
float vertOff = (toggleH - inputFrameH) * 0.5f;
float inputsStartX = tMax.x + Layout::spacingLg();
ImGui::SetCursorScreenPos(ImVec2(inputsStartX, tMin.y + vertOff));
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8 * dp, 4 * dp));
// Calculate remaining width from inputs start to end of content region
float inputFrameH2 = ImGui::GetFrameHeight();
float resetBtnW = inputFrameH2; // Square button matching input height
float contentEndX = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW - Layout::spacingSm();
float urlW = std::max(60.0f, remainW * 0.30f);
float wrkW = std::max(40.0f, remainW * 0.70f);
ImGui::SetNextItemWidth(urlW);
if (ImGui::InputTextWithHint("##PoolURL", "Pool URL", s_pool_url, sizeof(s_pool_url))) {
s_pool_settings_dirty = true;
}
ImGui::SameLine(0, Layout::spacingSm());
ImGui::SetNextItemWidth(wrkW);
if (ImGui::InputTextWithHint("##PoolWorker", "Payout Address", s_pool_worker, sizeof(s_pool_worker))) {
s_pool_settings_dirty = true;
}
if (ImGui::IsItemHovered()) {
ImGui::SetTooltip("Your DRAGONX address for receiving pool payouts");
}
// Reset to defaults button (matching input height)
ImGui::SameLine(0, Layout::spacingSm());
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(inputFrameH2, inputFrameH2);
ImGui::InvisibleButton("##ResetPoolDefaults", btnSize);
bool btnHov = ImGui::IsItemHovered();
bool btnClk = ImGui::IsItemClicked();
ImDrawList* dl2 = ImGui::GetWindowDrawList();
ImVec2 btnCenter(btnPos.x + btnSize.x * 0.5f, btnPos.y + btnSize.y * 0.5f);
// Hover highlight
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Reset to defaults");
}
// Icon
ImFont* iconFont = Type().iconSmall();
const char* resetIcon = ICON_MD_REFRESH;
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, resetIcon);
dl2->AddText(iconFont, iconFont->LegacySize,
ImVec2(btnCenter.x - iconSz.x * 0.5f, btnCenter.y - iconSz.y * 0.5f),
OnSurfaceMedium(), resetIcon);
if (btnClk) {
strncpy(s_pool_url, "pool.dragonx.is", sizeof(s_pool_url) - 1);
// Default to user's first shielded address for pool payouts
std::string defaultAddr;
for (const auto& addr : state.addresses) {
if (addr.type == "shielded" && !addr.address.empty()) {
defaultAddr = addr.address;
break;
}
}
if (defaultAddr.empty()) {
// Fallback to transparent if no shielded available
for (const auto& addr : state.addresses) {
if (addr.type == "transparent" && !addr.address.empty()) {
defaultAddr = addr.address;
break;
}
}
}
strncpy(s_pool_worker, defaultAddr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true;
}
}
ImGui::PopStyleVar(2);
}
// Ensure cursor Y is at toggle bottom regardless of pool input widgets,
// so the cards below stay at the same position in both solo and pool modes.
ImGui::SetCursorScreenPos(ImVec2(tMin.x, tMax.y));
ImGui::Dummy(ImVec2(0, gap * 0.5f));
}
// ================================================================
// CONTROLS — Glass card with CPU core grid (no heading)
// ================================================================
{
// Mining button beside the controls card
float miningBtnGap = gap;
float miningBtnMaxW = availWidth * schema::UI().drawElement("tabs.mining", "btn-max-width-ratio").size;
// --- Compute thread grid layout based on controls card width ---
// Estimate controlsW first to compute cols correctly
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size, miningBtnMaxW) - miningBtnGap;
float innerW = estControlsW - pad * 2;
float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f));
float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size);
int cols = std::max(1, std::min(max_threads, (int)(innerW / (cellSz + cellGap))));
int rows = (max_threads + cols - 1) / cols;
float gridW = cols * cellSz + (cols - 1) * cellGap;
float gridH = rows * cellSz + (rows - 1) * cellGap;
// Card sections: header(label+info+RAM inline) | grid
float headerH = capFont->LegacySize + Layout::spacingXs();
float secGap = Layout::spacingLg();
// Card height from actual content, capped by proportional budget
float cardH = pad + headerH + secGap + gridH + pad;
cardH = std::clamp(cardH, schema::UI().drawElement("tabs.mining", "control-card-min-height").size, controlsBudgetH);
// Mining button — sized to match card height (square)
float miningBtnSz = cardH;
if (miningBtnSz + miningBtnGap > miningBtnMaxW)
miningBtnSz = miningBtnMaxW;
float controlsW = availWidth - miningBtnSz - miningBtnGap;
ImVec2 cardMin = ImGui::GetCursorScreenPos();
ImVec2 cardMax(cardMin.x + controlsW, cardMin.y + cardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
float curY = cardMin.y + pad;
// --- Header row: "THREADS 4 / 16" + RAM est + active indicator ---
{
ImVec2 labelPos(cardMin.x + pad, curY);
dl->AddText(ovFont, ovFont->LegacySize, labelPos, OnSurfaceMedium(), "THREADS");
float labelW = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, "THREADS").x;
snprintf(buf, sizeof(buf), " %d / %d", s_selected_threads, max_threads);
ImVec2 countPos(labelPos.x + labelW, curY);
dl->AddText(sub1, sub1->LegacySize, countPos, OnSurface(), buf);
// RAM estimate inline (after thread count)
// Model matches hush3 RandomX: shared ~2080MB dataset + ~256MB cache (allocated once),
// plus ~2MB scratchpad per mining thread VM.
{
float countW = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf).x;
double ram_estimate_mb = schema::UI().drawElement("business", "ram-base-mb").size
+ schema::UI().drawElement("business", "ram-dataset-mb").size
+ schema::UI().drawElement("business", "ram-cache-mb").size
+ s_selected_threads * schema::UI().drawElement("business", "ram-per-thread-mb").size;
double ram_estimate_gb = ram_estimate_mb / 1024.0;
snprintf(buf, sizeof(buf), " RAM ~%.1fGB", ram_estimate_gb);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(countPos.x + countW, curY + (sub1->LegacySize - capFont->LegacySize) * 0.5f),
OnSurfaceDisabled(), buf);
}
// Active mining indicator (top-right)
if (mining.generate) {
float pulse = effects::isLowSpecMode()
? schema::UI().drawElement("animations", "pulse-base-normal").size
: schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size);
ImU32 pulseCol = WithAlpha(Success(), (int)(255 * pulse));
float dotR = schema::UI().drawElement("tabs.mining", "active-dot-radius").size + 2.0f * hs;
dl->AddCircleFilled(ImVec2(cardMax.x - pad - dotR * 2, curY + dotR + 1 * dp), dotR, pulseCol);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cardMax.x - pad - dotR * 2 - 60 * hs, curY),
WithAlpha(Success(), 200), "Mining");
}
curY += headerH + secGap;
}
// --- Thread Grid (drag-to-select) ---
{
// Center the grid horizontally within the card
float gridX = cardMin.x + pad + (innerW - gridW) * 0.5f;
float gridY = curY;
bool threads_changed = false;
// Track which thread the mouse is currently over (-1 = none)
int hovered_thread = -1;
// First pass: hit-test all cells to find hovered thread
for (int i = 0; i < max_threads; i++) {
int row = i / cols;
int col = i % cols;
float cx = gridX + col * (cellSz + cellGap);
float cy = gridY + row * (cellSz + cellGap);
if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + cellSz, cy + cellSz))) {
hovered_thread = i + 1;
break;
}
}
// Show pointer cursor when hovering the thread grid
if (hovered_thread > 0)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Drag-to-select logic
if (ImGui::IsMouseClicked(0) && hovered_thread > 0) {
// Begin drag
s_drag_active = true;
s_drag_anchor_thread = hovered_thread;
s_selected_threads = hovered_thread;
threads_changed = true;
}
if (s_drag_active) {
if (ImGui::IsMouseDown(0)) {
if (hovered_thread > 0 && hovered_thread != s_selected_threads) {
// Drag selects up to the hovered thread in either direction
s_selected_threads = hovered_thread;
threads_changed = true;
}
} else {
// Mouse released — end drag
s_drag_active = false;
}
}
// Render cells
for (int i = 0; i < max_threads; i++) {
int row = i / cols;
int col = i % cols;
float cx = gridX + col * (cellSz + cellGap);
float cy = gridY + row * (cellSz + cellGap);
ImVec2 cMin(cx, cy);
ImVec2 cMax(cx + cellSz, cy + cellSz);
int threadNum = i + 1;
bool active = threadNum <= s_selected_threads;
bool hovered = (threadNum == hovered_thread);
// Determine visual state
float rounding = schema::UI().drawElement("tabs.mining", "cell-rounding").size;
if (active && mining.generate) {
// Mining + active: animated heat glow using theme colors
float t = (float)ImGui::GetTime();
float phase = t * schema::UI().drawElement("animations", "heat-glow-speed").size + i * schema::UI().drawElement("animations", "heat-glow-phase-offset").size;
float glow = effects::isLowSpecMode() ? 0.5f : 0.5f + 0.5f * (float)std::sin(phase);
// Base color from theme (--mining-heat-glow) falling back to Primary
ImU32 heatBase = schema::UI().resolveColor("var(--mining-heat-glow)", Primary());
int baseR = (heatBase >> 0) & 0xFF;
int baseG = (heatBase >> 8) & 0xFF;
int baseB = (heatBase >> 16) & 0xFF;
// Brighten toward white as glow increases
int r = std::min(255, (int)(baseR + (255 - baseR) * 0.3f * glow));
int g = std::min(255, (int)(baseG + (255 - baseG) * 0.3f * glow));
int b = std::min(255, (int)(baseB + (255 - baseB) * 0.3f * glow));
int a = (int)(180 + 60 * glow);
ImU32 fillCol = IM_COL32(r, g, b, a);
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
// Bright border
dl->AddRect(cMin, cMax, WithAlpha(Primary(), (int)(160 + 60 * glow)), rounding, 0, schema::UI().drawElement("tabs.mining", "active-cell-border-thickness").size);
} else if (active) {
// Active but not mining: solid primary fill
ImU32 pri = Primary();
int priR = (pri >> 0) & 0xFF;
int priG = (pri >> 8) & 0xFF;
int priB = (pri >> 16) & 0xFF;
ImU32 fillCol = hovered
? IM_COL32(priR, priG, priB, 220)
: IM_COL32(priR, priG, priB, 180);
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
dl->AddRect(cMin, cMax, IM_COL32(priR, priG, priB, 255), rounding, 0, schema::UI().drawElement("tabs.mining", "cell-border-thickness").size);
} else {
// Inactive: dim outline
ImU32 fillCol = hovered
? WithAlpha(OnSurface(), 25)
: WithAlpha(OnSurface(), 8);
dl->AddRectFilled(cMin, cMax, fillCol, rounding);
dl->AddRect(cMin, cMax, WithAlpha(OnSurface(), hovered ? 80 : 35), rounding);
}
// Thread number label (centered)
snprintf(buf, sizeof(buf), "%d", threadNum);
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
ImVec2 txtPos(cx + (cellSz - txtSz.x) * 0.5f, cy + (cellSz - txtSz.y) * 0.5f);
ImU32 txtCol = active ? schema::UI().resolveColor("var(--on-primary)", IM_COL32(255, 255, 255, 230)) : WithAlpha(OnSurface(), 80);
dl->AddText(capFont, capFont->LegacySize, txtPos, txtCol, buf);
}
if (threads_changed && mining.generate) {
app->startMining(s_selected_threads);
}
if (threads_changed && s_pool_mode && state.pool_mining.xmrig_running) {
app->stopPoolMining();
app->startPoolMining(s_selected_threads);
}
curY += gridH + secGap;
}
// ============================================================
// Large square mining button — right of the controls card
// ============================================================
{
float btnX = cardMin.x + controlsW + miningBtnGap;
float btnY = cardMin.y;
ImVec2 bMin(btnX, btnY);
ImVec2 bMax(btnX + miningBtnSz, btnY + cardH);
bool btnHovered = material::IsRectHovered(bMin, bMax);
bool btnClicked = btnHovered && ImGui::IsMouseClicked(0);
bool isSyncing = state.sync.syncing;
bool poolBlockedBySolo = s_pool_mode && mining.generate && !state.pool_mining.xmrig_running;
bool isToggling = app->isMiningToggleInProgress();
bool disabled = !app->isConnected() || isToggling || isSyncing || poolBlockedBySolo;
// Glass panel background with state-dependent tint
GlassPanelSpec btnGlass;
btnGlass.rounding = Layout::glassRounding();
if (isToggling) {
// Toggling: subtle pulsing glow to indicate activity
float pulse = effects::isLowSpecMode()
? 0.5f
: 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 4.0);
int glowA = (int)(15 + 25 * pulse);
btnGlass.fillAlpha = glowA;
} else if (isMiningActive) {
// Active mining: warm glow
float pulse = effects::isLowSpecMode()
? schema::UI().drawElement("animations", "pulse-base-glow").size
: schema::UI().drawElement("animations", "pulse-base-glow").size + schema::UI().drawElement("animations", "pulse-amp-glow").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-slow").size);
int glowA = (int)(20 + 30 * pulse);
btnGlass.fillAlpha = glowA;
} else {
btnGlass.fillAlpha = btnHovered ? 30 : 18;
}
DrawGlassPanel(dl, bMin, bMax, btnGlass);
// Hover highlight
if (btnHovered && !disabled) {
dl->AddRectFilled(bMin, bMax,
isMiningActive ? WithAlpha(Error(), 25) : WithAlpha(Success(), 20),
btnGlass.rounding);
}
// Draw mining icon centered in button — Material Design ICON_MD_CONSTRUCTION
{
float btnW = bMax.x - bMin.x;
float btnH = bMax.y - bMin.y;
float cx = bMin.x + btnW * 0.5f;
float cy = bMin.y + btnH * schema::UI().drawElement("tabs.mining", "button-icon-y-ratio").size; // shift up to leave room for label
if (isToggling) {
// Draw a spinning arc spinner to indicate progress
float spinnerR = std::min(btnW, btnH) * 0.18f;
float thickness = std::max(2.5f, spinnerR * 0.15f);
float time = (float)ImGui::GetTime();
// Track circle (faint)
ImU32 trackCol = WithAlpha(Primary(), 40);
dl->AddCircle(ImVec2(cx, cy), spinnerR, trackCol, 0, thickness);
// Animated arc
float rotation = fmodf(time * 2.0f * IM_PI / 1.4f, IM_PI * 2.0f);
float cycleTime = fmodf(time, 1.333f);
float arcLength = (cycleTime < 0.666f)
? (cycleTime / 0.666f) * 0.75f + 0.1f
: ((1.333f - cycleTime) / 0.666f) * 0.75f + 0.1f;
float startAngle = rotation - IM_PI * 0.5f;
float endAngle = startAngle + IM_PI * 2.0f * arcLength;
int segments = (int)(32 * arcLength) + 1;
float angleStep = (endAngle - startAngle) / segments;
ImU32 arcCol = Primary();
for (int i = 0; i < segments; i++) {
float a1 = startAngle + angleStep * i;
float a2 = startAngle + angleStep * (i + 1);
ImVec2 p1(cx + cosf(a1) * spinnerR, cy + sinf(a1) * spinnerR);
ImVec2 p2(cx + cosf(a2) * spinnerR, cy + sinf(a2) * spinnerR);
dl->AddLine(p1, p2, arcCol, thickness);
}
} else {
ImU32 iconCol;
if (disabled) {
iconCol = OnSurfaceDisabled();
} else if (isMiningActive) {
float pulse = effects::isLowSpecMode()
? schema::UI().drawElement("animations", "pulse-base-normal").size
: schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size);
iconCol = WithAlpha(Error(), (int)(200 + 55 * pulse));
} else {
iconCol = btnHovered ? WithAlpha(Success(), 255) : OnSurfaceMedium();
}
// Use XL icon for the large mining button
ImFont* iconFont = Type().iconXL();
const char* mineIcon = isMiningActive ? ICON_MD_CLOSE : ICON_MD_CONSTRUCTION;
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, mineIcon);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(cx - iconSz.x * 0.5f, cy - iconSz.y * 0.5f), iconCol, mineIcon);
}
}
// Label below icon
{
float btnW = bMax.x - bMin.x;
float btnH = bMax.y - bMin.y;
const char* label;
ImU32 lblCol;
if (isToggling) {
label = isMiningActive ? "STOPPING" : "STARTING";
// Animated dots effect via alpha pulse
float pulse = effects::isLowSpecMode()
? 0.7f
: 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 3.0);
lblCol = WithAlpha(Primary(), (int)(120 + 135 * pulse));
} else if (isMiningActive) {
label = "STOP";
lblCol = WithAlpha(Error(), 220);
} else if (disabled) {
label = "MINE";
lblCol = WithAlpha(OnSurface(), 50);
} else {
label = "MINE";
lblCol = WithAlpha(OnSurface(), 160);
}
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
float lblX = bMin.x + (btnW - lblSz.x) * 0.5f;
float lblY = bMin.y + btnH * schema::UI().drawElement("tabs.mining", "button-label-y-ratio").size;
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lblX, lblY), lblCol, label);
}
// Tooltip + pointer cursor
if (btnHovered) {
if (!disabled)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (isToggling)
ImGui::SetTooltip(isMiningActive ? "Stopping miner..." : "Starting miner...");
else if (isSyncing)
ImGui::SetTooltip("Syncing blockchain... (%.1f%%)", state.sync.verification_progress * 100.0);
else if (poolBlockedBySolo)
ImGui::SetTooltip("Stop solo mining before starting pool mining");
else
ImGui::SetTooltip(isMiningActive ? "Stop Mining" : "Start Mining");
}
// Click action — pool or solo
if (btnClicked && !disabled) {
if (s_pool_mode) {
if (state.pool_mining.xmrig_running)
app->stopPoolMining();
else
app->startPoolMining(s_selected_threads);
} else {
if (mining.generate)
app->stopMining();
else
app->startMining(s_selected_threads);
}
}
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
ImGui::Dummy(ImVec2(0, gap));
}
// ================================================================
// 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();
bool hasLogContent = s_pool_mode && !state.pool_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);
// --- Toggle button in top-right corner (pool mode only) ---
if (s_pool_mode && (hasLogContent || hasChartContent)) {
ImFont* iconFont = Type().iconSmall();
const char* toggleIcon = s_show_pool_log ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
const char* toggleTip = s_show_pool_log ? "Show hashrate chart" : "Show miner 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)) {
s_show_pool_log = !s_show_pool_log;
}
}
if (showLogView) {
// --- Full-card log view ---
float logPad = pad * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + logPad, cardMin.y + logPad));
ImGui::BeginChild("##PoolLogInCard", ImVec2(availWidth - logPad * 2, totalCardH - logPad * 2), false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_HorizontalScrollbar);
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface()));
ImFont* monoFont = Type().body2();
ImGui::PushFont(monoFont);
for (const auto& line : state.pool_mining.log_lines) {
if (!line.empty())
ImGui::TextUnformatted(line.c_str());
}
ImGui::PopFont();
ImGui::PopStyleColor();
// Auto-scroll to bottom only if user is already near the bottom
// This allows manual scrolling up to read history
float scrollY = ImGui::GetScrollY();
float scrollMaxY = ImGui::GetScrollMaxY();
if (scrollMaxY <= 0.0f || scrollY >= scrollMaxY - 20.0f * dp)
ImGui::SetScrollHereY(1.0f);
ImGui::EndChild();
// Reset cursor to end of card after the child window
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 = "POOL HASHRATE";
col1Str = FormatHashrate(state.pool_mining.hashrate_10s);
col1Col = state.pool_mining.xmrig_running ? greenCol : OnSurfaceDisabled();
col2Label = "THREADS / MEM";
{
char buf[64];
int64_t memMB = state.pool_mining.memory_used / (1024 * 1024);
if (memMB > 0)
snprintf(buf, sizeof(buf), "%d / %lld MB", state.pool_mining.threads_active, (long long)memMB);
else
snprintf(buf, sizeof(buf), "%d / --", state.pool_mining.threads_active);
col2Str = buf;
}
col2Col = OnSurface();
col3Label = "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 = "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 = "LOCAL HASHRATE";
col1Str = FormatHashrate(mining.localHashrate);
col1Col = mining.generate ? greenCol : OnSurfaceDisabled();
col2Label = "NETWORK";
col2Str = FormatHashrate(mining.networkHashrate);
col2Col = OnSurface();
col3Label = "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);
// --- Smooth scroll: detect new data and measure interval ---
size_t n = chartHistory.size();
double newestVal = chartHistory.back();
double nowTime = ImGui::GetTime();
bool dataChanged = (n != s_chart_last_n) || (newestVal != s_chart_last_newest);
if (dataChanged) {
float dt = (float)(nowTime - s_chart_update_time);
if (dt > 0.3f && dt < 10.0f)
s_chart_interval = s_chart_interval * 0.6f + dt * 0.4f; // smoothed
s_chart_last_n = n;
s_chart_last_newest = newestVal;
s_chart_update_time = nowTime;
}
float elapsed = (float)(nowTime - s_chart_update_time);
float scrollFrac = std::clamp(elapsed / s_chart_interval, 0.0f, 1.0f);
// Build raw data points with smooth scroll offset.
// Newest point is anchored at plotRight; as scrollFrac grows
// the spacing compresses by one virtual slot so the next
// incoming point will appear seamlessly at plotRight.
float virtualSlots = (float)(n - 1) + scrollFrac;
if (virtualSlots < 1.0f) virtualSlots = 1.0f;
float stepW = plotW / virtualSlots;
std::vector<ImVec2> rawPts(n);
for (size_t i = 0; i < n; i++) {
float x = plotRight - (float)(n - 1 - i) * stepW;
float y = plotBottom - (float)((chartHistory[i] - yMin) / (yMax - yMin)) * plotH;
rawPts[i] = ImVec2(x, y);
}
// Catmull-Rom spline interpolation for smooth curve
std::vector<ImVec2> points;
if (n <= 2) {
points = rawPts;
} else {
const int subdivs = 8; // segments between each pair of data points
points.reserve((n - 1) * subdivs + 1);
for (size_t i = 0; i + 1 < n; i++) {
// Four control points: p0, p1, p2, p3
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];
for (int s = 0; s < subdivs; s++) {
float t = (float)s / (float)subdivs;
float t2 = t * t;
float t3 = t2 * t;
// Catmull-Rom basis
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);
points.push_back(ImVec2(sx, sy));
}
}
points.push_back(rawPts[n - 1]); // final point
}
// Fill under curve
for (size_t i = 0; i + 1 < points.size(); i++) {
ImVec2 quad[4] = {
points[i], points[i + 1],
ImVec2(points[i + 1].x, plotBottom),
ImVec2(points[i].x, plotBottom)
};
dl->AddConvexPolyFilled(quad, 4, 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 ? "5m ago" :
chartHistory.size() >= 60 ? "1m ago" : "start");
std::string nowLbl = "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));
}
ImGui::Dummy(ImVec2(0, gap));
}
// ================================================================
// EARNINGS — Horizontal row card (Today | Yesterday | All Time | Est. Daily)
// ================================================================
{
// Gather mining transactions from state
double minedToday = 0.0, minedYesterday = 0.0, minedAllTime = 0.0;
int minedTodayCount = 0, minedYesterdayCount = 0, minedAllTimeCount = 0;
int64_t now = (int64_t)std::time(nullptr);
// Calendar-day boundaries (local time)
time_t nowT = (time_t)now;
struct tm local;
#ifdef _WIN32
localtime_s(&local, &nowT);
#else
localtime_r(&nowT, &local);
#endif
local.tm_hour = 0;
local.tm_min = 0;
local.tm_sec = 0;
int64_t todayStart = (int64_t)mktime(&local);
int64_t yesterdayStart = todayStart - 86400;
struct MinedTx {
int64_t timestamp;
double amount;
int confirmations;
bool mature;
};
std::vector<MinedTx> recentMined;
for (const auto& tx : state.transactions) {
if (tx.type == "generate" || tx.type == "immature" || tx.type == "mined") {
double amt = std::abs(tx.amount);
minedAllTime += amt;
minedAllTimeCount++;
if (tx.timestamp >= todayStart) {
minedToday += amt;
minedTodayCount++;
} else if (tx.timestamp >= yesterdayStart) {
minedYesterday += amt;
minedYesterdayCount++;
}
if (recentMined.size() < 4) {
recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100});
}
}
}
// Use pool hashrate for EST. DAILY when in pool mode
double estHashrate = s_pool_mode ? state.pool_mining.hashrate_10s : mining.localHashrate;
double est_hours_2 = EstimateHoursToBlock(estHashrate, mining.networkHashrate, mining.difficulty);
double estDailyBlocks = (est_hours_2 > 0) ? (24.0 / est_hours_2) : 0.0;
double blockReward = schema::UI().drawElement("business", "block-reward").size;
double estDaily = estDailyBlocks * blockReward;
bool estActive = isMiningActive && estDaily > 0;
ImU32 greenCol2 = Success();
// --- Combined Earnings + Details card ---
float earningsRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + pad * 1.5f;
float detailsContentH = pad * 0.5f + capFont->LegacySize + pad * 0.5f;
float barH_est = capFont->LegacySize + Layout::spacingMd() * 2.0f;
float combinedCardH = earningsRowH + detailsContentH + barH_est + pad;
combinedCardH = std::max(combinedCardH,
schema::UI().drawElement("tabs.mining", "details-card-min-height").size + earningsRowH);
ImVec2 cardMin = ImGui::GetCursorScreenPos();
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + combinedCardH);
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
// === Earnings section (top of combined card) ===
{
const int numCols = 4;
float colW = (availWidth - pad * 2) / (float)numCols;
float ey = cardMin.y + pad * 0.5f;
char valBuf[64], subBuf2[64];
struct EarningsEntry {
const char* label;
const char* value;
const char* sub;
ImU32 col;
};
snprintf(valBuf, sizeof(valBuf), "+%.4f", minedToday);
snprintf(subBuf2, sizeof(subBuf2), "(%d blk)", minedTodayCount);
char todayVal[64], todaySub[64];
strncpy(todayVal, valBuf, sizeof(todayVal));
strncpy(todaySub, subBuf2, sizeof(todaySub));
char yesterdayVal[64], yesterdaySub[64];
snprintf(yesterdayVal, sizeof(yesterdayVal), "+%.4f", minedYesterday);
snprintf(yesterdaySub, sizeof(yesterdaySub), "(%d blk)", minedYesterdayCount);
char allVal[64], allSub[64];
snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime);
snprintf(allSub, sizeof(allSub), "(%d blk)", minedAllTimeCount);
char estVal[64];
if (estActive)
snprintf(estVal, sizeof(estVal), "~%.4f", estDaily);
else
snprintf(estVal, sizeof(estVal), "N/A");
EarningsEntry entries[] = {
{ "TODAY", todayVal, todaySub, greenCol2 },
{ "YESTERDAY", yesterdayVal, yesterdaySub, OnSurface() },
{ "ALL TIME", allVal, allSub, OnSurface() },
{ "EST. DAILY", estVal, nullptr, estActive ? greenCol2 : OnSurfaceDisabled() },
};
for (int ei = 0; ei < numCols; ei++) {
float sx = cardMin.x + pad + ei * colW;
float centerX = sx + colW * 0.5f;
// Overline label (centered)
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, entries[ei].label);
dl->AddText(ovFont, ovFont->LegacySize,
ImVec2(centerX - lblSz.x * 0.5f, ey), OnSurfaceMedium(), entries[ei].label);
// Value (centered)
float valY = ey + ovFont->LegacySize + Layout::spacingXs();
ImVec2 valSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, entries[ei].value);
if (entries[ei].sub) {
// Value + sub annotation side by side, centered together
ImVec2 subSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, entries[ei].sub);
float totalW = valSz.x + 4 * dp + subSz.x;
float startX = centerX - totalW * 0.5f;
dl->AddText(sub1, sub1->LegacySize, ImVec2(startX, valY), entries[ei].col, entries[ei].value);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(startX + valSz.x + 4 * dp, valY + (sub1->LegacySize - capFont->LegacySize) * 0.5f),
OnSurfaceDisabled(), entries[ei].sub);
} else {
dl->AddText(sub1, sub1->LegacySize,
ImVec2(centerX - valSz.x * 0.5f, valY), entries[ei].col, entries[ei].value);
}
}
}
// === Separator between earnings & details ===
float earningsSepY = cardMin.y + earningsRowH;
{
float rnd = glassSpec.rounding;
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, earningsSepY),
ImVec2(cardMax.x - rnd * 0.5f, earningsSepY),
WithAlpha(OnSurface(), 15), 1.0f * dp);
}
// === Details section (below separator) ===
{
float cx = cardMin.x + pad;
float cy = earningsSepY + pad * 0.5f;
// Three equal columns: Difficulty | Block | Mining Address
float colW = availWidth / 3.0f;
float valOffX = availWidth * schema::UI().drawElement("tabs.mining", "stats-col1-value-x-ratio").size;
float col1X = cx;
float col2X = cx + colW;
float col3X = cx + colW * 2.0f;
// -- Difficulty --
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X, cy), OnSurfaceMedium(), "Difficulty");
if (mining.difficulty > 0) {
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
dl->AddText(capFont, capFont->LegacySize, ImVec2(col1X + valOffX, cy), OnSurface(), buf);
ImVec2 diffSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
ImGui::SetCursorScreenPos(ImVec2(col1X + valOffX, cy));
ImGui::InvisibleButton("##DiffCopy", ImVec2(diffSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy difficulty");
dl->AddLine(ImVec2(col1X + valOffX, cy + capFont->LegacySize + 1 * dp),
ImVec2(col1X + valOffX + diffSz.x, cy + capFont->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(buf);
Notifications::instance().info("Difficulty copied");
}
}
// -- Block --
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X, cy), OnSurfaceMedium(), "Block");
if (mining.blocks > 0) {
snprintf(buf, sizeof(buf), "%d", mining.blocks);
dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X + valOffX, cy), OnSurface(), buf);
ImVec2 blkSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf);
ImGui::SetCursorScreenPos(ImVec2(col2X + valOffX, cy));
ImGui::InvisibleButton("##BlockCopy", ImVec2(blkSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy block height");
dl->AddLine(ImVec2(col2X + valOffX, cy + capFont->LegacySize + 1 * dp),
ImVec2(col2X + valOffX + blkSz.x, cy + capFont->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(buf);
Notifications::instance().info("Block height copied");
}
}
// -- Mining Address --
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), "Mining Addr");
std::string mining_address = "";
for (const auto& addr : state.addresses) {
if (addr.type == "transparent" && !addr.address.empty()) {
mining_address = addr.address;
break;
}
}
if (!mining_address.empty()) {
float addrAvailW = colW - pad - valOffX;
float charW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, "M").x;
int maxChars = std::max(8, (int)(addrAvailW / charW));
std::string truncAddr = mining_address;
if ((int)truncAddr.length() > maxChars) {
int half = (maxChars - 3) / 2;
truncAddr = truncAddr.substr(0, half) + "..." + truncAddr.substr(truncAddr.length() - half);
}
dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X + valOffX, cy), OnSurface(), truncAddr.c_str());
ImVec2 addrTextSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, truncAddr.c_str());
ImGui::SetCursorScreenPos(ImVec2(col3X + valOffX, cy));
ImGui::InvisibleButton("##MiningAddrCopy", ImVec2(addrTextSz.x + Layout::spacingMd(), capFont->LegacySize + 4 * dp));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("Click to copy mining address");
dl->AddLine(ImVec2(col3X + valOffX, cy + capFont->LegacySize + 1 * dp),
ImVec2(col3X + valOffX + addrTextSz.x, cy + capFont->LegacySize + 1 * dp),
WithAlpha(OnSurface(), 60), 1.0f * dp);
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(mining_address.c_str());
Notifications::instance().info("Mining address copied");
}
}
}
// ---- Memory bar — centered in remaining space below details ----
{
double totalRAM = dragonx::util::Platform::getTotalSystemRAM_MB();
double usedRAM = dragonx::util::Platform::getUsedSystemRAM_MB();
double selfRAM = dragonx::util::Platform::getSelfMemoryUsageMB();
double daemonRAM = app->getDaemonMemoryUsageMB();
// Include xmrig memory when pool mining
double xmrigRAM = state.pool_mining.memory_used / (1024.0 * 1024.0); // bytes -> MB
double appRAM = selfRAM + daemonRAM + xmrigRAM; // App + daemon + xmrig combined
// Fixed bar height (text + padding)
float barH = capFont->LegacySize + Layout::spacingMd() * 2.0f;
float barRnd = barH * 0.5f; // fully rounded corners
// Details content ends here
float detailsEndY = earningsSepY + detailsContentH;
// Subtle top separator at the boundary
float rnd = glassSpec.rounding;
float stripY = detailsEndY;
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, stripY),
ImVec2(cardMax.x - rnd * 0.5f, stripY),
WithAlpha(OnSurface(), 15), 1.0f * dp);
float remainingH = cardMax.y - stripY;
float barX = cardMin.x + pad;
float barW = cardMax.x - pad - barX;
float barY = stripY + (remainingH - barH) * 0.5f;
float textY = barY + (barH - capFont->LegacySize) * 0.5f;
float textPadX = Layout::spacingMd();
// Background track
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
WithAlpha(OnSurface(), 20), barRnd);
float sysFrac = 0.0f, appFrac = 0.0f;
float sysFillW = 0.0f, appFillW = 0.0f;
// Helper: draw a fill bar that perfectly matches the track's rounded
// left edge regardless of fill width. We draw a full-width rounded
// rect (same shape as the track) but clip it to just the fill portion.
auto drawFillBar = [&](float fillW, ImU32 col) {
if (fillW <= 1.0f) return;
dl->PushClipRect(ImVec2(barX, barY), ImVec2(barX + fillW, barY + barH), true);
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
col, barRnd);
dl->PopClipRect();
};
if (usedRAM > 0 && totalRAM > 0) {
sysFrac = std::clamp((float)(usedRAM / totalRAM), 0.0f, 1.0f);
sysFillW = barW * sysFrac;
// System memory bar (subtle fill)
ImU32 ramBarCol = schema::UI().resolveColor("var(--ram-bar-system)",
IsDarkTheme() ? IM_COL32(255, 255, 255, 46) : IM_COL32(0, 0, 0, 46));
drawFillBar(sysFillW, ramBarCol);
// App+daemon memory bar (saturated accent)
if (appRAM > 0) {
appFrac = std::clamp((float)(appRAM / totalRAM), 0.0f, sysFrac);
appFillW = barW * appFrac;
ImU32 appBarCol = schema::UI().resolveColor("var(--ram-bar-app)",
ImGui::ColorConvertFloat4ToU32(ImGui::GetStyle().Colors[ImGuiCol_ButtonActive]));
drawFillBar(appFillW, appBarCol);
}
}
// --- Text overlaying the bar ---
float accentEdge = barX + appFillW;
float whiteEdge = barX + sysFillW;
// Determine text colors for bar segments
// On dark themes: white text on empty, dark on filled bars (both system and app)
// On light themes: dark text on empty/system, white on app accent bar
ImU32 barTextOnEmpty = IsDarkTheme() ? IM_COL32(255, 255, 255, 230)
: IM_COL32(30, 30, 30, 230);
ImU32 barTextOnFill = IsDarkTheme() ? IM_COL32(30, 30, 30, 230)
: IM_COL32(255, 255, 255, 230);
ImU32 barTextOnAccent = IsDarkTheme() ? IM_COL32(0, 0, 0, 210)
: IM_COL32(255, 255, 255, 230);
// App usage on the left
char appBuf[64] = {};
if (appRAM > 0) {
if (appRAM >= 1024.0)
snprintf(appBuf, sizeof(appBuf), "%.1f GB", appRAM / 1024.0);
else
snprintf(appBuf, sizeof(appBuf), "%.0f MB", appRAM);
float appTextX = barX + textPadX;
float appTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, appBuf).x;
float appTextEnd = appTextX + appTextW;
// Inside accent bar: white | inside white bar: dark | outside: white
if (appTextEnd <= accentEdge) {
dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY),
barTextOnAccent, appBuf);
} else if (appTextX >= whiteEdge) {
dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY),
barTextOnEmpty, appBuf);
} else {
// Part in accent bar: white
if (accentEdge > appTextX) {
dl->PushClipRect(ImVec2(appTextX, barY), ImVec2(accentEdge, barY + barH));
dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY),
barTextOnAccent, appBuf);
dl->PopClipRect();
}
// Part in white bar (past accent): dark
float dkS = std::max(appTextX, accentEdge);
float dkE = std::min(appTextEnd, whiteEdge);
if (dkE > dkS) {
dl->PushClipRect(ImVec2(dkS, barY), ImVec2(dkE, barY + barH));
dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY),
barTextOnFill, appBuf);
dl->PopClipRect();
}
// Part outside white bar: white
if (appTextEnd > whiteEdge) {
dl->PushClipRect(ImVec2(whiteEdge, barY), ImVec2(appTextEnd + 1, barY + barH));
dl->AddText(capFont, capFont->LegacySize, ImVec2(appTextX, textY),
barTextOnEmpty, appBuf);
dl->PopClipRect();
}
}
}
// System usage on the right
char sysBuf[64] = {};
if (usedRAM > 0 && totalRAM > 0)
snprintf(sysBuf, sizeof(sysBuf), "%.1f / %.0f GB", usedRAM / 1024.0, totalRAM / 1024.0);
else if (totalRAM > 0)
snprintf(sysBuf, sizeof(sysBuf), "-- / %.0f GB", totalRAM / 1024.0);
else
snprintf(sysBuf, sizeof(sysBuf), "N/A");
float sysTextW = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, sysBuf).x;
float sysTextX = barX + barW - textPadX - sysTextW;
if (sysTextX >= whiteEdge) {
dl->AddText(capFont, capFont->LegacySize, ImVec2(sysTextX, textY),
barTextOnEmpty, sysBuf);
} else if (sysTextX + sysTextW <= whiteEdge) {
dl->AddText(capFont, capFont->LegacySize, ImVec2(sysTextX, textY),
barTextOnFill, sysBuf);
} else {
dl->PushClipRect(ImVec2(sysTextX, barY), ImVec2(whiteEdge, barY + barH));
dl->AddText(capFont, capFont->LegacySize, ImVec2(sysTextX, textY),
barTextOnFill, sysBuf);
dl->PopClipRect();
dl->PushClipRect(ImVec2(whiteEdge, barY), ImVec2(sysTextX + sysTextW + 1, barY + barH));
dl->AddText(capFont, capFont->LegacySize, ImVec2(sysTextX, textY),
barTextOnEmpty, sysBuf);
dl->PopClipRect();
}
// Invisible button over the bar for tooltip interaction
ImGui::SetCursorScreenPos(ImVec2(barX, barY));
ImGui::InvisibleButton("##rambar", ImVec2(barW, barH));
if (ImGui::IsItemHovered()) {
ImGui::BeginTooltip();
if (selfRAM >= 1024.0)
ImGui::Text("Wallet: %.1f GB", selfRAM / 1024.0);
else
ImGui::Text("Wallet: %.0f MB", selfRAM);
if (daemonRAM >= 1024.0)
ImGui::Text("Daemon: %.1f GB (%s)", daemonRAM / 1024.0, app->getDaemonMemDiag().c_str());
else
ImGui::Text("Daemon: %.0f MB (%s)", daemonRAM, app->getDaemonMemDiag().c_str());
ImGui::Separator();
ImGui::Text("System: %.1f / %.0f GB", usedRAM / 1024.0, totalRAM / 1024.0);
ImGui::EndTooltip();
}
}
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
ImGui::Dummy(ImVec2(0, gap));
// ============================================================
// RECENT BLOCKS — last 4 mined blocks
// ============================================================
if (!recentMined.empty()) {
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT BLOCKS");
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float rowH_blocks = std::max(schema::UI().drawElement("tabs.mining", "recent-row-min-height").size, schema::UI().drawElement("tabs.mining", "recent-row-height").size * vs);
// Size to remaining space — proportional budget ensures fit
float recentAvailH = ImGui::GetContentRegionAvail().y - sHdr - gapOver;
float contentH_blocks = rowH_blocks * (float)recentMined.size() + pad * 2.5f;
float recentH = std::clamp(contentH_blocks, 30.0f * dp, std::max(30.0f * dp, recentAvailH));
// Glass panel wrapping the list + scroll-edge mask state
ImVec2 recentPanelMin = ImGui::GetCursorScreenPos();
ImVec2 recentPanelMax(recentPanelMin.x + availWidth, recentPanelMin.y + recentH);
GlassPanelSpec recentGlass;
recentGlass.rounding = Layout::glassRounding();
DrawGlassPanel(dl, recentPanelMin, recentPanelMax, recentGlass);
float miningScrollY = 0.0f, miningScrollMaxY = 0.0f;
int miningParentVtx = dl->VtxBuffer.Size;
ImGui::BeginChild("##RecentBlocks", ImVec2(availWidth, recentH), false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
ImDrawList* miningChildDL = ImGui::GetWindowDrawList();
int miningChildVtx = miningChildDL->VtxBuffer.Size;
miningScrollY = ImGui::GetScrollY();
miningScrollMaxY = ImGui::GetScrollMaxY();
// Top padding inside glass card
ImGui::Dummy(ImVec2(0, pad * 0.5f));
for (size_t mi = 0; mi < recentMined.size(); mi++) {
const auto& mtx = recentMined[mi];
ImVec2 rMin = ImGui::GetCursorScreenPos();
float rH = std::max(schema::UI().drawElement("tabs.mining", "recent-row-min-height").size, schema::UI().drawElement("tabs.mining", "recent-row-height").size * vs);
ImVec2 rMax(rMin.x + availWidth, rMin.y + rH);
// Subtle background on hover (inset from card edges)
bool hovered = material::IsRectHovered(rMin, rMax);
if (hovered) {
dl->AddRectFilled(ImVec2(rMin.x + pad * 0.5f, rMin.y),
ImVec2(rMax.x - pad * 0.5f, rMax.y),
IM_COL32(255, 255, 255, 8), 3.0f * dp);
}
float rx = rMin.x + pad;
float ry = rMin.y + Layout::spacingXs();
// Mining icon — Material Design
ImFont* iconFont = Type().iconSmall();
const char* mIcon = ICON_MD_CONSTRUCTION;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, mIcon);
float iconX = rx + 2 * dp, iconY = ry + 2 * dp;
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(iconX, iconY),
WithAlpha(Warning(), 200), mIcon);
// Time
int64_t diff = now - mtx.timestamp;
if (diff < 60)
snprintf(buf, sizeof(buf), "%llds ago", (long long)diff);
else if (diff < 3600)
snprintf(buf, sizeof(buf), "%lldm ago", (long long)(diff / 60));
else if (diff < 86400)
snprintf(buf, sizeof(buf), "%lldh ago", (long long)(diff / 3600));
else
snprintf(buf, sizeof(buf), "%lldd ago", (long long)(diff / 86400));
dl->AddText(capFont, capFont->LegacySize, ImVec2(rx + iSz.x + 8 * dp, ry), OnSurfaceDisabled(), buf);
// Amount
snprintf(buf, sizeof(buf), "+%.8f %s", mtx.amount, DRAGONX_TICKER);
float amtX = rMin.x + pad + (availWidth - pad * 2) * 0.35f;
dl->AddText(capFont, capFont->LegacySize, ImVec2(amtX, ry), greenCol2, buf);
// Maturity badge — inset from right edge
float badgeX = rMax.x - pad - Layout::spacingXl() * 3.5f;
if (mtx.mature) {
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry),
WithAlpha(Success(), 180), "Mature");
} else {
snprintf(buf, sizeof(buf), "%d conf", mtx.confirmations);
dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry),
WithAlpha(Warning(), 200), buf);
}
ImGui::Dummy(ImVec2(availWidth, rH));
}
ImGui::EndChild(); // ##RecentBlocks
// CSS-style clipping mask
{
float fadeZone = std::min(capFont->LegacySize * 3.0f, recentH * 0.18f);
ApplyScrollEdgeMask(dl, miningParentVtx, miningChildDL, miningChildVtx,
recentPanelMin.y, recentPanelMax.y, fadeZone, miningScrollY, miningScrollMaxY);
}
ImGui::Dummy(ImVec2(0, gap));
}
}
// ================================================================
// POOL CONNECTION STATUS — inline indicator (pool mode, no log)
// ================================================================
if (s_pool_mode && state.pool_mining.log_lines.empty() && state.pool_mining.xmrig_running) {
ImFont* iconFont = Type().iconSmall();
const char* dotIcon = ICON_MD_CIRCLE;
ImU32 dotCol = state.pool_mining.connected ? WithAlpha(Success(), 200) : WithAlpha(Error(), 200);
const char* statusText = state.pool_mining.connected
? (state.pool_mining.pool_url.empty() ? "Connected" : state.pool_mining.pool_url.c_str())
: "Connecting...";
ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 dotSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, dotIcon);
dl->AddText(iconFont, iconFont->LegacySize, pos, dotCol, dotIcon);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(pos.x + dotSz.x + 4 * dp, pos.y + (dotSz.y - capFont->LegacySize) * 0.5f),
OnSurfaceMedium(), statusText);
ImGui::Dummy(ImVec2(availWidth, capFont->LegacySize + 4 * dp));
}
ImGui::EndChild(); // ##MiningScroll
}
} // namespace ui
} // namespace dragonx