Files
ObsidianDragon/src/ui/windows/mining_tab.cpp
dan_s d684db446e Refactor app services and stabilize refresh/UI flows
- Add refresh scheduler and network refresh service boundaries for typed
  refresh results, ordered RPC collectors, applicators, and price parsing.
- Add daemon lifecycle and wallet security workflow helpers while preserving
  App-owned command RPC, decrypt, cancellation, and UI handoff behavior.
- Split balance, console, mining, amount formatting, and async task logic into
  focused modules with expanded Phase 4 test coverage.
- Fix market price loading by triggering price refresh immediately, avoiding
  queue-pressure drops, tracking loading/error state, and adding translations.
- Polish send, explorer, peers, settings, theme/schema, and related tab UI.
- Replace checked-in generated language headers with build-generated resources.
- Document the cleanup audit, UI static-state guidance, and architecture updates.
2026-04-29 12:47:57 -05:00

2545 lines
132 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#include "mining_tab.h"
#include "mining_benchmark.h"
#include "mining_tab_helpers.h"
#include "mining_pool_panel.h"
#include "../../app.h"
#include "../../util/i18n.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 "imgui_internal.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
// Earnings filter: 0 = All, 1 = Solo, 2 = Pool
static int s_earnings_filter = 0;
static ThreadBenchmark s_benchmark;
bool IsMiningBenchmarkActive() {
return s_benchmark.active();
}
// Pool mode state
static bool s_pool_mode = false;
static char s_pool_url[256] = "pool.dragonx.is:3433";
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
static bool s_show_solo_log = false; // Toggle: false=chart, true=log (solo mode)
static void RenderMiningTabContent(App* app);
void RenderMiningTab(App* app)
{
// Scrollable child to contain all content within available space
ImVec2 miningAvail = ImGui::GetContentRegionAvail();
ImGui::BeginChild("##MiningScroll", miningAvail, false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
ImGuiErrorRecoveryState erState;
ImGui::ErrorRecoveryStoreState(&erState);
try {
RenderMiningTabContent(app);
} catch (const std::exception& e) {
DEBUG_LOGF("[MiningTab] Exception: %s\n", e.what());
ImGui::ErrorRecoveryTryToRecoverState(&erState);
} catch (...) {
DEBUG_LOGF("[MiningTab] Unknown exception\n");
ImGui::ErrorRecoveryTryToRecoverState(&erState);
}
ImGui::EndChild(); // ##MiningScroll
}
static void RenderMiningTabContent(App* app)
{
auto& S = schema::UI();
ImVec2 miningAvail = ImGui::GetContentRegionAvail();
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;
// 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) {
int saved = app->settings()->getPoolThreads();
if (mining.generate)
s_selected_threads = ClampMiningThreads(mining.genproclimit, max_threads);
else if (saved > 0)
s_selected_threads = ClampMiningThreads(saved, max_threads);
else
s_selected_threads = 1;
s_threads_initialized = true;
}
// Sync thread grid with actual count when idle thread scaling adjusts threads
// Skip during benchmark — the benchmark controls thread counts directly
if (app->settings()->getMineWhenIdle() && app->settings()->getIdleThreadScaling() && !s_drag_active && !IsMiningBenchmarkActive()) {
if (s_pool_mode && state.pool_mining.xmrig_running) {
// Use the requested thread count (available immediately) rather
// than threads_active from the xmrig API which lags during restarts.
int reqThreads = app->getXmrigRequestedThreads();
if (reqThreads > 0)
s_selected_threads = ClampMiningThreads(reqThreads, max_threads);
else if (state.pool_mining.threads_active > 0)
s_selected_threads = ClampMiningThreads(state.pool_mining.threads_active, max_threads);
} else if (mining.generate && mining.genproclimit > 0) {
s_selected_threads = ClampMiningThreads(mining.genproclimit, max_threads);
}
}
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
glassSpec.rounding = Layout::glassRounding();
ImFont* ovFont = Type().overline();
ImFont* capFont = Type().caption();
ImFont* sub1 = Type().subtitle1();
if (!ovFont || !capFont || !sub1) {
return;
}
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);
s_pool_state_loaded = true;
}
// Default pool worker to user's first shielded (z) address once available.
// For new wallets without a z-address, leave the field blank so the user
// is prompted to generate one before mining.
{
static bool s_pool_worker_defaulted = false;
std::string workerStr(s_pool_worker);
if (shouldDefaultPoolWorker(workerStr, s_pool_worker_defaulted) && !state.addresses.empty()) {
std::string defaultAddr = defaultPoolWorkerAddress(state.addresses);
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_settings_dirty = true;
} else {
// No z-address yet — clear the placeholder "x" so field shows empty
s_pool_worker[0] = '\0';
s_pool_settings_dirty = true;
}
s_pool_worker_defaulted = 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;
// Auto-restart pool miner if it is currently running so the new
// URL / worker address takes effect immediately.
if (state.pool_mining.xmrig_running) {
app->stopPoolMining();
app->startPoolMining(s_selected_threads);
}
}
// Determine active mining state for UI
// Include pool mining running state even when user just switched to solo,
// so the button shows STOP/STOPPING while xmrig shuts down.
bool isMiningActive = IsPoolMiningActive(s_pool_mode,
state.pool_mining.xmrig_running,
mining.generate);
// ================================================================
// Thread Benchmark state machine — runs pool mining at each candidate
// thread count to find the optimal setting for this CPU.
// ================================================================
if (s_benchmark.active()) {
auto benchmarkUpdate = AdvanceThreadBenchmark(
s_benchmark, ImGui::GetIO().DeltaTime, state.pool_mining.hashrate_10s);
if (benchmarkUpdate.stopPoolMining) {
app->stopPoolMining();
}
if (benchmarkUpdate.saveOptimalThreads) {
s_selected_threads = benchmarkUpdate.optimalThreads;
app->settings()->setPoolThreads(s_selected_threads);
app->settings()->save();
}
if (benchmarkUpdate.startPoolMining) {
app->startPoolMining(benchmarkUpdate.startThreads);
}
}
// ================================================================
// 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 = TR("mining_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 = TR("mining_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();
// Note: soloMiningActive is already false (checked above),
// so no need to call stopMining() — it would just set the
// toggle-in-progress flag and make the button show "STARTING".
}
if (poolHov && !soloMiningActive) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (poolHov && soloMiningActive && !s_pool_mode) {
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
}
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(TR("mining_stop_solo_for_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));
float inputFrameH2 = ImGui::GetFrameHeight();
float iconBtnW = inputFrameH2;
float resetBtnW = iconBtnW;
float contentEndX = ImGui::GetWindowPos().x + ImGui::GetWindowContentRegionMax().x;
// Each input group: [input][▼][bookmark]
// Layout: [URL group] [spacing] [Worker group] [spacing] [reset]
float perGroupExtra = iconBtnW * 2; // dropdown + bookmark
float remainW = contentEndX - inputsStartX - Layout::spacingSm() - resetBtnW
- Layout::spacingSm() - perGroupExtra * 2;
float urlW = std::max(60.0f, remainW * 0.30f);
float wrkW = std::max(40.0f, remainW * 0.70f);
// Track positions for popup alignment
float urlGroupStartX = ImGui::GetCursorScreenPos().x;
float urlGroupStartY = ImGui::GetCursorScreenPos().y;
float urlGroupW = urlW + perGroupExtra;
// === Pool URL input ===
ImGui::SetNextItemWidth(urlW);
if (ImGui::InputTextWithHint("##PoolURL", TR("mining_pool_url"), s_pool_url, sizeof(s_pool_url))) {
s_pool_settings_dirty = true;
}
// --- URL: Dropdown arrow button ---
ImGui::SameLine(0, 0);
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(iconBtnW, inputFrameH2);
ImGui::InvisibleButton("##PoolDropdown", 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);
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", TR("mining_saved_pools"));
}
ImFont* icoFont = Type().iconSmall();
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon);
dl2->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
OnSurfaceMedium(), dropIcon);
if (btnClk) {
ImGui::OpenPopup("##SavedPoolsPopup");
}
}
// --- URL: Bookmark button ---
ImGui::SameLine(0, 0);
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(iconBtnW, inputFrameH2);
ImGui::InvisibleButton("##SavePoolUrl", 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);
std::string currentUrl(s_pool_url);
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolUrls(), currentUrl);
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_pool_url"));
}
ImFont* icoFont = Type().iconSmall();
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
dl2->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
if (btnClk && !currentUrl.empty() && !alreadySaved) {
app->settings()->addSavedPoolUrl(currentUrl);
app->settings()->save();
}
}
// --- URL: Popup positioned below the input group ---
// Match popup width to input group; zero horizontal padding so
// item highlights are flush with the popup container edges.
ImGui::SetNextWindowPos(ImVec2(urlGroupStartX, urlGroupStartY + inputFrameH2));
ImGui::SetNextWindowSize(ImVec2(urlGroupW, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
if (ImGui::BeginPopup("##SavedPoolsPopup")) {
const auto& savedUrls = app->settings()->getSavedPoolUrls();
if (savedUrls.empty()) {
ImGui::SetCursorPosX(8 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("%s", TR("mining_no_saved_pools"));
ImGui::PopFont();
ImGui::SetCursorPosX(8 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("%s", TR("mining_click"));
ImGui::PopFont();
ImGui::SameLine(0, 2 * dp);
ImGui::PushFont(Type().iconSmall());
ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER);
ImGui::PopFont();
ImGui::SameLine(0, 2 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("%s", TR("mining_to_save"));
ImGui::PopFont();
} else {
std::string urlToRemove;
float popupInnerW = ImGui::GetContentRegionAvail().x;
float xZoneW = ImGui::GetFrameHeight();
float textPadX = 8 * dp;
ImFont* rowFont = ImGui::GetFont();
float rowFontSz = ImGui::GetFontSize();
float rowH = ImGui::GetFrameHeight();
for (const auto& url : savedUrls) {
ImGui::PushID(url.c_str());
bool isCurrent = (std::string(s_pool_url) == url);
ImVec2 rowMin = ImGui::GetCursorScreenPos();
ImVec2 rowMax(rowMin.x + popupInnerW, rowMin.y + rowH);
ImGui::InvisibleButton("##row", ImVec2(popupInnerW, rowH));
bool rowHov = ImGui::IsItemHovered();
bool rowClk = ImGui::IsItemClicked();
ImDrawList* pdl = ImGui::GetWindowDrawList();
bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - xZoneW;
// Row background — flush with popup edges
if (isCurrent)
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
if (rowHov && !inXZone)
pdl->AddRectFilled(rowMin, rowMax, StateHover());
// Item text with internal padding
float textY = rowMin.y + (rowH - rowFontSz) * 0.5f;
pdl->AddText(rowFont, rowFontSz,
ImVec2(rowMin.x + textPadX, textY),
isCurrent ? Primary() : OnSurface(), url.c_str());
// X button — flush with right edge, icon centered
{
ImVec2 xMin(rowMax.x - xZoneW, rowMin.y);
ImVec2 xMax(rowMax.x, rowMax.y);
if (inXZone) {
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", TR("mining_remove"));
} else if (rowHov) {
// Show faint X when row is hovered
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
pdl->AddText(icoF, icoF->LegacySize,
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
OnSurfaceDisabled(), xIcon);
}
// Always draw icon when hovering X zone
if (inXZone) {
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
pdl->AddText(icoF, icoF->LegacySize,
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
Error(), xIcon);
}
}
// Click handling
if (rowClk) {
if (inXZone) {
urlToRemove = url;
} else {
strncpy(s_pool_url, url.c_str(), sizeof(s_pool_url) - 1);
s_pool_url[sizeof(s_pool_url) - 1] = '\0';
s_pool_settings_dirty = true;
ImGui::CloseCurrentPopup();
}
}
if (rowHov && !inXZone)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::PopID();
}
if (!urlToRemove.empty()) {
app->settings()->removeSavedPoolUrl(urlToRemove);
app->settings()->save();
}
}
ImGui::EndPopup();
}
ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for URL popup
ImGui::SameLine(0, Layout::spacingSm());
float wrkGroupStartX = ImGui::GetCursorScreenPos().x;
float wrkGroupStartY = ImGui::GetCursorScreenPos().y;
float wrkGroupW = wrkW + perGroupExtra;
ImGui::SetNextItemWidth(wrkW);
if (ImGui::InputTextWithHint("##PoolWorker", TR("mining_payout_address"), s_pool_worker, sizeof(s_pool_worker))) {
s_pool_settings_dirty = true;
}
if (ImGui::IsItemHovered()) {
std::string currentWorkerStr(s_pool_worker);
if (currentWorkerStr.empty()) {
ImGui::SetTooltip("%s", TR("mining_generate_z_address_hint"));
} else {
ImGui::SetTooltip("%s", TR("mining_payout_tooltip"));
}
}
// --- Worker: Dropdown arrow button ---
ImGui::SameLine(0, 0);
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(iconBtnW, inputFrameH2);
ImGui::InvisibleButton("##WorkerDropdown", 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);
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", TR("mining_saved_addresses"));
}
ImFont* icoFont = Type().iconSmall();
const char* dropIcon = ICON_MD_ARROW_DROP_DOWN;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, dropIcon);
dl2->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
OnSurfaceMedium(), dropIcon);
if (btnClk) {
ImGui::OpenPopup("##SavedWorkersPopup");
}
}
// --- Worker: Bookmark button ---
ImGui::SameLine(0, 0);
{
ImVec2 btnPos = ImGui::GetCursorScreenPos();
ImVec2 btnSize(iconBtnW, inputFrameH2);
ImGui::InvisibleButton("##SaveWorkerAddr", 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);
std::string currentWorker(s_pool_worker);
bool alreadySaved = miningValueAlreadySaved(app->settings()->getSavedPoolWorkers(), currentWorker);
if (btnHov) {
dl2->AddRectFilled(btnPos, ImVec2(btnPos.x + btnSize.x, btnPos.y + btnSize.y),
StateHover(), 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", alreadySaved ? TR("mining_already_saved") : TR("mining_save_payout_address"));
}
ImFont* icoFont = Type().iconSmall();
const char* saveIcon = alreadySaved ? ICON_MD_BOOKMARK : ICON_MD_BOOKMARK_BORDER;
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, saveIcon);
dl2->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnCenter.x - icoSz.x * 0.5f, btnCenter.y - icoSz.y * 0.5f),
alreadySaved ? Primary() : OnSurfaceMedium(), saveIcon);
if (btnClk && !currentWorker.empty() && currentWorker != "x" && !alreadySaved) {
app->settings()->addSavedPoolWorker(currentWorker);
app->settings()->save();
}
}
// --- Worker: Popup positioned below the input group ---
// Popup sized to fit full z-addresses without truncation;
// zero horizontal padding so item highlights are flush with edges.
float addrPopupW = std::max(wrkGroupW, availWidth * 0.55f);
ImGui::SetNextWindowPos(ImVec2(wrkGroupStartX, wrkGroupStartY + inputFrameH2));
ImGui::SetNextWindowSize(ImVec2(addrPopupW, 0));
ImGui::PushStyleVar(ImGuiStyleVar_WindowRounding, 4.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp));
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
if (ImGui::BeginPopup("##SavedWorkersPopup")) {
const auto& savedWorkers = app->settings()->getSavedPoolWorkers();
if (savedWorkers.empty()) {
ImGui::SetCursorPosX(8 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("%s", TR("mining_no_saved_addresses"));
ImGui::PopFont();
ImGui::SetCursorPosX(8 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("%s", TR("mining_click"));
ImGui::PopFont();
ImGui::SameLine(0, 2 * dp);
ImGui::PushFont(Type().iconSmall());
ImGui::TextDisabled(ICON_MD_BOOKMARK_BORDER);
ImGui::PopFont();
ImGui::SameLine(0, 2 * dp);
ImGui::PushFont(Type().caption());
ImGui::TextDisabled("%s", TR("mining_to_save"));
ImGui::PopFont();
} else {
std::string addrToRemove;
float wPopupInnerW = ImGui::GetContentRegionAvail().x;
float wXZoneW = ImGui::GetFrameHeight();
float wTextPadX = 8 * dp;
ImFont* wRowFont = ImGui::GetFont();
float wRowFontSz = ImGui::GetFontSize();
float wRowH = ImGui::GetFrameHeight();
for (const auto& addr : savedWorkers) {
ImGui::PushID(addr.c_str());
bool isCurrent = (std::string(s_pool_worker) == addr);
ImVec2 rowMin = ImGui::GetCursorScreenPos();
ImVec2 rowMax(rowMin.x + wPopupInnerW, rowMin.y + wRowH);
ImGui::InvisibleButton("##row", ImVec2(wPopupInnerW, wRowH));
bool rowHov = ImGui::IsItemHovered();
bool rowClk = ImGui::IsItemClicked();
ImDrawList* pdl = ImGui::GetWindowDrawList();
bool inXZone = rowHov && ImGui::GetMousePos().x >= rowMax.x - wXZoneW;
// Row background — flush with popup edges
if (isCurrent)
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
if (rowHov && !inXZone)
pdl->AddRectFilled(rowMin, rowMax, StateHover());
// Full address text with internal padding
float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f;
pdl->AddText(wRowFont, wRowFontSz,
ImVec2(rowMin.x + wTextPadX, textY),
isCurrent ? Primary() : OnSurface(), addr.c_str());
// Tooltip for long addresses
if (rowHov && !inXZone)
ImGui::SetTooltip("%s", addr.c_str());
// X button — flush with right edge, icon centered
{
ImVec2 xMin(rowMax.x - wXZoneW, rowMin.y);
ImVec2 xMax(rowMax.x, rowMax.y);
if (inXZone) {
pdl->AddRectFilled(xMin, xMax, IM_COL32(255, 80, 80, 30));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", TR("mining_remove"));
} else if (rowHov) {
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
pdl->AddText(icoF, icoF->LegacySize,
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
OnSurfaceDisabled(), xIcon);
}
if (inXZone) {
ImFont* icoF = Type().iconSmall();
const char* xIcon = ICON_MD_CLOSE;
ImVec2 iSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, xIcon);
ImVec2 xCenter((xMin.x + xMax.x) * 0.5f, (xMin.y + xMax.y) * 0.5f);
pdl->AddText(icoF, icoF->LegacySize,
ImVec2(xCenter.x - iSz.x * 0.5f, xCenter.y - iSz.y * 0.5f),
Error(), xIcon);
}
}
// Click handling
if (rowClk) {
if (inXZone) {
addrToRemove = addr;
} else {
strncpy(s_pool_worker, addr.c_str(), sizeof(s_pool_worker) - 1);
s_pool_worker[sizeof(s_pool_worker) - 1] = '\0';
s_pool_settings_dirty = true;
ImGui::CloseCurrentPopup();
}
}
if (rowHov && !inXZone)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::PopID();
}
if (!addrToRemove.empty()) {
app->settings()->removeSavedPoolWorker(addrToRemove);
app->settings()->save();
}
}
ImGui::EndPopup();
}
ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for Worker popup
// === Reset to defaults button ===
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("%s", TR("mining_reset_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, defaultPoolUrl(), sizeof(s_pool_url) - 1);
// Default to user's first shielded (z) address for pool payouts.
// Leave blank if no z-address exists yet.
std::string defaultAddr = defaultPoolWorkerAddress(state.addresses);
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(), TR("mining_threads"));
float labelW = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, TR("mining_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 DragonX 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);
}
// Idle mining toggle (top-right corner of card)
float idleRightEdge = cardMax.x - pad;
{
bool idleOn = app->settings()->getMineWhenIdle();
bool threadScaling = app->settings()->getIdleThreadScaling();
ImFont* icoFont = Type().iconSmall();
const char* idleIcon = ICON_MD_SCHEDULE;
float icoH = icoFont->LegacySize;
float btnSz = icoH + 8.0f * dp;
float btnX = idleRightEdge - btnSz;
float btnY = curY + (headerH - btnSz) * 0.5f;
// Pill background when active
if (idleOn) {
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
WithAlpha(Primary(), 60), btnSz * 0.5f);
}
// Icon centered in button
ImVec2 icoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, idleIcon);
ImU32 icoCol = idleOn ? Primary() : OnSurfaceDisabled();
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnX + (btnSz - icoSz.x) * 0.5f, btnY + (btnSz - icoSz.y) * 0.5f),
icoCol, idleIcon);
// Click target (save/restore cursor so layout is unaffected)
ImVec2 savedCur = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
ImGui::InvisibleButton("##IdleMining", ImVec2(btnSz, btnSz));
if (ImGui::IsItemClicked()) {
app->settings()->setMineWhenIdle(!idleOn);
app->settings()->save();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", idleOn ? TR("mining_idle_on_tooltip") : TR("mining_idle_off_tooltip"));
}
idleRightEdge = btnX - 4.0f * dp;
// Thread scaling mode toggle (to the left of idle icon, shown when idle is on)
if (idleOn) {
const char* scaleIcon = threadScaling ? ICON_MD_TUNE : ICON_MD_POWER_SETTINGS_NEW;
float sBtnX = idleRightEdge - btnSz;
float sBtnY = btnY;
if (threadScaling) {
dl->AddRectFilled(ImVec2(sBtnX, sBtnY), ImVec2(sBtnX + btnSz, sBtnY + btnSz),
WithAlpha(Primary(), 40), btnSz * 0.5f);
}
ImVec2 sIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, scaleIcon);
ImU32 sIcoCol = threadScaling ? Primary() : OnSurfaceMedium();
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(sBtnX + (btnSz - sIcoSz.x) * 0.5f, sBtnY + (btnSz - sIcoSz.y) * 0.5f),
sIcoCol, scaleIcon);
ImGui::SetCursorScreenPos(ImVec2(sBtnX, sBtnY));
ImGui::InvisibleButton("##IdleScaleMode", ImVec2(btnSz, btnSz));
if (ImGui::IsItemClicked()) {
app->settings()->setIdleThreadScaling(!threadScaling);
app->settings()->save();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", threadScaling
? TR("mining_idle_scale_on_tooltip")
: TR("mining_idle_scale_off_tooltip"));
}
idleRightEdge = sBtnX - 4.0f * dp;
}
// GPU-aware idle toggle (to the left, when idle is on)
// When ON (default): GPU utilization >= 10% counts as "not idle"
// When OFF: unrestricted mode, only keyboard/mouse input matters
if (idleOn) {
bool gpuAware = app->settings()->getIdleGpuAware();
const char* gpuIcon = gpuAware ? ICON_MD_MONITOR : ICON_MD_MONITOR;
float gBtnX = idleRightEdge - btnSz;
float gBtnY = btnY;
if (gpuAware) {
dl->AddRectFilled(ImVec2(gBtnX, gBtnY), ImVec2(gBtnX + btnSz, gBtnY + btnSz),
WithAlpha(Primary(), 40), btnSz * 0.5f);
}
ImVec2 gIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, gpuIcon);
ImU32 gIcoCol = gpuAware ? Primary() : OnSurfaceDisabled();
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(gBtnX + (btnSz - gIcoSz.x) * 0.5f, gBtnY + (btnSz - gIcoSz.y) * 0.5f),
gIcoCol, gpuIcon);
ImGui::SetCursorScreenPos(ImVec2(gBtnX, gBtnY));
ImGui::InvisibleButton("##IdleGpuAware", ImVec2(btnSz, btnSz));
if (ImGui::IsItemClicked()) {
app->settings()->setIdleGpuAware(!gpuAware);
app->settings()->save();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", gpuAware
? TR("mining_idle_gpu_on_tooltip")
: TR("mining_idle_gpu_off_tooltip"));
}
idleRightEdge = gBtnX - 4.0f * dp;
}
// Idle delay combo (to the left, when idle is enabled and NOT in thread scaling mode)
if (idleOn && !threadScaling) {
struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
};
int curDelay = app->settings()->getMineIdleDelay();
const char* previewLabel = "2m";
for (const auto& d : delays) {
if (d.seconds == curDelay) { previewLabel = d.label; break; }
}
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleDelay", previewLabel, ImGuiComboFlags_NoArrowButton)) {
for (const auto& d : delays) {
bool selected = (d.seconds == curDelay);
if (ImGui::Selectable(d.label, selected)) {
app->settings()->setMineIdleDelay(d.seconds);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
idleRightEdge = comboX - 4.0f * dp;
}
// Thread scaling controls: idle delay + active threads / idle threads combos
if (idleOn && threadScaling) {
int hwThreads = std::max(1, (int)std::thread::hardware_concurrency());
// Idle delay combo
{
struct DelayOption { int seconds; const char* label; };
static const DelayOption delays[] = {
{30, "30s"}, {60, "1m"}, {120, "2m"}, {300, "5m"}, {600, "10m"}
};
int curDelay = app->settings()->getMineIdleDelay();
const char* previewLabel = "2m";
for (const auto& d : delays) {
if (d.seconds == curDelay) { previewLabel = d.label; break; }
}
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleDelayScale", previewLabel, ImGuiComboFlags_NoArrowButton)) {
for (const auto& d : delays) {
bool selected = (d.seconds == curDelay);
if (ImGui::Selectable(d.label, selected)) {
app->settings()->setMineIdleDelay(d.seconds);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("tt_idle_delay"));
idleRightEdge = comboX - 4.0f * dp;
}
// Idle threads combo (threads when system is idle)
{
int curVal = app->settings()->getIdleThreadsIdle();
if (curVal <= 0) curVal = hwThreads;
char previewBuf[16];
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleThreadsIdle", previewBuf, ImGuiComboFlags_NoArrowButton)) {
for (int t = 1; t <= hwThreads; t++) {
char lbl[16];
snprintf(lbl, sizeof(lbl), "%d", t);
bool selected = (t == curVal);
if (ImGui::Selectable(lbl, selected)) {
app->settings()->setIdleThreadsIdle(t);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("mining_idle_threads_idle_tooltip"));
idleRightEdge = comboX - 4.0f * dp;
}
// Separator arrow icon
{
const char* arrowIcon = ICON_MD_ARROW_BACK;
ImVec2 arrSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, arrowIcon);
float arrX = idleRightEdge - arrSz.x;
float arrY = curY + (headerH - arrSz.y) * 0.5f;
dl->AddText(icoFont, icoFont->LegacySize, ImVec2(arrX, arrY), OnSurfaceDisabled(), arrowIcon);
idleRightEdge = arrX - 4.0f * dp;
}
// Active threads combo (threads when user is active)
{
int curVal = app->settings()->getIdleThreadsActive();
if (curVal <= 0) curVal = std::max(1, hwThreads / 2);
char previewBuf[16];
snprintf(previewBuf, sizeof(previewBuf), "%d", curVal);
float comboW = schema::UI().drawElement("components.settings-page", "idle-combo-width").sizeOr(64.0f);
float comboX = idleRightEdge - comboW;
float comboY = curY + (headerH - ImGui::GetFrameHeight()) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(comboX, comboY));
ImGui::SetNextItemWidth(comboW);
if (ImGui::BeginCombo("##IdleThreadsActive", previewBuf, ImGuiComboFlags_NoArrowButton)) {
for (int t = 1; t <= hwThreads; t++) {
char lbl[16];
snprintf(lbl, sizeof(lbl), "%d", t);
bool selected = (t == curVal);
if (ImGui::Selectable(lbl, selected)) {
app->settings()->setIdleThreadsActive(t);
app->settings()->save();
}
if (selected) ImGui::SetItemDefaultFocus();
}
ImGui::EndCombo();
}
if (ImGui::IsItemHovered())
ImGui::SetTooltip("%s", TR("mining_idle_threads_active_tooltip"));
idleRightEdge = comboX - 4.0f * dp;
}
}
ImGui::SetCursorScreenPos(savedCur);
}
// --- Thread Benchmark button / progress (left of idle toggle) ---
{
ImVec2 benchSavedCur = ImGui::GetCursorScreenPos();
bool benchRunning = s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
s_benchmark.phase != ThreadBenchmark::Phase::Done;
bool benchDone = s_benchmark.phase == ThreadBenchmark::Phase::Done;
ImFont* icoFont = Type().iconSmall();
if (benchRunning) {
// Show progress bar + current test info
float barW = std::min(180.0f * hs, idleRightEdge - (cardMin.x + pad) - 10.0f * dp);
float barH = 4.0f * dp;
float barX = idleRightEdge - barW;
float barY = curY + headerH - barH - 2.0f * dp;
// Progress bar track
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH),
WithAlpha(OnSurface(), 30), barH * 0.5f);
// Progress bar fill
float pct = s_benchmark.progress();
dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW * pct, barY + barH),
Primary(), barH * 0.5f);
// Status text above bar
int ct = s_benchmark.current_index < (int)s_benchmark.candidates.size()
? s_benchmark.candidates[s_benchmark.current_index] : 0;
// Estimated remaining time (uses observed warmup for better accuracy)
int remaining_tests = (int)s_benchmark.candidates.size() - s_benchmark.current_index;
float elapsed_in_phase = s_benchmark.phase_timer;
float phase_total;
if (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp)
phase_total = s_benchmark.avgWarmupSecs(); // adaptive estimate
else if (s_benchmark.phase == ThreadBenchmark::Phase::CoolingDown)
phase_total = ThreadBenchmark::COOLDOWN_SECS;
else
phase_total = ThreadBenchmark::MEASURE_SECS;
float remaining_in_current = std::max(0.0f, phase_total - elapsed_in_phase);
// Remaining tests after current each need warmup + measure + cooldown
float est_secs = remaining_in_current
+ (remaining_tests - 1) * (s_benchmark.avgWarmupSecs() + ThreadBenchmark::MEASURE_SECS + ThreadBenchmark::COOLDOWN_SECS);
int est_min = (int)(est_secs / 60.0f);
int est_sec = (int)est_secs % 60;
const char* phase_label;
if (s_benchmark.phase == ThreadBenchmark::Phase::CoolingDown)
phase_label = TR("mining_benchmark_cooling");
else if (s_benchmark.phase == ThreadBenchmark::Phase::WarmingUp
&& s_benchmark.phase_timer >= ThreadBenchmark::MIN_WARMUP_SECS)
phase_label = TR("mining_benchmark_stabilizing");
else
phase_label = TR("mining_benchmark_testing");
if (est_min > 0)
snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%dm%ds",
phase_label,
s_benchmark.current_index + 1,
(int)s_benchmark.candidates.size(), ct, est_min, est_sec);
else
snprintf(buf, sizeof(buf), "%s %d/%d (%dt) ~%ds",
phase_label,
s_benchmark.current_index + 1,
(int)s_benchmark.candidates.size(), ct, est_sec);
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(barX + (barW - txtSz.x) * 0.5f, barY - txtSz.y - 2.0f * dp),
OnSurfaceMedium(), buf);
// Cancel button (small X)
float cancelSz = icoFont->LegacySize + 4.0f * dp;
float cancelX = barX - cancelSz - 4.0f * dp;
float cancelY = curY + (headerH - cancelSz) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(cancelX, cancelY));
ImGui::InvisibleButton("##BenchCancel", ImVec2(cancelSz, cancelSz));
if (ImGui::IsItemClicked()) {
app->stopPoolMining();
if (s_benchmark.was_pool_running)
app->startPoolMining(s_benchmark.prev_threads);
s_benchmark.reset();
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", TR("mining_benchmark_cancel"));
}
const char* cancelIcon = ICON_MD_CLOSE;
ImVec2 cIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, cancelIcon);
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(cancelX + (cancelSz - cIcoSz.x) * 0.5f,
cancelY + (cancelSz - cIcoSz.y) * 0.5f),
OnSurfaceMedium(), cancelIcon);
idleRightEdge = cancelX - 4.0f * dp;
} else if (benchDone && s_benchmark.optimal_threads > 0) {
// Show result briefly, then reset on next click
snprintf(buf, sizeof(buf), "%s: %dt (%.1f H/s)",
TR("mining_benchmark_result"),
s_benchmark.optimal_threads, s_benchmark.optimal_hashrate);
ImVec2 txtSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
float txtX = idleRightEdge - txtSz.x;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(txtX, curY + (headerH - txtSz.y) * 0.5f),
WithAlpha(Success(), 220), buf);
// Dismiss button
float dismissSz = icoFont->LegacySize + 4.0f * dp;
float dismissX = txtX - dismissSz - 4.0f * dp;
float dismissY = curY + (headerH - dismissSz) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(dismissX, dismissY));
ImGui::InvisibleButton("##BenchDismiss", ImVec2(dismissSz, dismissSz));
if (ImGui::IsItemClicked())
s_benchmark.reset();
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", TR("mining_benchmark_dismiss"));
}
const char* okIcon = ICON_MD_CHECK;
ImVec2 oIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, okIcon);
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(dismissX + (dismissSz - oIcoSz.x) * 0.5f,
dismissY + (dismissSz - oIcoSz.y) * 0.5f),
WithAlpha(Success(), 200), okIcon);
idleRightEdge = dismissX - 4.0f * dp;
} else if (s_pool_mode) {
// Show benchmark button (only in pool mode)
float btnSz = icoFont->LegacySize + 8.0f * dp;
float btnX = idleRightEdge - btnSz;
float btnY = curY + (headerH - btnSz) * 0.5f;
ImGui::SetCursorScreenPos(ImVec2(btnX, btnY));
ImGui::InvisibleButton("##BenchStart", ImVec2(btnSz, btnSz));
bool benchHovered = ImGui::IsItemHovered();
bool benchClicked = ImGui::IsItemClicked();
// Hover highlight
if (benchHovered) {
dl->AddRectFilled(ImVec2(btnX, btnY), ImVec2(btnX + btnSz, btnY + btnSz),
StateHover(), btnSz * 0.5f);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::SetTooltip("%s", TR("mining_benchmark_tooltip"));
}
const char* benchIcon = ICON_MD_SPEED;
ImVec2 bIcoSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, benchIcon);
dl->AddText(icoFont, icoFont->LegacySize,
ImVec2(btnX + (btnSz - bIcoSz.x) * 0.5f,
btnY + (btnSz - bIcoSz.y) * 0.5f),
OnSurfaceMedium(), benchIcon);
if (benchClicked) {
// Require a wallet address for pool mining
std::string worker(s_pool_worker);
if (!worker.empty()) {
s_benchmark.reset();
s_benchmark.was_pool_running = state.pool_mining.xmrig_running;
s_benchmark.prev_threads = s_selected_threads;
s_benchmark.buildCandidates(max_threads);
s_benchmark.phase = ThreadBenchmark::Phase::Starting;
// Stop any active solo mining first
if (mining.generate)
app->stopMining();
}
}
idleRightEdge = btnX - 4.0f * dp;
}
ImGui::SetCursorScreenPos(benchSavedCur);
}
// Active mining indicator (left of idle toggle)
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(idleRightEdge - dotR, curY + dotR + 1 * dp), dotR, pulseCol);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(idleRightEdge - dotR - dotR - 60 * hs, curY),
WithAlpha(Success(), 200), TR("mining_active"));
}
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
bool benchActive = s_benchmark.phase != ThreadBenchmark::Phase::Idle &&
s_benchmark.phase != ThreadBenchmark::Phase::Done;
if (hovered_thread > 0 && !benchActive)
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
// Drag-to-select logic (disabled during benchmark)
if (!benchActive && 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) {
app->settings()->setPoolThreads(s_selected_threads);
app->settings()->save();
if (mining.generate) {
app->startMining(s_selected_threads);
}
if (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();
// Pool mining connects to an external pool via xmrig — it does not
// need the local blockchain synced or even the daemon connected.
// If pool mining is still shutting down after switching to solo,
// keep the button enabled so user can stop it.
bool poolStillRunning = !s_pool_mode && state.pool_mining.xmrig_running;
bool disabled = s_pool_mode
? (isToggling || poolBlockedBySolo)
: (poolStillRunning ? false : (!app->isConnected() || isToggling || isSyncing));
// 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 ? TR("mining_stopping") : TR("mining_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 = TR("mining_stop");
lblCol = WithAlpha(Error(), 220);
} else if (disabled) {
label = TR("mining_mine");
lblCol = WithAlpha(OnSurface(), 50);
} else {
label = TR("mining_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("%s", isMiningActive ? TR("mining_stopping_tooltip") : TR("mining_starting_tooltip"));
else if (isSyncing && !s_pool_mode)
ImGui::SetTooltip(TR("mining_syncing_tooltip"), state.sync.verification_progress * 100.0);
else if (poolBlockedBySolo)
ImGui::SetTooltip("%s", TR("mining_stop_solo_for_pool"));
else
ImGui::SetTooltip("%s", isMiningActive ? TR("stop_mining") : TR("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 pool mining is still running (user just switched from pool to solo),
// stop pool mining first
if (state.pool_mining.xmrig_running)
app->stopPoolMining();
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())
: (s_show_solo_log && !mining.log_lines.empty());
bool hasLogContent = s_pool_mode
? !state.pool_mining.log_lines.empty()
: !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);
bool& showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log;
if (showLogView) {
// --- Full-card log view (selectable + copyable) ---
const std::vector<std::string>& logLines = s_pool_mode
? state.pool_mining.log_lines
: mining.log_lines;
// Build a single string buffer for InputTextMultiline
static std::string s_log_buf;
s_log_buf.clear();
for (const auto& line : logLines) {
if (!line.empty()) {
s_log_buf += line;
s_log_buf += '\n';
}
}
float logPad = pad * 0.5f;
float logW = availWidth - logPad * 2;
float logH = totalCardH - logPad * 2;
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + logPad, cardMin.y + logPad));
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface()));
ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0));
ImFont* monoFont = Type().body2();
ImGui::PushFont(monoFont);
const char* inputId = s_pool_mode ? "##PoolLogText" : "##SoloLogText";
ImGui::InputTextMultiline(inputId,
const_cast<char*>(s_log_buf.c_str()), s_log_buf.size() + 1,
ImVec2(logW, logH),
ImGuiInputTextFlags_ReadOnly);
ImGui::PopFont();
ImGui::PopStyleColor(2);
// Reset cursor to end of card after the input
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 = TR("mining_local_hashrate");
col1Str = FormatHashrate(state.pool_mining.hashrate_10s);
col1Col = state.pool_mining.xmrig_running ? greenCol : OnSurfaceDisabled();
col2Label = TR("mining_pool_hashrate");
col2Str = FormatHashrate(state.pool_mining.pool_hashrate);
col2Col = state.pool_mining.pool_hashrate > 0 ? OnSurface() : OnSurfaceDisabled();
col3Label = TR("mining_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 = TR("mining_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 = TR("mining_local_hashrate");
col1Str = FormatHashrate(mining.localHashrate);
col1Col = mining.generate ? greenCol : OnSurfaceDisabled();
col2Label = TR("mining_network");
col2Str = FormatHashrate(mining.networkHashrate);
col2Col = OnSurface();
col3Label = TR("mining_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);
// Build raw data points — evenly spaced across the plot.
// No smooth-scroll animation: the chart updates in-place
// when new data arrives without any interim compression.
size_t n = chartHistory.size();
float stepW = (n > 1) ? plotW / (float)(n - 1) : plotW;
std::vector<ImVec2> rawPts(n);
for (size_t i = 0; i < n; i++) {
float x = plotLeft + (float)i * stepW;
float y = plotBottom - (float)((chartHistory[i] - yMin) / (yMax - yMin)) * plotH;
rawPts[i] = ImVec2(x, y);
}
// Catmull-Rom spline interpolation for smooth curve
// Subdivisions are adaptive: more when points are far apart,
// none when points are already sub-2px apart.
std::vector<ImVec2> points;
if (n <= 2) {
points = rawPts;
} else {
points.reserve(n * 4); // conservative estimate
for (size_t i = 0; i + 1 < n; i++) {
ImVec2 p0 = rawPts[i > 0 ? i - 1 : 0];
ImVec2 p1 = rawPts[i];
ImVec2 p2 = rawPts[i + 1];
ImVec2 p3 = rawPts[i + 2 < n ? i + 2 : n - 1];
// Adaptive subdivision: ~1 segment per 3px of distance
float dx = p2.x - p1.x, dy = p2.y - p1.y;
float dist = sqrtf(dx * dx + dy * dy);
int subdivs = std::clamp((int)(dist / 3.0f), 1, 16);
for (int s = 0; s < subdivs; s++) {
float t = (float)s / (float)subdivs;
float t2 = t * t;
float t3 = t2 * t;
float q0 = -t3 + 2.0f * t2 - t;
float q1 = 3.0f * t3 - 5.0f * t2 + 2.0f;
float q2 = -3.0f * t3 + 4.0f * t2 + t;
float q3 = t3 - t2;
float sx = 0.5f * (p0.x * q0 + p1.x * q1 + p2.x * q2 + p3.x * q3);
float sy = 0.5f * (p0.y * q0 + p1.y * q1 + p2.y * q2 + p3.y * q3);
// Clamp Y to plot bounds to prevent Catmull-Rom overshoot
sy = std::clamp(sy, plotTop, plotBottom);
points.push_back(ImVec2(sx, sy));
}
}
points.push_back(rawPts[n - 1]); // final point
}
// Fill under curve (single concave polygon to avoid AA seam shimmer)
if (points.size() >= 2) {
for (size_t i = 0; i < points.size(); i++)
dl->PathLineTo(points[i]);
dl->PathLineTo(ImVec2(points.back().x, plotBottom));
dl->PathLineTo(ImVec2(points.front().x, plotBottom));
dl->PathFillConcave(WithAlpha(Success(), 25));
}
// 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 ? TR("mining_chart_5m_ago") :
chartHistory.size() >= 60 ? TR("mining_chart_1m_ago") : TR("mining_chart_start"));
std::string nowLbl = TR("mining_chart_now");
ImVec2 nowSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, nowLbl.c_str());
dl->AddText(capFont, capFont->LegacySize,
ImVec2(plotRight - nowSz.x, chartBot - capFont->LegacySize - 2 * dp),
OnSurfaceDisabled(), nowLbl.c_str());
}
// Advance cursor past the card (stats/chart view only)
ImGui::Dummy(ImVec2(availWidth, totalCardH));
}
// --- Toggle button in top-right corner ---
// Rendered after content so the Hand cursor takes priority over
// the InputTextMultiline text-cursor when hovering the button.
if (hasLogContent || hasChartContent) {
ImFont* iconFont = Type().iconSmall();
const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
const char* toggleTip = showLogFlag ? TR("mining_show_chart") : TR("mining_show_log");
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, toggleIcon);
float btnSize = iconSz.y + 8 * dp;
float btnX = cardMax.x - pad - btnSize;
float btnY = cardMin.y + pad * 0.5f;
ImVec2 btnMin(btnX, btnY);
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f);
bool hov = IsRectHovered(btnMin, btnMax);
if (hov) {
dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
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)) {
showLogFlag = !showLogFlag;
}
}
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::string txid;
bool isPoolPayout;
};
std::vector<MinedTx> 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 (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 char* filterLabels[] = { TR("mining_filter_all"), TR("mining_solo"), TR("mining_pool") };
const char* filterIcons[] = { ICON_MD_FUNCTIONS, ICON_MD_MEMORY, ICON_MD_CLOUD };
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) % 3;
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
const char* tips[] = {
TR("mining_filter_tip_all"),
TR("mining_filter_tip_solo"),
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));
}
}
// ================================================================
// 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() ? TR("mining_connected") : state.pool_mining.pool_url.c_str())
: TR("mining_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));
}
}
} // namespace ui
} // namespace dragonx