From 47f228fa47939cc9da746ed77eddce324ac5bb56 Mon Sep 17 00:00:00 2001 From: DanS Date: Wed, 10 Jun 2026 15:47:34 -0500 Subject: [PATCH] refactor(mining): extract the Earnings card into mining_earnings.{h,cpp} (audit #10, slice 1) First incremental slice of decomposing the 2628-line mining_tab.cpp monolith (one giant RenderMiningTabContent function). The ~636-line "Earnings" card section is moved verbatim into RenderMiningEarnings(); mining_tab.cpp is now 1992 lines and calls it with the immediate-mode layout context as parameters (draw list, fonts, scale/spacing, glass spec, pool-mode flag). Behavior-preserving by construction: the body is byte-identical (the only additions are a `const bool s_pool_mode = poolMode` alias and a local scratch `buf` so the moved code keeps its original identifiers). The earnings-filter static moved with the card it belongs to. The compiler surfaced every enclosing dependency, which became explicit parameters. Verified: full-node + Windows + lite build, tests, hygiene, clean smoke start. Pending hands-on visual check of the Earnings card before extracting the next section. Co-Authored-By: Claude Opus 4.8 --- CMakeLists.txt | 1 + src/ui/windows/mining_earnings.cpp | 683 +++++++++++++++++++++++++++++ src/ui/windows/mining_earnings.h | 26 ++ src/ui/windows/mining_tab.cpp | 641 +-------------------------- 4 files changed, 713 insertions(+), 638 deletions(-) create mode 100644 src/ui/windows/mining_earnings.cpp create mode 100644 src/ui/windows/mining_earnings.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 3d1b315..fa3c166 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -442,6 +442,7 @@ set(APP_SOURCES src/ui/windows/receive_tab.cpp src/ui/windows/transactions_tab.cpp src/ui/windows/mining_tab.cpp + src/ui/windows/mining_earnings.cpp src/ui/windows/mining_benchmark.cpp src/ui/windows/mining_pool_panel.cpp src/ui/windows/mining_tab_helpers.cpp diff --git a/src/ui/windows/mining_earnings.cpp b/src/ui/windows/mining_earnings.cpp new file mode 100644 index 0000000..695a070 --- /dev/null +++ b/src/ui/windows/mining_earnings.cpp @@ -0,0 +1,683 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 + +#include "mining_earnings.h" +#include "mining_tab_helpers.h" // EstimateHoursToBlock + +#include "../../app.h" +#include "../../config/version.h" +#include "../../util/i18n.h" +#include "../../util/platform.h" +#include "../../config/settings.h" +#include "../schema/ui_schema.h" +#include "../material/type.h" +#include "../material/draw_helpers.h" +#include "../material/colors.h" +#include "../layout.h" +#include "../notifications.h" +#include "../../embedded/IconsMaterialDesign.h" +#include "imgui.h" +#include "imgui_internal.h" + +#include +#include +#include +#include +#include +#include + +namespace dragonx { +namespace ui { + +using namespace material; + +// Earnings filter: 0 = All, 1 = Solo, 2 = Pool. (Moved here with the card it belongs to.) +static int s_earnings_filter = 0; + +void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo& mining, + ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont, + float dp, float vs, float gap, float pad, bool isMiningActive, + bool poolMode, float availWidth, + const GlassPanelSpec& glassSpec, float sHdr, float gapOver) +{ + const bool s_pool_mode = poolMode; // alias: keep the moved body's identifier verbatim + char buf[128]; // section-local scratch (was shared in the parent) + // 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::string txid; + bool isPoolPayout; + }; + std::vector recentMined; + + for (const auto& tx : state.transactions) { + bool isSoloMined = (tx.type == "generate" || tx.type == "immature" || tx.type == "mined"); + bool isPoolPayout = (tx.type == "receive" + && !tx.memo.empty() + && tx.memo.find("Mining Pool payout") != std::string::npos); + if (isSoloMined || isPoolPayout) { + // Apply earnings filter + if (!app->supportsSoloMining()) { + if (s_earnings_filter == 1 && !isPoolPayout) continue; + } else { + if (s_earnings_filter == 1 && !isSoloMined) continue; + if (s_earnings_filter == 2 && !isPoolPayout) continue; + } + + double amt = std::abs(tx.amount); + minedAllTime += amt; + minedAllTimeCount++; + if (tx.timestamp >= todayStart) { + minedToday += amt; + minedTodayCount++; + } else if (tx.timestamp >= yesterdayStart) { + minedYesterday += amt; + minedYesterdayCount++; + } + // Separate solo blocks from pool payouts based on current mode + bool showInCurrentMode = s_pool_mode ? isPoolPayout : isSoloMined; + if (showInCurrentMode && recentMined.size() < 4) { + recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100, tx.txid, isPoolPayout}); + } + } + } + + // 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 filter toggle button (top-right of card) === + { + const bool soloMiningAvailable = app->supportsSoloMining(); + const char* filterLabels[] = { TR("mining_filter_all"), soloMiningAvailable ? TR("mining_solo") : TR("mining_pool"), TR("mining_pool") }; + const char* filterIcons[] = { ICON_MD_FUNCTIONS, soloMiningAvailable ? ICON_MD_MEMORY : ICON_MD_CLOUD, ICON_MD_CLOUD }; + const int filterCount = soloMiningAvailable ? 3 : 2; + const char* curIcon = filterIcons[s_earnings_filter]; + const char* curLabel = filterLabels[s_earnings_filter]; + + ImFont* icoFont = Type().iconSmall(); + float icoH = icoFont->LegacySize; + ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, curIcon); + ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, curLabel); + float padH = Layout::spacingSm(); + float btnW = padH + icoSz.x + Layout::spacingXs() + lblSz.x + padH; + float btnH = icoH + 8.0f * dp; + float btnX = cardMax.x - pad - btnW; + float btnY = cardMin.y + (earningsRowH - btnH) * 0.5f; + + ImVec2 bMin(btnX, btnY), bMax(btnX + btnW, btnY + btnH); + bool hov = material::IsRectHovered(bMin, bMax); + + // Pill background + ImU32 pillBg = s_earnings_filter != 0 + ? WithAlpha(Primary(), 60) + : WithAlpha(OnSurface(), hov ? 25 : 12); + dl->AddRectFilled(bMin, bMax, pillBg, btnH * 0.5f); + + // Icon + ImU32 icoCol = s_earnings_filter != 0 ? Primary() : (hov ? OnSurface() : OnSurfaceDisabled()); + float cx = bMin.x + padH; + float cy = bMin.y + (btnH - icoSz.y) * 0.5f; + dl->AddText(icoFont, icoFont->LegacySize, ImVec2(cx, cy), icoCol, curIcon); + + // Label + ImU32 lblCol = s_earnings_filter != 0 ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()); + float lx = cx + icoSz.x + Layout::spacingXs(); + float ly = bMin.y + (btnH - lblSz.y) * 0.5f; + dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), lblCol, curLabel); + + // Click target + ImVec2 savedCur = ImGui::GetCursorScreenPos(); + ImGui::SetCursorScreenPos(bMin); + ImGui::InvisibleButton("##EarningsFilter", ImVec2(btnW, btnH)); + if (ImGui::IsItemClicked()) { + s_earnings_filter = (s_earnings_filter + 1) % filterCount; + } + if (ImGui::IsItemHovered()) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + const char* tips[] = { + TR("mining_filter_tip_all"), + soloMiningAvailable ? TR("mining_filter_tip_solo") : TR("mining_filter_tip_pool"), + TR("mining_filter_tip_pool") + }; + ImGui::SetTooltip("%s", tips[s_earnings_filter]); + } + ImGui::SetCursorScreenPos(savedCur); + } + + // === 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 txn)", 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 txn)", minedYesterdayCount); + + char allVal[64], allSub[64]; + snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime); + snprintf(allSub, sizeof(allSub), "(%d txn)", minedAllTimeCount); + + char estVal[64]; + if (estActive) + snprintf(estVal, sizeof(estVal), "~%.4f", estDaily); + else + snprintf(estVal, sizeof(estVal), "N/A"); + + EarningsEntry entries[] = { + { TR("mining_today"), todayVal, todaySub, greenCol2 }, + { TR("mining_yesterday"), yesterdayVal, yesterdaySub, OnSurface() }, + { TR("mining_all_time"), allVal, allSub, OnSurface() }, + { TR("mining_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(), TR("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("%s", TR("mining_click_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(TR("mining_difficulty_copied")); + } + } + + // -- Block -- + dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X, cy), OnSurfaceMedium(), TR("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("%s", TR("mining_click_copy_block")); + 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(TR("mining_block_copied")); + } + } + + // -- Mining Address -- + dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), TR("mining_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; + if (charW <= 0.0f) charW = 8.0f; + int maxChars = std::max(8, (int)(addrAvailW / charW)); + std::string truncAddr = mining_address; + if ((int)truncAddr.length() > maxChars && maxChars > 5) { + int half = (maxChars - 3) / 2; + if (half > 0 && (size_t)half < truncAddr.length()) + 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("%s", TR("mining_click_copy_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(TR("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(TR("ram_wallet_gb"), selfRAM / 1024.0); + else + ImGui::Text(TR("ram_wallet_mb"), selfRAM); + if (daemonRAM >= 1024.0) + ImGui::Text(TR("ram_daemon_gb"), daemonRAM / 1024.0, app->getDaemonMemDiag().c_str()); + else + ImGui::Text(TR("ram_daemon_mb"), daemonRAM, app->getDaemonMemDiag().c_str()); + ImGui::Separator(); + ImGui::Text(TR("ram_system_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 (always shown in pool mode) + // ============================================================ + if (!recentMined.empty() || s_pool_mode) { + Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), + s_pool_mode ? TR("mining_recent_payouts") : TR("mining_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 minRows = recentMined.empty() ? 2.0f : (float)recentMined.size(); + float contentH_blocks = rowH_blocks * minRows + 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)); + + if (recentMined.empty()) { + // Empty state — card is visible but no rows yet + float emptyY = ImGui::GetCursorScreenPos().y; + float emptyX = ImGui::GetCursorScreenPos().x; + float centerX = emptyX + availWidth * 0.5f; + ImFont* icoFont = Type().iconMed(); + const char* emptyIcon = ICON_MD_HOURGLASS_EMPTY; + ImVec2 iSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, emptyIcon); + miningChildDL->AddText(icoFont, icoFont->LegacySize, + ImVec2(centerX - iSz.x * 0.5f, emptyY), + OnSurfaceDisabled(), emptyIcon); + const char* emptyMsg = s_pool_mode + ? TR("mining_no_payouts_yet") + : TR("mining_no_blocks_yet"); + ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, emptyMsg); + miningChildDL->AddText(capFont, capFont->LegacySize, + ImVec2(centerX - msgSz.x * 0.5f, emptyY + iSz.y + Layout::spacingXs()), + OnSurfaceDisabled(), emptyMsg); + } + + 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); + bool isClickable = !mtx.txid.empty(); + 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); + if (isClickable) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + } + } + + 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), TR("time_seconds_ago"), (long long)diff); + else if (diff < 3600) + snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); + else if (diff < 86400) + snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); + else + snprintf(buf, sizeof(buf), TR("time_days_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), TR("mature")); + } else { + snprintf(buf, sizeof(buf), TR("conf_count"), mtx.confirmations); + dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry), + WithAlpha(Warning(), 200), buf); + } + + // Click to open in block explorer + ImGui::SetCursorScreenPos(rMin); + char blockBtnId[32]; + snprintf(blockBtnId, sizeof(blockBtnId), "##RecentBlock%zu", mi); + ImGui::InvisibleButton(blockBtnId, ImVec2(availWidth, rH)); + if (ImGui::IsItemClicked() && !mtx.txid.empty()) { + std::string url = app->settings()->getTxExplorerUrl() + mtx.txid; + dragonx::util::Platform::openUrl(url); + } + if (ImGui::IsItemHovered() && !mtx.txid.empty()) { + ImGui::SetTooltip("%s", TR("mining_open_in_explorer")); + } + } + + 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)); + } +} + +} // namespace ui +} // namespace dragonx diff --git a/src/ui/windows/mining_earnings.h b/src/ui/windows/mining_earnings.h new file mode 100644 index 0000000..e38717d --- /dev/null +++ b/src/ui/windows/mining_earnings.h @@ -0,0 +1,26 @@ +// DragonX Wallet - ImGui Edition +// Copyright 2024-2026 The Hush Developers +// Released under the GPLv3 + +#pragma once + +#include "../../data/wallet_state.h" +#include "../material/draw_helpers.h" // GlassPanelSpec +#include "imgui.h" + +namespace dragonx { +class App; +namespace ui { + +// Renders the mining "Earnings" card (Today | Yesterday | All Time | Est. Daily). Extracted +// verbatim from the monolithic mining tab; the immediate-mode layout context (draw list, fonts, +// scale/spacing, the glass-panel spec, and the parent's pool-mode flag) is passed in so the +// rendering flows in sequence exactly as before. +void RenderMiningEarnings(App* app, const WalletState& state, const MiningInfo& mining, + ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont, + float dp, float vs, float gap, float pad, bool isMiningActive, + bool poolMode, float availWidth, + const material::GlassPanelSpec& glassSpec, float sHdr, float gapOver); + +} // namespace ui +} // namespace dragonx diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index bf43d05..4649e16 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -6,6 +6,7 @@ #include "mining_benchmark.h" #include "mining_tab_helpers.h" #include "mining_pool_panel.h" +#include "mining_earnings.h" #include "xmrig_download_dialog.h" #include "../../util/xmrig_updater.h" #include "../../app.h" @@ -45,8 +46,6 @@ static bool s_threads_initialized = false; static bool s_drag_active = false; static int s_drag_anchor_thread = 0; // thread# where drag started -// Earnings filter: 0 = All, 1 = Solo, 2 = Pool -static int s_earnings_filter = 0; static ThreadBenchmark s_benchmark; @@ -1966,642 +1965,8 @@ static void RenderMiningTabContent(App* app) // ================================================================ // 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::string txid; - bool isPoolPayout; - }; - std::vector recentMined; - - for (const auto& tx : state.transactions) { - bool isSoloMined = (tx.type == "generate" || tx.type == "immature" || tx.type == "mined"); - bool isPoolPayout = (tx.type == "receive" - && !tx.memo.empty() - && tx.memo.find("Mining Pool payout") != std::string::npos); - if (isSoloMined || isPoolPayout) { - // Apply earnings filter - if (!app->supportsSoloMining()) { - if (s_earnings_filter == 1 && !isPoolPayout) continue; - } else { - if (s_earnings_filter == 1 && !isSoloMined) continue; - if (s_earnings_filter == 2 && !isPoolPayout) continue; - } - - double amt = std::abs(tx.amount); - minedAllTime += amt; - minedAllTimeCount++; - if (tx.timestamp >= todayStart) { - minedToday += amt; - minedTodayCount++; - } else if (tx.timestamp >= yesterdayStart) { - minedYesterday += amt; - minedYesterdayCount++; - } - // Separate solo blocks from pool payouts based on current mode - bool showInCurrentMode = s_pool_mode ? isPoolPayout : isSoloMined; - if (showInCurrentMode && recentMined.size() < 4) { - recentMined.push_back({tx.timestamp, amt, tx.confirmations, tx.confirmations >= 100, tx.txid, isPoolPayout}); - } - } - } - - // 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 filter toggle button (top-right of card) === - { - const bool soloMiningAvailable = app->supportsSoloMining(); - const char* filterLabels[] = { TR("mining_filter_all"), soloMiningAvailable ? TR("mining_solo") : TR("mining_pool"), TR("mining_pool") }; - const char* filterIcons[] = { ICON_MD_FUNCTIONS, soloMiningAvailable ? ICON_MD_MEMORY : ICON_MD_CLOUD, ICON_MD_CLOUD }; - const int filterCount = soloMiningAvailable ? 3 : 2; - const char* curIcon = filterIcons[s_earnings_filter]; - const char* curLabel = filterLabels[s_earnings_filter]; - - ImFont* icoFont = Type().iconSmall(); - float icoH = icoFont->LegacySize; - ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, curIcon); - ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, curLabel); - float padH = Layout::spacingSm(); - float btnW = padH + icoSz.x + Layout::spacingXs() + lblSz.x + padH; - float btnH = icoH + 8.0f * dp; - float btnX = cardMax.x - pad - btnW; - float btnY = cardMin.y + (earningsRowH - btnH) * 0.5f; - - ImVec2 bMin(btnX, btnY), bMax(btnX + btnW, btnY + btnH); - bool hov = material::IsRectHovered(bMin, bMax); - - // Pill background - ImU32 pillBg = s_earnings_filter != 0 - ? WithAlpha(Primary(), 60) - : WithAlpha(OnSurface(), hov ? 25 : 12); - dl->AddRectFilled(bMin, bMax, pillBg, btnH * 0.5f); - - // Icon - ImU32 icoCol = s_earnings_filter != 0 ? Primary() : (hov ? OnSurface() : OnSurfaceDisabled()); - float cx = bMin.x + padH; - float cy = bMin.y + (btnH - icoSz.y) * 0.5f; - dl->AddText(icoFont, icoFont->LegacySize, ImVec2(cx, cy), icoCol, curIcon); - - // Label - ImU32 lblCol = s_earnings_filter != 0 ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()); - float lx = cx + icoSz.x + Layout::spacingXs(); - float ly = bMin.y + (btnH - lblSz.y) * 0.5f; - dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lx, ly), lblCol, curLabel); - - // Click target - ImVec2 savedCur = ImGui::GetCursorScreenPos(); - ImGui::SetCursorScreenPos(bMin); - ImGui::InvisibleButton("##EarningsFilter", ImVec2(btnW, btnH)); - if (ImGui::IsItemClicked()) { - s_earnings_filter = (s_earnings_filter + 1) % filterCount; - } - if (ImGui::IsItemHovered()) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - const char* tips[] = { - TR("mining_filter_tip_all"), - soloMiningAvailable ? TR("mining_filter_tip_solo") : TR("mining_filter_tip_pool"), - TR("mining_filter_tip_pool") - }; - ImGui::SetTooltip("%s", tips[s_earnings_filter]); - } - ImGui::SetCursorScreenPos(savedCur); - } - - // === 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 txn)", 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 txn)", minedYesterdayCount); - - char allVal[64], allSub[64]; - snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime); - snprintf(allSub, sizeof(allSub), "(%d txn)", minedAllTimeCount); - - char estVal[64]; - if (estActive) - snprintf(estVal, sizeof(estVal), "~%.4f", estDaily); - else - snprintf(estVal, sizeof(estVal), "N/A"); - - EarningsEntry entries[] = { - { TR("mining_today"), todayVal, todaySub, greenCol2 }, - { TR("mining_yesterday"), yesterdayVal, yesterdaySub, OnSurface() }, - { TR("mining_all_time"), allVal, allSub, OnSurface() }, - { TR("mining_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(), TR("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("%s", TR("mining_click_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(TR("mining_difficulty_copied")); - } - } - - // -- Block -- - dl->AddText(capFont, capFont->LegacySize, ImVec2(col2X, cy), OnSurfaceMedium(), TR("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("%s", TR("mining_click_copy_block")); - 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(TR("mining_block_copied")); - } - } - - // -- Mining Address -- - dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), TR("mining_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; - if (charW <= 0.0f) charW = 8.0f; - int maxChars = std::max(8, (int)(addrAvailW / charW)); - std::string truncAddr = mining_address; - if ((int)truncAddr.length() > maxChars && maxChars > 5) { - int half = (maxChars - 3) / 2; - if (half > 0 && (size_t)half < truncAddr.length()) - 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("%s", TR("mining_click_copy_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(TR("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(TR("ram_wallet_gb"), selfRAM / 1024.0); - else - ImGui::Text(TR("ram_wallet_mb"), selfRAM); - if (daemonRAM >= 1024.0) - ImGui::Text(TR("ram_daemon_gb"), daemonRAM / 1024.0, app->getDaemonMemDiag().c_str()); - else - ImGui::Text(TR("ram_daemon_mb"), daemonRAM, app->getDaemonMemDiag().c_str()); - ImGui::Separator(); - ImGui::Text(TR("ram_system_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 (always shown in pool mode) - // ============================================================ - if (!recentMined.empty() || s_pool_mode) { - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), - s_pool_mode ? TR("mining_recent_payouts") : TR("mining_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 minRows = recentMined.empty() ? 2.0f : (float)recentMined.size(); - float contentH_blocks = rowH_blocks * minRows + 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)); - - if (recentMined.empty()) { - // Empty state — card is visible but no rows yet - float emptyY = ImGui::GetCursorScreenPos().y; - float emptyX = ImGui::GetCursorScreenPos().x; - float centerX = emptyX + availWidth * 0.5f; - ImFont* icoFont = Type().iconMed(); - const char* emptyIcon = ICON_MD_HOURGLASS_EMPTY; - ImVec2 iSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, emptyIcon); - miningChildDL->AddText(icoFont, icoFont->LegacySize, - ImVec2(centerX - iSz.x * 0.5f, emptyY), - OnSurfaceDisabled(), emptyIcon); - const char* emptyMsg = s_pool_mode - ? TR("mining_no_payouts_yet") - : TR("mining_no_blocks_yet"); - ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, emptyMsg); - miningChildDL->AddText(capFont, capFont->LegacySize, - ImVec2(centerX - msgSz.x * 0.5f, emptyY + iSz.y + Layout::spacingXs()), - OnSurfaceDisabled(), emptyMsg); - } - - 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); - bool isClickable = !mtx.txid.empty(); - 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); - if (isClickable) { - ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); - } - } - - 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), TR("time_seconds_ago"), (long long)diff); - else if (diff < 3600) - snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); - else if (diff < 86400) - snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); - else - snprintf(buf, sizeof(buf), TR("time_days_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), TR("mature")); - } else { - snprintf(buf, sizeof(buf), TR("conf_count"), mtx.confirmations); - dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX, ry), - WithAlpha(Warning(), 200), buf); - } - - // Click to open in block explorer - ImGui::SetCursorScreenPos(rMin); - char blockBtnId[32]; - snprintf(blockBtnId, sizeof(blockBtnId), "##RecentBlock%zu", mi); - ImGui::InvisibleButton(blockBtnId, ImVec2(availWidth, rH)); - if (ImGui::IsItemClicked() && !mtx.txid.empty()) { - std::string url = app->settings()->getTxExplorerUrl() + mtx.txid; - dragonx::util::Platform::openUrl(url); - } - if (ImGui::IsItemHovered() && !mtx.txid.empty()) { - ImGui::SetTooltip("%s", TR("mining_open_in_explorer")); - } - } - - 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)); - } - } + RenderMiningEarnings(app, state, mining, dl, capFont, sub1, ovFont, dp, vs, gap, pad, + isMiningActive, s_pool_mode, availWidth, glassSpec, sHdr, gapOver); // ================================================================ // POOL CONNECTION STATUS — inline indicator (pool mode, no log)