// 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 "../../util/pool_registry.h" #include "../schema/ui_schema.h" #include "../material/type.h" #include "../material/draw_helpers.h" #include "../material/colors.h" #include "../layout.h" #include "../notifications.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include "imgui_internal.h" #include #include #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) // Mining earnings totals + the 4 most recent rows, memoised (MiningEarningsCache): the O(transactions) // pass reruns only when the tx list (WalletState::tx_revision), the filter pill, the Solo/Pool mode, the // solo capability or the local day changes, or at most every 2 s as a safety net - not every frame. // `now` stays per frame (the "x ago" labels below use it). int64_t now = (int64_t)std::time(nullptr); static MiningEarningsCache s_earningsCache; static LocalDayStartMemo s_dayStart; MiningEarningsKey earnKey; earnKey.txRevision = state.tx_revision; earnKey.txCount = state.transactions.size(); earnKey.filter = s_earnings_filter; earnKey.soloSupported = app->supportsSoloMining(); earnKey.poolMode = s_pool_mode; earnKey.todayStart = s_dayStart.get(now); const int64_t nowMs = (int64_t)std::chrono::duration_cast( std::chrono::steady_clock::now().time_since_epoch()).count(); const MiningEarningsTotals& earn = s_earningsCache.get(earnKey, state.transactions, nowMs); // The names the (unchanged) rendering code below already uses. const double minedToday = earn.today, minedYesterday = earn.yesterday, minedAllTime = earn.allTime; const int minedTodayCount = earn.todayCount, minedYesterdayCount = earn.yesterdayCount, minedAllTimeCount = earn.allTimeCount; const std::vector& recentMined = earn.recent; // Est. Daily is a statistical long-run AVERAGE of your reward for your hashrate share of the // network -- NOT a steady income. Actual rewards arrive at random (Poisson): often nothing for // long stretches, then a lump. We feed EstimateHoursToBlock a TRAILING-AVERAGE hashrate (not an // instantaneous / 10s snapshot) so a spike or a momentary dip doesn't swing the figure, and in // pool mode we subtract the known pool fee so the number is net. Pool payouts are share- // proportional, so your expected daily equals this solo-equivalent value (using the pool's // aggregate rate would show the POOL's earnings, not yours). (M-05 / A+B) double estHashrate; if (s_pool_mode) { const auto& pm = state.pool_mining; estHashrate = pm.hashrate_15m > 0.0 ? pm.hashrate_15m : pm.hashrate_60s > 0.0 ? pm.hashrate_60s : pm.hashrate_10s; // 15m avg preferred; fall back as it warms up } else { estHashrate = mining.localHashrate; if (mining.hashrate_history.size() >= 10) { // enough samples -> trailing mean double sum = 0.0; for (double h : mining.hashrate_history) sum += h; estHashrate = sum / (double)mining.hashrate_history.size(); } } 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; // Pool fee: compile-time known fee for the configured pool (<0 = unknown/synthetic pool -> gross). double estFeePct = -1.0; if (s_pool_mode) { const util::KnownPool* kp = util::findKnownPoolByUrl(state.pool_mining.pool_url); if (kp && kp->feePercent >= 0.0) estFeePct = kp->feePercent; } if (s_pool_mode && estFeePct > 0.0) estDaily *= (1.0 - estFeePct / 100.0); 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") }; material::Tooltip("%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), TR("mining_txn_count"), 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), TR("mining_txn_count"), minedYesterdayCount); char allVal[64], allSub[64]; snprintf(allVal, sizeof(allVal), "+%.4f", minedAllTime); snprintf(allSub, sizeof(allSub), TR("mining_txn_count"), minedAllTimeCount); char estVal[64]; if (estActive) snprintf(estVal, sizeof(estVal), "~%.4f", estDaily); else snprintf(estVal, sizeof(estVal), "%s", TR("grpc_na")); // Flag that Est. Daily is a long-run AVERAGE in both modes (so it doesn't read as realized // income), and whether it is net of the pool fee. Tooltip below carries the full variance note. const char* estSub = !estActive ? nullptr : (s_pool_mode && estFeePct > 0.0) ? TR("mining_est_daily_sub_net") : TR("mining_est_daily_sub"); // Neutral (not green/"income") colour for the estimate — it's a probability, not money earned. const ImU32 estCol = estActive ? OnSurfaceMedium() : OnSurfaceDisabled(); 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, estSub, estCol }, }; 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); } } // Hover tooltip over the Est. Daily column (last column): explain the Poisson variance and, // in pool mode, whether the figure is net of the fee. { float estColX = cardMin.x + pad + (numCols - 1) * colW; ImVec2 estMin(estColX, ey); ImVec2 estMax(estColX + colW, cardMin.y + earningsRowH); if (material::IsRectHovered(estMin, estMax)) { char tip[512]; if (s_pool_mode && estFeePct > 0.0) { char feeNote[160]; snprintf(feeNote, sizeof(feeNote), TR("tt_mining_est_daily_fee_fmt"), estFeePct); snprintf(tip, sizeof(tip), "%s\n%s", TR("tt_mining_est_daily"), feeNote); } else if (s_pool_mode && estFeePct < 0.0) { // fee unknown (synthetic/unmatched pool) snprintf(tip, sizeof(tip), "%s\n%s", TR("tt_mining_est_daily"), TR("tt_mining_est_daily_nofee")); } else { // solo, or a known 0%-fee pool (net == gross) snprintf(tip, sizeof(tip), "%s", TR("tt_mining_est_daily")); } material::Tooltip("%s", tip); } } } // === 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); if (material::CopyableTextOverlay("##DiffCopy", ImVec2(col1X + valOffX, cy), diffSz, capFont->LegacySize, buf, TR("mining_click_copy_difficulty"), ImVec2(Layout::spacingMd(), 4 * dp))) { 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); if (material::CopyableTextOverlay("##BlockCopy", ImVec2(col2X + valOffX, cy), blkSz, capFont->LegacySize, buf, TR("mining_click_copy_block"), ImVec2(Layout::spacingMd(), 4 * dp))) { Notifications::instance().info(TR("mining_block_copied")); } } // -- Mining Address -- dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X, cy), OnSurfaceMedium(), TR("mining_mining_addr")); { ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, TR("mining_mining_addr")); if (material::IsRectHovered(ImVec2(col3X, cy), ImVec2(col3X + lblSz.x, cy + lblSz.y))) material::Tooltip("%s", TR("mining_addr_explainer")); } 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()); if (material::CopyableTextOverlay("##MiningAddrCopy", ImVec2(col3X + valOffX, cy), addrTextSz, capFont->LegacySize, mining_address.c_str(), TR("mining_click_copy_address"), ImVec2(Layout::spacingMd(), 4 * dp))) { Notifications::instance().info(TR("mining_address_copied")); } } else { // No mining (transparent) address yet — explain where rewards go instead of a blank. dl->AddText(capFont, capFont->LegacySize, ImVec2(col3X + valOffX, cy), OnSurfaceMedium(), TR("mining_addr_none")); ImVec2 emSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, TR("mining_addr_none")); if (material::IsRectHovered(ImVec2(col3X + valOffX, cy), ImVec2(col3X + valOffX + emSz.x, cy + emSz.y))) material::Tooltip("%s", TR("mining_addr_none_tip")); } } // ---- Memory bar — centered in remaining space below details ---- { double totalRAM = dragonx::util::Platform::getTotalSystemRAM_MB(); // cached inside Platform // Used/self RAM are synchronous OS reads (/proc/meminfo + /proc/self/status on Linux): sample them at // most once a second, not every frame. The first frame after >1 s idle resamples immediately. static double s_usedRAM = 0.0, s_selfRAM = 0.0; static int64_t s_ramSampledMs = 0; static bool s_ramSampled = false; if (!s_ramSampled || IntervalElapsed(nowMs, s_ramSampledMs, 1000)) { s_usedRAM = dragonx::util::Platform::getUsedSystemRAM_MB(); s_selfRAM = dragonx::util::Platform::getSelfMemoryUsageMB(); s_ramSampledMs = nowMs; s_ramSampled = true; } double usedRAM = s_usedRAM; double selfRAM = s_selfRAM; 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), "%s", TR("grpc_na")); 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()) { material::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); material::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; // Lower bound for the panel height. When the list is empty the centred empty-state // (top pad + icon + gap + caption + bottom pad) needs more vertical room than the bare // 30*dp floor, otherwise the caption is clipped once the thread-tile grid wraps to two // rows and recentAvailH shrinks (font metrics/Layout helpers are already DPI-scaled — do // not multiply them by dp; pad is a scaled param). float recentMinH = 30.0f * dp; if (recentMined.empty()) { recentMinH = std::max(recentMinH, pad * 0.5f + Type().iconMed()->LegacySize + Layout::spacingXs() + capFont->LegacySize + pad * 0.5f); } float recentH = std::clamp(contentH_blocks, recentMinH, std::max(recentMinH, 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()) { material::Tooltip("%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