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)
307 lines
10 KiB
C++
307 lines
10 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 "../../embedded/IconsMaterialDesign.h"
|
|
#include "imgui.h"
|
|
#include "imgui_internal.h"
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
namespace material {
|
|
|
|
// ============================================================================
|
|
// Material Design List Component
|
|
// ============================================================================
|
|
// Based on https://m2.material.io/components/lists
|
|
//
|
|
// Lists present content in a way that makes it easy to identify a specific
|
|
// item in a collection and act on it.
|
|
|
|
/**
|
|
* @brief List item configuration
|
|
*/
|
|
struct ListItemSpec {
|
|
const char* leadingIcon = nullptr; // Optional leading icon (text representation)
|
|
const char* leadingAvatar = nullptr; // Optional avatar text (for initials)
|
|
ImU32 leadingAvatarColor = 0; // Avatar background color (0 = primary)
|
|
bool leadingCheckbox = false; // Show checkbox instead of icon
|
|
bool checkboxChecked = false; // Checkbox state
|
|
const char* primaryText = nullptr; // Main text (required)
|
|
const char* secondaryText = nullptr; // Secondary text (optional)
|
|
const char* trailingIcon = nullptr; // Optional trailing icon
|
|
const char* trailingText = nullptr; // Optional trailing metadata text
|
|
bool selected = false; // Selected state (highlight)
|
|
bool disabled = false; // Disabled state
|
|
bool dividerBelow = false; // Draw divider below item
|
|
};
|
|
|
|
/**
|
|
* @brief Begin a list container
|
|
*
|
|
* @param id Unique identifier
|
|
* @param withPadding Add top/bottom padding
|
|
*/
|
|
void BeginList(const char* id, bool withPadding = true);
|
|
|
|
/**
|
|
* @brief End a list container
|
|
*/
|
|
void EndList();
|
|
|
|
/**
|
|
* @brief Render a list item
|
|
*
|
|
* @param spec Item configuration
|
|
* @return true if clicked
|
|
*/
|
|
bool ListItem(const ListItemSpec& spec);
|
|
|
|
/**
|
|
* @brief Simple single-line list item
|
|
*/
|
|
bool ListItem(const char* text);
|
|
|
|
/**
|
|
* @brief Two-line list item with primary and secondary text
|
|
*/
|
|
bool ListItem(const char* primary, const char* secondary);
|
|
|
|
/**
|
|
* @brief List divider (full width or inset)
|
|
*
|
|
* @param inset If true, indented to align with text
|
|
*/
|
|
void ListDivider(bool inset = false);
|
|
|
|
/**
|
|
* @brief List subheader text
|
|
*/
|
|
void ListSubheader(const char* text);
|
|
|
|
// ============================================================================
|
|
// Implementation
|
|
// ============================================================================
|
|
|
|
inline void BeginList(const char* id, bool withPadding) {
|
|
ImGui::PushID(id);
|
|
if (withPadding) {
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp top padding
|
|
}
|
|
}
|
|
|
|
inline void EndList() {
|
|
ImGui::PopID();
|
|
}
|
|
|
|
inline bool ListItem(const ListItemSpec& spec) {
|
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return false;
|
|
|
|
// Calculate item height based on content
|
|
bool hasSecondary = (spec.secondaryText != nullptr);
|
|
bool hasLeadingElement = (spec.leadingIcon || spec.leadingAvatar || spec.leadingCheckbox);
|
|
|
|
float itemHeight;
|
|
if (hasSecondary) {
|
|
itemHeight = size::ListItemTwoLineHeight; // 72dp for two-line
|
|
} else if (hasLeadingElement) {
|
|
itemHeight = size::ListItemHeight; // 56dp with leading element
|
|
} else {
|
|
itemHeight = 48.0f; // 48dp simple one-line
|
|
}
|
|
|
|
// Item dimensions
|
|
ImVec2 pos = window->DC.CursorPos;
|
|
float itemWidth = ImGui::GetContentRegionAvail().x;
|
|
ImRect bb(pos, ImVec2(pos.x + itemWidth, pos.y + itemHeight));
|
|
|
|
// Interaction
|
|
ImGuiID itemId = window->GetID(spec.primaryText);
|
|
ImGui::ItemSize(bb);
|
|
if (!ImGui::ItemAdd(bb, itemId))
|
|
return false;
|
|
|
|
bool hovered, held;
|
|
bool pressed = ImGui::ButtonBehavior(bb, itemId, &hovered, &held) && !spec.disabled;
|
|
|
|
// Draw background
|
|
ImDrawList* drawList = window->DrawList;
|
|
ImU32 bgColor = 0;
|
|
|
|
if (spec.selected) {
|
|
bgColor = PrimaryContainer();
|
|
} else if (held && !spec.disabled) {
|
|
bgColor = schema::UI().resolveColor("var(--hover-overlay)", 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(bb.Min, bb.Max, bgColor);
|
|
}
|
|
|
|
// Layout positions
|
|
float leftPadding = spacing::dp(2); // 16dp left padding
|
|
float currentX = bb.Min.x + leftPadding;
|
|
float centerY = bb.Min.y + itemHeight * 0.5f;
|
|
|
|
// Draw leading element
|
|
if (spec.leadingAvatar) {
|
|
// Avatar circle with text
|
|
float avatarRadius = 20.0f; // 40dp diameter
|
|
ImVec2 avatarCenter(currentX + avatarRadius, centerY);
|
|
|
|
ImU32 avatarBg = spec.leadingAvatarColor ? spec.leadingAvatarColor : Primary();
|
|
drawList->AddCircleFilled(avatarCenter, avatarRadius, avatarBg);
|
|
|
|
// Avatar text (centered)
|
|
ImVec2 textSize = ImGui::CalcTextSize(spec.leadingAvatar);
|
|
ImVec2 textPos(avatarCenter.x - textSize.x * 0.5f, avatarCenter.y - textSize.y * 0.5f);
|
|
drawList->AddText(textPos, OnPrimary(), spec.leadingAvatar);
|
|
|
|
currentX += 40.0f + spacing::dp(2); // 40dp avatar + 16dp gap
|
|
} else if (spec.leadingIcon) {
|
|
// Icon
|
|
ImVec2 iconSize = ImGui::CalcTextSize(spec.leadingIcon);
|
|
float iconY = centerY - iconSize.y * 0.5f;
|
|
ImU32 iconColor = spec.disabled ? OnSurfaceDisabled() : OnSurfaceMedium();
|
|
drawList->AddText(ImVec2(currentX, iconY), iconColor, spec.leadingIcon);
|
|
currentX += 24.0f + spacing::dp(2); // 24dp icon + 16dp gap
|
|
} else if (spec.leadingCheckbox) {
|
|
// Checkbox (simplified visual)
|
|
float checkboxSize = 18.0f;
|
|
ImVec2 checkMin(currentX, centerY - checkboxSize * 0.5f);
|
|
ImVec2 checkMax(currentX + checkboxSize, centerY + checkboxSize * 0.5f);
|
|
|
|
if (spec.checkboxChecked) {
|
|
drawList->AddRectFilled(checkMin, checkMax, Primary(), 2.0f);
|
|
// Checkmark
|
|
ImFont* iconFont = Typography::instance().iconSmall();
|
|
ImVec2 chkSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, ICON_MD_CHECK);
|
|
drawList->AddText(iconFont, iconFont->LegacySize,
|
|
ImVec2(checkMin.x + (checkboxSize - chkSz.x) * 0.5f, checkMin.y + (checkboxSize - chkSz.y) * 0.5f),
|
|
OnPrimary(), ICON_MD_CHECK);
|
|
} else {
|
|
drawList->AddRect(checkMin, checkMax, OnSurfaceMedium(), 2.0f, 0, 2.0f);
|
|
}
|
|
|
|
currentX += checkboxSize + spacing::dp(2);
|
|
}
|
|
|
|
// Calculate text area
|
|
float rightPadding = spacing::dp(2); // 16dp right padding
|
|
float trailingSpace = 0;
|
|
if (spec.trailingIcon) trailingSpace += 24.0f + spacing::dp(1);
|
|
if (spec.trailingText) trailingSpace += ImGui::CalcTextSize(spec.trailingText).x + spacing::dp(1);
|
|
|
|
float textMaxX = bb.Max.x - rightPadding - trailingSpace;
|
|
|
|
// Draw text
|
|
ImU32 primaryColor = spec.disabled ? OnSurfaceDisabled() : OnSurface();
|
|
ImU32 secondaryColor = spec.disabled ? OnSurfaceDisabled() : OnSurfaceMedium();
|
|
|
|
if (hasSecondary) {
|
|
// Two-line layout
|
|
float primaryY = bb.Min.y + 16.0f;
|
|
float secondaryY = primaryY + 20.0f;
|
|
|
|
Typography::instance().pushFont(TypeStyle::Body1);
|
|
drawList->AddText(ImVec2(currentX, primaryY), primaryColor, spec.primaryText);
|
|
Typography::instance().popFont();
|
|
|
|
Typography::instance().pushFont(TypeStyle::Body2);
|
|
drawList->AddText(ImVec2(currentX, secondaryY), secondaryColor, spec.secondaryText);
|
|
Typography::instance().popFont();
|
|
} else {
|
|
// Single-line layout
|
|
Typography::instance().pushFont(TypeStyle::Body1);
|
|
float textY = centerY - Typography::instance().getFont(TypeStyle::Body1)->FontSize * 0.5f;
|
|
drawList->AddText(ImVec2(currentX, textY), primaryColor, spec.primaryText);
|
|
Typography::instance().popFont();
|
|
}
|
|
|
|
// Draw trailing elements
|
|
float trailingX = bb.Max.x - rightPadding;
|
|
|
|
if (spec.trailingText) {
|
|
ImVec2 textSize = ImGui::CalcTextSize(spec.trailingText);
|
|
trailingX -= textSize.x;
|
|
float textY = centerY - textSize.y * 0.5f;
|
|
drawList->AddText(ImVec2(trailingX, textY), secondaryColor, spec.trailingText);
|
|
trailingX -= spacing::dp(1);
|
|
}
|
|
|
|
if (spec.trailingIcon) {
|
|
ImVec2 iconSize = ImGui::CalcTextSize(spec.trailingIcon);
|
|
trailingX -= 24.0f;
|
|
float iconY = centerY - iconSize.y * 0.5f;
|
|
drawList->AddText(ImVec2(trailingX, iconY), OnSurfaceMedium(), spec.trailingIcon);
|
|
}
|
|
|
|
// Draw divider
|
|
if (spec.dividerBelow) {
|
|
float dividerY = bb.Max.y - 0.5f;
|
|
drawList->AddLine(
|
|
ImVec2(bb.Min.x + leftPadding, dividerY),
|
|
ImVec2(bb.Max.x, dividerY),
|
|
OnSurfaceDisabled()
|
|
);
|
|
}
|
|
|
|
return pressed;
|
|
}
|
|
|
|
inline bool ListItem(const char* text) {
|
|
ListItemSpec spec;
|
|
spec.primaryText = text;
|
|
return ListItem(spec);
|
|
}
|
|
|
|
inline bool ListItem(const char* primary, const char* secondary) {
|
|
ListItemSpec spec;
|
|
spec.primaryText = primary;
|
|
spec.secondaryText = secondary;
|
|
return ListItem(spec);
|
|
}
|
|
|
|
inline void ListDivider(bool inset) {
|
|
ImGuiWindow* window = ImGui::GetCurrentWindow();
|
|
if (window->SkipItems)
|
|
return;
|
|
|
|
float width = ImGui::GetContentRegionAvail().x;
|
|
float leftOffset = inset ? 72.0f : 0; // Align with text after avatar/icon
|
|
|
|
ImVec2 pos = window->DC.CursorPos;
|
|
ImDrawList* drawList = window->DrawList;
|
|
|
|
drawList->AddLine(
|
|
ImVec2(pos.x + leftOffset, pos.y),
|
|
ImVec2(pos.x + width, pos.y),
|
|
OnSurfaceDisabled()
|
|
);
|
|
|
|
ImGui::Dummy(ImVec2(width, 1.0f));
|
|
}
|
|
|
|
inline void ListSubheader(const char* text) {
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp top padding
|
|
|
|
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + spacing::dp(2)); // 16dp left padding
|
|
Typography::instance().textColored(TypeStyle::Caption, Primary(), text);
|
|
|
|
ImGui::Dummy(ImVec2(0, spacing::dp(1))); // 8dp bottom padding
|
|
}
|
|
|
|
} // namespace material
|
|
} // namespace ui
|
|
} // namespace dragonx
|