ObsidianDragon - DragonX ImGui Wallet
Full-node GUI wallet for DragonX cryptocurrency. Built with Dear ImGui, SDL3, and OpenGL3/DX11. Features: - Send/receive shielded and transparent transactions - Autoshield with merged transaction display - Built-in CPU mining (xmrig) - Peer management and network monitoring - Wallet encryption with PIN lock - QR code generation for receive addresses - Transaction history with pagination - Console for direct RPC commands - Cross-platform (Linux, Windows)
This commit is contained in:
808
src/ui/windows/peers_tab.cpp
Normal file
808
src/ui/windows/peers_tab.cpp
Normal file
@@ -0,0 +1,808 @@
|
||||
// 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 "../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 <string>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
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 = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
||||
if (blocks > 0) {
|
||||
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);
|
||||
|
||||
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<int>(i));
|
||||
|
||||
ImGui::PushID(static_cast<int>(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);
|
||||
|
||||
dl->AddText(body2, body2->LegacySize, ImVec2(cx + S.drawElement("tabs.peers", "address-x-offset").size, cy), OnSurface(), peer.addr.c_str());
|
||||
|
||||
{
|
||||
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<int>(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<int>(i));
|
||||
|
||||
ImGui::PushID(static_cast<int>(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<int>(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(rowPos.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 — Refresh + Clear Bans (material styled)
|
||||
// ================================================================
|
||||
{
|
||||
ImGui::BeginDisabled(!app->isConnected());
|
||||
|
||||
if (TactileSmallButton("Refresh Peers", S.resolveFont("button"))) {
|
||||
app->refreshPeerInfo();
|
||||
}
|
||||
|
||||
if (s_show_banned && !state.bannedPeers.empty()) {
|
||||
ImGui::SameLine();
|
||||
if (TactileSmallButton("Clear All Bans", S.resolveFont("button"))) {
|
||||
app->clearBans();
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
ImGui::EndChild(); // ##PeersScroll
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
Reference in New Issue
Block a user