// 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 #include 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(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