- 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.
2545 lines
132 KiB
C++
2545 lines
132 KiB
C++
// 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
|