// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "peers_tab.h" #include "../../app.h" #include "../../data/wallet_state.h" #include "../theme.h" #include "../effects/imgui_acrylic.h" #include "../effects/low_spec.h" #include "../schema/ui_schema.h" #include "../material/type.h" #include "../material/draw_helpers.h" #include "../material/colors.h" #include "../layout.h" #include "../notifications.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include #include #include #include #include namespace dragonx { namespace ui { using namespace material; // Track selected peer for ban action static int s_selected_peer_idx = -1; static int s_selected_banned_idx = -1; // Helper: Extract IP without port static std::string ExtractIP(const std::string& addr) { std::string ip = addr; if (ip[0] == '[') { auto pos = ip.rfind("]:"); if (pos != std::string::npos) ip = ip.substr(1, pos - 1); } else { auto pos = ip.rfind(':'); if (pos != std::string::npos) ip = ip.substr(0, pos); } return ip; } // Known seed/addnode IPs for the DragonX network. // These are the official seed nodes that the daemon connects to on startup. static bool IsSeedNode(const std::string& addr) { static const std::unordered_set seeds = { "176.126.87.241", // embedded daemon -addnode "94.72.112.24", // node1.hush.is "37.60.252.160", // node2.hush.is "176.57.70.185", // node3.hush.is / node6.hush.is "185.213.209.89", // node4.hush.is "137.74.4.198", // node5.hush.is "18.193.113.121", // node7.hush.is "38.60.224.94", // node8.hush.is }; return seeds.count(ExtractIP(addr)) > 0; } void RenderPeersTab(App* app) { auto& S = schema::UI(); auto peerTable = S.table("tabs.peers", "peer-table"); auto bannedTable = S.table("tabs.peers", "banned-table"); const auto& state = app->getWalletState(); // Scrollable child to contain all content within available space ImVec2 peersAvail = ImGui::GetContentRegionAvail(); ImGui::BeginChild("##PeersScroll", peersAvail, false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar); // Responsive: scale factors per frame float availWidth = ImGui::GetContentRegionAvail().x; float hs = Layout::hScale(availWidth); float pad = Layout::cardInnerPadding(); float gap = Layout::cardGap(); ImDrawList* dl = ImGui::GetWindowDrawList(); GlassPanelSpec glassSpec; glassSpec.rounding = Layout::glassRounding(); ImFont* ovFont = Type().overline(); ImFont* capFont = Type().caption(); ImFont* sub1 = Type().subtitle1(); ImFont* body2 = Type().body2(); char buf[128]; // ================================================================ // BLOCKCHAIN & PEERS CARDS — Side by side // ================================================================ float infoCardsH = 0; { const auto& mining = state.mining; // Compute peer stats int totalPeers = (int)state.peers.size(); int inboundCount = 0; int outboundCount = 0; double totalPing = 0; int64_t totalBytesSent = 0, totalBytesRecv = 0; int tlsCount = 0; for (const auto& p : state.peers) { if (p.inbound) inboundCount++; else outboundCount++; totalPing += p.pingtime; totalBytesSent += p.bytessent; totalBytesRecv += p.bytesrecv; if (!p.tls_cipher.empty()) tlsCount++; } double avgPing = totalPeers > 0 ? (totalPing / totalPeers) * 1000.0 : 0; // Format bytes helper auto fmtBytes = [](int64_t bytes) -> std::string { char b[32]; if (bytes >= 1073741824LL) snprintf(b, sizeof(b), "%.1f GB", bytes / 1073741824.0); else if (bytes >= 1048576LL) snprintf(b, sizeof(b), "%.1f MB", bytes / 1048576.0); else if (bytes >= 1024LL) snprintf(b, sizeof(b), "%.0f KB", bytes / 1024.0); else snprintf(b, sizeof(b), "%lld B", (long long)bytes); return b; }; // Blockchain card: 5 rows, Peers card: 4 rows (2 cols per row) float rowH = capFont->LegacySize + Layout::spacingXs() + sub1->LegacySize; float headerH = ovFont->LegacySize + Layout::spacingSm(); float dividerH = 1.0f * Layout::dpiScale(); // Use 5 rows for blockchain card (peers card will have empty space at bottom) float cardInnerH = pad * 0.5f + headerH + rowH * 5 + (Layout::spacingSm() + dividerH) * 4 + pad * 0.5f; infoCardsH = cardInnerH; float cardW = (availWidth - gap) * 0.5f; ImVec2 basePos = ImGui::GetCursorScreenPos(); float dp = Layout::dpiScale(); // ================================================================ // BLOCKCHAIN CARD (left) // ================================================================ { ImVec2 cardMin = basePos; ImVec2 cardMax(cardMin.x + cardW, cardMin.y + infoCardsH); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); // Card header dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), "BLOCKCHAIN"); float colW = (cardW - pad * 2) / 2.0f; 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); }; // Row 1: Blocks | Longest Chain { // Blocks float cx = cardMin.x + pad; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Blocks"); int blocks = state.sync.blocks; if (blocks > 0) { int blocksLeft = state.sync.headers - 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]; snprintf(blockStr, sizeof(blockStr), "%d ", blocks); ImVec2 numSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, blockStr); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), blockStr); // Draw "(X left)" in warning color char leftStr[32]; snprintf(leftStr, sizeof(leftStr), "(%d left)", blocksLeft); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + numSz.x, valY + (sub1->LegacySize - capFont->LegacySize) * 0.5f), Warning(), leftStr); } else { snprintf(buf, sizeof(buf), "%d", blocks); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf); } } else { dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), "\xE2\x80\x94"); } // Longest Chain cx = cardMin.x + pad + colW; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "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); } else { dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), "\xE2\x80\x94"); } } ry += rowH + Layout::spacingSm() * 0.5f; drawDivider(ry); ry += Layout::spacingSm() * 0.5f + dividerH; // Row 2: Hashrate | Difficulty { // Hashrate float cx = cardMin.x + pad; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Hashrate"); float valY = ry + capFont->LegacySize + Layout::spacingXs(); if (mining.networkHashrate > 0) { if (mining.networkHashrate >= 1e12) snprintf(buf, sizeof(buf), "%.2f TH/s", mining.networkHashrate / 1e12); else if (mining.networkHashrate >= 1e9) snprintf(buf, sizeof(buf), "%.2f GH/s", mining.networkHashrate / 1e9); else if (mining.networkHashrate >= 1e6) snprintf(buf, sizeof(buf), "%.2f MH/s", mining.networkHashrate / 1e6); else if (mining.networkHashrate >= 1e3) snprintf(buf, sizeof(buf), "%.2f KH/s", mining.networkHashrate / 1e3); else snprintf(buf, sizeof(buf), "%.2f H/s", mining.networkHashrate); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), Success(), buf); } else { dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94"); } // Difficulty cx = cardMin.x + pad + colW; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "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"); } } ry += rowH + Layout::spacingSm() * 0.5f; drawDivider(ry); ry += Layout::spacingSm() * 0.5f + dividerH; // Row 3: Notarized | Protocol { // Notarized float cx = cardMin.x + pad; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "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"); } // Protocol cx = cardMin.x + pad + colW; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "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"); } } ry += rowH + Layout::spacingSm() * 0.5f; drawDivider(ry); ry += Layout::spacingSm() * 0.5f + dividerH; // Row 4: Version | Memory { // Version float cx = cardMin.x + pad; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "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"); } // Memory cx = cardMin.x + pad + colW; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "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"); } } ry += rowH + Layout::spacingSm() * 0.5f; drawDivider(ry); ry += Layout::spacingSm() * 0.5f + dividerH; // Row 5: Longest Chain | Best Block { // Longest Chain float cx = cardMin.x + pad; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "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); } else { dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94"); } // Best Block (truncated hash) cx = cardMin.x + pad + colW; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "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; std::string truncHash; if (hash.length() > 15) { truncHash = hash.substr(0, 6) + "..." + hash.substr(hash.length() - 6); } else { truncHash = hash; } dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), truncHash.c_str()); // Click to copy full hash ImVec2 hashSz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, truncHash.c_str()); ImGui::SetCursorScreenPos(ImVec2(cx, valY)); ImGui::InvisibleButton("##BestBlockCopy", ImVec2(hashSz.x + Layout::spacingSm(), sub1->LegacySize + 2 * dp)); if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("Click to copy: %s", hash.c_str()); dl->AddLine(ImVec2(cx, valY + sub1->LegacySize + 1 * dp), ImVec2(cx + hashSz.x, valY + sub1->LegacySize + 1 * dp), WithAlpha(OnSurface(), 60), 1.0f * dp); } if (ImGui::IsItemClicked()) { ImGui::SetClipboardText(hash.c_str()); ui::Notifications::instance().info("Block hash copied"); } } else { dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94"); } } } // ================================================================ // PEERS CARD (right) // ================================================================ { ImVec2 cardMin(basePos.x + cardW + gap, basePos.y); ImVec2 cardMax(cardMin.x + cardW, cardMin.y + infoCardsH); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); // Card header dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), "PEERS"); float colW = (cardW - pad * 2) / 2.0f; 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); }; // Row 1: Connected | In/Out { // Connected float cx = cardMin.x + pad; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Connected"); snprintf(buf, sizeof(buf), "%d", totalPeers); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf); // In / Out cx = cardMin.x + pad + colW; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "In / Out"); snprintf(buf, sizeof(buf), "%d / %d", inboundCount, outboundCount); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf); } ry += rowH + Layout::spacingSm() * 0.5f; drawPeerDivider(ry); ry += Layout::spacingSm() * 0.5f + dividerH; // Row 2: TLS | Avg Ping { // TLS float cx = cardMin.x + pad; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "TLS"); float valY = ry + capFont->LegacySize + Layout::spacingXs(); if (totalPeers > 0) { snprintf(buf, sizeof(buf), "%d / %d", tlsCount, totalPeers); ImU32 tlsCol = (tlsCount == totalPeers) ? Success() : OnSurface(); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), tlsCol, buf); if (tlsCount == totalPeers) { ImFont* iconFont = Type().iconSmall(); ImVec2 txtSize = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx + txtSize.x + 4, valY), Success(), ICON_MD_CHECK); } } else { 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(), "Avg Ping"); 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); } ry += rowH + Layout::spacingSm() * 0.5f; drawPeerDivider(ry); ry += Layout::spacingSm() * 0.5f + dividerH; // Row 3: Received | Sent { // Received float cx = cardMin.x + pad; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Received"); std::string recvStr = fmtBytes(totalBytesRecv); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), recvStr.c_str()); // Sent cx = cardMin.x + pad + colW; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "Sent"); std::string sentStr = fmtBytes(totalBytesSent); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), sentStr.c_str()); } ry += rowH + Layout::spacingSm() * 0.5f; drawPeerDivider(ry); ry += Layout::spacingSm() * 0.5f + dividerH; // Row 4: P2P Port | Banned { // P2P Port float cx = cardMin.x + pad; dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "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(), "Banned"); valY = ry + capFont->LegacySize + Layout::spacingXs(); 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); } } ImGui::Dummy(ImVec2(availWidth, infoCardsH)); ImGui::Dummy(ImVec2(0, gap)); } // ================================================================ // Compute remaining space for peer list + footer // ================================================================ float footerH = ImGui::GetFrameHeight() + Layout::spacingSm(); float toggleH = body2->LegacySize + Layout::spacingMd() * 2; float remainForPeers = std::max(60.0f, peersAvail.y - (ImGui::GetCursorScreenPos().y - ImGui::GetWindowPos().y) - footerH - Layout::spacingSm()); float peerPanelHeight = remainForPeers - toggleH; peerPanelHeight = std::max(S.drawElement("tabs.peers", "peer-panel-min-height").size, peerPanelHeight); // ================================================================ // PEERS — Single glass card with Connected / Banned toggle // ================================================================ static bool s_show_banned = false; { // Toggle header: "Connected (N)" / "Banned (N)" float toggleY = ImGui::GetCursorScreenPos().y; { char connLabel[64], banLabel[64]; snprintf(connLabel, sizeof(connLabel), "Connected (%zu)", state.peers.size()); snprintf(banLabel, sizeof(banLabel), "Banned (%zu)", state.bannedPeers.size()); ImVec2 connSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, connLabel); ImVec2 banSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, banLabel); float tabGap = Layout::spacingXl(); float tabStartX = ImGui::GetCursorScreenPos().x + pad; float dp = Layout::dpiScale(); // Connected tab ImVec2 connPos(tabStartX, toggleY); ImU32 connCol = s_show_banned ? OnSurfaceDisabled() : OnSurface(); dl->AddText(body2, body2->LegacySize, ImVec2(connPos.x, connPos.y + Layout::spacingMd() * 0.5f), connCol, connLabel); if (!s_show_banned) { float underY = connPos.y + Layout::spacingMd() * 0.5f + body2->LegacySize + 3.0f * dp; dl->AddRectFilled(ImVec2(connPos.x, underY), ImVec2(connPos.x + connSz.x, underY + 2.0f * dp), Primary(), 1.0f * dp); } ImGui::SetCursorScreenPos(connPos); if (ImGui::InvisibleButton("##tabConn", ImVec2(connSz.x, toggleH))) { s_show_banned = false; } if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); // Banned tab float banX = tabStartX + connSz.x + tabGap; ImVec2 banPos(banX, toggleY); ImU32 banCol = s_show_banned ? OnSurface() : OnSurfaceDisabled(); dl->AddText(body2, body2->LegacySize, ImVec2(banPos.x, banPos.y + Layout::spacingMd() * 0.5f), banCol, banLabel); if (s_show_banned) { float underY = banPos.y + Layout::spacingMd() * 0.5f + body2->LegacySize + 3.0f * dp; dl->AddRectFilled(ImVec2(banPos.x, underY), ImVec2(banPos.x + banSz.x, underY + 2.0f * dp), Primary(), 1.0f * dp); } ImGui::SetCursorScreenPos(banPos); if (ImGui::InvisibleButton("##tabBan", ImVec2(banSz.x, toggleH))) { s_show_banned = true; } if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); // Refresh button — top-right, glass panel style (similar to mining button) { bool isRefreshing = app->isPeerRefreshInProgress(); auto refreshBtn = S.drawElement("tabs.peers", "refresh-button"); float btnW = refreshBtn.size; float btnH = toggleH - 4.0f * Layout::dpiScale(); float btnX = ImGui::GetWindowPos().x + availWidth - btnW - Layout::spacingSm(); float btnY = toggleY + (toggleH - btnH) * 0.5f; ImVec2 bMin(btnX, btnY); ImVec2 bMax(btnX + btnW, btnY + btnH); bool btnHovered = material::IsRectHovered(bMin, bMax); bool btnClicked = btnHovered && ImGui::IsMouseClicked(0); // Glass panel background GlassPanelSpec btnGlass; btnGlass.rounding = Layout::glassRounding(); if (isRefreshing) { float pulse = effects::isLowSpecMode() ? 0.5f : 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 4.0); btnGlass.fillAlpha = (int)(15 + 25 * pulse); } else { btnGlass.fillAlpha = btnHovered ? 30 : 18; } DrawGlassPanel(dl, bMin, bMax, btnGlass); // Hover highlight if (btnHovered && !isRefreshing) { dl->AddRectFilled(bMin, bMax, WithAlpha(Primary(), 20), btnGlass.rounding); } // Icon: spinner while refreshing, refresh icon otherwise float cx = bMin.x + btnW * 0.35f; float cy = bMin.y + btnH * 0.5f; ImFont* iconFont = Type().iconMed(); float iconSz = iconFont->LegacySize; if (isRefreshing) { // Spinning arc spinner (same style as mining toggle) float spinnerR = iconSz * 0.5f; float thickness = std::max(1.5f, spinnerR * 0.18f); float time = (float)ImGui::GetTime(); // Track circle (faint) dl->AddCircle(ImVec2(cx, cy), spinnerR, WithAlpha(Primary(), 40), 0, thickness); // Animated arc float rotation = fmodf(time * 2.0f * IM_PI / 1.4f, IM_PI * 2.0f); float cycleTime = fmodf(time, 1.333f); float arcLength = (cycleTime < 0.666f) ? (cycleTime / 0.666f) * 0.75f + 0.1f : ((1.333f - cycleTime) / 0.666f) * 0.75f + 0.1f; float startAngle = rotation - IM_PI * 0.5f; float endAngle = startAngle + IM_PI * 2.0f * arcLength; int segments = (int)(32 * arcLength) + 1; float angleStep = (endAngle - startAngle) / segments; ImU32 arcCol = Primary(); for (int si = 0; si < segments; si++) { float a1 = startAngle + angleStep * si; float a2 = startAngle + angleStep * (si + 1); ImVec2 p1(cx + cosf(a1) * spinnerR, cy + sinf(a1) * spinnerR); ImVec2 p2(cx + cosf(a2) * spinnerR, cy + sinf(a2) * spinnerR); dl->AddLine(p1, p2, arcCol, thickness); } } else { // Static refresh icon ImU32 iconCol = btnHovered ? OnSurface() : OnSurfaceMedium(); ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx - iSz.x * 0.5f, cy - iSz.y * 0.5f), iconCol, ICON_MD_REFRESH); } // Label to the right of icon { const char* label = isRefreshing ? "REFRESHING" : "REFRESH"; ImU32 lblCol; if (isRefreshing) { float pulse = effects::isLowSpecMode() ? 0.7f : 0.5f + 0.5f * (float)std::sin((double)ImGui::GetTime() * 3.0); lblCol = WithAlpha(Primary(), (int)(120 + 135 * pulse)); } else { lblCol = btnHovered ? OnSurface() : WithAlpha(OnSurface(), 160); } ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label); float lblX = cx + iconSz * 0.5f + Layout::spacingXs(); float lblY = cy - lblSz.y * 0.5f; dl->AddText(ovFont, ovFont->LegacySize, ImVec2(lblX, lblY), lblCol, label); } // Invisible button for click handling ImGui::SetCursorScreenPos(bMin); ImGui::PushID("##peersRefresh"); if (ImGui::InvisibleButton("##btn", ImVec2(btnW, btnH))) { if (!isRefreshing) { app->refreshPeerInfo(); app->refreshNow(); } } if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (!isRefreshing) ImGui::SetTooltip("Refresh peers & blockchain"); } ImGui::PopID(); } ImGui::SetCursorScreenPos(ImVec2(ImGui::GetWindowPos().x, toggleY + toggleH)); } // Glass panel background ImVec2 panelMin = ImGui::GetCursorScreenPos(); ImVec2 panelMax(panelMin.x + availWidth, panelMin.y + peerPanelHeight); DrawGlassPanel(dl, panelMin, panelMax, glassSpec); // Scroll-edge mask state float listScrollY = 0.0f, listScrollMaxY = 0.0f; int listParentVtx = dl->VtxBuffer.Size; ImGui::BeginChild("##PeersList", ImVec2(availWidth, peerPanelHeight), false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse); ApplySmoothScroll(); ImDrawList* listChildDL = ImGui::GetWindowDrawList(); int listChildVtx = listChildDL->VtxBuffer.Size; ImGui::Dummy(ImVec2(0, Layout::spacingSm())); if (!s_show_banned) { // ---- Connected Peers ---- if (!app->isConnected()) { ImGui::Dummy(ImVec2(0, 20)); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " Not connected to daemon..."); } else if (state.peers.empty()) { ImGui::Dummy(ImVec2(0, 20)); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No connected peers"); } else { float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg(); float rowInset = Layout::spacingLg(); float innerW = ImGui::GetContentRegionAvail().x - rowInset * 2; listScrollY = ImGui::GetScrollY(); listScrollMaxY = ImGui::GetScrollMaxY(); for (size_t i = 0; i < state.peers.size(); i++) { const auto& peer = state.peers[i]; bool is_selected = (s_selected_peer_idx == static_cast(i)); ImGui::PushID(static_cast(i)); ImVec2 rawRowPos = ImGui::GetCursorScreenPos(); ImVec2 rowPos(rawRowPos.x + rowInset, rawRowPos.y); ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH); if (is_selected) { dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "row-selection-rounding").size); dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size, rowEnd.y), Primary(), S.drawElement("tabs.peers", "row-accent-rounding").size); } bool hovered = material::IsRectHovered(rowPos, rowEnd); if (hovered && !is_selected) { dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "row-selection-rounding").size); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } float cx = rowPos.x + pad; float cy = rowPos.y + Layout::spacingSm(); double ping_ms = peer.pingtime * 1000.0; ImU32 dotCol; if (ping_ms < 100) dotCol = Success(); else if (ping_ms < 500) dotCol = Warning(); else dotCol = Error(); float pingDotR = S.drawElement("tabs.peers", "ping-dot-radius-base").size + S.drawElement("tabs.peers", "ping-dot-radius-scale").size * hs; dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ping-dot-x-offset").size, cy + body2->LegacySize * 0.5f), pingDotR, dotCol); float addrX = cx + S.drawElement("tabs.peers", "address-x-offset").size; dl->AddText(body2, body2->LegacySize, ImVec2(addrX, cy), OnSurface(), peer.addr.c_str()); // Seed node icon — rendered right after the IP address if (IsSeedNode(peer.addr)) { ImVec2 addrSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, peer.addr.c_str()); ImFont* iconFont = Type().iconSmall(); float iconY = cy + (body2->LegacySize - iconFont->LegacySize) * 0.5f; dl->AddText(iconFont, iconFont->LegacySize, ImVec2(addrX + addrSz.x + Layout::spacingSm(), iconY), WithAlpha(Success(), 200), ICON_MD_GRASS); } { const char* dirLabel = peer.inbound ? "In" : "Out"; ImU32 dirBg = peer.inbound ? WithAlpha(Success(), 30) : WithAlpha(Secondary(), 30); ImU32 dirFg = peer.inbound ? WithAlpha(Success(), 200) : WithAlpha(Secondary(), 200); ImVec2 dirSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, dirLabel); float dirX = rowPos.x + innerW - dirSz.x - Layout::spacingXl(); ImVec2 pillMin(dirX - S.drawElement("tabs.peers", "dir-pill-padding").size, cy + S.drawElement("tabs.peers", "dir-pill-y-offset").size); ImVec2 pillMax(dirX + dirSz.x + S.drawElement("tabs.peers", "dir-pill-padding").size, cy + capFont->LegacySize + S.drawElement("tabs.peers", "dir-pill-y-bottom").size); dl->AddRectFilled(pillMin, pillMax, dirBg, S.drawElement("tabs.peers", "dir-pill-rounding").size); dl->AddText(capFont, capFont->LegacySize, ImVec2(dirX, cy + 2), dirFg, dirLabel); } { char pingBuf[32]; snprintf(pingBuf, sizeof(pingBuf), "%.0fms", ping_ms); ImVec2 pingSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, pingBuf); float pingX = rowPos.x + innerW - pingSz.x - Layout::spacingXl() * 3; dl->AddText(capFont, capFont->LegacySize, ImVec2(pingX, cy + 2), dotCol, pingBuf); } float cy2 = cy + body2->LegacySize + Layout::spacingXs(); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size, cy2), OnSurfaceDisabled(), peer.subver.c_str()); float verW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, peer.subver.c_str()).x; float tlsBadgeW = std::max(S.drawElement("tabs.peers", "tls-badge-min-width").size, S.drawElement("tabs.peers", "tls-badge-width").size * hs); if (!peer.tls_cipher.empty()) { ImU32 tlsBg = WithAlpha(Success(), 25); ImU32 tlsFg = WithAlpha(Success(), 200); ImVec2 tlsMin(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2); ImVec2 tlsMax(tlsMin.x + tlsBadgeW, tlsMin.y + capFont->LegacySize + 2); dl->AddRectFilled(tlsMin, tlsMax, tlsBg, S.drawElement("tabs.peers", "tls-badge-rounding").size); dl->AddText(capFont, capFont->LegacySize, ImVec2(tlsMin.x + 4, cy2 + 1), tlsFg, "TLS"); } else { dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size + verW + Layout::spacingSm(), cy2), WithAlpha(Error(), 140), "No TLS"); } if (peer.banscore > 0) { char banBuf[16]; snprintf(banBuf, sizeof(banBuf), "Ban: %d", peer.banscore); ImU32 banCol = peer.banscore > 50 ? Error() : Warning(); ImVec2 banSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banBuf); dl->AddText(capFont, capFont->LegacySize, ImVec2(rowPos.x + innerW - banSz.x - Layout::spacingLg(), cy2), banCol, banBuf); } ImGui::InvisibleButton("##peerRow", ImVec2(innerW, rowH)); if (ImGui::IsItemClicked(0)) { s_selected_peer_idx = static_cast(i); } const auto& acrylicTheme = GetCurrentAcrylicTheme(); if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme.menu)) { ImGui::Text("Peer: %s", peer.addr.c_str()); ImGui::Separator(); if (ImGui::MenuItem("Copy Address")) { ImGui::SetClipboardText(peer.addr.c_str()); } if (ImGui::MenuItem("Copy IP")) { ImGui::SetClipboardText(ExtractIP(peer.addr).c_str()); } ImGui::Separator(); if (ImGui::MenuItem("Ban Peer (24h)")) { app->banPeer(ExtractIP(peer.addr), 86400); } effects::ImGuiAcrylic::EndAcrylicPopup(); } if (ImGui::IsItemHovered()) { ImGui::BeginTooltip(); ImGui::PushStyleVar(ImGuiStyleVar_CellPadding, ImVec2(8, 3)); if (ImGui::BeginTable("##PeerTT", 2, ImGuiTableFlags_SizingFixedFit)) { auto TTRow = [](const char* label, const char* value) { ImGui::TableNextRow(); ImGui::TableNextColumn(); ImGui::TextDisabled("%s", label); ImGui::TableNextColumn(); ImGui::Text("%s", value); }; char ttBuf[128]; snprintf(ttBuf, sizeof(ttBuf), "%d", peer.id); TTRow("ID", ttBuf); TTRow("Services", peer.services.c_str()); snprintf(ttBuf, sizeof(ttBuf), "%d", peer.startingheight); TTRow("Start Height", ttBuf); snprintf(ttBuf, sizeof(ttBuf), "%ld bytes", peer.bytessent); TTRow("Sent", ttBuf); snprintf(ttBuf, sizeof(ttBuf), "%ld bytes", peer.bytesrecv); TTRow("Received", ttBuf); snprintf(ttBuf, sizeof(ttBuf), "%d / %d", peer.synced_headers, peer.synced_blocks); TTRow("Synced H/B", ttBuf); if (!peer.tls_cipher.empty()) TTRow("TLS Cipher", peer.tls_cipher.c_str()); ImGui::EndTable(); } ImGui::PopStyleVar(); ImGui::EndTooltip(); } if (i < state.peers.size() - 1) { ImVec2 divStart = ImGui::GetCursorScreenPos(); dl->AddLine(ImVec2(divStart.x + pad + 18, divStart.y), ImVec2(divStart.x + innerW - pad, divStart.y), IM_COL32(255, 255, 255, 15)); } ImGui::PopID(); } } } else { // ---- Banned Peers ---- if (!app->isConnected()) { ImGui::Dummy(ImVec2(0, 20)); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " Not connected to daemon..."); } else if (state.bannedPeers.empty()) { ImGui::Dummy(ImVec2(0, 20)); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), " No banned peers"); } else { float rowH = capFont->LegacySize + S.drawElement("tabs.peers", "banned-row-height-padding").size; float rowInsetB = pad; float innerW = ImGui::GetContentRegionAvail().x - rowInsetB * 2; listScrollY = ImGui::GetScrollY(); listScrollMaxY = ImGui::GetScrollMaxY(); for (size_t i = 0; i < state.bannedPeers.size(); i++) { const auto& banned = state.bannedPeers[i]; bool is_selected = (s_selected_banned_idx == static_cast(i)); ImGui::PushID(static_cast(i)); ImVec2 rawRowPosB = ImGui::GetCursorScreenPos(); ImVec2 rowPos(rawRowPosB.x + rowInsetB, rawRowPosB.y); ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH); if (is_selected) { dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), S.drawElement("tabs.peers", "banned-row-rounding").size); dl->AddRectFilled(rowPos, ImVec2(rowPos.x + S.drawElement("tabs.peers", "row-accent-width").size, rowEnd.y), WithAlpha(Error(), 200), S.drawElement("tabs.peers", "banned-accent-rounding").size); } if (material::IsRectHovered(rowPos, rowEnd) && !is_selected) { dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.peers", "banned-row-rounding").size); } float cx = rowPos.x + pad; float cy = rowPos.y + Layout::spacingXs(); float banDotR = S.drawElement("tabs.peers", "ban-dot-radius-base").size + S.drawElement("tabs.peers", "ban-dot-radius-scale").size * hs; dl->AddCircleFilled(ImVec2(cx + S.drawElement("tabs.peers", "ban-dot-x-offset").size, cy + capFont->LegacySize * 0.4f), banDotR, WithAlpha(Error(), 200)); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "banned-address-x-offset").size, cy), OnSurfaceDisabled(), banned.address.c_str()); std::string banUntil = banned.getBannedUntilString(); ImVec2 banSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, banUntil.c_str()); dl->AddText(capFont, capFont->LegacySize, ImVec2(rowPos.x + innerW - banSz.x - Layout::spacingXl() * 5, cy), OnSurfaceDisabled(), banUntil.c_str()); float btnX = rowPos.x + innerW - Layout::spacingXl() * S.drawElement("tabs.peers", "unban-btn-right-offset-multiplier").size; ImGui::SetCursorScreenPos(ImVec2(btnX, cy - 1)); if (TactileSmallButton("Unban", S.resolveFont("button"))) { app->unbanPeer(banned.address); } ImGui::SetCursorScreenPos(rowPos); ImGui::InvisibleButton("##bannedRow", ImVec2(innerW - S.drawElement("tabs.peers", "banned-row-btn-reserve").size, rowH)); if (ImGui::IsItemClicked(0)) { s_selected_banned_idx = static_cast(i); } const auto& acrylicTheme2 = GetCurrentAcrylicTheme(); if (effects::ImGuiAcrylic::BeginAcrylicContextItem(nullptr, 0, acrylicTheme2.menu)) { if (ImGui::MenuItem("Copy Address")) { ImGui::SetClipboardText(banned.address.c_str()); } if (ImGui::MenuItem("Unban")) { app->unbanPeer(banned.address); } effects::ImGuiAcrylic::EndAcrylicPopup(); } ImGui::SetCursorScreenPos(ImVec2(rawRowPosB.x, rowEnd.y)); if (i < state.bannedPeers.size() - 1) { ImVec2 divStart = ImGui::GetCursorScreenPos(); dl->AddLine(ImVec2(divStart.x + pad + 8, divStart.y), ImVec2(divStart.x + innerW - pad, divStart.y), IM_COL32(255, 255, 255, 15)); } ImGui::PopID(); } } } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::EndChild(); // CSS-style clipping mask { float fadeFont = s_show_banned ? capFont->LegacySize : body2->LegacySize; float fadeZone = std::min(fadeFont * 3.0f, peerPanelHeight * 0.18f); ApplyScrollEdgeMask(dl, listParentVtx, listChildDL, listChildVtx, panelMin.y, panelMax.y, fadeZone, listScrollY, listScrollMaxY); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); } // ================================================================ // Footer — Clear Bans (material styled) // ================================================================ if (s_show_banned && !state.bannedPeers.empty()) { ImGui::BeginDisabled(!app->isConnected()); if (TactileSmallButton("Clear All Bans", S.resolveFont("button"))) { app->clearBans(); } ImGui::EndDisabled(); } ImGui::EndChild(); // ##PeersScroll } } // namespace ui } // namespace dragonx