fix(ui): overview/mining polish + default-pool cleanup

- balance: inset the address-row favorite (star) button by the card's inner
  padding so it mirrors the left margin instead of hugging the card edge.
- mining: remove pool.dragonx.cc from the built-in default pools (pool.dragonx.is
  is now the sole default); update the pool-registry test accordingly.
- mining: middle-truncate saved pool-URL and payout-address dropdown rows (new
  shared material::TruncateToWidth helper) so a full z-address no longer runs
  under the trailing delete (X) button.
- mining: fix the thread-grid cells overflowing the card at >100% display
  scaling — the reserved Mine-button width used a raw clamp that didn't scale;
  scale it by dp so cols is estimated correctly (no-op at 100%).

Full-node build + test suite green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 16:01:51 -05:00
parent bcee4bfe72
commit c53b7f771e
6 changed files with 46 additions and 28 deletions

View File

@@ -57,6 +57,21 @@ inline ImU32 ReadableError() {
return IM_COL32(r, g, b, (e >> IM_COL32_A_SHIFT) & 0xFF);
}
// Middle-ellipsis truncation ("front...back", roughly equal halves) so `text` fits within
// maxWidth pixels when drawn with `font` at `fontSize`. Returns `text` unchanged if it already
// fits (or maxWidth is non-positive). Display-only — never mutate the underlying value with this.
inline std::string TruncateToWidth(const std::string& text, ImFont* font, float fontSize, float maxWidth) {
if (text.empty() || !font || maxWidth <= 0.0f) return text;
if (font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, text.c_str()).x <= maxWidth) return text;
const int n = static_cast<int>(text.size());
for (int f = n / 2; f >= 3; --f) {
const int b = (f - 2 > 3) ? (f - 2) : 3; // keep the two halves roughly equal
std::string t = text.substr(0, f) + "..." + text.substr(n - b);
if (font->CalcTextSizeA(fontSize, FLT_MAX, 0.0f, t.c_str()).x <= maxWidth) return t;
}
return n > 6 ? (text.substr(0, 3) + "..." + text.substr(n - 3)) : text;
}
// Animated "loading" ellipsis: "", ".", "..", "..." cycling on a ~3Hz phase.
inline const char* LoadingDots() {
int n = ((int)(ImGui::GetTime() * 3.0f)) % 4;

View File

@@ -70,6 +70,7 @@ AddressRowLayout ComputeAddressRowLayout(float rowX,
float spacingSm,
float spacingXs)
{
(void)spacingXs; // trailing button now insets by rowPadLeft (mirrors the left margin)
AddressRowLayout layout;
layout.contentStartX = rowX + rowPadLeft;
layout.contentStartY = rowY + spacingMd;
@@ -77,7 +78,9 @@ AddressRowLayout ComputeAddressRowLayout(float rowX,
const float buttonY = rowY + (rowHeight - layout.buttonSize) * 0.5f;
const float rightEdge = rowX + rowWidth;
const float favoriteX = rightEdge - layout.buttonSize - spacingXs;
// Inset the trailing (favorite/star) button by the card's inner padding — the same margin the
// left content uses (rowPadLeft) — so it mirrors the left edge instead of hugging the card edge.
const float favoriteX = rightEdge - layout.buttonSize - rowPadLeft;
const float visibilityX = favoriteX - spacingSm - layout.buttonSize;
layout.favoriteButton = {favoriteX, buttonY, layout.buttonSize, layout.buttonSize};

View File

@@ -57,7 +57,10 @@ void RenderMiningControls(App* app, const WalletState& state, const MiningInfo&
// --- Compute thread grid layout based on controls card width ---
// Estimate controlsW first to compute cols correctly
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size, miningBtnMaxW) - miningBtnGap;
// The Mine button is square (= card height, which scales with DPI), so the width we
// reserve for it here must scale too — a RAW clamp under-reserves at >100% scaling, which
// over-estimates the grid width and lets the thread cells overflow the card (e.g. at 150%).
float estControlsW = availWidth - std::min(schema::UI().drawElement("tabs.mining", "button-max-width-clamp").size * dp, miningBtnMaxW) - miningBtnGap;
float innerW = estControlsW - pad * 2;
float cellSz = std::clamp(schema::UI().drawElement("tabs.mining", "cell-size").size * vs, schema::UI().drawElement("tabs.mining", "cell-min-size").size, schema::UI().drawElement("tabs.mining", "cell-max-size").sizeOr(42.0f));
float cellGap = std::max(schema::UI().drawElement("tabs.mining", "cell-gap-min").size, cellSz * schema::UI().drawElement("tabs.mining", "cell-gap-ratio").size);

View File

@@ -308,11 +308,14 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
if (rowHov && !inXZone)
pdl->AddRectFilled(rowMin, rowMax, StateHover());
// Item text with internal padding
// Item text with internal padding, middle-truncated so a long saved URL
// can't run under the trailing X (delete) button.
float textY = rowMin.y + (rowH - rowFontSz) * 0.5f;
float maxTextW = popupInnerW - xZoneW - textPadX * 2.0f;
std::string urlDisp = material::TruncateToWidth(url, rowFont, rowFontSz, maxTextW);
pdl->AddText(rowFont, rowFontSz,
ImVec2(rowMin.x + textPadX, textY),
isCurrent ? Primary() : OnSurface(), url.c_str());
isCurrent ? Primary() : OnSurface(), urlDisp.c_str());
// X button — flush with right edge, icon centered
{
ImVec2 xMin(rowMax.x - xZoneW, rowMin.y);
@@ -467,11 +470,14 @@ void RenderMiningModeToggle(App* app, const WalletState& state, const MiningInfo
pdl->AddRectFilled(rowMin, rowMax, IM_COL32(255, 255, 255, 10));
if (rowHov && !inXZone)
pdl->AddRectFilled(rowMin, rowMax, StateHover());
// Full address text with internal padding
// Address text with internal padding, middle-truncated so a full z-address
// (~78 chars) can't run under the trailing X (delete) button.
float textY = rowMin.y + (wRowH - wRowFontSz) * 0.5f;
float wMaxTextW = wPopupInnerW - wXZoneW - wTextPadX * 2.0f;
std::string addrDisp = material::TruncateToWidth(addr, wRowFont, wRowFontSz, wMaxTextW);
pdl->AddText(wRowFont, wRowFontSz,
ImVec2(rowMin.x + wTextPadX, textY),
isCurrent ? Primary() : OnSurface(), addr.c_str());
isCurrent ? Primary() : OnSurface(), addrDisp.c_str());
// Tooltip for long addresses
if (rowHov && !inXZone)
material::Tooltip("%s", addr.c_str());

View File

@@ -30,14 +30,6 @@ const std::vector<KnownPool>& knownPools()
"https://pool.dragonx.is/api/stats", PoolStatsSchema::DragonXIs,
/*miningcorePoolId=*/"", /*feePercent=*/0.0, /*official=*/true,
},
KnownPool{
// The mining (stratum) host is us.dragonx.cc — pool.dragonx.cc is the
// Cloudflare-proxied web/API host and does NOT accept stratum on :3333.
// Stats still come from pool.dragonx.cc/api/pools (proxied HTTP is fine).
"dragonx-cc-pplns", "pool.dragonx.cc", "us.dragonx.cc:3333", "rx/dragonx",
"https://pool.dragonx.cc/api/pools", PoolStatsSchema::Miningcore,
/*miningcorePoolId=*/"dragonx-pplns", /*feePercent=*/3.0, /*official=*/true,
},
};
return pools;
}

