- 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).
1644 lines
80 KiB
C++
1644 lines
80 KiB
C++
// 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
|