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:
2026-07-02 20:18:02 -05:00
parent 262891229a
commit 121a79e768
3 changed files with 144 additions and 795 deletions

View File

@@ -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);
}
}

View File

@@ -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);

View File

@@ -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) {