View File

@@ -3434,9 +3434,11 @@ void testBalanceAddressListModel()
EXPECT_NEAR(layout.contentStartX, 22.0, 0.0001);
EXPECT_NEAR(layout.contentStartY, 26.0, 0.0001);
EXPECT_NEAR(layout.buttonSize, 38.0, 0.0001);
EXPECT_NEAR(layout.favoriteButton.x, 270.0, 0.0001);
EXPECT_NEAR(layout.visibilityButton.x, 228.0, 0.0001);
EXPECT_NEAR(layout.contentRight, 224.0, 0.0001);
// Trailing (favorite) button is inset by rowPadLeft (12) so it mirrors the left margin
// instead of hugging the card edge: 310 - 38 - 12 = 260.
EXPECT_NEAR(layout.favoriteButton.x, 260.0, 0.0001);
EXPECT_NEAR(layout.visibilityButton.x, 218.0, 0.0001);
EXPECT_NEAR(layout.contentRight, 214.0, 0.0001);
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(2.0, 3.5), std::string("$7.00"));
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.001, 2.0), std::string("$0.002000"));
EXPECT_EQ(dragonx::ui::FormatAddressUsdValue(0.0, 2.0), std::string(""));
@@ -5774,23 +5776,20 @@ void testXmrigLiveInstall()
void testPoolRegistryLookup()
{
using namespace dragonx::util;
EXPECT_EQ(knownPools().size(), static_cast<std::size_t>(2));
// pool.dragonx.cc was removed from the built-in defaults; pool.dragonx.is is the sole entry.
EXPECT_EQ(knownPools().size(), static_cast<std::size_t>(1));
// The algo follows the pool. Note pool.dragonx.cc's stratum host is us.dragonx.cc
// (the .cc domain is only the Cloudflare-proxied web/API host).
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc:3333", "rx/hush"), std::string("rx/dragonx"));
// The algo follows the pool: pool.dragonx.is resolves to rx/hush regardless of the caller default.
EXPECT_EQ(resolvePoolAlgo("pool.dragonx.is:3433", "rx/dragonx"), std::string("rx/hush"));
// Bare host (no port) still matches; scheme + path are tolerated.
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc", "rx/hush"), std::string("rx/dragonx"));
EXPECT_EQ(resolvePoolAlgo("stratum+tcp://us.dragonx.cc:3333/x", "rx/hush"),
std::string("rx/dragonx"));
// Unknown host -> fallback algo.
// The former us.dragonx.cc (pool.dragonx.cc) host is now unknown -> caller's fallback algo.
EXPECT_EQ(resolvePoolAlgo("us.dragonx.cc:3333", "rx/hush"), std::string("rx/hush"));
EXPECT_EQ(resolvePoolAlgo("my.pool.example:1234", "rx/hush"), std::string("rx/hush"));
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:3333") != nullptr);
EXPECT_TRUE(findKnownPoolByUrl("pool.dragonx.is:3433") != nullptr);
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:3333") == nullptr); // removed built-in default
EXPECT_TRUE(findKnownPoolByUrl("unknown.host:1") == nullptr);
// A mismatched explicit port must NOT match a known pool.
EXPECT_TRUE(findKnownPoolByUrl("us.dragonx.cc:9999") == nullptr);
EXPECT_TRUE(findKnownPoolByUrl("pool.dragonx.is:9999") == nullptr);
}
// Schema-aware pool hashrate parsing (the two pools speak different APIs).