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)
354 lines
14 KiB
C++
354 lines
14 KiB
C++
// DragonX Wallet - ImGui Edition
|
|
// Copyright 2024-2026 The Hush Developers
|
|
// Released under the GPLv3
|
|
|
|
#include "theme_loader.h"
|
|
#include "schema/color_var_resolver.h"
|
|
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
|
|
namespace dragonx {
|
|
namespace ui {
|
|
|
|
// ============================================================================
|
|
// Color Parsing
|
|
// ============================================================================
|
|
|
|
bool ThemeLoader::parseHexColor(const std::string& hexStr, ImU32& outColor)
|
|
{
|
|
if (hexStr.empty()) return false;
|
|
|
|
std::string hex = hexStr;
|
|
|
|
// Remove leading # or 0x
|
|
if (hex[0] == '#') {
|
|
hex = hex.substr(1);
|
|
} else if (hex.size() > 2 && hex[0] == '0' && (hex[1] == 'x' || hex[1] == 'X')) {
|
|
hex = hex.substr(2);
|
|
}
|
|
|
|
// Validate length (6 for RGB, 8 for RGBA)
|
|
if (hex.size() != 6 && hex.size() != 8) {
|
|
return false;
|
|
}
|
|
|
|
// Validate hex characters
|
|
for (char c : hex) {
|
|
if (!std::isxdigit(static_cast<unsigned char>(c))) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
// Parse hex value
|
|
unsigned long value = std::strtoul(hex.c_str(), nullptr, 16);
|
|
|
|
if (hex.size() == 6) {
|
|
// RGB format: 0xRRGGBB -> IM_COL32(R, G, B, 255)
|
|
uint8_t r = (value >> 16) & 0xFF;
|
|
uint8_t g = (value >> 8) & 0xFF;
|
|
uint8_t b = value & 0xFF;
|
|
outColor = IM_COL32(r, g, b, 255);
|
|
} else {
|
|
// RGBA format: 0xRRGGBBAA -> IM_COL32(R, G, B, A)
|
|
uint8_t r = (value >> 24) & 0xFF;
|
|
uint8_t g = (value >> 16) & 0xFF;
|
|
uint8_t b = (value >> 8) & 0xFF;
|
|
uint8_t a = value & 0xFF;
|
|
outColor = IM_COL32(r, g, b, a);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
std::string ThemeLoader::colorToHexString(ImU32 color, bool includeAlpha)
|
|
{
|
|
uint8_t r = (color >> IM_COL32_R_SHIFT) & 0xFF;
|
|
uint8_t g = (color >> IM_COL32_G_SHIFT) & 0xFF;
|
|
uint8_t b = (color >> IM_COL32_B_SHIFT) & 0xFF;
|
|
uint8_t a = (color >> IM_COL32_A_SHIFT) & 0xFF;
|
|
|
|
char buf[16];
|
|
if (includeAlpha || a != 255) {
|
|
snprintf(buf, sizeof(buf), "0x%02X%02X%02X%02X", r, g, b, a);
|
|
} else {
|
|
snprintf(buf, sizeof(buf), "0x%02X%02X%02X", r, g, b);
|
|
}
|
|
return std::string(buf);
|
|
}
|
|
|
|
// ============================================================================
|
|
// Multi-format Color Parsing
|
|
// ============================================================================
|
|
|
|
bool ThemeLoader::parseColorString(const std::string& str, ImU32& outColor)
|
|
{
|
|
if (str.empty()) return false;
|
|
|
|
// Try #hex / 0xhex first (handles both formats)
|
|
if (parseHexColor(str, outColor)) return true;
|
|
|
|
// Try rgba(r,g,b,a)
|
|
if (schema::ColorVarResolver::parseRgba(str, outColor)) return true;
|
|
|
|
// "transparent"
|
|
if (str == "transparent") {
|
|
outColor = IM_COL32(0, 0, 0, 0);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Luminance Calculation
|
|
// ============================================================================
|
|
|
|
float ThemeLoader::getLuminance(ImU32 color)
|
|
{
|
|
// Extract RGB components (0-255)
|
|
float r = ((color >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
|
|
float g = ((color >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
|
|
float b = ((color >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
|
|
|
|
// Convert to linear RGB (sRGB gamma correction)
|
|
auto linearize = [](float c) {
|
|
return c <= 0.03928f ? c / 12.92f : std::pow((c + 0.055f) / 1.055f, 2.4f);
|
|
};
|
|
|
|
r = linearize(r);
|
|
g = linearize(g);
|
|
b = linearize(b);
|
|
|
|
// Calculate relative luminance (ITU-R BT.709)
|
|
return 0.2126f * r + 0.7152f * g + 0.0722f * b;
|
|
}
|
|
|
|
// ============================================================================
|
|
// Default Color Computation
|
|
// ============================================================================
|
|
|
|
void ThemeLoader::computeDefaults(material::ColorTheme& theme, bool isDark)
|
|
{
|
|
using namespace material;
|
|
|
|
// Helper to check if a color is "unset" (black with no alpha)
|
|
auto isUnset = [](ImU32 c) { return c == 0; };
|
|
|
|
if (isDark) {
|
|
// Dark theme defaults
|
|
if (isUnset(theme.primaryVariant)) theme.primaryVariant = Darken(theme.primary, 0.2f);
|
|
if (isUnset(theme.primaryLight)) theme.primaryLight = Lighten(theme.primary, 0.3f);
|
|
|
|
if (isUnset(theme.secondary)) theme.secondary = Hex(0x03DAC6);
|
|
if (isUnset(theme.secondaryVariant)) theme.secondaryVariant = Darken(theme.secondary, 0.2f);
|
|
if (isUnset(theme.secondaryLight)) theme.secondaryLight = Lighten(theme.secondary, 0.3f);
|
|
|
|
if (isUnset(theme.surface)) theme.surface = Lighten(theme.background, 0.05f);
|
|
if (isUnset(theme.surfaceVariant)) theme.surfaceVariant = Lighten(theme.background, 0.10f);
|
|
|
|
if (isUnset(theme.onPrimary)) theme.onPrimary = Hex(0xFFFFFF);
|
|
if (isUnset(theme.onSecondary)) theme.onSecondary = Hex(0x000000);
|
|
if (isUnset(theme.onBackground)) theme.onBackground = Hex(0xFFFFFF);
|
|
if (isUnset(theme.onSurface)) theme.onSurface = Hex(0xFFFFFF);
|
|
if (isUnset(theme.onSurfaceMedium)) theme.onSurfaceMedium = HexA(0xFFFFFF, 179);
|
|
if (isUnset(theme.onSurfaceDisabled)) theme.onSurfaceDisabled = HexA(0xFFFFFF, 97);
|
|
|
|
if (isUnset(theme.error)) theme.error = Hex(0xCF6679);
|
|
if (isUnset(theme.onError)) theme.onError = Hex(0x000000);
|
|
|
|
if (isUnset(theme.success)) theme.success = Hex(0x81C784);
|
|
if (isUnset(theme.onSuccess)) theme.onSuccess = Hex(0x000000);
|
|
if (isUnset(theme.warning)) theme.warning = Hex(0xFFB74D);
|
|
if (isUnset(theme.onWarning)) theme.onWarning = Hex(0x000000);
|
|
|
|
if (isUnset(theme.stateHover)) theme.stateHover = HexA(0xFFFFFF, 10);
|
|
if (isUnset(theme.stateFocus)) theme.stateFocus = HexA(0xFFFFFF, 31);
|
|
if (isUnset(theme.statePressed)) theme.statePressed = HexA(0xFFFFFF, 25);
|
|
if (isUnset(theme.stateSelected)) theme.stateSelected = HexA(0xFFFFFF, 20);
|
|
if (isUnset(theme.stateDragged)) theme.stateDragged = HexA(0xFFFFFF, 20);
|
|
|
|
if (isUnset(theme.divider)) theme.divider = HexA(0xFFFFFF, 31);
|
|
if (isUnset(theme.outline)) theme.outline = HexA(0xFFFFFF, 31);
|
|
if (isUnset(theme.scrim)) theme.scrim = HexA(0x000000, 128);
|
|
} else {
|
|
// Light theme defaults
|
|
if (isUnset(theme.primaryVariant)) theme.primaryVariant = Darken(theme.primary, 0.15f);
|
|
if (isUnset(theme.primaryLight)) theme.primaryLight = Lighten(theme.primary, 0.4f);
|
|
|
|
if (isUnset(theme.secondary)) theme.secondary = Hex(0x03DAC6);
|
|
if (isUnset(theme.secondaryVariant)) theme.secondaryVariant = Darken(theme.secondary, 0.15f);
|
|
if (isUnset(theme.secondaryLight)) theme.secondaryLight = Lighten(theme.secondary, 0.4f);
|
|
|
|
if (isUnset(theme.surface)) theme.surface = Hex(0xFFFFFF);
|
|
if (isUnset(theme.surfaceVariant)) theme.surfaceVariant = Darken(theme.background, 0.02f);
|
|
|
|
if (isUnset(theme.onPrimary)) theme.onPrimary = Hex(0xFFFFFF);
|
|
if (isUnset(theme.onSecondary)) theme.onSecondary = Hex(0x000000);
|
|
if (isUnset(theme.onBackground)) theme.onBackground = Hex(0x000000);
|
|
if (isUnset(theme.onSurface)) theme.onSurface = Hex(0x000000);
|
|
if (isUnset(theme.onSurfaceMedium)) theme.onSurfaceMedium = HexA(0x000000, 179);
|
|
if (isUnset(theme.onSurfaceDisabled)) theme.onSurfaceDisabled = HexA(0x000000, 97);
|
|
|
|
if (isUnset(theme.error)) theme.error = Hex(0xB00020);
|
|
if (isUnset(theme.onError)) theme.onError = Hex(0xFFFFFF);
|
|
|
|
if (isUnset(theme.success)) theme.success = Hex(0x4CAF50);
|
|
if (isUnset(theme.onSuccess)) theme.onSuccess = Hex(0xFFFFFF);
|
|
if (isUnset(theme.warning)) theme.warning = Hex(0xFF9800);
|
|
if (isUnset(theme.onWarning)) theme.onWarning = Hex(0x000000);
|
|
|
|
if (isUnset(theme.stateHover)) theme.stateHover = HexA(0x000000, 10);
|
|
if (isUnset(theme.stateFocus)) theme.stateFocus = HexA(0x000000, 31);
|
|
if (isUnset(theme.statePressed)) theme.statePressed = HexA(0x000000, 25);
|
|
if (isUnset(theme.stateSelected)) theme.stateSelected = HexA(0x000000, 20);
|
|
if (isUnset(theme.stateDragged)) theme.stateDragged = HexA(0x000000, 20);
|
|
|
|
if (isUnset(theme.divider)) theme.divider = HexA(0x000000, 31);
|
|
if (isUnset(theme.outline)) theme.outline = HexA(0x000000, 31);
|
|
if (isUnset(theme.scrim)) theme.scrim = HexA(0x000000, 128);
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Acrylic Theme Derivation
|
|
// ============================================================================
|
|
|
|
AcrylicTheme ThemeLoader::deriveAcrylicTheme(const material::ColorTheme& theme)
|
|
{
|
|
AcrylicTheme acrylic;
|
|
|
|
// Extract primary color components for tinting
|
|
float primaryR = ((theme.primary >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
|
|
float primaryG = ((theme.primary >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
|
|
float primaryB = ((theme.primary >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
|
|
|
|
// Extract background color components
|
|
float bgR = ((theme.background >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
|
|
float bgG = ((theme.background >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
|
|
float bgB = ((theme.background >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
|
|
|
|
bool isDark = getLuminance(theme.background) < 0.5f;
|
|
|
|
if (isDark) {
|
|
// Dark theme: tint towards primary color
|
|
|
|
// Sidebar: Strong primary tint
|
|
acrylic.sidebar.tintColor = ImVec4(
|
|
bgR * 0.5f + primaryR * 0.5f,
|
|
bgG * 0.5f + primaryG * 0.5f,
|
|
bgB * 0.5f + primaryB * 0.5f,
|
|
1.0f
|
|
);
|
|
acrylic.sidebar.tintOpacity = 0.85f;
|
|
acrylic.sidebar.luminosityOpacity = 0.4f;
|
|
acrylic.sidebar.blurRadius = 40.0f;
|
|
acrylic.sidebar.noiseOpacity = 0.02f;
|
|
acrylic.sidebar.fallbackColor = ImVec4(bgR * 0.7f, bgG * 0.7f, bgB * 0.7f, 1.0f);
|
|
acrylic.sidebar.enabled = true;
|
|
|
|
// Popups: Subtle dark with slight primary tint
|
|
acrylic.popup.tintColor = ImVec4(
|
|
bgR * 0.8f + primaryR * 0.2f,
|
|
bgG * 0.8f + primaryG * 0.2f,
|
|
bgB * 0.8f + primaryB * 0.2f,
|
|
1.0f
|
|
);
|
|
acrylic.popup.tintOpacity = 0.80f;
|
|
acrylic.popup.luminosityOpacity = 0.5f;
|
|
acrylic.popup.blurRadius = 30.0f;
|
|
acrylic.popup.noiseOpacity = 0.02f;
|
|
acrylic.popup.fallbackColor = ImVec4(bgR, bgG, bgB, 0.98f);
|
|
acrylic.popup.enabled = true;
|
|
|
|
// Cards: Very subtle primary tint
|
|
acrylic.card.tintColor = ImVec4(
|
|
bgR * 0.9f + primaryR * 0.1f,
|
|
bgG * 0.9f + primaryG * 0.1f,
|
|
bgB * 0.9f + primaryB * 0.1f,
|
|
1.0f
|
|
);
|
|
acrylic.card.tintOpacity = 0.65f;
|
|
acrylic.card.luminosityOpacity = 0.6f;
|
|
acrylic.card.blurRadius = 20.0f;
|
|
acrylic.card.noiseOpacity = 0.015f;
|
|
acrylic.card.fallbackColor = ImVec4(bgR + 0.05f, bgG + 0.05f, bgB + 0.05f, 1.0f);
|
|
acrylic.card.enabled = true;
|
|
|
|
// Context menus: Dark and crisp
|
|
acrylic.menu.tintColor = ImVec4(bgR, bgG, bgB, 1.0f);
|
|
acrylic.menu.tintOpacity = 0.88f;
|
|
acrylic.menu.luminosityOpacity = 0.45f;
|
|
acrylic.menu.blurRadius = 25.0f;
|
|
acrylic.menu.noiseOpacity = 0.02f;
|
|
acrylic.menu.fallbackColor = ImVec4(bgR, bgG, bgB, 0.98f);
|
|
acrylic.menu.enabled = true;
|
|
|
|
// Tooltips: Very transparent
|
|
acrylic.tooltip.tintColor = ImVec4(bgR, bgG, bgB, 1.0f);
|
|
acrylic.tooltip.tintOpacity = 0.75f;
|
|
acrylic.tooltip.luminosityOpacity = 0.5f;
|
|
acrylic.tooltip.blurRadius = 15.0f;
|
|
acrylic.tooltip.noiseOpacity = 0.01f;
|
|
acrylic.tooltip.fallbackColor = ImVec4(bgR + 0.02f, bgG + 0.02f, bgB + 0.02f, 0.95f);
|
|
acrylic.tooltip.enabled = true;
|
|
} else {
|
|
// Light theme
|
|
|
|
// Sidebar: Light with slight primary tint
|
|
acrylic.sidebar.tintColor = ImVec4(
|
|
bgR * 0.9f + primaryR * 0.1f,
|
|
bgG * 0.9f + primaryG * 0.1f,
|
|
bgB * 0.9f + primaryB * 0.1f,
|
|
1.0f
|
|
);
|
|
acrylic.sidebar.tintOpacity = 0.82f;
|
|
acrylic.sidebar.luminosityOpacity = 0.7f;
|
|
acrylic.sidebar.blurRadius = 30.0f;
|
|
acrylic.sidebar.noiseOpacity = 0.015f;
|
|
acrylic.sidebar.fallbackColor = ImVec4(bgR, bgG, bgB, 1.0f);
|
|
acrylic.sidebar.enabled = true;
|
|
|
|
// Popups
|
|
acrylic.popup.tintColor = ImVec4(1.0f, 1.0f, 1.0f, 1.0f);
|
|
acrylic.popup.tintOpacity = 0.85f;
|
|
acrylic.popup.luminosityOpacity = 0.75f;
|
|
acrylic.popup.blurRadius = 25.0f;
|
|
acrylic.popup.noiseOpacity = 0.015f;
|
|
acrylic.popup.fallbackColor = ImVec4(bgR, bgG, bgB, 0.98f);
|
|
acrylic.popup.enabled = true;
|
|
|
|
// Cards
|
|
acrylic.card.tintColor = ImVec4(bgR, bgG, bgB, 1.0f);
|
|
acrylic.card.tintOpacity = 0.70f;
|
|
acrylic.card.luminosityOpacity = 0.75f;
|
|
acrylic.card.blurRadius = 18.0f;
|
|
acrylic.card.noiseOpacity = 0.01f;
|
|
acrylic.card.fallbackColor = ImVec4(bgR, bgG, bgB, 1.0f);
|
|
acrylic.card.enabled = true;
|
|
|
|
// Menus
|
|
acrylic.menu.tintColor = ImVec4(bgR, bgG, bgB, 1.0f);
|
|
acrylic.menu.tintOpacity = 0.88f;
|
|
acrylic.menu.luminosityOpacity = 0.7f;
|
|
acrylic.menu.blurRadius = 22.0f;
|
|
acrylic.menu.noiseOpacity = 0.015f;
|
|
acrylic.menu.fallbackColor = ImVec4(bgR, bgG, bgB, 0.98f);
|
|
acrylic.menu.enabled = true;
|
|
|
|
// Tooltips
|
|
acrylic.tooltip.tintColor = ImVec4(bgR - 0.02f, bgG - 0.02f, bgB - 0.02f, 1.0f);
|
|
acrylic.tooltip.tintOpacity = 0.80f;
|
|
acrylic.tooltip.luminosityOpacity = 0.7f;
|
|
acrylic.tooltip.blurRadius = 12.0f;
|
|
acrylic.tooltip.noiseOpacity = 0.01f;
|
|
acrylic.tooltip.fallbackColor = ImVec4(bgR - 0.02f, bgG - 0.02f, bgB - 0.02f, 0.95f);
|
|
acrylic.tooltip.enabled = true;
|
|
}
|
|
|
|
return acrylic;
|
|
}
|
|
|
|
} // namespace ui
|
|
} // namespace dragonx
|