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)
380 lines
12 KiB
C++
380 lines
12 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#pragma once
|
|
|
|
#include "../colors.h"
|
|
#include "../typography.h"
|
|
#include "../layout.h"
|
|
#include "../../schema/ui_schema.h"
|
|
#include "imgui.h"
|
|
#include "imgui_internal.h"
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
namespace material {
|
|
|
|
// ============================================================================
|
|
// Material Design Navigation Drawer Component
|
|
// ============================================================================
|
|
// Based on https://m2.material.io/components/navigation-drawer
|
|
//
|
|
// Navigation drawers provide access to destinations in your app.
|
|
|
|
enum class NavDrawerType {
|
|
Standard, // Permanent, always visible
|
|
Modal, // Overlay with scrim, can be dismissed
|
|
Dismissible // Can be shown/hidden, no scrim
|
|
};
|
|
|
|
/**
|
|
* @brief Navigation drawer configuration
|
|
*/
|
|
struct NavDrawerSpec {
|
|
NavDrawerType type = NavDrawerType::Standard;
|
|
float width = 256.0f; // 256dp standard width
|
|
bool showDividerBottom = true; // Divider at bottom
|
|
const char* headerTitle = nullptr; // Optional header title
|
|
const char* headerSubtitle = nullptr;
|
|
};
|
|
|
|
/**
|
|
* @brief Navigation item configuration
|
|
*/
|
|
struct NavItemSpec {
|
|
const char* icon = nullptr; // Leading icon
|
|
const char* label = nullptr; // Item label (required)
|
|
bool selected = false; // Selected state
|
|
bool disabled = false;
|
|
int badgeCount = 0; // Badge (0 = no badge)
|
|
};
|
|
|
|
/**
|
|
* @brief Begin a navigation drawer
|
|
*
|
|
* @param id Unique identifier
|
|
* @param open Pointer to open state (for modal/dismissible)
|
|
* @param spec Drawer configuration
|
|
* @return true if drawer is visible
|
|
*/
|
|
bool BeginNavDrawer(const char* id, bool* open, const NavDrawerSpec& spec = NavDrawerSpec());
|
|
|
|
/**
|
|
* @brief Begin standard (always visible) navigation drawer
|
|
*/
|
|
bool BeginNavDrawer(const char* id, const NavDrawerSpec& spec = NavDrawerSpec());
|
|
|
|
/**
|
|
* @brief End navigation drawer
|
|
*/
|
|
void EndNavDrawer();
|
|
|
|
/**
|
|
* @brief Render a navigation item
|
|
*
|
|
* @param spec Item configuration
|
|
* @return true if clicked
|
|
*/
|
|
bool NavItem(const NavItemSpec& spec);
|
|
|
|
/**
|
|
* @brief Simple navigation item
|
|
*/
|
|
bool NavItem(const char* icon, const char* label, bool selected = false);
|
|
|
|
/**
|
|
* @brief Navigation divider
|
|
*/
|
|
void NavDivider();
|
|
|
|
/**
|
|
* @brief Navigation subheader
|
|
*/
|
|
void NavSubheader(const char* text);
|
|
|
|
// ============================================================================
|
|
// Implementation
|
|
// ============================================================================
|
|
|
|
struct NavDrawerState {
|
|
float width;
|
|
ImVec2 contentMin;
|
|
ImVec2 contentMax;
|
|
bool isModal;
|
|
};
|
|
|
|
static NavDrawerState g_navDrawerState;
|
|
|
|
inline bool BeginNavDrawer(const char* id, bool* open, const NavDrawerSpec& spec) {
|
|
// For modal drawers, check open state
|
|
if (spec.type == NavDrawerType::Modal && !*open) {
|
|
return false;
|
|
}
|
|
|
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGui::PushID(id);
|
|
|
|
g_navDrawerState.width = spec.width;
|
|
g_navDrawerState.isModal = (spec.type == NavDrawerType::Modal);
|
|
|
|
ImGuiIO& io = ImGui::GetIO();
|
|
ImDrawList* drawList = window->DrawList;
|
|
|
|
// For modal, draw scrim and handle dismiss
|
|
if (spec.type == NavDrawerType::Modal) {
|
|
ImDrawList* bgDrawList = ImGui::GetBackgroundDrawList();
|
|
bgDrawList->AddRectFilled(
|
|
ImVec2(0, 0), io.DisplaySize,
|
|
schema::UI().resolveColor("var(--scrim)", IM_COL32(0, 0, 0, (int)(0.32f * 255)))
|
|
);
|
|
|
|
// Click outside to dismiss
|
|
if (ImGui::IsMouseClicked(0)) {
|
|
ImVec2 mousePos = io.MousePos;
|
|
if (mousePos.x > spec.width) {
|
|
*open = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Drawer position and size
|
|
ImVec2 drawerPos(0, 0);
|
|
ImVec2 drawerSize(spec.width, io.DisplaySize.y);
|
|
|
|
// If not modal, account for app bar
|
|
if (spec.type != NavDrawerType::Modal) {
|
|
drawerPos.y = size::AppBarHeight;
|
|
drawerSize.y = io.DisplaySize.y - size::AppBarHeight;
|
|
}
|
|
|
|
ImRect drawerBB(drawerPos, ImVec2(drawerPos.x + drawerSize.x, drawerPos.y + drawerSize.y));
|
|
|
|
// Draw drawer background
|
|
ImU32 bgColor = Surface(Elevation::Dp16);
|
|
drawList->AddRectFilled(drawerBB.Min, drawerBB.Max, bgColor);
|
|
|
|
// Store content region
|
|
g_navDrawerState.contentMin = ImVec2(drawerBB.Min.x, drawerBB.Min.y);
|
|
g_navDrawerState.contentMax = drawerBB.Max;
|
|
|
|
// Header
|
|
float currentY = drawerBB.Min.y;
|
|
|
|
if (spec.headerTitle || spec.headerSubtitle) {
|
|
// Header area (optional)
|
|
float headerHeight = 64.0f;
|
|
|
|
ImVec2 headerMin(drawerBB.Min.x, currentY);
|
|
ImVec2 headerMax(drawerBB.Max.x, currentY + headerHeight);
|
|
|
|
// Header background (slightly elevated)
|
|
drawList->AddRectFilled(headerMin, headerMax, Surface(Elevation::Dp16));
|
|
|
|
// Header title
|
|
if (spec.headerTitle) {
|
|
ImGui::SetCursorScreenPos(ImVec2(drawerBB.Min.x + spacing::dp(2), currentY + 20.0f));
|
|
Typography::instance().text(TypeStyle::H6, spec.headerTitle);
|
|
}
|
|
|
|
if (spec.headerSubtitle) {
|
|
ImGui::SetCursorScreenPos(ImVec2(drawerBB.Min.x + spacing::dp(2), currentY + 42.0f));
|
|
Typography::instance().textColored(TypeStyle::Body2, OnSurfaceMedium(), spec.headerSubtitle);
|
|
}
|
|
|
|
currentY += headerHeight;
|
|
|
|
// Divider under header
|
|
drawList->AddLine(
|
|
ImVec2(drawerBB.Min.x, currentY),
|
|
ImVec2(drawerBB.Max.x, currentY),
|
|
OnSurfaceDisabled()
|
|
);
|
|
}
|
|
|
|
// Set cursor for nav items
|
|
ImGui::SetCursorScreenPos(ImVec2(drawerBB.Min.x, currentY + spacing::dp(1)));
|
|
ImGui::BeginGroup();
|
|
|
|
return true;
|
|
}
|
|
|
|
inline bool BeginNavDrawer(const char* id, const NavDrawerSpec& spec) {
|
|
static bool alwaysOpen = true;
|
|
NavDrawerSpec standardSpec = spec;
|
|
standardSpec.type = NavDrawerType::Standard;
|
|
return BeginNavDrawer(id, &alwaysOpen, standardSpec);
|
|
}
|
|
|
|
inline void EndNavDrawer() {
|
|
ImGui::EndGroup();
|
|
|
|
// Divider at bottom if configured
|
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
ImDrawList* drawList = window->DrawList;
|
|
|
|
// Right edge divider
|
|
drawList->AddLine(
|
|
ImVec2(g_navDrawerState.contentMax.x - 1, g_navDrawerState.contentMin.y),
|
|
ImVec2(g_navDrawerState.contentMax.x - 1, g_navDrawerState.contentMax.y),
|
|
OnSurfaceDisabled()
|
|
);
|
|
|
|
ImGui::PopID();
|
|
}
|
|
|
|
inline bool NavItem(const NavItemSpec& spec) {
|
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGui::PushID(spec.label);
|
|
|
|
// Item dimensions
|
|
const float itemHeight = 48.0f;
|
|
const float iconSize = 24.0f;
|
|
const float horizontalPadding = spacing::dp(2); // 16dp
|
|
const float iconLabelGap = spacing::dp(4); // 32dp from left edge to label
|
|
|
|
float itemWidth = g_navDrawerState.width - spacing::dp(1); // 8dp margin right
|
|
|
|
ImVec2 pos = window->DC.CursorPos;
|
|
pos.x += spacing::dp(1); // 8dp margin left
|
|
|
|
ImRect itemBB(pos, ImVec2(pos.x + itemWidth, pos.y + itemHeight));
|
|
|
|
// Interaction
|
|
ImGuiID id = window->GetID("##navitem");
|
|
ImGui::ItemSize(ImRect(window->DC.CursorPos, ImVec2(window->DC.CursorPos.x + g_navDrawerState.width, window->DC.CursorPos.y + itemHeight)));
|
|
if (!ImGui::ItemAdd(itemBB, id))
|
|
return false;
|
|
|
|
bool hovered, held;
|
|
bool pressed = ImGui::ButtonBehavior(itemBB, id, &hovered, &held) && !spec.disabled;
|
|
|
|
// Draw background
|
|
ImDrawList* drawList = window->DrawList;
|
|
|
|
ImU32 bgColor = 0;
|
|
if (spec.selected) {
|
|
bgColor = WithAlpha(Primary(), 30); // Primary at ~12%
|
|
} else if (held && !spec.disabled) {
|
|
bgColor = schema::UI().resolveColor("var(--sidebar-hover)", IM_COL32(255, 255, 255, 25));
|
|
} else if (hovered && !spec.disabled) {
|
|
bgColor = schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10));
|
|
}
|
|
|
|
if (bgColor) {
|
|
drawList->AddRectFilled(itemBB.Min, itemBB.Max, bgColor, size::ButtonCornerRadius);
|
|
}
|
|
|
|
// Selected indicator (left edge)
|
|
if (spec.selected) {
|
|
drawList->AddRectFilled(
|
|
ImVec2(itemBB.Min.x, itemBB.Min.y + 8.0f),
|
|
ImVec2(itemBB.Min.x + 4.0f, itemBB.Max.y - 8.0f),
|
|
Primary(), 2.0f
|
|
);
|
|
}
|
|
|
|
// Content
|
|
float contentX = pos.x + horizontalPadding;
|
|
float centerY = pos.y + itemHeight * 0.5f;
|
|
|
|
ImU32 iconColor = spec.disabled ? OnSurfaceDisabled() :
|
|
spec.selected ? Primary() : OnSurfaceMedium();
|
|
ImU32 labelColor = spec.disabled ? OnSurfaceDisabled() :
|
|
spec.selected ? Primary() : OnSurface();
|
|
|
|
// Icon
|
|
if (spec.icon) {
|
|
drawList->AddText(
|
|
ImVec2(contentX, centerY - iconSize * 0.5f),
|
|
iconColor, spec.icon
|
|
);
|
|
contentX += iconSize + spacing::dp(2); // 16dp gap after icon
|
|
}
|
|
|
|
// Label
|
|
Typography::instance().pushFont(TypeStyle::Body1);
|
|
float labelY = centerY - ImGui::GetFontSize() * 0.5f;
|
|
drawList->AddText(ImVec2(contentX, labelY), labelColor, spec.label);
|
|
Typography::instance().popFont();
|
|
|
|
// Badge
|
|
if (spec.badgeCount > 0) {
|
|
char badgeText[8];
|
|
if (spec.badgeCount > 999) {
|
|
snprintf(badgeText, sizeof(badgeText), "999+");
|
|
} else {
|
|
snprintf(badgeText, sizeof(badgeText), "%d", spec.badgeCount);
|
|
}
|
|
|
|
ImVec2 badgeSize = ImGui::CalcTextSize(badgeText);
|
|
float badgeWidth = ImMax(24.0f, badgeSize.x + 12.0f);
|
|
float badgeHeight = 20.0f;
|
|
float badgeX = itemBB.Max.x - horizontalPadding - badgeWidth;
|
|
float badgeY = centerY - badgeHeight * 0.5f;
|
|
|
|
drawList->AddRectFilled(
|
|
ImVec2(badgeX, badgeY),
|
|
ImVec2(badgeX + badgeWidth, badgeY + badgeHeight),
|
|
Primary(), badgeHeight * 0.5f
|
|
);
|
|
|
|
Typography::instance().pushFont(TypeStyle::Caption);
|
|
ImVec2 textPos(badgeX + (badgeWidth - badgeSize.x) * 0.5f, badgeY + (badgeHeight - badgeSize.y) * 0.5f);
|
|
drawList->AddText(textPos, OnPrimary(), badgeText);
|
|
Typography::instance().popFont();
|
|
}
|
|
|
|
ImGui::PopID();
|
|
|
|
return pressed;
|
|
}
|
|
|
|
inline bool NavItem(const char* icon, const char* label, bool selected) {
|
|
NavItemSpec spec;
|
|
spec.icon = icon;
|
|
spec.label = label;
|
|
spec.selected = selected;
|
|
return NavItem(spec);
|
|
}
|
|
|
|
inline void NavDivider() {
|
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return;
|
|
|
|
ImVec2 pos = window->DC.CursorPos;
|
|
ImDrawList* drawList = window->DrawList;
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp spacing above
|
|
|
|
drawList->AddLine(
|
|
ImVec2(pos.x + spacing::dp(2), pos.y + spacing::dp(1)),
|
|
ImVec2(pos.x + g_navDrawerState.width - spacing::dp(2), pos.y + spacing::dp(1)),
|
|
OnSurfaceDisabled()
|
|
);
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp spacing below
|
|
}
|
|
|
|
inline void NavSubheader(const char* text) {
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp above
|
|
|
|
ImVec2 pos = ImGui::GetCursorScreenPos();
|
|
ImGui::SetCursorScreenPos(ImVec2(pos.x + spacing::dp(2), pos.y));
|
|
|
|
Typography::instance().textColored(TypeStyle::Caption, OnSurfaceMedium(), text);
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp below
|
|
}
|
|
|
|
} // namespace material
|
|
} // namespace ui
|
|
} // namespace dragonx
|