feat(mining): left pool card (Manual/Auto, metrics, scrollable pool list)
Split the pool-mode hashrate card into a 25%-width left card + 75% chart so the
Manual/Auto toggle and pool metrics no longer reflow the tab:
- left card: Manual|Auto toggle (with tooltips), a Local/Pool/Shares/Uptime
metric stack, and a fixed-height scrolling pool list — rows click-to-select in
Manual (reusing the existing save+restart path) and show a "mining here" dot in
Auto. Fixed height keeps the card constant as pools are added.
- mode-toggle row: keep the suggested-pools dropdown + auto-mode URL disable;
the toggle and pool list moved into the card.
- mining_controls: show the installed/running miner version ("Current: …")
before and during mining instead of "none".
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -528,7 +528,18 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
|
|||||||
|
|
||||||
// --- Miner update: "Update <latest>" button + current version (left of benchmark) ---
|
// --- Miner update: "Update <latest>" button + current version (left of benchmark) ---
|
||||||
if (s_pool_mode && !benchRunning) {
|
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];
|
char xbtn[64];
|
||||||
if (!s_xmrig_latest_tag.empty())
|
if (!s_xmrig_latest_tag.empty())
|
||||||
snprintf(xbtn, sizeof(xbtn), "%s %s", TR("xmrig_update_short"), s_xmrig_latest_tag.c_str());
|
snprintf(xbtn, sizeof(xbtn), "%s %s", TR("xmrig_update_short"), s_xmrig_latest_tag.c_str());
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
#include "../../config/version.h"
|
#include "../../config/version.h"
|
||||||
#include "../../config/settings.h"
|
#include "../../config/settings.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/pool_registry.h"
|
||||||
#include "../../util/platform.h"
|
#include "../../util/platform.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
#include "../material/type.h"
|
#include "../material/type.h"
|
||||||
@@ -148,6 +149,18 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
float inputsStartX = tMax.x + Layout::spacingLg();
|
float inputsStartX = tMax.x + Layout::spacingLg();
|
||||||
ImGui::SetCursorScreenPos(ImVec2(inputsStartX, tMin.y + vertOff));
|
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_FrameRounding, 4.0f * dp);
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(8 * dp, 4 * 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 urlGroupStartY = ImGui::GetCursorScreenPos().y;
|
||||||
float urlGroupW = urlW + perGroupExtra;
|
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);
|
ImGui::SetNextItemWidth(urlW);
|
||||||
if (ImGui::InputTextWithHint("##PoolURL", TR("mining_pool_url"), s_pool_url, sizeof(s_pool_url))) {
|
if (ImGui::InputTextWithHint("##PoolURL", TR("mining_pool_url"), s_pool_url, sizeof(s_pool_url))) {
|
||||||
s_pool_settings_dirty = true;
|
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_WindowPadding, ImVec2(0, 2 * dp));
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
ImGui::PushStyleVar(ImGuiStyleVar_ItemSpacing, ImVec2(0, 0));
|
||||||
if (ImGui::BeginPopup("##SavedPoolsPopup")) {
|
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();
|
const auto& savedUrls = app->settings()->getSavedPoolUrls();
|
||||||
if (savedUrls.empty()) {
|
if (savedUrls.empty()) {
|
||||||
ImGui::SetCursorPosX(8 * dp);
|
ImGui::SetCursorPosX(8 * dp);
|
||||||
@@ -338,6 +391,7 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
|
|||||||
ImGui::EndPopup();
|
ImGui::EndPopup();
|
||||||
}
|
}
|
||||||
ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for URL popup
|
ImGui::PopStyleVar(3); // WindowRounding, WindowPadding, ItemSpacing for URL popup
|
||||||
|
if (autoMode) ImGui::EndDisabled();
|
||||||
ImGui::SameLine(0, Layout::spacingSm());
|
ImGui::SameLine(0, Layout::spacingSm());
|
||||||
float wrkGroupStartX = ImGui::GetCursorScreenPos().x;
|
float wrkGroupStartX = ImGui::GetCursorScreenPos().x;
|
||||||
float wrkGroupStartY = ImGui::GetCursorScreenPos().y;
|
float wrkGroupStartY = ImGui::GetCursorScreenPos().y;
|
||||||
|
|||||||
@@ -5,7 +5,10 @@
|
|||||||
#include "mining_stats.h"
|
#include "mining_stats.h"
|
||||||
#include "mining_tab_helpers.h" // FormatHashrate, EstimateHoursToBlock
|
#include "mining_tab_helpers.h" // FormatHashrate, EstimateHoursToBlock
|
||||||
|
|
||||||
|
#include "../../app.h"
|
||||||
|
#include "../../config/settings.h"
|
||||||
#include "../../util/i18n.h"
|
#include "../../util/i18n.h"
|
||||||
|
#include "../../util/pool_registry.h"
|
||||||
#include "../schema/ui_schema.h"
|
#include "../schema/ui_schema.h"
|
||||||
#include "../material/type.h"
|
#include "../material/type.h"
|
||||||
#include "../material/draw_helpers.h"
|
#include "../material/draw_helpers.h"
|
||||||
@@ -17,6 +20,7 @@
|
|||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -29,297 +33,495 @@ using namespace material;
|
|||||||
static bool s_show_pool_log = false;
|
static bool s_show_pool_log = false;
|
||||||
static bool s_show_solo_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<double>& 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<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 a smooth curve (adaptive subdivision).
|
||||||
|
std::vector<ImVec2> 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<std::string>& 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<char*>(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,
|
void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
||||||
ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
|
ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
|
||||||
float dp, float vs, float gap, float pad, float availWidth,
|
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();
|
ImU32 greenCol = Success();
|
||||||
|
|
||||||
// Determine view mode first
|
|
||||||
bool showLogView = s_pool_mode
|
bool showLogView = s_pool_mode
|
||||||
? (s_show_pool_log && !state.pool_mining.log_lines.empty())
|
? (s_show_pool_log && !state.pool_mining.log_lines.empty())
|
||||||
: (s_show_solo_log && !mining.log_lines.empty());
|
: (s_show_solo_log && !mining.log_lines.empty());
|
||||||
bool hasLogContent = s_pool_mode
|
bool hasLogContent = s_pool_mode
|
||||||
? !state.pool_mining.log_lines.empty()
|
? !state.pool_mining.log_lines.empty()
|
||||||
: !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
|
const std::vector<double>& chartHistory = s_pool_mode
|
||||||
? state.pool_mining.hashrate_history
|
? state.pool_mining.hashrate_history
|
||||||
: mining.hashrate_history;
|
: mining.hashrate_history;
|
||||||
bool hasChartContent = chartHistory.size() >= 2;
|
bool hasChartContent = chartHistory.size() >= 2;
|
||||||
|
|
||||||
// Stat row height (single line: overline + value)
|
|
||||||
float statRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + Layout::spacingSm();
|
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 cardMin = ImGui::GetCursorScreenPos();
|
||||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + totalCardH);
|
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + totalCardH);
|
||||||
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
DrawGlassPanel(dl, cardMin, cardMax, glassSpec);
|
||||||
|
|
||||||
bool& showLogFlag = s_pool_mode ? s_show_pool_log : s_show_solo_log;
|
|
||||||
|
|
||||||
if (showLogView) {
|
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 logPad = pad * 0.5f;
|
||||||
float logW = availWidth - logPad * 2;
|
DrawMiningLogView(ImVec2(cardMin.x + logPad, cardMin.y + logPad),
|
||||||
float logH = totalCardH - logPad * 2;
|
availWidth - logPad * 2, totalCardH - logPad * 2,
|
||||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x + logPad, cardMin.y + logPad));
|
mining.log_lines, "##SoloLogText");
|
||||||
|
|
||||||
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::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
|
||||||
ImGui::Dummy(ImVec2(availWidth, 0)); // Register position with layout system
|
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||||
} else {
|
} else {
|
||||||
// --- Stats + Chart view ---
|
double est_hours = EstimateHoursToBlock(mining.localHashrate, mining.networkHashrate, mining.difficulty);
|
||||||
|
|
||||||
// Pool vs Solo stats — different columns
|
const char* col1Label = TR("mining_local_hashrate");
|
||||||
std::string col1Str, col2Str, col3Str, col4Str;
|
std::string col1Str = FormatHashrate(mining.localHashrate);
|
||||||
const char* col1Label;
|
ImU32 col1Col = mining.generate ? greenCol : OnSurfaceDisabled();
|
||||||
const char* col2Label;
|
const char* col2Label = TR("mining_network");
|
||||||
const char* col3Label;
|
std::string col2Str = FormatHashrate(mining.networkHashrate);
|
||||||
const char* col4Label = nullptr;
|
const char* col3Label = TR("mining_est_block");
|
||||||
ImU32 col1Col, col2Col, col3Col, col4Col = OnSurface();
|
std::string col3Str = FormatEstTime(est_hours);
|
||||||
int numStats = 3;
|
int numStats = 3;
|
||||||
|
|
||||||
if (s_pool_mode) {
|
float statColW = (availWidth - pad * 2) / (float)numStats;
|
||||||
col1Label = TR("mining_local_hashrate");
|
float sy = cardMin.y + pad * 0.5f;
|
||||||
col1Str = FormatHashrate(state.pool_mining.hashrate_10s);
|
struct StatEntry { const char* label; std::string value; ImU32 col; };
|
||||||
col1Col = state.pool_mining.xmrig_running ? greenCol : OnSurfaceDisabled();
|
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");
|
if (si == 0 && chartHistory.size() >= 6) {
|
||||||
col2Str = FormatHashrate(state.pool_mining.pool_hashrate);
|
size_t hn = chartHistory.size();
|
||||||
col2Col = state.pool_mining.pool_hashrate > 0 ? OnSurface() : OnSurfaceDisabled();
|
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;
|
||||||
col3Label = TR("mining_shares");
|
ImFont* iconFont = Type().iconSmall();
|
||||||
char sharesBuf[64];
|
float arrowX = centerX + valSz.x * 0.5f + Layout::spacingSm();
|
||||||
snprintf(sharesBuf, sizeof(sharesBuf), "%lld / %lld",
|
if (recent > older * 1.02) {
|
||||||
(long long)state.pool_mining.accepted,
|
const char* icon = ICON_MD_TRENDING_UP;
|
||||||
(long long)state.pool_mining.rejected);
|
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
|
||||||
col3Str = sharesBuf;
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
col3Col = OnSurface();
|
ImVec2(arrowX, valY + sub1->LegacySize * 0.5f - iSz.y * 0.5f),
|
||||||
|
WithAlpha(Success(), 220), icon);
|
||||||
col4Label = TR("mining_uptime");
|
} else if (recent < older * 0.98) {
|
||||||
int64_t up = state.pool_mining.uptime_sec;
|
const char* icon = ICON_MD_TRENDING_DOWN;
|
||||||
char uptBuf[64];
|
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon);
|
||||||
if (up <= 0)
|
dl->AddText(iconFont, iconFont->LegacySize,
|
||||||
snprintf(uptBuf, sizeof(uptBuf), "N/A");
|
ImVec2(arrowX, valY + sub1->LegacySize * 0.5f - iSz.y * 0.5f),
|
||||||
else if (up < 3600)
|
WithAlpha(Error(), 220), icon);
|
||||||
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) {
|
if (hasChartContent) {
|
||||||
float chartTop = cardMin.y + statRowH;
|
DrawHashrateSparkline(dl,
|
||||||
float chartBot = cardMax.y;
|
ImVec2(cardMin.x + pad, cardMin.y + statRowH),
|
||||||
|
ImVec2(cardMax.x - pad, cardMax.y),
|
||||||
// Compute Y range
|
chartHistory, capFont, dp);
|
||||||
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));
|
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) {
|
if (hasLogContent || hasChartContent) {
|
||||||
ImFont* iconFont = Type().iconSmall();
|
ImFont* iconFont = Type().iconSmall();
|
||||||
const char* toggleIcon = showLogFlag ? ICON_MD_SHOW_CHART : ICON_MD_ARTICLE;
|
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 btnMin(btnX, btnY);
|
||||||
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
|
ImVec2 btnMax(btnX + btnSize, btnY + btnSize);
|
||||||
ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f);
|
ImVec2 btnCenter((btnMin.x + btnMax.x) * 0.5f, (btnMin.y + btnMax.y) * 0.5f);
|
||||||
|
|
||||||
bool hov = IsRectHovered(btnMin, btnMax);
|
bool hov = IsRectHovered(btnMin, btnMax);
|
||||||
if (hov) {
|
if (hov) {
|
||||||
dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
|
dl->AddCircleFilled(btnCenter, btnSize * 0.5f, StateHover());
|
||||||
|
|||||||
@@ -9,16 +9,20 @@
|
|||||||
#include "imgui.h"
|
#include "imgui.h"
|
||||||
|
|
||||||
namespace dragonx {
|
namespace dragonx {
|
||||||
|
class App;
|
||||||
namespace ui {
|
namespace ui {
|
||||||
|
|
||||||
// Renders the mining "Hashrate + Stats" card (stat values on top, hashrate chart or live log
|
// Renders the mining "Hashrate + Stats" card. In SOLO mode this is a full-width card (stat values
|
||||||
// below). Extracted verbatim from the monolithic mining tab; the immediate-mode layout context is
|
// on top, hashrate chart or live log below). In POOL mode it splits into a left card (Manual/Auto
|
||||||
// passed in so the rendering flows in sequence exactly as before. `poolMode` is the parent's
|
// toggle + pool metric stack + a fixed-height scrolling pool list) and a right region (chart or
|
||||||
// solo/pool toggle.
|
// 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,
|
void RenderMiningStats(const WalletState& state, const MiningInfo& mining,
|
||||||
ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
|
ImDrawList* dl, ImFont* capFont, ImFont* sub1, ImFont* ovFont,
|
||||||
float dp, float vs, float gap, float pad, float availWidth,
|
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 ui
|
||||||
} // namespace dragonx
|
} // namespace dragonx
|
||||||
|
|||||||
@@ -278,7 +278,8 @@ static void RenderMiningTabContent(App* app)
|
|||||||
// (Or full-card log view when toggled in pool mode)
|
// (Or full-card log view when toggled in pool mode)
|
||||||
// ================================================================
|
// ================================================================
|
||||||
RenderMiningStats(state, mining, dl, capFont, sub1, ovFont, dp, vs, gap, pad,
|
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)
|
// EARNINGS — Horizontal row card (Today | Yesterday | All Time | Est. Daily)
|
||||||
|
|||||||
Reference in New Issue
Block a user