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)
320 lines
9.6 KiB
C++
320 lines
9.6 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 Tabs Component
|
|
// ============================================================================
|
|
// Based on https://m2.material.io/components/tabs
|
|
//
|
|
// Tabs organize content across different screens, data sets, and other
|
|
// interactions.
|
|
|
|
/**
|
|
* @brief Tab bar configuration
|
|
*/
|
|
struct TabBarSpec {
|
|
bool scrollable = false; // Enable horizontal scrolling
|
|
bool fullWidth = true; // Tabs fill available width
|
|
bool showIndicator = true; // Show selection indicator
|
|
bool centered = false; // Center tabs (when not full width)
|
|
};
|
|
|
|
/**
|
|
* @brief Individual tab configuration
|
|
*/
|
|
struct TabSpec {
|
|
const char* label = nullptr;
|
|
const char* icon = nullptr; // Optional icon (text representation)
|
|
bool disabled = false;
|
|
int badgeCount = 0; // Badge count (0 = no badge)
|
|
};
|
|
|
|
/**
|
|
* @brief Begin a tab bar
|
|
*
|
|
* @param id Unique identifier
|
|
* @param selectedIndex Pointer to selected tab index
|
|
* @param spec Tab bar configuration
|
|
* @return true if tab bar is visible
|
|
*/
|
|
bool BeginTabBar(const char* id, int* selectedIndex, const TabBarSpec& spec = TabBarSpec());
|
|
|
|
/**
|
|
* @brief End a tab bar
|
|
*/
|
|
void EndTabBar();
|
|
|
|
/**
|
|
* @brief Add a tab to current tab bar
|
|
*
|
|
* @param spec Tab configuration
|
|
* @return true if this tab is selected
|
|
*/
|
|
bool Tab(const TabSpec& spec);
|
|
|
|
/**
|
|
* @brief Simple tab with just label
|
|
*/
|
|
bool Tab(const char* label);
|
|
|
|
/**
|
|
* @brief Simple tab bar - returns selected index
|
|
*
|
|
* @param id Unique identifier
|
|
* @param labels Array of tab labels
|
|
* @param count Number of tabs
|
|
* @param selectedIndex Current selected index (will be updated)
|
|
* @return true if selection changed
|
|
*/
|
|
bool TabBar(const char* id, const char** labels, int count, int* selectedIndex);
|
|
|
|
// ============================================================================
|
|
// Implementation
|
|
// ============================================================================
|
|
|
|
// Internal state for tab rendering
|
|
struct TabBarState {
|
|
int* selectedIndex;
|
|
int currentTabIndex;
|
|
TabBarSpec spec;
|
|
float tabBarWidth;
|
|
float tabWidth;
|
|
float indicatorX;
|
|
float indicatorWidth;
|
|
ImVec2 barPos;
|
|
};
|
|
|
|
static TabBarState g_tabBarState;
|
|
|
|
inline bool BeginTabBar(const char* id, int* selectedIndex, const TabBarSpec& spec) {
|
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
ImGui::PushID(id);
|
|
|
|
g_tabBarState.selectedIndex = selectedIndex;
|
|
g_tabBarState.currentTabIndex = 0;
|
|
g_tabBarState.spec = spec;
|
|
g_tabBarState.tabBarWidth = ImGui::GetContentRegionAvail().x;
|
|
g_tabBarState.tabWidth = 0; // Will be calculated if fullWidth
|
|
g_tabBarState.barPos = window->DC.CursorPos;
|
|
g_tabBarState.indicatorX = 0;
|
|
g_tabBarState.indicatorWidth = 0;
|
|
|
|
// Reserve space for tab bar
|
|
float barHeight = size::TabBarHeight;
|
|
ImRect bb(g_tabBarState.barPos,
|
|
ImVec2(g_tabBarState.barPos.x + g_tabBarState.tabBarWidth,
|
|
g_tabBarState.barPos.y + barHeight));
|
|
|
|
ImGui::ItemSize(bb);
|
|
|
|
// Draw tab bar background
|
|
ImDrawList* drawList = window->DrawList;
|
|
drawList->AddRectFilled(bb.Min, bb.Max, Surface(Elevation::Dp4));
|
|
|
|
// Begin horizontal layout for tabs
|
|
ImGui::SetCursorScreenPos(g_tabBarState.barPos);
|
|
ImGui::BeginGroup();
|
|
|
|
return true;
|
|
}
|
|
|
|
inline void EndTabBar() {
|
|
ImGui::EndGroup();
|
|
|
|
// Draw indicator line
|
|
if (g_tabBarState.spec.showIndicator && g_tabBarState.indicatorWidth > 0) {
|
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
ImDrawList* drawList = window->DrawList;
|
|
|
|
float indicatorY = g_tabBarState.barPos.y + size::TabBarHeight - 2.0f;
|
|
drawList->AddRectFilled(
|
|
ImVec2(g_tabBarState.indicatorX, indicatorY),
|
|
ImVec2(g_tabBarState.indicatorX + g_tabBarState.indicatorWidth, indicatorY + 2.0f),
|
|
Primary()
|
|
);
|
|
}
|
|
|
|
// Add bottom divider
|
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
ImDrawList* drawList = window->DrawList;
|
|
float dividerY = g_tabBarState.barPos.y + size::TabBarHeight;
|
|
drawList->AddLine(
|
|
ImVec2(g_tabBarState.barPos.x, dividerY),
|
|
ImVec2(g_tabBarState.barPos.x + g_tabBarState.tabBarWidth, dividerY),
|
|
OnSurfaceDisabled()
|
|
);
|
|
|
|
ImGui::PopID();
|
|
}
|
|
|
|
inline bool Tab(const TabSpec& spec) {
|
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
int tabIndex = g_tabBarState.currentTabIndex++;
|
|
bool isSelected = (*g_tabBarState.selectedIndex == tabIndex);
|
|
|
|
// Calculate tab dimensions
|
|
float minTabWidth = spec.icon ? 72.0f : 90.0f; // Material min widths
|
|
float maxTabWidth = 360.0f;
|
|
float labelWidth = ImGui::CalcTextSize(spec.label).x;
|
|
float iconWidth = spec.icon ? 24.0f + spacing::dp(1) : 0;
|
|
float contentWidth = labelWidth + iconWidth + spacing::dp(4); // 32dp padding
|
|
|
|
float tabWidth;
|
|
if (g_tabBarState.spec.fullWidth) {
|
|
// Divide evenly (assuming we don't know total count here - simplified)
|
|
tabWidth = ImMax(minTabWidth, contentWidth);
|
|
} else {
|
|
tabWidth = ImClamp(contentWidth, minTabWidth, maxTabWidth);
|
|
}
|
|
|
|
float tabHeight = size::TabBarHeight;
|
|
|
|
ImVec2 tabPos = window->DC.CursorPos;
|
|
ImRect tabBB(tabPos, ImVec2(tabPos.x + tabWidth, tabPos.y + tabHeight));
|
|
|
|
// Interaction
|
|
ImGuiID id = window->GetID(spec.label);
|
|
bool hovered, held;
|
|
bool pressed = ImGui::ButtonBehavior(tabBB, id, &hovered, &held) && !spec.disabled;
|
|
|
|
if (pressed && !isSelected) {
|
|
*g_tabBarState.selectedIndex = tabIndex;
|
|
}
|
|
|
|
// Update indicator position for selected tab
|
|
if (isSelected) {
|
|
g_tabBarState.indicatorX = tabPos.x;
|
|
g_tabBarState.indicatorWidth = tabWidth;
|
|
}
|
|
|
|
// Draw
|
|
ImDrawList* drawList = window->DrawList;
|
|
|
|
// Hover/press state overlay
|
|
if (!spec.disabled) {
|
|
if (held) {
|
|
drawList->AddRectFilled(tabBB.Min, tabBB.Max, schema::UI().resolveColor("var(--hover-overlay)", IM_COL32(255, 255, 255, 25)));
|
|
} else if (hovered) {
|
|
drawList->AddRectFilled(tabBB.Min, tabBB.Max, schema::UI().resolveColor("var(--active-overlay)", IM_COL32(255, 255, 255, 10)));
|
|
}
|
|
}
|
|
|
|
// Content color
|
|
ImU32 contentColor;
|
|
if (spec.disabled) {
|
|
contentColor = OnSurfaceDisabled();
|
|
} else if (isSelected) {
|
|
contentColor = Primary();
|
|
} else {
|
|
contentColor = OnSurfaceMedium();
|
|
}
|
|
|
|
// Draw content (icon and/or label)
|
|
float contentX = tabPos.x + (tabWidth - labelWidth - iconWidth) * 0.5f;
|
|
float centerY = tabPos.y + tabHeight * 0.5f;
|
|
|
|
if (spec.icon) {
|
|
ImFont* iconFont = Type().iconMed();
|
|
ImVec2 iconSize = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, spec.icon);
|
|
ImVec2 iconPos(contentX, centerY - iconSize.y * 0.5f);
|
|
drawList->AddText(iconFont, iconFont->LegacySize, iconPos, contentColor, spec.icon);
|
|
contentX += iconSize.x + spacing::Xs;
|
|
}
|
|
|
|
// Label (uppercase)
|
|
Typography::instance().pushFont(TypeStyle::Button);
|
|
float labelY = centerY - ImGui::GetFontSize() * 0.5f;
|
|
|
|
// Convert to uppercase
|
|
char upperLabel[128];
|
|
size_t i = 0;
|
|
for (const char* p = spec.label; *p && i < sizeof(upperLabel) - 1; p++, i++) {
|
|
upperLabel[i] = (*p >= 'a' && *p <= 'z') ? (*p - 32) : *p;
|
|
}
|
|
upperLabel[i] = '\0';
|
|
|
|
drawList->AddText(ImVec2(contentX, labelY), contentColor, upperLabel);
|
|
Typography::instance().popFont();
|
|
|
|
// Badge
|
|
if (spec.badgeCount > 0) {
|
|
float badgeX = tabPos.x + tabWidth - 16.0f;
|
|
float badgeY = tabPos.y + 8.0f;
|
|
float badgeRadius = 8.0f;
|
|
|
|
drawList->AddCircleFilled(ImVec2(badgeX, badgeY), badgeRadius, Error());
|
|
|
|
char badgeText[8];
|
|
if (spec.badgeCount > 99) {
|
|
snprintf(badgeText, sizeof(badgeText), "99+");
|
|
} else {
|
|
snprintf(badgeText, sizeof(badgeText), "%d", spec.badgeCount);
|
|
}
|
|
|
|
ImVec2 badgeTextSize = ImGui::CalcTextSize(badgeText);
|
|
ImVec2 badgeTextPos(badgeX - badgeTextSize.x * 0.5f, badgeY - badgeTextSize.y * 0.5f);
|
|
|
|
Typography::instance().pushFont(TypeStyle::Caption);
|
|
drawList->AddText(badgeTextPos, OnError(), badgeText);
|
|
Typography::instance().popFont();
|
|
}
|
|
|
|
// Advance cursor
|
|
ImGui::SameLine(0, 0);
|
|
ImGui::SetCursorScreenPos(ImVec2(tabPos.x + tabWidth, tabPos.y));
|
|
|
|
return isSelected;
|
|
}
|
|
|
|
inline bool Tab(const char* label) {
|
|
TabSpec spec;
|
|
spec.label = label;
|
|
return Tab(spec);
|
|
}
|
|
|
|
inline bool TabBar(const char* id, const char** labels, int count, int* selectedIndex) {
|
|
int oldIndex = *selectedIndex;
|
|
|
|
TabBarSpec spec;
|
|
spec.fullWidth = true;
|
|
|
|
if (BeginTabBar(id, selectedIndex, spec)) {
|
|
// Calculate tab width for full-width mode
|
|
float tabWidth = ImGui::GetContentRegionAvail().x / count;
|
|
|
|
for (int i = 0; i < count; i++) {
|
|
TabSpec tabSpec;
|
|
tabSpec.label = labels[i];
|
|
Tab(tabSpec);
|
|
}
|
|
|
|
EndTabBar();
|
|
}
|
|
|
|
return (*selectedIndex != oldIndex);
|
|
}
|
|
|
|
} // namespace material
|
|
} // namespace ui
|
|
} // namespace dragonx
|