// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #pragma once #include "../material/material.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include #include #include namespace dragonx { namespace ui { namespace screens { using namespace material; // ============================================================================ // Peers Screen // ============================================================================ // Display connected peers with Material list /** * @brief Peer connection info */ struct PeerInfo { int id; std::string address; std::string subversion; int startingHeight; int currentHeight; int latency; // ms int64_t bytesSent; int64_t bytesReceived; std::string connTime; // Connection duration bool isInbound; }; /** * @brief Network statistics */ struct NetworkStats { int totalConnections; int inboundCount; int outboundCount; int64_t totalBytesSent; int64_t totalBytesReceived; int currentHeight; std::string networkName; }; /** * @brief Peers screen with network info */ class PeersScreen { public: void setPeers(const std::vector& peers) { m_peers = peers; } void setNetworkStats(const NetworkStats& stats) { m_networkStats = stats; } void setOnDisconnectPeer(std::function callback) { m_onDisconnectPeer = callback; } void setOnAddNode(std::function callback) { m_onAddNode = callback; } void render(); private: void renderNetworkStats(); void renderPeersList(); void renderPeerDetails(); void renderAddNodeDialog(); std::vector m_peers; NetworkStats m_networkStats; std::function m_onDisconnectPeer; std::function m_onAddNode; int m_selectedPeerId = -1; bool m_showDetails = false; bool m_showAddNode = false; char m_addNodeBuffer[256] = {0}; }; // ============================================================================ // Implementation // ============================================================================ inline void PeersScreen::render() { // Title ImGui::BeginGroup(); { Typography::instance().text(TypeStyle::H5, "Network Peers"); ImGui::SameLine(ImGui::GetContentRegionAvail().x - 120); if (OutlinedButton("+ ADD NODE")) { m_showAddNode = true; } } ImGui::EndGroup(); ImGui::Dummy(ImVec2(0, spacing::dp(2))); // Network stats cards renderNetworkStats(); ImGui::Dummy(ImVec2(0, spacing::dp(2))); // Peers list renderPeersList(); // Dialogs if (m_showDetails) { renderPeerDetails(); } if (m_showAddNode) { renderAddNodeDialog(); } } inline void PeersScreen::renderNetworkStats() { float availWidth = ImGui::GetContentRegionAvail().x; float cardWidth = (availWidth - spacing::dp(2) * 3) / 4.0f; // Connections card ImGui::BeginGroup(); { CardSpec cardSpec; cardSpec.elevation = 1; cardSpec.padding = spacing::dp(2); cardSpec.width = cardWidth; ImGui::PushID("conn_card"); if (BeginCard(cardSpec)) { Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "CONNECTIONS"); ImGui::Dummy(ImVec2(0, spacing::dp(1))); char connStr[32]; snprintf(connStr, sizeof(connStr), "%d", m_networkStats.totalConnections); Typography::instance().text(TypeStyle::H4, connStr); char detailStr[64]; snprintf(detailStr, sizeof(detailStr), ICON_MD_ARROW_DOWNWARD "%d in " ICON_MD_ARROW_UPWARD "%d out", m_networkStats.inboundCount, m_networkStats.outboundCount); Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), detailStr); EndCard(); } ImGui::PopID(); } ImGui::EndGroup(); ImGui::SameLine(0, spacing::dp(2)); // Block height card ImGui::BeginGroup(); { CardSpec cardSpec; cardSpec.elevation = 1; cardSpec.padding = spacing::dp(2); cardSpec.width = cardWidth; ImGui::PushID("height_card"); if (BeginCard(cardSpec)) { Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCK HEIGHT"); ImGui::Dummy(ImVec2(0, spacing::dp(1))); char heightStr[32]; snprintf(heightStr, sizeof(heightStr), "%d", m_networkStats.currentHeight); Typography::instance().text(TypeStyle::H4, heightStr); Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), m_networkStats.networkName.c_str()); EndCard(); } ImGui::PopID(); } ImGui::EndGroup(); ImGui::SameLine(0, spacing::dp(2)); // Data sent card ImGui::BeginGroup(); { CardSpec cardSpec; cardSpec.elevation = 1; cardSpec.padding = spacing::dp(2); cardSpec.width = cardWidth; ImGui::PushID("sent_card"); if (BeginCard(cardSpec)) { Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "DATA SENT"); ImGui::Dummy(ImVec2(0, spacing::dp(1))); char dataStr[32]; double mb = m_networkStats.totalBytesSent / (1024.0 * 1024.0); if (mb > 1024) { snprintf(dataStr, sizeof(dataStr), "%.1f GB", mb / 1024.0); } else { snprintf(dataStr, sizeof(dataStr), "%.1f MB", mb); } Typography::instance().text(TypeStyle::H5, dataStr); EndCard(); } ImGui::PopID(); } ImGui::EndGroup(); ImGui::SameLine(0, spacing::dp(2)); // Data received card ImGui::BeginGroup(); { CardSpec cardSpec; cardSpec.elevation = 1; cardSpec.padding = spacing::dp(2); cardSpec.width = cardWidth; ImGui::PushID("recv_card"); if (BeginCard(cardSpec)) { Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "DATA RECEIVED"); ImGui::Dummy(ImVec2(0, spacing::dp(1))); char dataStr[32]; double mb = m_networkStats.totalBytesReceived / (1024.0 * 1024.0); if (mb > 1024) { snprintf(dataStr, sizeof(dataStr), "%.1f GB", mb / 1024.0); } else { snprintf(dataStr, sizeof(dataStr), "%.1f MB", mb); } Typography::instance().text(TypeStyle::H5, dataStr); EndCard(); } ImGui::PopID(); } ImGui::EndGroup(); } inline void PeersScreen::renderPeersList() { CardSpec cardSpec; cardSpec.elevation = 1; cardSpec.padding = 0; if (BeginCard(cardSpec)) { if (m_peers.empty()) { ImGui::Dummy(ImVec2(0, spacing::dp(4))); float availWidth = ImGui::GetContentRegionAvail().x; const char* emptyText = "No peers connected"; ImVec2 textSize = ImGui::CalcTextSize(emptyText); ImGui::SetCursorPosX(ImGui::GetCursorPosX() + (availWidth - textSize.x) * 0.5f); Typography::instance().textColored(TypeStyle::Body1, OnSurfaceMedium(), emptyText); ImGui::Dummy(ImVec2(0, spacing::dp(4))); } else { BeginList("peers_list", false); for (const auto& peer : m_peers) { ListItemSpec itemSpec; // Direction icon itemSpec.leadingIcon = peer.isInbound ? ICON_MD_ARROW_DOWNWARD : ICON_MD_ARROW_UPWARD; // Address as primary itemSpec.primaryText = peer.address.c_str(); // Version + latency as secondary char secondaryStr[128]; snprintf(secondaryStr, sizeof(secondaryStr), "%s • %dms • Height: %d", peer.subversion.c_str(), peer.latency, peer.currentHeight); itemSpec.secondaryText = secondaryStr; itemSpec.trailingIcon = ICON_MD_INFO; itemSpec.dividerBelow = true; char peerId[16]; snprintf(peerId, sizeof(peerId), "peer_%d", peer.id); ImGui::PushID(peerId); if (ListItem(itemSpec)) { m_selectedPeerId = peer.id; m_showDetails = true; } ImGui::PopID(); } EndList(); } EndCard(); } } inline void PeersScreen::renderPeerDetails() { // Find selected peer const PeerInfo* selected = nullptr; for (const auto& peer : m_peers) { if (peer.id == m_selectedPeerId) { selected = &peer; break; } } if (!selected) { m_showDetails = false; return; } DialogSpec dialogSpec; dialogSpec.title = "Peer Details"; dialogSpec.maxWidth = 450.0f; DialogResult result = BeginDialog("peer_details", dialogSpec); if (result.isOpen) { const PeerInfo& peer = *selected; // Direction badge ChipSpec chipSpec; chipSpec.variant = ChipVariant::Filled; chipSpec.color = peer.isInbound ? colors::Blue500 : colors::Orange500; Chip(peer.isInbound ? ICON_MD_ARROW_DOWNWARD " Inbound" : ICON_MD_ARROW_UPWARD " Outbound", chipSpec); ImGui::Dummy(ImVec2(0, spacing::dp(2))); // Address Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ADDRESS"); Typography::instance().text(TypeStyle::Body1, peer.address.c_str()); ImGui::Dummy(ImVec2(0, spacing::dp(1))); // Client Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "CLIENT"); Typography::instance().text(TypeStyle::Body2, peer.subversion.c_str()); ImGui::Dummy(ImVec2(0, spacing::dp(1))); // Height char heightStr[64]; snprintf(heightStr, sizeof(heightStr), "%d (started at %d)", peer.currentHeight, peer.startingHeight); Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "BLOCK HEIGHT"); Typography::instance().text(TypeStyle::Body2, heightStr); ImGui::Dummy(ImVec2(0, spacing::dp(1))); // Latency char latencyStr[32]; snprintf(latencyStr, sizeof(latencyStr), "%d ms", peer.latency); Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "LATENCY"); Typography::instance().text(TypeStyle::Body2, latencyStr); ImGui::Dummy(ImVec2(0, spacing::dp(1))); // Connected time Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "CONNECTED"); Typography::instance().text(TypeStyle::Body2, peer.connTime.c_str()); ImGui::Dummy(ImVec2(0, spacing::dp(1))); // Data transferred char dataStr[64]; snprintf(dataStr, sizeof(dataStr), ICON_MD_ARROW_UPWARD " %.2f MB " ICON_MD_ARROW_DOWNWARD " %.2f MB", peer.bytesSent / (1024.0 * 1024.0), peer.bytesReceived / (1024.0 * 1024.0)); Typography::instance().textColored(TypeStyle::Overline, OnSurfaceMedium(), "DATA TRANSFERRED"); Typography::instance().text(TypeStyle::Body2, dataStr); ImGui::Dummy(ImVec2(0, spacing::dp(3))); // Actions float availWidth = ImGui::GetContentRegionAvail().x; // Disconnect button (danger) ImGui::PushStyleColor(ImGuiCol_Button, colors::withAlpha(colors::Red500, 0.1f)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, colors::withAlpha(colors::Red500, 0.2f)); ImGui::PushStyleColor(ImGuiCol_Text, colors::Red500); if (ImGui::Button("DISCONNECT", ImVec2(120, 36))) { if (m_onDisconnectPeer) m_onDisconnectPeer(peer.id); m_showDetails = false; } ImGui::PopStyleColor(3); ImGui::SameLine(availWidth - 80); if (ContainedButton("CLOSE")) { m_showDetails = false; } EndDialog(); } if (result.dismissed) { m_showDetails = false; } } inline void PeersScreen::renderAddNodeDialog() { DialogSpec dialogSpec; dialogSpec.title = "Add Node"; dialogSpec.maxWidth = 400.0f; DialogResult result = BeginDialog("add_node", dialogSpec); if (result.isOpen) { Typography::instance().text(TypeStyle::Body1, "Enter the IP address or hostname of a node to connect to:"); ImGui::Dummy(ImVec2(0, spacing::dp(2))); TextFieldSpec textSpec; textSpec.label = "Node Address"; textSpec.placeholder = "192.168.1.1:18030 or node.example.com"; textSpec.variant = TextFieldVariant::Outlined; textSpec.width = -1; TextField("add_node_addr", m_addNodeBuffer, sizeof(m_addNodeBuffer), textSpec); ImGui::Dummy(ImVec2(0, spacing::dp(3))); float availWidth = ImGui::GetContentRegionAvail().x; ImGui::SetCursorPosX(ImGui::GetCursorPosX() + availWidth - 200); if (TextButton("CANCEL")) { m_showAddNode = false; memset(m_addNodeBuffer, 0, sizeof(m_addNodeBuffer)); } ImGui::SameLine(0, spacing::dp(2)); bool canAdd = strlen(m_addNodeBuffer) > 0; ImGui::BeginDisabled(!canAdd); if (ContainedButton("ADD")) { if (m_onAddNode) m_onAddNode(m_addNodeBuffer); m_showAddNode = false; memset(m_addNodeBuffer, 0, sizeof(m_addNodeBuffer)); } ImGui::EndDisabled(); EndDialog(); } if (result.dismissed) { m_showAddNode = false; } } } // namespace screens } // namespace ui } // namespace dragonx