From 121a79e7682806b1c442fff9718a32d00cc0f53a Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 2 Jul 2026 20:18:02 -0500 Subject: [PATCH] =?UTF-8?q?refactor(audit):=20batch=209=20=E2=80=94=20de-d?= =?UTF-8?q?uplicate=20balance/peers/transactions=20UI=20cards?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/ui/windows/balance_tab.cpp | 577 +--------------------------- src/ui/windows/peers_tab.cpp | 224 ++++------- src/ui/windows/transactions_tab.cpp | 138 +++---- 3 files changed, 144 insertions(+), 795 deletions(-) diff --git a/src/ui/windows/balance_tab.cpp b/src/ui/windows/balance_tab.cpp index eafdb91..46c8aa6 100644 --- a/src/ui/windows/balance_tab.cpp +++ b/src/ui/windows/balance_tab.cpp @@ -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 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); } } diff --git a/src/ui/windows/peers_tab.cpp b/src/ui/windows/peers_tab.cpp index da73c89..6de74f6 100644 --- a/src/ui/windows/peers_tab.cpp +++ b/src/ui/windows/peers_tab.cpp @@ -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); diff --git a/src/ui/windows/transactions_tab.cpp b/src/ui/windows/transactions_tab.cpp index 82e31a0..b0a43a3 100644 --- a/src/ui/windows/transactions_tab.cpp +++ b/src/ui/windows/transactions_tab.cpp @@ -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) {