refactor(audit): batch 9 — de-duplicate balance/peers/transactions UI cards
Three UI de-duplications (rendering changes; the test suite skips the GUI, so these need a manual screenshot check of the Balance/Peers/Transactions tabs). 1. Classic balance layout — the DEFAULT layout — hand-rolled its own ~550-line address-list + recent-tx renderers that had DIVERGED from the shared RenderSharedAddressList/RenderSharedRecentTx used by the other 9 layouts. Deleted the inline copies and called the shared renderers (as the minimal layout already does), so the default layout now gains set-label + add-to-portfolio context items, custom address icons, drag-to-reorder, keyboard nav, copy-flash, and fully TR()'d strings. Hero row and Classic sizing (tabs.balance.classic address-table-height=340) preserved; addrH is computed before the call exactly as before. This is a deliberate feature-parity behavior change for the default layout. 2. peers_tab info cards — extracted drawStatCell() (label + value/em-dash with the shared offset math) and one drawCardDivider() replacing two duplicate divider lambdas; plain cells drive off a per-card loop. Bespoke cells (Blocks "(X left)", copy-on-click Best Block, TLS check-icon) kept inline. 3. transactions_tab — one drawSummaryCard() replaces the three near-identical Received/Sent/Mined blocks (same glass panel, icon, hover outline, click-to-filter). Byte-equivalent. Full-node + Lite build clean (net -651 lines, no new warnings); ctest 1/1; hygiene clean. NEEDS SCREENSHOT VERIFICATION. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -218,17 +218,11 @@ static void RenderBalanceClassic(App* app)
|
||||
{
|
||||
using namespace material;
|
||||
const auto& S = schema::UISchema::instance();
|
||||
const auto addrBtn = S.button("tabs.balance", "address-button");
|
||||
const auto actionBtn = S.button("tabs.balance", "action-button");
|
||||
const auto searchIn = S.input("tabs.balance", "search-input");
|
||||
const auto addrTable = S.table("tabs.balance", "address-table");
|
||||
const auto syncBar = S.drawElement("tabs.balance", "sync-bar");
|
||||
|
||||
// Read layout properties from schema
|
||||
const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
|
||||
const float kHeroPadTop = S.drawElement("tabs.balance", "hero-pad-top").sizeOr(12.0f);
|
||||
const float kRecentTxRowHeight = S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f);
|
||||
const float kButtonRowRightMargin = S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f);
|
||||
|
||||
const auto& state = app->state();
|
||||
|
||||
@@ -241,17 +235,6 @@ static void RenderBalanceClassic(App* app)
|
||||
const float cGap = Layout::cardGap();
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
// Responsive constants (scale with window size)
|
||||
const float kSparklineHeight = std::max(S.drawElement("tabs.balance", "sparkline-min-height").size, S.drawElement("tabs.balance", "sparkline-height").size * vs);
|
||||
const float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size, S.drawElement("tabs.balance", "buttons-position").size * hs);
|
||||
|
||||
// Dynamic recent tx count — fit as many as space allows
|
||||
const float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
|
||||
// At minimum size ~601px avail, reserve ~300px for hero+cards+addr header,
|
||||
// rest for address list + recent txs. Show 3-5 rows depending on space.
|
||||
const int kRecentTxCount = std::clamp(
|
||||
(int)((contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f)) / scaledRowH), 2, 5);
|
||||
|
||||
// Lerp displayed balances toward actual values
|
||||
{
|
||||
float dt = ImGui::GetIO().DeltaTime;
|
||||
@@ -634,559 +617,19 @@ static void RenderBalanceClassic(App* app)
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Address list — DrawList-based rows (matches recent tx style)
|
||||
// Address list + Recent transactions — shared renderers (feature-complete:
|
||||
// set-label / add-to-portfolio context items, custom address icons, drag-to-reorder,
|
||||
// keyboard nav, copy-flash, i18n). Classic keeps its own address-table-height override
|
||||
// (see tabs.balance.classic) so the card sizing is unchanged.
|
||||
// ================================================================
|
||||
{
|
||||
// Header row: title only
|
||||
Type().text(TypeStyle::H6, TR("your_addresses"));
|
||||
|
||||
ImGui::Spacing();
|
||||
|
||||
// Static filter state (declared here, UI rendered below with ADDRESSES overline)
|
||||
static char addr_search[128] = "";
|
||||
static bool s_hideZeroBalances = true;
|
||||
static bool s_showHidden = false;
|
||||
|
||||
// Build a merged + sorted list of all addresses
|
||||
struct AddrRow {
|
||||
const AddressInfo* info;
|
||||
bool isZ;
|
||||
bool hidden;
|
||||
bool favorite;
|
||||
bool mining;
|
||||
};
|
||||
std::vector<AddrRow> rows;
|
||||
rows.reserve(state.z_addresses.size() + state.t_addresses.size());
|
||||
// The search text is constant across the loop and containsIgnoreCase is now allocation-free,
|
||||
// so build the filter view ONCE (was a per-address std::string + two tolower-copies/frame).
|
||||
const std::string_view addrFilter(addr_search);
|
||||
for (const auto& a : state.z_addresses) {
|
||||
if (!util::containsIgnoreCase(a.address, addrFilter) &&
|
||||
!util::containsIgnoreCase(a.label, addrFilter))
|
||||
continue;
|
||||
bool isHidden = app->isAddressHidden(a.address);
|
||||
if (isHidden && !s_showHidden) continue;
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav)
|
||||
continue;
|
||||
rows.push_back({&a, true, isHidden, isFav, app->isMiningAddress(a.address)});
|
||||
}
|
||||
for (const auto& a : state.t_addresses) {
|
||||
if (!util::containsIgnoreCase(a.address, addrFilter) &&
|
||||
!util::containsIgnoreCase(a.label, addrFilter))
|
||||
continue;
|
||||
bool isHidden = app->isAddressHidden(a.address);
|
||||
if (isHidden && !s_showHidden) continue;
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav)
|
||||
continue;
|
||||
rows.push_back({&a, false, isHidden, isFav, app->isMiningAddress(a.address)});
|
||||
}
|
||||
|
||||
// Sort: favorites first, then Z addresses, then by balance descending
|
||||
static int s_sortCol = 3; // default sort by balance
|
||||
static bool s_sortAsc = false;
|
||||
std::sort(rows.begin(), rows.end(),
|
||||
[](const AddrRow& a, const AddrRow& b) -> bool {
|
||||
if (a.favorite != b.favorite) return a.favorite > b.favorite;
|
||||
if (a.isZ != b.isZ) return a.isZ > b.isZ; // Z first
|
||||
if (s_sortAsc)
|
||||
return a.info->balance < b.info->balance;
|
||||
else
|
||||
return a.info->balance > b.info->balance;
|
||||
});
|
||||
|
||||
// Recent TX gets sizing priority — compute its reserve first,
|
||||
// then the address list gets whatever remains.
|
||||
float scaledTxRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
|
||||
// Recent TX section: header + kRecentTxCount rows + status label + gaps
|
||||
float recentTxReserve = S.drawElement("tabs.balance", "recent-tx-header-height").size * vs + kRecentTxCount * scaledTxRowH + Layout::spacingXl();
|
||||
|
||||
// Search + create buttons row
|
||||
float avail = ImGui::GetContentRegionAvail().x;
|
||||
float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f;
|
||||
float schemaRatio = (searchIn.widthRatio >= 0) ? searchIn.widthRatio : 0.30f;
|
||||
float searchW = std::min(schemaMaxW * hs, avail * schemaRatio);
|
||||
ImGui::SetNextItemWidth(searchW);
|
||||
ImGui::InputTextWithHint("##AddrSearch", "Filter...", addr_search, sizeof(addr_search));
|
||||
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
ImGui::Checkbox(TrId("hide_zero_balances", "hide0").c_str(), &s_hideZeroBalances);
|
||||
{
|
||||
int hc = app->getHiddenAddressCount();
|
||||
if (hc > 0) {
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
char hlbl[64];
|
||||
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
|
||||
ImGui::Checkbox(hlbl, &s_showHidden);
|
||||
} else {
|
||||
s_showHidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
float buttonWidth = (addrBtn.width > 0) ? addrBtn.width : 140.0f;
|
||||
float spacing = (addrBtn.gap > 0) ? addrBtn.gap : 8.0f;
|
||||
float totalButtonsWidth = buttonWidth * 2 + spacing;
|
||||
ImGui::SameLine(std::max(kMinButtonsPosition,
|
||||
ImGui::GetWindowWidth() - totalButtonsWidth - kButtonRowRightMargin));
|
||||
|
||||
bool addrSyncing = state.sync.syncing && !state.sync.isSynced();
|
||||
ImGui::BeginDisabled(addrSyncing);
|
||||
if (s_generating_z_address) {
|
||||
char genLabel[64];
|
||||
snprintf(genLabel, sizeof(genLabel), "%s%s##bal_z", TR("generating"), material::LoadingDots());
|
||||
TactileButton(genLabel, ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
|
||||
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
||||
s_generating_z_address = true;
|
||||
app->createNewZAddress([](const std::string& addr) {
|
||||
s_generating_z_address = false;
|
||||
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
|
||||
});
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton(TR("new_t_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
||||
app->createNewTAddress([](const std::string& addr) {
|
||||
DEBUG_LOGF("Created new t-address: %s\n", addr.c_str());
|
||||
});
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
||||
// Address list gets whatever height remains after recent TX reserve
|
||||
float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
|
||||
float classicAddrH = S.drawElement("tabs.balance.classic", "address-table-height").size;
|
||||
float addrListH;
|
||||
if (classicAddrH >= 0.0f) {
|
||||
addrListH = classicAddrH * dp; // scaled for DPI + font scale
|
||||
} else {
|
||||
addrListH = ImGui::GetContentRegionAvail().y - recentTxReserve;
|
||||
}
|
||||
// Keep address list at a reasonable minimum; if too tight,
|
||||
// shrink recent-tx reserve instead so both remain visible.
|
||||
float addrListMin = S.drawElement("tabs.balance", "addr-list-min-height").sizeOr(40.0f);
|
||||
if (addrListH < addrListMin) {
|
||||
addrListH = addrListMin;
|
||||
}
|
||||
|
||||
// Glass panel wrapping the list area (matching tx list)
|
||||
ImDrawList* dlPanel = ImGui::GetWindowDrawList();
|
||||
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 listPanelMax(listPanelMin.x + availWidth, listPanelMin.y + addrListH);
|
||||
GlassPanelSpec addrGlassSpec;
|
||||
addrGlassSpec.rounding = glassRound;
|
||||
DrawGlassPanel(dlPanel, listPanelMin, listPanelMax, addrGlassSpec);
|
||||
|
||||
// Scroll-edge mask state
|
||||
float addrScrollY = 0.0f, addrScrollMaxY = 0.0f;
|
||||
int addrParentVtx = dlPanel->VtxBuffer.Size;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingSm()));
|
||||
ImGui::BeginChild("AddressList", ImVec2(availWidth, addrListH), false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ApplySmoothScroll();
|
||||
ImDrawList* addrChildDL = ImGui::GetWindowDrawList();
|
||||
int addrChildVtx = addrChildDL->VtxBuffer.Size;
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
if (!app->isConnected()) {
|
||||
ImGui::Dummy(ImVec2(0, 16 * dp));
|
||||
float cw = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 ts = ImGui::CalcTextSize(TR("not_connected"));
|
||||
ImGui::SetCursorPosX((cw - ts.x) * 0.5f);
|
||||
ImGui::TextDisabled("%s", TR("not_connected"));
|
||||
} else if (rows.empty()) {
|
||||
// Empty state
|
||||
float cw = ImGui::GetContentRegionAvail().x;
|
||||
float ch = ImGui::GetContentRegionAvail().y;
|
||||
if (ch < 60) ch = 60;
|
||||
|
||||
if (addr_search[0]) {
|
||||
ImVec2 textSz = ImGui::CalcTextSize("No matching addresses");
|
||||
ImGui::SetCursorPosX((cw - textSz.x) * 0.5f);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
|
||||
ImGui::TextDisabled("No matching addresses");
|
||||
} else {
|
||||
const char* msg = "No addresses yet";
|
||||
ImVec2 msgSz = ImGui::CalcTextSize(msg);
|
||||
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
|
||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
|
||||
ImGui::TextDisabled("%s", msg);
|
||||
}
|
||||
} else {
|
||||
// DrawList-based address rows (matching transaction list style)
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImFont* capFont = Type().caption();
|
||||
ImFont* body2 = Type().body2();
|
||||
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
|
||||
static int selected_row = -1;
|
||||
addrScrollY = ImGui::GetScrollY();
|
||||
addrScrollMaxY = ImGui::GetScrollMaxY();
|
||||
|
||||
ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
|
||||
ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning());
|
||||
float rowPadLeft = Layout::cardInnerPadding();
|
||||
float rowPadRight = Layout::cardInnerPadding();
|
||||
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size, S.drawElement("tabs.balance", "address-icon-size").size * hs);
|
||||
float innerW = ImGui::GetContentRegionAvail().x;
|
||||
|
||||
for (int row_idx = 0; row_idx < (int)rows.size(); row_idx++) {
|
||||
const auto& row = rows[row_idx];
|
||||
const auto& addr = *row.info;
|
||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
||||
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
|
||||
|
||||
ImU32 typeCol = row.isZ ? greenCol : goldCol;
|
||||
if (row.hidden) typeCol = OnSurfaceDisabled();
|
||||
|
||||
// Golden border for favorites
|
||||
if (row.favorite) {
|
||||
ImU32 favBorder = IM_COL32(255, 200, 50, 120);
|
||||
dl->AddRect(rowPos, rowEnd, favBorder, 4.0f * dp, 0, 1.5f * dp);
|
||||
}
|
||||
|
||||
// Selected indicator (left accent bar)
|
||||
if (selected_row == row_idx) {
|
||||
ImDrawFlags accentFlags = 0;
|
||||
float accentRound = 2.0f * dp;
|
||||
if (row_idx == 0) {
|
||||
accentFlags = ImDrawFlags_RoundCornersTopLeft;
|
||||
accentRound = glassRound;
|
||||
}
|
||||
if (row_idx == (int)rows.size() - 1) {
|
||||
accentFlags |= ImDrawFlags_RoundCornersBottomLeft;
|
||||
accentRound = glassRound;
|
||||
}
|
||||
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + 3 * dp, rowEnd.y), typeCol, accentRound, accentFlags);
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), 4.0f * dp);
|
||||
}
|
||||
|
||||
// Hover glow
|
||||
bool hovered = material::IsRectHovered(rowPos, rowEnd);
|
||||
if (hovered && selected_row != row_idx) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), 4.0f * dp);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
}
|
||||
|
||||
float cx = rowPos.x + rowPadLeft;
|
||||
float cy = rowPos.y + Layout::spacingMd();
|
||||
|
||||
// --- Button zone (right edge): [eye] [star] ---
|
||||
float btnH = rowH - Layout::spacingSm() * 2.0f;
|
||||
float btnW = btnH;
|
||||
float btnGap = Layout::spacingSm();
|
||||
float btnY = rowPos.y + (rowH - btnH) * 0.5f;
|
||||
float rightEdge = rowPos.x + innerW;
|
||||
float starX = rightEdge - btnW - rowPadRight;
|
||||
float eyeX = starX - btnGap - btnW;
|
||||
float btnRound = 6.0f * dp;
|
||||
bool btnClicked = false;
|
||||
|
||||
// Star button (always shown, rightmost)
|
||||
{
|
||||
ImVec2 bMin(starX, btnY), bMax(starX + btnW, btnY + btnH);
|
||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
||||
ImU32 starFill = row.favorite ? IM_COL32(255, 200, 50, 40) : IM_COL32(255, 255, 255, bHov ? 25 : 12);
|
||||
ImU32 starBorder = row.favorite ? IM_COL32(255, 200, 50, 100) : IM_COL32(255, 255, 255, bHov ? 50 : 25);
|
||||
dl->AddRectFilled(bMin, bMax, starFill, btnRound);
|
||||
dl->AddRect(bMin, bMax, starBorder, btnRound, 0, 1.0f * dp);
|
||||
ImFont* iconFont = material::Type().iconSmall();
|
||||
const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER;
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
|
||||
ImU32 starCol = row.favorite ? IM_COL32(255, 200, 50, 255) : (bHov ? OnSurface() : OnSurfaceDisabled());
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(starX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), starCol, starIcon);
|
||||
if (bHov && ImGui::IsMouseClicked(0)) {
|
||||
if (row.favorite) app->unfavoriteAddress(addr.address);
|
||||
else app->favoriteAddress(addr.address);
|
||||
btnClicked = true;
|
||||
}
|
||||
if (bHov) material::Tooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
|
||||
}
|
||||
|
||||
// Eye button (zero balance or hidden)
|
||||
if (addr.balance < 1e-9 || row.hidden) {
|
||||
ImVec2 bMin(eyeX, btnY), bMax(eyeX + btnW, btnY + btnH);
|
||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
||||
ImU32 eyeFill = IM_COL32(255, 255, 255, bHov ? 25 : 12);
|
||||
ImU32 eyeBorder = IM_COL32(255, 255, 255, bHov ? 50 : 25);
|
||||
dl->AddRectFilled(bMin, bMax, eyeFill, btnRound);
|
||||
dl->AddRect(bMin, bMax, eyeBorder, btnRound, 0, 1.0f * dp);
|
||||
ImFont* iconFont = material::Type().iconSmall();
|
||||
const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
|
||||
ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(eyeX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), iconCol, hideIcon);
|
||||
if (bHov && ImGui::IsMouseClicked(0)) {
|
||||
if (row.hidden) app->unhideAddress(addr.address);
|
||||
else app->hideAddress(addr.address);
|
||||
btnClicked = true;
|
||||
}
|
||||
if (bHov) material::Tooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
|
||||
}
|
||||
|
||||
// Content zone ends before buttons
|
||||
float contentRight = eyeX - Layout::spacingSm();
|
||||
|
||||
// Type icon (shield for Z, circle for T)
|
||||
float iconCx = cx + rowIconSz;
|
||||
float iconCy = cy + body2->LegacySize * 0.5f;
|
||||
if (row.isZ) {
|
||||
ImFont* iconFont = material::Type().iconSmall();
|
||||
const char* shieldIcon = ICON_MD_SHIELD;
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, shieldIcon);
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, shieldIcon);
|
||||
} else {
|
||||
ImFont* iconFont = material::Type().iconSmall();
|
||||
const char* circIcon = ICON_MD_CIRCLE;
|
||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, circIcon);
|
||||
dl->AddText(iconFont, iconFont->LegacySize,
|
||||
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, circIcon);
|
||||
}
|
||||
|
||||
// Type label (first line, next to icon)
|
||||
float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
|
||||
const char* typeLabel = row.isZ ? "Shielded" : "Transparent";
|
||||
const char* hiddenTag = row.hidden ? " (hidden)" : "";
|
||||
const char* viewOnlyTag = (!addr.has_spending_key) ? " (view-only)" : "";
|
||||
const char* miningTag = row.mining ? TR("mining_tag") : "";
|
||||
char typeBuf[64];
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s%s", typeLabel, hiddenTag, viewOnlyTag, miningTag);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||
|
||||
// Label (if present, next to type)
|
||||
if (!addr.label.empty()) {
|
||||
float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy),
|
||||
OnSurfaceMedium(), addr.label.c_str());
|
||||
}
|
||||
|
||||
// Address (second line) — show full if it fits, otherwise truncate
|
||||
float addrAvailW = contentRight - labelX;
|
||||
ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str());
|
||||
std::string display_addr;
|
||||
if (fullAddrSz.x <= addrAvailW) {
|
||||
display_addr = addr.address;
|
||||
} else {
|
||||
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0)
|
||||
? addrTable.columns.at("address").truncate : 32;
|
||||
display_addr = truncateAddress(addr.address, addrTruncLen);
|
||||
}
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceMedium(), display_addr.c_str());
|
||||
|
||||
// Balance (right-aligned within content zone)
|
||||
char balBuf[32];
|
||||
snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance);
|
||||
ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf);
|
||||
float balX = contentRight - balSz.x;
|
||||
ImU32 balCol = addr.balance > 0.0
|
||||
? (row.isZ ? greenCol : OnSurface())
|
||||
: OnSurfaceDisabled();
|
||||
if (row.hidden) balCol = OnSurfaceDisabled();
|
||||
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf);
|
||||
|
||||
// USD equivalent (right-aligned, second line)
|
||||
double priceUsd = state.market.price_usd;
|
||||
if (priceUsd > 0.0 && addr.balance > 0.0) {
|
||||
char usdBuf[32];
|
||||
double usdVal = addr.balance * priceUsd;
|
||||
if (usdVal >= 1.0)
|
||||
snprintf(usdBuf, sizeof(usdBuf), "$%.2f", usdVal);
|
||||
else if (usdVal >= 0.01)
|
||||
snprintf(usdBuf, sizeof(usdBuf), "$%.4f", usdVal);
|
||||
else
|
||||
snprintf(usdBuf, sizeof(usdBuf), "$%.6f", usdVal);
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdBuf);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(contentRight - usdSz.x,
|
||||
cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), usdBuf);
|
||||
}
|
||||
|
||||
// Click to copy + select
|
||||
if (hovered && ImGui::IsMouseClicked(0) && !btnClicked) {
|
||||
ImGui::SetClipboardText(addr.address.c_str());
|
||||
selected_row = row_idx;
|
||||
}
|
||||
|
||||
// Invisible button for context menu + tooltip
|
||||
ImGui::PushID(row_idx);
|
||||
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
|
||||
|
||||
// Tooltip with full address
|
||||
if (ImGui::IsItemHovered() && !btnClicked) {
|
||||
material::Tooltip("%s", addr.address.c_str());
|
||||
}
|
||||
|
||||
// Right-click context menu
|
||||
const auto& acrTheme = GetCurrentAcrylicTheme();
|
||||
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("AddressContext", 0, acrTheme.menu)) {
|
||||
if (ImGui::MenuItem(TR("copy_address"))) {
|
||||
ImGui::SetClipboardText(addr.address.c_str());
|
||||
}
|
||||
if (ImGui::MenuItem(TR("send_from_this_address"))) {
|
||||
SetSendFromAddress(addr.address);
|
||||
app->setCurrentPage(NavPage::Send);
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
|
||||
app->setMiningAddress(addr.address, !row.mining);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("export_private_key"))) {
|
||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
|
||||
}
|
||||
if (row.isZ) {
|
||||
if (ImGui::MenuItem(TR("export_viewing_key"))) {
|
||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Viewing);
|
||||
}
|
||||
}
|
||||
if (ImGui::MenuItem(TR("show_qr_code"))) {
|
||||
QRPopupDialog::show(addr.address,
|
||||
row.isZ ? "Z-Address" : "T-Address");
|
||||
}
|
||||
ImGui::Separator();
|
||||
if (row.hidden) {
|
||||
if (ImGui::MenuItem(TR("restore_address")))
|
||||
app->unhideAddress(addr.address);
|
||||
} else if (addr.balance < 1e-9) {
|
||||
if (ImGui::MenuItem(TR("hide_address")))
|
||||
app->hideAddress(addr.address);
|
||||
}
|
||||
if (row.favorite) {
|
||||
if (ImGui::MenuItem(TR("remove_favorite")))
|
||||
app->unfavoriteAddress(addr.address);
|
||||
} else {
|
||||
if (ImGui::MenuItem(TR("favorite_address")))
|
||||
app->favoriteAddress(addr.address);
|
||||
}
|
||||
effects::ImGuiAcrylic::EndAcrylicPopup();
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
// Subtle divider between rows (matching tx list)
|
||||
if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) {
|
||||
ImVec2 divStart = ImGui::GetCursorScreenPos();
|
||||
dl->AddLine(
|
||||
ImVec2(divStart.x + rowPadLeft + rowIconSz * 2.0f, divStart.y),
|
||||
ImVec2(divStart.x + innerW - Layout::spacingLg(), divStart.y),
|
||||
IM_COL32(255, 255, 255, 15));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
ImGui::EndChild();
|
||||
ImGui::PopStyleVar(); // WindowPadding for address list
|
||||
|
||||
// CSS-style clipping mask (same as history list)
|
||||
{
|
||||
float fadeZone = std::min(
|
||||
(Type().body2()->LegacySize + Type().caption()->LegacySize + Layout::spacingLg() + Layout::spacingMd()) * 1.2f,
|
||||
addrListH * 0.18f);
|
||||
ApplyScrollEdgeMask(dlPanel, addrParentVtx, addrChildDL, addrChildVtx,
|
||||
listPanelMin.y, listPanelMax.y, fadeZone, addrScrollY, addrScrollMaxY);
|
||||
}
|
||||
|
||||
// Status line (matching tx list)
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
char countBuf[128];
|
||||
int totalAddrs = (int)(state.z_addresses.size() + state.t_addresses.size());
|
||||
snprintf(countBuf, sizeof(countBuf), "Showing %d of %d addresses",
|
||||
(int)rows.size(), totalAddrs);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf);
|
||||
}
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// Recent Transactions
|
||||
// ================================================================
|
||||
{
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS");
|
||||
ImGui::SameLine();
|
||||
if (TactileSmallButton("View All", S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) {
|
||||
app->setCurrentPage(NavPage::History);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
ImFont* capFont = Type().caption();
|
||||
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
|
||||
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
|
||||
ImGui::BeginChild("##BalanceClassicRecentRows", ImVec2(0, listH), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
|
||||
const auto& txs = state.transactions;
|
||||
int count = (int)txs.size();
|
||||
if (count == 0) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
||||
"No transactions yet");
|
||||
} else {
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size, S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
const auto& tx = txs[i];
|
||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
||||
float rowY = rowPos.y + rowH * 0.5f;
|
||||
|
||||
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
|
||||
|
||||
// Type label
|
||||
float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(tx_x, rowPos.y + 2 * dp),
|
||||
OnSurfaceMedium(), display.typeText.c_str());
|
||||
|
||||
// Address (truncated)
|
||||
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(addrX, rowPos.y + 2 * dp),
|
||||
OnSurfaceDisabled(), display.addressText.c_str());
|
||||
|
||||
// Amount (right-aligned area)
|
||||
ImVec2 amtSz = capFont->CalcTextSizeA(
|
||||
capFont->LegacySize, 10000, 0, display.amountText.c_str());
|
||||
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
|
||||
float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size, S.drawElement("tabs.balance", "amount-right-margin").size * hs);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(amtX, rowPos.y + 2 * dp),
|
||||
recentTxAmountColor(tx.type),
|
||||
display.amountText.c_str());
|
||||
|
||||
// Time ago
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(
|
||||
capFont->LegacySize, 10000, 0, display.timeText.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
|
||||
OnSurfaceDisabled(), display.timeText.c_str());
|
||||
|
||||
// Clickable row — hover highlight + navigate to History
|
||||
float rowW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
|
||||
if (material::IsRectHovered(rowPos, rowEnd)) {
|
||||
dl->AddRectFilled(rowPos, rowEnd,
|
||||
IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
// Show the full, untruncated address — two z-addresses can truncate to the
|
||||
// same first/last window, so the truncated text alone can't disambiguate.
|
||||
if (!tx.address.empty())
|
||||
material::Tooltip("%s", tx.address.c_str());
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::History);
|
||||
}
|
||||
|
||||
ImGui::Dummy(ImVec2(0, rowH));
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
float addrH = (classicAddrH >= 0.0f) ? classicAddrH * dp
|
||||
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||
RenderSharedAddressList(app, addrH, contentAvail.x, glassRound, hs, vs);
|
||||
RenderSharedRecentTx(app, recentReserve, contentAvail.x, hs, vs);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,30 @@ static bool IsSeedNode(const std::string& addr) {
|
||||
return seeds.count(ExtractIP(addr)) > 0;
|
||||
}
|
||||
|
||||
// Draw one label/value stat cell in an info card. The caption label sits on the first line
|
||||
// (OnSurfaceMedium); the value sits below it (valueFont) — or an em-dash "—" in the disabled
|
||||
// color when the value is null/empty. Matches the hand-positioned blocks it replaces exactly:
|
||||
// value baseline is ry + capFont->LegacySize + spacingXs().
|
||||
static void drawStatCell(ImDrawList* dl, float cx, float ry, ImFont* capFont, ImFont* valueFont,
|
||||
const char* label, const char* valueText, ImU32 valueCol)
|
||||
{
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), label);
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (valueText && valueText[0]) {
|
||||
dl->AddText(valueFont, valueFont->LegacySize, ImVec2(cx, valY), valueCol, valueText);
|
||||
} else {
|
||||
dl->AddText(valueFont, valueFont->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a subtle horizontal divider between info-card rows, inset to the card's rounded corners.
|
||||
static void drawCardDivider(ImDrawList* dl, float leftX, float rightX, float y, float rounding, float dp)
|
||||
{
|
||||
dl->AddLine(ImVec2(leftX + rounding * 0.5f, y),
|
||||
ImVec2(rightX - rounding * 0.5f, y),
|
||||
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
||||
}
|
||||
|
||||
void RenderPeersTab(App* app)
|
||||
{
|
||||
auto& S = schema::UI();
|
||||
@@ -153,20 +177,22 @@ void RenderPeersTab(App* app)
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_blockchain"));
|
||||
|
||||
float colW = (cardW - pad * 2) / 2.0f;
|
||||
float leftX = cardMin.x + pad;
|
||||
float rightX = cardMin.x + pad + colW;
|
||||
float ry = cardMin.y + pad * 0.5f + headerH;
|
||||
|
||||
// Helper to draw a subtle horizontal divider
|
||||
auto drawDivider = [&](float y) {
|
||||
float rnd = glassSpec.rounding;
|
||||
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, y),
|
||||
ImVec2(cardMax.x - rnd * 0.5f, y),
|
||||
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
||||
// Advance ry past a row and draw a divider before the next one. Keeps the row
|
||||
// stepping + divider in one place (was a per-row copy + a duplicate divider lambda).
|
||||
auto stepRow = [&]() {
|
||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||
drawCardDivider(dl, cardMin.x, cardMax.x, ry, glassSpec.rounding, dp);
|
||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||
};
|
||||
|
||||
// Row 1: Blocks | Longest Chain
|
||||
{
|
||||
// Blocks
|
||||
float cx = cardMin.x + pad;
|
||||
// Blocks — bespoke: block number + "(X left)" in a second color
|
||||
float cx = leftX;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_blocks"));
|
||||
int blocks = state.sync.blocks;
|
||||
if (blocks > 0) {
|
||||
@@ -174,7 +200,6 @@ void RenderPeersTab(App* app)
|
||||
int blocksLeft = chainTip - blocks;
|
||||
if (blocksLeft < 0) blocksLeft = 0;
|
||||
if (blocksLeft > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d (%d left)", blocks, blocksLeft);
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
// Draw block number in normal color
|
||||
char blockStr[32];
|
||||
@@ -196,136 +221,85 @@ void RenderPeersTab(App* app)
|
||||
}
|
||||
|
||||
// Longest Chain
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_longest_chain"));
|
||||
if (state.longestchain > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
||||
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
||||
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), chainCol, buf);
|
||||
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_longest_chain"), buf, chainCol);
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_longest_chain"), nullptr, OnSurface());
|
||||
}
|
||||
}
|
||||
|
||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||
drawDivider(ry);
|
||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||
stepRow();
|
||||
|
||||
// Row 2: Hashrate | Difficulty
|
||||
{
|
||||
// Hashrate
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_hashrate"));
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (mining.networkHashrate > 0) {
|
||||
std::string hr = FormatHashrate(mining.networkHashrate);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), Success(), hr.c_str());
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
}
|
||||
std::string hr = mining.networkHashrate > 0 ? FormatHashrate(mining.networkHashrate) : std::string();
|
||||
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_hashrate"),
|
||||
hr.empty() ? nullptr : hr.c_str(), Success());
|
||||
|
||||
// Difficulty
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("difficulty"));
|
||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (mining.difficulty > 0) {
|
||||
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
}
|
||||
if (mining.difficulty > 0) snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
|
||||
drawStatCell(dl, rightX, ry, capFont, sub1, TR("difficulty"),
|
||||
mining.difficulty > 0 ? buf : nullptr, OnSurface());
|
||||
}
|
||||
|
||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||
drawDivider(ry);
|
||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||
stepRow();
|
||||
|
||||
// Row 3: Notarized | Protocol
|
||||
{
|
||||
// Notarized
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_notarized"));
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (state.notarized > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", state.notarized);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
}
|
||||
if (state.notarized > 0) snprintf(buf, sizeof(buf), "%d", state.notarized);
|
||||
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_notarized"),
|
||||
state.notarized > 0 ? buf : nullptr, OnSurface());
|
||||
|
||||
// Protocol
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_protocol"));
|
||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (state.protocol_version > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", state.protocol_version);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
}
|
||||
if (state.protocol_version > 0) snprintf(buf, sizeof(buf), "%d", state.protocol_version);
|
||||
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_protocol"),
|
||||
state.protocol_version > 0 ? buf : nullptr, OnSurface());
|
||||
}
|
||||
|
||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||
drawDivider(ry);
|
||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||
stepRow();
|
||||
|
||||
// Row 4: Version | Memory
|
||||
{
|
||||
// Version
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_version"));
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (state.daemon_version > 0) {
|
||||
int major = state.daemon_version / 1000000;
|
||||
int minor = (state.daemon_version / 10000) % 100;
|
||||
int patch = (state.daemon_version / 100) % 100;
|
||||
snprintf(buf, sizeof(buf), "%d.%d.%d", major, minor, patch);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
}
|
||||
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_version"),
|
||||
state.daemon_version > 0 ? buf : nullptr, OnSurface());
|
||||
|
||||
// Memory
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_memory"));
|
||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
double memMb = state.mining.daemon_memory_mb;
|
||||
if (memMb > 0) {
|
||||
if (memMb >= 1024.0)
|
||||
snprintf(buf, sizeof(buf), "%.1f GB", memMb / 1024.0);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "%.0f MB", memMb);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
}
|
||||
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_memory"),
|
||||
memMb > 0 ? buf : nullptr, OnSurface());
|
||||
}
|
||||
|
||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||
drawDivider(ry);
|
||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||
stepRow();
|
||||
|
||||
// Row 5: Longest Chain | Best Block
|
||||
{
|
||||
// Longest Chain
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_longest"));
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (state.longestchain > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
||||
// Color green if local matches longest, warning if behind
|
||||
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
||||
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), chainCol, buf);
|
||||
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_longest"), buf, chainCol);
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_longest"), nullptr, OnSurface());
|
||||
}
|
||||
|
||||
// Best Block (truncated hash)
|
||||
cx = cardMin.x + pad + colW;
|
||||
// Best Block — bespoke: truncated hash with click-to-copy + hover underline
|
||||
float cx = rightX;
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_best_block"));
|
||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (!state.sync.best_blockhash.empty()) {
|
||||
// Truncate hash to fit: first 6 + "..." + last 6
|
||||
std::string hash = state.sync.best_blockhash;
|
||||
@@ -372,39 +346,32 @@ void RenderPeersTab(App* app)
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_upper"));
|
||||
|
||||
float colW = (cardW - pad * 2) / 2.0f;
|
||||
float leftX = cardMin.x + pad;
|
||||
float rightX = cardMin.x + pad + colW;
|
||||
float ry = cardMin.y + pad * 0.5f + headerH;
|
||||
|
||||
// Helper to draw a subtle horizontal divider
|
||||
auto drawPeerDivider = [&](float y) {
|
||||
float rnd = glassSpec.rounding;
|
||||
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, y),
|
||||
ImVec2(cardMax.x - rnd * 0.5f, y),
|
||||
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
||||
// Advance ry past a row and draw a divider before the next one (see blockchain card).
|
||||
auto stepRow = [&]() {
|
||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||
drawCardDivider(dl, cardMin.x, cardMax.x, ry, glassSpec.rounding, dp);
|
||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||
};
|
||||
|
||||
// Row 1: Connected | In/Out
|
||||
{
|
||||
// Connected
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_connected"));
|
||||
snprintf(buf, sizeof(buf), "%d", totalPeers);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_connected"), buf, OnSurface());
|
||||
|
||||
// In / Out
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_in_out"));
|
||||
snprintf(buf, sizeof(buf), "%d / %d", inboundCount, outboundCount);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
||||
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_in_out"), buf, OnSurface());
|
||||
}
|
||||
|
||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||
drawPeerDivider(ry);
|
||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||
stepRow();
|
||||
|
||||
// Row 2: TLS | Avg Ping
|
||||
{
|
||||
// TLS
|
||||
float cx = cardMin.x + pad;
|
||||
// TLS — bespoke: appends a check icon when every peer is TLS
|
||||
float cx = leftX;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "TLS");
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (totalPeers > 0) {
|
||||
@@ -420,60 +387,39 @@ void RenderPeersTab(App* app)
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
}
|
||||
|
||||
// Avg Ping
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_avg_ping"));
|
||||
// Avg Ping — color by latency
|
||||
ImU32 pingCol;
|
||||
if (avgPing < 100) pingCol = Success();
|
||||
else if (avgPing < 500) pingCol = Warning();
|
||||
else pingCol = Error();
|
||||
snprintf(buf, sizeof(buf), "%.0f ms", avgPing);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), pingCol, buf);
|
||||
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_avg_ping"), buf, pingCol);
|
||||
}
|
||||
|
||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||
drawPeerDivider(ry);
|
||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||
stepRow();
|
||||
|
||||
// Row 3: Received | Sent
|
||||
{
|
||||
// Received
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_received"));
|
||||
std::string recvStr = fmtBytes(totalBytesRecv);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), recvStr.c_str());
|
||||
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_received"), recvStr.c_str(), OnSurface());
|
||||
|
||||
// Sent
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_sent"));
|
||||
std::string sentStr = fmtBytes(totalBytesSent);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), sentStr.c_str());
|
||||
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_sent"), sentStr.c_str(), OnSurface());
|
||||
}
|
||||
|
||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||
drawPeerDivider(ry);
|
||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||
stepRow();
|
||||
|
||||
// Row 4: P2P Port | Banned
|
||||
{
|
||||
// P2P Port
|
||||
float cx = cardMin.x + pad;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_p2p_port"));
|
||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (state.p2p_port > 0) {
|
||||
snprintf(buf, sizeof(buf), "%d", state.p2p_port);
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||
}
|
||||
// Banned count
|
||||
cx = cardMin.x + pad + colW;
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_banned"));
|
||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||
if (state.p2p_port > 0) snprintf(buf, sizeof(buf), "%d", state.p2p_port);
|
||||
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_p2p_port"),
|
||||
state.p2p_port > 0 ? buf : nullptr, OnSurface());
|
||||
|
||||
size_t bannedCount = state.bannedPeers.size();
|
||||
snprintf(buf, sizeof(buf), "%zu", bannedCount);
|
||||
ImU32 bannedCol = (bannedCount > 0) ? Warning() : OnSurface();
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), bannedCol, buf); }
|
||||
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_banned"), buf, bannedCol);
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(basePos);
|
||||
|
||||
@@ -96,6 +96,44 @@ static void DrawTxIcon(ImDrawList* dl, const std::string& type,
|
||||
ImVec2(cx - sz.x * 0.5f, cy - sz.y * 0.5f), col, icon);
|
||||
}
|
||||
|
||||
// Draw one Received/Sent/Mined summary card: icon + label + count + signed total, with a
|
||||
// hover outline and click-to-toggle type filter. All three cards are identical apart from the
|
||||
// per-card data passed here. cMin is the card's top-left; filterValue is the type_filter value
|
||||
// this card selects (clicking again clears it).
|
||||
static void drawSummaryCard(ImDrawList* dl, ImVec2 cMin, float cardW, float cardH,
|
||||
float innerPad, float iconSz, const GlassPanelSpec& glassSpec,
|
||||
ImFont* ovFont, ImFont* capFont, ImFont* body2,
|
||||
const char* iconType, ImU32 accentCol, const char* labelKey,
|
||||
int count, double total, const char* signPrefix,
|
||||
int filterValue, int& type_filter)
|
||||
{
|
||||
char buf[128];
|
||||
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
|
||||
DrawGlassPanel(dl, cMin, cMax, glassSpec);
|
||||
|
||||
float cx = cMin.x + innerPad;
|
||||
float cy = cMin.y + Layout::spacingMd();
|
||||
|
||||
DrawTxIcon(dl, iconType, cx + iconSz, cy + iconSz * 1.33f, iconSz, accentCol);
|
||||
|
||||
float labelX = cx + iconSz * 3.0f;
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR(labelKey));
|
||||
cy += ovFont->LegacySize + Layout::spacingSm();
|
||||
|
||||
snprintf(buf, sizeof(buf), TR("txs_count"), count);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
|
||||
cy += capFont->LegacySize + Layout::spacingXs();
|
||||
|
||||
snprintf(buf, sizeof(buf), "%s%.4f %s", signPrefix, total, DRAGONX_TICKER);
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), accentCol, buf);
|
||||
|
||||
if (material::IsRectHovered(cMin, cMax)) {
|
||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == filterValue) ? 0 : filterValue;
|
||||
}
|
||||
}
|
||||
|
||||
void RenderTransactionsTab(App* app)
|
||||
{
|
||||
auto& S = schema::UISchema::instance();
|
||||
@@ -203,95 +241,17 @@ void RenderTransactionsTab(App* app)
|
||||
// Sort mode: 0 = newest first, 1 = oldest first, 2 = largest amount, 3 = smallest amount.
|
||||
static int s_sort_mode = 0;
|
||||
|
||||
// --- Received card ---
|
||||
{
|
||||
ImVec2 cMin = origin;
|
||||
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
|
||||
DrawGlassPanel(dl, cMin, cMax, glassSpec);
|
||||
|
||||
float cx = cMin.x + innerPad;
|
||||
float cy = cMin.y + Layout::spacingMd();
|
||||
|
||||
// Icon
|
||||
DrawTxIcon(dl, "receive", cx + iconSz, cy + iconSz * 1.33f, iconSz, greenCol);
|
||||
|
||||
float labelX = cx + iconSz * 3.0f;
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("received_upper"));
|
||||
cy += ovFont->LegacySize + Layout::spacingSm();
|
||||
|
||||
snprintf(buf, sizeof(buf), TR("txs_count"), recvCount);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
|
||||
cy += capFont->LegacySize + Layout::spacingXs();
|
||||
|
||||
snprintf(buf, sizeof(buf), "+%.4f %s", recvTotal, DRAGONX_TICKER);
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), greenCol, buf);
|
||||
|
||||
if (material::IsRectHovered(cMin, cMax)) {
|
||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 2) ? 0 : 2;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Sent card ---
|
||||
{
|
||||
float xOff = cardW + cardGap;
|
||||
ImVec2 cMin(origin.x + xOff, origin.y);
|
||||
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
|
||||
DrawGlassPanel(dl, cMin, cMax, glassSpec);
|
||||
|
||||
float cx = cMin.x + innerPad;
|
||||
float cy = cMin.y + Layout::spacingMd();
|
||||
|
||||
DrawTxIcon(dl, "send", cx + iconSz, cy + iconSz * 1.33f, iconSz, redCol);
|
||||
|
||||
float labelX = cx + iconSz * 3.0f;
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("sent_upper"));
|
||||
cy += ovFont->LegacySize + Layout::spacingSm();
|
||||
|
||||
snprintf(buf, sizeof(buf), TR("txs_count"), sendCount);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
|
||||
cy += capFont->LegacySize + Layout::spacingXs();
|
||||
|
||||
snprintf(buf, sizeof(buf), "-%.4f %s", sendTotal, DRAGONX_TICKER);
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), redCol, buf);
|
||||
|
||||
if (material::IsRectHovered(cMin, cMax)) {
|
||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 1) ? 0 : 1;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Mined card ---
|
||||
{
|
||||
float xOff = 2 * (cardW + cardGap);
|
||||
ImVec2 cMin(origin.x + xOff, origin.y);
|
||||
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
|
||||
DrawGlassPanel(dl, cMin, cMax, glassSpec);
|
||||
|
||||
float cx = cMin.x + innerPad;
|
||||
float cy = cMin.y + Layout::spacingMd();
|
||||
|
||||
DrawTxIcon(dl, "mined", cx + iconSz, cy + iconSz * 1.33f, iconSz, goldCol);
|
||||
|
||||
float labelX = cx + iconSz * 3.0f;
|
||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("mined_upper"));
|
||||
cy += ovFont->LegacySize + Layout::spacingSm();
|
||||
|
||||
snprintf(buf, sizeof(buf), TR("txs_count"), minedCount);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
|
||||
cy += capFont->LegacySize + Layout::spacingXs();
|
||||
|
||||
snprintf(buf, sizeof(buf), "+%.4f %s", minedTotal, DRAGONX_TICKER);
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), goldCol, buf);
|
||||
|
||||
if (material::IsRectHovered(cMin, cMax)) {
|
||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 3) ? 0 : 3;
|
||||
}
|
||||
}
|
||||
// --- Received / Sent / Mined cards ---
|
||||
// filter values: Received = 2, Sent = 1, Mined = 3 (see idx_map for the accent bar below)
|
||||
drawSummaryCard(dl, origin, cardW, cardH, innerPad, iconSz, glassSpec,
|
||||
ovFont, capFont, body2, "receive", greenCol, "received_upper",
|
||||
recvCount, recvTotal, "+", 2, type_filter);
|
||||
drawSummaryCard(dl, ImVec2(origin.x + (cardW + cardGap), origin.y), cardW, cardH,
|
||||
innerPad, iconSz, glassSpec, ovFont, capFont, body2, "send", redCol, "sent_upper",
|
||||
sendCount, sendTotal, "-", 1, type_filter);
|
||||
drawSummaryCard(dl, ImVec2(origin.x + 2 * (cardW + cardGap), origin.y), cardW, cardH,
|
||||
innerPad, iconSz, glassSpec, ovFont, capFont, body2, "mined", goldCol, "mined_upper",
|
||||
minedCount, minedTotal, "+", 3, type_filter);
|
||||
|
||||
// Selected card accent
|
||||
if (type_filter > 0) {
|
||||
|
||||
Reference in New Issue
Block a user