Files
ObsidianDragon/src/ui/screens/peers_screen.h
DanS 3aee55b49c 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)
2026-02-27 00:26:01 -06:00

463 lines
14 KiB
C++

// 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 <string>
#include <vector>
#include <functional>
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<PeerInfo>& peers) { m_peers = peers; }
void setNetworkStats(const NetworkStats& stats) { m_networkStats = stats; }
void setOnDisconnectPeer(std::function<void(int peerId)> callback) {
m_onDisconnectPeer = callback;
}
void setOnAddNode(std::function<void(const std::string&)> callback) {
m_onAddNode = callback;
}
void render();
private:
void renderNetworkStats();
void renderPeersList();
void renderPeerDetails();
void renderAddNodeDialog();
std::vector<PeerInfo> m_peers;
NetworkStats m_networkStats;
std::function<void(int)> m_onDisconnectPeer;
std::function<void(const std::string&)> 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