diff --git a/src/ui/windows/mining_controls.cpp b/src/ui/windows/mining_controls.cpp index b3297d4..c857e75 100644 --- a/src/ui/windows/mining_controls.cpp +++ b/src/ui/windows/mining_controls.cpp @@ -528,7 +528,18 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo& // --- Miner update: "Update " button + current version (left of benchmark) --- if (s_pool_mode && !benchRunning) { - const std::string curVer = app->settings()->getXmrigVersion(); + // "Current" prefers the live version reported by the running miner (so a + // bundled miner with no persisted release tag still shows a version). + // Remembered for the session so it persists after the miner stops; falls + // back to the persisted release tag (set by the updater), else "none". + static std::string s_live_miner_ver; + if (!state.pool_mining.version.empty()) + s_live_miner_ver = state.pool_mining.version; + // Live running version → detected installed version (before mining) → + // persisted release tag (updater) → "none". + std::string curVer = s_live_miner_ver; + if (curVer.empty()) curVer = app->poolMiningInstalledVersion(); + if (curVer.empty()) curVer = app->settings()->getXmrigVersion(); char xbtn[64]; if (!s_xmrig_latest_tag.empty()) snprintf(xbtn, sizeof(xbtn), "%s %s", TR("xmrig_update_short"), s_xmrig_latest_tag.c_str()); diff --git a/src/ui/windows/mining_mode_toggle.cpp b/src/ui/windows/mining_mode_toggle.cpp index 83e581c..c4029ed 100644 --- a/src/ui/windows/mining_mode_toggle.cpp +++ b/src/ui/windows/mining_mode_toggle.cpp @@ -10,6 +10,7 @@ #include "../../config/version.h" #include "../../config/settings.h" #include "../../util/i18n.h" +#include "../../util/pool_registry.h" #include "../../util/platform.h" #include "../schema/ui_schema.h" #include "../material/type.h" @@ -148,6 +149,18 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo float inputsStartX = tMax.x + Layout::spacingLg(); ImGui::SetCursorScreenPos(ImVec2(inputsStartX, tMin.y + vertOff)); + // Manual vs Auto-balance pool selection. In Auto mode the wallet picks the + // pool by hashrate (App::updatePoolAutoBalance), so the URL group is shown + // disabled and mirrors the auto-selected pool. + const bool autoMode = app->settings()->getPoolSelectMode() + == config::Settings::PoolSelectMode::AutoBalance; + if (autoMode) { + const std::string cur = app->settings()->getPoolUrl(); + if (cur != s_pool_url) { + strncpy(s_pool_url, cur.c_str(), sizeof(s_pool_url) - 1); + s_pool_url[sizeof(s_pool_url) - 1] = '\0'; + } + } ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 4.0f * dp); ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8 * dp, 4 * dp)); @@ -168,7 +181,8 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo float urlGroupStartY = ImGui::GetCursorScreenPos().y; float urlGroupW = urlW + perGroupExtra; - // === Pool URL input === + // === Pool URL input === (disabled in Auto mode — the wallet picks the pool) + if (autoMode) ImGui::BeginDisabled(); ImGui::SetNextItemWidth(urlW); if (ImGui::InputTextWithHint("##PoolURL", TR("mining_pool_url"), s_pool_url, sizeof(s_pool_url))) { s_pool_settings_dirty = true; @@ -240,6 +254,45 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(0, 2 * dp)); ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0)); if (ImGui::BeginPopup("##SavedPoolsPopup")) { + // Suggested (official) pools — one click to select, shown above the + // user's saved favorites. + { + const float sPopupInnerW = ImGui::GetContentRegionAvail().x; + const float sRowH = ImGui::GetFrameHeight(); + const float sTextPadX = 8 * dp; + ImGui::SetCursorPosX(sTextPadX); + ImGui::PushFont(Type().caption()); + ImGui::TextDisabled("%s", TR("mining_suggested_pools")); + ImGui::PopFont(); + ImFont* sRowFont = ImGui::GetFont(); + const float sRowFontSz = ImGui::GetFontSize(); + for (const auto& kp : util::knownPools()) { + ImGui::PushID(kp.id.c_str()); + ImVec2 rowMin = ImGui::GetCursorScreenPos(); + ImVec2 rowMax(rowMin.x + sPopupInnerW, rowMin.y + sRowH); + ImGui::InvisibleButton("##sugg", ImVec2(sPopupInnerW, sRowH)); + bool rowHov = ImGui::IsItemHovered(); + bool rowClk = ImGui::IsItemClicked(); + bool isCurrent = (std::string(s_pool_url) == kp.stratum); + ImDrawList* pdl = ImGui::GetWindowDrawList(); + if (rowHov) pdl->AddRectFilled(rowMin, rowMax, StateHover()); + float textY = rowMin.y + (sRowH - sRowFontSz) * 0.5f; + pdl->AddText(sRowFont, sRowFontSz, ImVec2(rowMin.x + sTextPadX, textY), + isCurrent ? Primary() : OnSurface(), kp.stratum.c_str()); + if (rowHov) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + material::Tooltip("%s", kp.label.c_str()); + } + if (rowClk) { + strncpy(s_pool_url, kp.stratum.c_str(), sizeof(s_pool_url) - 1); + s_pool_url[sizeof(s_pool_url) - 1] = '\0'; + s_pool_settings_dirty = true; + ImGui::CloseCurrentPopup(); + } + ImGui::PopID(); + } + ImGui::Separator(); + } const auto& savedUrls = app->settings()->getSavedPoolUrls(); if (savedUrls.empty()) { ImGui::SetCursorPosX(8 * dp); @@ -338,6 +391,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo ImGui::EndPopup(); } ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for URL popup + if (autoMode) ImGui::EndDisabled(); ImGui::SameLine(0, Layout::spacingSm()); float wrkGroupStartX = ImGui::GetCursorScreenPos().x; float wrkGroupStartY = ImGui::GetCursorScreenPos().y; diff --git a/src/ui/windows/mining_stats.cpp b/src/ui/windows/mining_stats.cpp index 943f5a1..0c0a46f 100644 --- a/src/ui/windows/mining_stats.cpp +++ b/src/ui/windows/mining_stats.cpp @@ -5,7 +5,10 @@ #include "mining_stats.h" #include "mining_tab_helpers.h" // FormatHashrate, EstimateHoursToBlock +#include "../../app.h" +#include "../../config/settings.h" #include "../../util/i18n.h" +#include "../../util/pool_registry.h" #include "../schema/ui_schema.h" #include "../material/type.h" #include "../material/draw_helpers.h" @@ -17,6 +20,7 @@ #include #include #include +#include #include #include @@ -29,297 +33,495 @@ using namespace material; static bool s_show_pool_log = false; static bool s_show_solo_log = false; +// --------------------------------------------------------------------------- +// Sparkline chart of a hashrate history, drawn inside [regionMin, regionMax] +// (the plot area within a card, already inset by padding). Extracted so both +// the solo full-width card and the pool split card share one implementation. +// --------------------------------------------------------------------------- +static void DrawHashrateSparkline(ImDrawList* dl, ImVec2 regionMin, ImVec2 regionMax, + const std::vector& chartHistory, + ImFont* capFont, float dp) +{ + if (chartHistory.size() < 2) return; + + const float chartBot = regionMax.y; + 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; + + const float plotLeft = regionMin.x; + const float plotRight = regionMax.x; + const float plotTop = regionMin.y + capFont->LegacySize + 4 * dp; + const float plotBottom = chartBot - capFont->LegacySize * 2 - 16 * dp; + const float plotW = plotRight - plotLeft; + const float plotH = std::max(1.0f, plotBottom - plotTop); + + size_t n = chartHistory.size(); + float stepW = (n > 1) ? plotW / (float)(n - 1) : plotW; + + std::vector 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 a smooth curve (adaptive subdivision). + std::vector points; + if (n <= 2) { + points = rawPts; + } else { + points.reserve(n * 4); + 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]; + 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); + sy = std::clamp(sy, plotTop, plotBottom); + points.push_back(ImVec2(sx, sy)); + } + } + points.push_back(rawPts[n - 1]); + } + + 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)); + } + + 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()); +} + +// --------------------------------------------------------------------------- +// Read-only, selectable/copyable log view filling [regionMin, size]. +// --------------------------------------------------------------------------- +static void DrawMiningLogView(ImVec2 regionMin, float regionW, float regionH, + const std::vector& logLines, const char* inputId) +{ + 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'; } + + ImGui::SetCursorScreenPos(regionMin); + ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurface())); + ImGui::PushStyleColor(ImGuiCol_FrameBg, ImVec4(0, 0, 0, 0)); + ImGui::PushFont(Type().body2()); + ImGui::InputTextMultiline(inputId, const_cast(s_log_buf.c_str()), s_log_buf.size() + 1, + ImVec2(regionW, regionH), ImGuiInputTextFlags_ReadOnly); + ImGui::PopFont(); + ImGui::PopStyleColor(2); +} + +// --------------------------------------------------------------------------- +// The left pool card (pool mode only): Manual/Auto toggle, a metric stack, and +// a fixed-height scrolling pool list. Pool rows are clickable to select in +// Manual mode (routed through the parent's dirty flag → save + restart) and +// show a "mining here" dot in Auto mode. The list scrolls, so any number of +// pools keeps the card — and everything below it — at a constant height. +// --------------------------------------------------------------------------- +static void RenderLeftPoolCard(App* app, const WalletState& state, ImDrawList* dl, + ImFont* capFont, ImFont* sub1, ImFont* ovFont, + float dp, float gap, float pad, + ImVec2 cardMin, ImVec2 cardMax, + char (&s_pool_url)[256], bool& s_pool_settings_dirty) +{ + const bool autoMode = app->settings()->getPoolSelectMode() + == config::Settings::PoolSelectMode::AutoBalance; + + const float x = cardMin.x + pad; + const float w = (cardMax.x - cardMin.x) - pad * 2; + float y = cardMin.y + pad * 0.75f; + + const float toggleH = sub1->LegacySize + 10 * dp; + const float rowH = sub1->LegacySize + 7 * dp; + + // --- Manual | Auto-balance toggle --- + { + ImFont* segFont = Type().caption(); + const char* segA = TR("mining_select_manual"); + const char* segB = TR("mining_select_auto"); + const float rnd = 4 * dp; + const float half = w * 0.5f; + const ImVec2 aMin(x, y), aMax(x + half, y + toggleH); + const ImVec2 bMin(x + half, y), bMax(x + w, y + toggleH); + dl->AddRectFilled(aMin, bMax, WithAlpha(OnSurface(), 15), rnd); + dl->AddRect(aMin, bMax, WithAlpha(OnSurface(), 40), rnd); + auto seg = [&](const ImVec2& mn, const ImVec2& mx, const char* label, bool selected) { + if (selected) dl->AddRectFilled(mn, mx, WithAlpha(Primary(), 180), rnd); + else if (material::IsRectHovered(mn, mx)) dl->AddRectFilled(mn, mx, WithAlpha(OnSurface(), 20), rnd); + ImVec2 sz = segFont->CalcTextSizeA(segFont->LegacySize, FLT_MAX, 0, label); + dl->AddText(segFont, segFont->LegacySize, + ImVec2(mn.x + (mx.x - mn.x - sz.x) * 0.5f, mn.y + (mx.y - mn.y - sz.y) * 0.5f), + selected ? IM_COL32(255, 255, 255, 230) : OnSurfaceMedium(), label); + }; + seg(aMin, aMax, segA, !autoMode); + seg(bMin, bMax, segB, autoMode); + ImGui::SetCursorScreenPos(aMin); + ImGui::InvisibleButton("##CardSelManual", ImVec2(half, toggleH)); + if (ImGui::IsItemClicked() && autoMode) { + app->settings()->setPoolSelectMode(config::Settings::PoolSelectMode::Manual); + app->settings()->save(); + } + if (material::IsRectHovered(aMin, aMax)) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + material::Tooltip("%s", TR("mining_manual_desc")); + } + ImGui::SetCursorScreenPos(bMin); + ImGui::InvisibleButton("##CardSelAuto", ImVec2(w - half, toggleH)); + if (ImGui::IsItemClicked() && !autoMode) { + app->settings()->setPoolSelectMode(config::Settings::PoolSelectMode::AutoBalance); + app->settings()->save(); + app->requestPoolBalanceRefresh(); + } + if (material::IsRectHovered(bMin, bMax)) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + material::Tooltip("%s", TR("mining_auto_balance_desc")); + } + } + y += toggleH + gap; + + // --- Metric stack (label left, value right) --- + auto metricRow = [&](const char* label, const std::string& val, ImU32 valCol) { + dl->AddText(ovFont, ovFont->LegacySize, + ImVec2(x, y + (rowH - ovFont->LegacySize) * 0.5f), OnSurfaceMedium(), label); + ImVec2 vSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, val.c_str()); + dl->AddText(sub1, sub1->LegacySize, + ImVec2(x + w - vSz.x, y + (rowH - sub1->LegacySize) * 0.5f), valCol, val.c_str()); + y += rowH; + }; + + const auto& pm = state.pool_mining; + metricRow(TR("mining_local_hashrate"), FormatHashrate(pm.hashrate_10s), + pm.xmrig_running ? Success() : OnSurfaceDisabled()); + metricRow(TR("mining_pool_hashrate"), FormatHashrate(pm.pool_hashrate), + pm.pool_hashrate > 0 ? OnSurface() : OnSurfaceDisabled()); + { + char sharesBuf[64]; + snprintf(sharesBuf, sizeof(sharesBuf), "%lld / %lld", + (long long)pm.accepted, (long long)pm.rejected); + metricRow(TR("mining_shares"), sharesBuf, OnSurface()); + } + { + char uptBuf[64]; + int64_t up = pm.uptime_sec; + 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)); + metricRow(TR("mining_uptime"), uptBuf, OnSurface()); + } + y += gap * 0.5f; + + // --- POOLS (N) header + Refresh --- + { + char hdr[48]; + snprintf(hdr, sizeof(hdr), "%s (%d)", TR("mining_pools_header"), + (int)util::knownPools().size()); + dl->AddText(ovFont, ovFont->LegacySize, ImVec2(x, y), OnSurfaceMedium(), hdr); + + ImFont* icoFont = Type().iconSmall(); + const char* icon = ICON_MD_REFRESH; + ImVec2 iSz = icoFont->CalcTextSizeA(icoFont->LegacySize, FLT_MAX, 0, icon); + float btnS = ovFont->LegacySize + 6 * dp; + ImVec2 rbMin(x + w - btnS, y - 2 * dp); + ImVec2 rbMax(x + w, y - 2 * dp + btnS); + ImGui::SetCursorScreenPos(rbMin); + ImGui::InvisibleButton("##PoolRefresh", ImVec2(btnS, btnS)); + bool rhov = ImGui::IsItemHovered(); + if (rhov) { + dl->AddRectFilled(rbMin, rbMax, StateHover(), 3 * dp); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + material::Tooltip("%s", TR("mining_refresh")); + } + dl->AddText(icoFont, icoFont->LegacySize, + ImVec2((rbMin.x + rbMax.x - iSz.x) * 0.5f, (rbMin.y + rbMax.y - iSz.y) * 0.5f), + OnSurfaceMedium(), icon); + if (ImGui::IsItemClicked()) app->requestPoolBalanceRefresh(); + } + y += ovFont->LegacySize + 4 * dp; + + // --- Scrollable pool list (fixed height → constant card height for any N) --- + const float listH = std::max(rowH, cardMax.y - y - pad * 0.75f); + ImGui::SetCursorScreenPos(ImVec2(x, y)); + ImGui::BeginChild("##PoolList", ImVec2(w, listH), false, ImGuiWindowFlags_NoBackground); + { + ImDrawList* cdl = ImGui::GetWindowDrawList(); + const auto snap = app->poolStatsSnapshot(); + const util::KnownPool* current = util::findKnownPoolByUrl(app->settings()->getPoolUrl()); + const float childW = ImGui::GetContentRegionAvail().x; + const float listRowH = capFont->LegacySize + 10 * dp; + + for (const auto& kp : util::knownPools()) { + ImGui::PushID(kp.id.c_str()); + const bool isCurrent = current && current->id == kp.id; + const auto it = snap.byId.find(kp.id); + const bool haveHr = (it != snap.byId.end() && it->second.ok); + + ImVec2 rMin = ImGui::GetCursorScreenPos(); + ImVec2 rMax(rMin.x + childW, rMin.y + listRowH); + + bool hov = false, clk = false; + if (!autoMode) { + ImGui::InvisibleButton("##prow", ImVec2(childW, listRowH)); + hov = ImGui::IsItemHovered(); + clk = ImGui::IsItemClicked(); + } else { + ImGui::Dummy(ImVec2(childW, listRowH)); + } + + if (isCurrent) cdl->AddRectFilled(rMin, rMax, WithAlpha(Primary(), 30), 3 * dp); + else if (hov) cdl->AddRectFilled(rMin, rMax, StateHover(), 3 * dp); + + const float cy = rMin.y + listRowH * 0.5f; + cdl->AddCircleFilled(ImVec2(rMin.x + 6 * dp, cy), 3.0f * dp, + isCurrent ? Success() : OnSurfaceDisabled()); + + const float textY = rMin.y + (listRowH - capFont->LegacySize) * 0.5f; + // Show the short host label (the narrow card can't fit host:port); the + // full stratum URL is in the hover tooltip. + cdl->AddText(capFont, capFont->LegacySize, ImVec2(rMin.x + 16 * dp, textY), + isCurrent ? Primary() : OnSurface(), kp.label.c_str()); + + char right[64]; + std::string hrStr = haveHr ? FormatHashrate(it->second.hashrateHs) : std::string("—"); + snprintf(right, sizeof(right), "%s %.0f%%", hrStr.c_str(), kp.feePercent); + ImVec2 rSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, right); + cdl->AddText(capFont, capFont->LegacySize, + ImVec2(rMax.x - rSz.x - 6 * dp, textY), OnSurfaceMedium(), right); + + if (!autoMode && hov) { + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + material::Tooltip("%s", kp.stratum.c_str()); + } + if (clk) { + strncpy(s_pool_url, kp.stratum.c_str(), sizeof(s_pool_url) - 1); + s_pool_url[sizeof(s_pool_url) - 1] = '\0'; + s_pool_settings_dirty = true; + } + ImGui::PopID(); + } + } + ImGui::EndChild(); +} + void RenderMiningStats(const WalletState& state, const MiningInfo& mining, ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont, float dp, float vs, float gap, float pad, float availWidth, - const GlassPanelSpec& glassSpec, float chartBudgetH, bool poolMode) + const GlassPanelSpec& glassSpec, float chartBudgetH, bool poolMode, + App* app, char (&s_pool_url)[256], bool& s_pool_settings_dirty) { - const bool s_pool_mode = poolMode; // alias: keep the moved body's identifier verbatim + (void)vs; + const bool s_pool_mode = poolMode; 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& 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; + bool& showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log; + // ================================================================ + // POOL MODE — split card: [ left pool card ][ chart / log ] + // ================================================================ + if (s_pool_mode) { + // Left card height driven by its content so the pool list always fits; + // the whole row is at least as tall as the old stats+chart card. + const float toggleH = sub1->LegacySize + 10 * dp; + const float mRowH = sub1->LegacySize + 7 * dp; + const float listMinH = (capFont->LegacySize + 10 * dp) * 2.6f; // ~2–3 rows before scroll + float leftContentH = pad * 0.75f + toggleH + gap + + mRowH * 4.0f + gap * 0.5f + + (ovFont->LegacySize + 4 * dp) + listMinH + pad * 0.75f; + float cardH = std::max(leftContentH, statRowH + chartBudgetH + pad); + + ImVec2 cardMin = ImGui::GetCursorScreenPos(); + float leftW = std::clamp(availWidth * 0.25f, 180.0f * dp, availWidth * 0.45f); + float colGap = gap; + float rightW = availWidth - leftW - colGap; + + ImVec2 leftMin = cardMin; + ImVec2 leftMax(cardMin.x + leftW, cardMin.y + cardH); + ImVec2 rightMin(leftMax.x + colGap, cardMin.y); + ImVec2 rightMax(cardMin.x + availWidth, cardMin.y + cardH); + + DrawGlassPanel(dl, leftMin, leftMax, glassSpec); + DrawGlassPanel(dl, rightMin, rightMax, glassSpec); + + RenderLeftPoolCard(app, state, dl, capFont, sub1, ovFont, dp, gap, pad, + leftMin, leftMax, s_pool_url, s_pool_settings_dirty); + + // Right panel: live log (if toggled + available) else the sparkline. + if (showLogView) { + float logPad = pad * 0.5f; + DrawMiningLogView(ImVec2(rightMin.x + logPad, rightMin.y + logPad), + rightW - logPad * 2, cardH - logPad * 2, + state.pool_mining.log_lines, "##PoolLogText"); + } else if (hasChartContent) { + DrawHashrateSparkline(dl, + ImVec2(rightMin.x + pad, rightMin.y + statRowH * 0.5f), + ImVec2(rightMax.x - pad, rightMax.y), + chartHistory, capFont, dp); + } else { + const char* hint = TR("mining_chart_start"); + ImVec2 hs = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, hint); + dl->AddText(capFont, capFont->LegacySize, + ImVec2((rightMin.x + rightMax.x - hs.x) * 0.5f, + (rightMin.y + rightMax.y - hs.y) * 0.5f), + OnSurfaceDisabled(), hint); + } + + // Chart/log toggle button, top-right of the RIGHT panel. + 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; + ImVec2 btnMin(rightMax.x - pad - btnSize, rightMin.y + pad * 0.5f); + ImVec2 btnMax(btnMin.x + btnSize, btnMin.y + 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); + material::Tooltip("%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::SetCursorScreenPos(cardMin); + ImGui::Dummy(ImVec2(availWidth, cardH)); + ImGui::Dummy(ImVec2(0, gap)); + return; + } + + // ================================================================ + // SOLO MODE — full-width card: stat columns on top, chart/log below + // ================================================================ + 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& 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(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 + DrawMiningLogView(ImVec2(cardMin.x + logPad, cardMin.y + logPad), + availWidth - logPad * 2, totalCardH - logPad * 2, + mining.log_lines, "##SoloLogText"); ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y)); - ImGui::Dummy(ImVec2(availWidth, 0)); // Register position with layout system + ImGui::Dummy(ImVec2(availWidth, 0)); } 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(); + double est_hours = EstimateHoursToBlock(mining.localHashrate, mining.networkHashrate, mining.difficulty); + + const char* col1Label = TR("mining_local_hashrate"); + std::string col1Str = FormatHashrate(mining.localHashrate); + ImU32 col1Col = mining.generate ? greenCol : OnSurfaceDisabled(); + const char* col2Label = TR("mining_network"); + std::string col2Str = FormatHashrate(mining.networkHashrate); + const char* col3Label = TR("mining_est_block"); + std::string col3Str = FormatEstTime(est_hours); 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(); + float statColW = (availWidth - pad * 2) / (float)numStats; + float sy = cardMin.y + pad * 0.5f; + struct StatEntry { const char* label; std::string value; ImU32 col; }; + StatEntry stats[] = { + { col1Label, col1Str, col1Col }, + { col2Label, col2Str, OnSurface() }, + { col3Label, col3Str, OnSurface() }, + }; + 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.c_str()); + 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.c_str()); - 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); - } + 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 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 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()); + DrawHashrateSparkline(dl, + ImVec2(cardMin.x + pad, cardMin.y + statRowH), + ImVec2(cardMax.x - pad, cardMax.y), + chartHistory, capFont, dp); } - - // 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; @@ -331,7 +533,6 @@ void RenderMiningStats(const WalletState& state, const MiningInfo& mining, 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()); diff --git a/src/ui/windows/mining_stats.h b/src/ui/windows/mining_stats.h index 8bc4137..c760768 100644 --- a/src/ui/windows/mining_stats.h +++ b/src/ui/windows/mining_stats.h @@ -9,16 +9,20 @@ #include "imgui.h" namespace dragonx { +class App; namespace ui { -// Renders the mining "Hashrate + Stats" card (stat values on top, hashrate chart or live log -// below). Extracted verbatim from the monolithic mining tab; the immediate-mode layout context is -// passed in so the rendering flows in sequence exactly as before. `poolMode` is the parent's -// solo/pool toggle. +// Renders the mining "Hashrate + Stats" card. In SOLO mode this is a full-width card (stat values +// on top, hashrate chart or live log below). In POOL mode it splits into a left card (Manual/Auto +// toggle + pool metric stack + a fixed-height scrolling pool list) and a right region (chart or +// log) — so toggling Manual/Auto or adding pools never reflows the tab. `poolMode` is the parent's +// solo/pool toggle; `s_pool_url` / `s_pool_settings_dirty` are the parent's pool-selection buffer +// and dirty flag (a pool-row click writes them, reusing the parent's save+restart path). void RenderMiningStats(const WalletState& state, const MiningInfo& mining, ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont, float dp, float vs, float gap, float pad, float availWidth, - const material::GlassPanelSpec& glassSpec, float chartBudgetH, bool poolMode); + const material::GlassPanelSpec& glassSpec, float chartBudgetH, bool poolMode, + App* app, char (&s_pool_url)[256], bool& s_pool_settings_dirty); } // namespace ui } // namespace dragonx diff --git a/src/ui/windows/mining_tab.cpp b/src/ui/windows/mining_tab.cpp index 0656a45..a014570 100644 --- a/src/ui/windows/mining_tab.cpp +++ b/src/ui/windows/mining_tab.cpp @@ -278,7 +278,8 @@ static void RenderMiningTabContent(App* app) // (Or full-card log view when toggled in pool mode) // ================================================================ RenderMiningStats(state, mining, dl, capFont, sub1, ovFont, dp, vs, gap, pad, - availWidth, glassSpec, chartBudgetH, s_pool_mode); + availWidth, glassSpec, chartBudgetH, s_pool_mode, + app, s_pool_url, s_pool_settings_dirty); // ================================================================ // EARNINGS — Horizontal row card (Today | Yesterday | All Time | Est. Daily)