// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // Layout G: QR-Centered Hero // - QR code dominates center as hero element // - Address info wraps around the QR // - Payment request section below QR // - Horizontal address strip at bottom for fast switching #include "receive_tab.h" #include "send_tab.h" #include "../../app.h" #include "../../version.h" #include "../../wallet_state.h" #include "../../ui/widgets/qr_code.h" #include "../sidebar.h" #include "../layout.h" #include "../schema/ui_schema.h" #include "../material/type.h" #include "../material/draw_helpers.h" #include "../material/colors.h" #include "../notifications.h" #include "imgui.h" #include #include #include #include namespace dragonx { namespace ui { using namespace material; // ============================================================================ // State // ============================================================================ static int s_selected_address_idx = -1; static double s_request_amount = 0.0; static char s_request_memo[256] = ""; static std::string s_cached_qr_data; static uintptr_t s_qr_texture = 0; static bool s_payment_request_open = false; // Track newly created addresses for NEW badge static std::map s_new_address_timestamps; static size_t s_prev_address_count = 0; // Address labels (in-memory until persistent config) static std::map s_address_labels; static char s_label_edit_buf[64] = ""; // Address type filter static int s_addr_type_filter = 0; // 0=All, 1=Z, 2=T // ============================================================================ // Helpers // ============================================================================ static std::string TruncateAddress(const std::string& addr, size_t maxLen = 35) { if (addr.length() <= maxLen) return addr; size_t halfLen = (maxLen - 3) / 2; return addr.substr(0, halfLen) + "..." + addr.substr(addr.length() - halfLen); } static void OpenExplorerURL(const std::string& address) { std::string url = "https://explorer.dragonx.com/address/" + address; #ifdef _WIN32 std::string cmd = "start \"\" \"" + url + "\""; #elif __APPLE__ std::string cmd = "open \"" + url + "\""; #else std::string cmd = "xdg-open \"" + url + "\""; #endif system(cmd.c_str()); } // ============================================================================ // Sync banner // ============================================================================ static void RenderSyncBanner(const WalletState& state) { if (!state.sync.syncing || state.sync.isSynced()) return; float syncPct = (state.sync.headers > 0) ? (float)state.sync.blocks / state.sync.headers * 100.0f : 0.0f; char syncBuf[128]; snprintf(syncBuf, sizeof(syncBuf), "Blockchain syncing (%.1f%%)... Balances may be inaccurate.", syncPct); ImGui::PushStyleColor(ImGuiCol_ChildBg, ImVec4(0.6f, 0.4f, 0.0f, 0.15f)); ImGui::BeginChild("##SyncBannerRecv", ImVec2(ImGui::GetContentRegionAvail().x, 28), false, ImGuiWindowFlags_NoScrollbar); ImGui::SetCursorPos(ImVec2(Layout::spacingLg(), 6)); Type().textColored(TypeStyle::Caption, Warning(), syncBuf); ImGui::EndChild(); ImGui::PopStyleColor(); } // ============================================================================ // Track new addresses (detect creations) // ============================================================================ static void TrackNewAddresses(const WalletState& state) { if (state.addresses.size() > s_prev_address_count && s_prev_address_count > 0) { for (const auto& a : state.addresses) { if (s_new_address_timestamps.find(a.address) == s_new_address_timestamps.end()) { s_new_address_timestamps[a.address] = ImGui::GetTime(); } } } else if (s_prev_address_count == 0) { for (const auto& a : state.addresses) { s_new_address_timestamps[a.address] = 0.0; } } s_prev_address_count = state.addresses.size(); } // ============================================================================ // Build sorted address groups // ============================================================================ struct AddressGroups { std::vector shielded; std::vector transparent; }; static AddressGroups BuildSortedAddressGroups(const WalletState& state) { AddressGroups groups; for (int i = 0; i < (int)state.addresses.size(); i++) { if (state.addresses[i].type == "shielded") groups.shielded.push_back(i); else groups.transparent.push_back(i); } std::sort(groups.shielded.begin(), groups.shielded.end(), [&](int a, int b) { return state.addresses[a].balance > state.addresses[b].balance; }); std::sort(groups.transparent.begin(), groups.transparent.end(), [&](int a, int b) { return state.addresses[a].balance > state.addresses[b].balance; }); return groups; } // ============================================================================ // QR Hero — the centerpiece of Layout G // ============================================================================ static void RenderQRHero(App* app, ImDrawList* dl, const AddressInfo& addr, float width, float qrSize, const std::string& qr_data, const GlassPanelSpec& glassSpec, const WalletState& state, ImFont* sub1, ImFont* /*body2*/, ImFont* capFont) { char buf[128]; bool isZ = addr.type == "shielded"; ImU32 typeCol = isZ ? IM_COL32(77, 204, 77, 255) : IM_COL32(204, 170, 51, 255); const char* typeBadge = isZ ? "Shielded" : "Transparent"; float qrPadding = Layout::spacingLg(); float totalQrSize = qrSize + qrPadding * 2; float heroH = totalQrSize + 80.0f; // QR + info below ImVec2 heroMin = ImGui::GetCursorScreenPos(); ImVec2 heroMax(heroMin.x + width, heroMin.y + heroH); GlassPanelSpec heroGlass = glassSpec; heroGlass.fillAlpha = 16; heroGlass.borderAlpha = 35; DrawGlassPanel(dl, heroMin, heroMax, heroGlass); // --- Address info bar above QR --- float infoBarH = 32.0f; float cx = heroMin.x + Layout::spacingLg(); float cy = heroMin.y + Layout::spacingSm(); // Type badge circle + label dl->AddCircleFilled(ImVec2(cx + 8, cy + 10), 8.0f, IM_COL32(255, 255, 255, 20)); const char* typeChar = isZ ? "Z" : "T"; ImVec2 tcSz = sub1->CalcTextSizeA(sub1->LegacySize, 100, 0, typeChar); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx + 8 - tcSz.x * 0.5f, cy + 10 - tcSz.y * 0.5f), typeCol, typeChar); // Education tooltip on badge ImGui::SetCursorScreenPos(ImVec2(cx, cy)); ImGui::InvisibleButton("##TypeBadgeHero", ImVec2(22, 22)); if (ImGui::IsItemHovered()) { if (isZ) { ImGui::SetTooltip( "Shielded Address (Z)\n" "- Full transaction privacy\n" "- Encrypted sender, receiver, amount\n" "- Supports encrypted memos\n" "- Recommended for privacy"); } else { ImGui::SetTooltip( "Transparent Address (T)\n" "- Publicly visible on blockchain\n" "- Similar to Bitcoin addresses\n" "- No memo support\n" "- Use Z addresses for privacy"); } } // Type label text dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 24, cy + 4), typeCol, typeBadge); // Balance right-aligned snprintf(buf, sizeof(buf), "%.8f %s", addr.balance, DRAGONX_TICKER); ImVec2 balSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf); float balX = heroMax.x - balSz.x - Layout::spacingLg(); DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(balX, cy + 2), typeCol, buf); // USD value if (state.market.price_usd > 0 && addr.balance > 0) { double usd = addr.balance * state.market.price_usd; snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.2f", usd); ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(heroMax.x - usdSz.x - Layout::spacingLg(), cy + sub1->LegacySize + 4), OnSurfaceDisabled(), buf); } // --- QR Code centered --- float qrOffset = (width - totalQrSize) * 0.5f; ImVec2 qrPanelMin(heroMin.x + qrOffset, heroMin.y + infoBarH + Layout::spacingSm()); ImVec2 qrPanelMax(qrPanelMin.x + totalQrSize, qrPanelMin.y + totalQrSize); // Subtle inner panel for QR GlassPanelSpec qrGlass; qrGlass.rounding = glassSpec.rounding * 0.75f; qrGlass.fillAlpha = 12; qrGlass.borderAlpha = 25; DrawGlassPanel(dl, qrPanelMin, qrPanelMax, qrGlass); ImGui::SetCursorScreenPos(ImVec2(qrPanelMin.x + qrPadding, qrPanelMin.y + qrPadding)); if (s_qr_texture) { RenderQRCode(s_qr_texture, qrSize); } else { ImGui::Dummy(ImVec2(qrSize, qrSize)); ImVec2 textPos(qrPanelMin.x + totalQrSize * 0.5f - 50, qrPanelMin.y + totalQrSize * 0.5f); dl->AddText(capFont, capFont->LegacySize, textPos, OnSurfaceDisabled(), "QR unavailable"); } // Click QR to copy ImGui::SetCursorScreenPos(qrPanelMin); ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize)); if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("Click to copy %s", s_request_amount > 0 ? "payment URI" : "address"); } if (ImGui::IsItemClicked()) { ImGui::SetClipboardText(qr_data.c_str()); Notifications::instance().info(s_request_amount > 0 ? "Payment URI copied to clipboard" : "Address copied to clipboard"); } // --- Address strip below QR --- float addrStripY = qrPanelMax.y + Layout::spacingMd(); float addrStripX = heroMin.x + Layout::spacingLg(); float addrStripW = width - Layout::spacingXxl(); // Full address (word-wrapped) ImVec2 fullAddrPos(addrStripX, addrStripY); float wrapWidth = addrStripW; ImVec2 addrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, wrapWidth, addr.address.c_str()); dl->AddText(capFont, capFont->LegacySize, fullAddrPos, OnSurface(), addr.address.c_str(), nullptr, wrapWidth); // Address click-to-copy overlay ImGui::SetCursorScreenPos(fullAddrPos); ImGui::InvisibleButton("##addrCopyHero", ImVec2(wrapWidth, addrSz.y)); if (ImGui::IsItemHovered()) { ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("Click to copy address"); } if (ImGui::IsItemClicked()) { ImGui::SetClipboardText(addr.address.c_str()); Notifications::instance().info("Address copied to clipboard"); } // Action buttons row float btnRowY = addrStripY + addrSz.y + Layout::spacingMd(); ImGui::SetCursorScreenPos(ImVec2(addrStripX, btnRowY)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm()); { // Copy — primary (uses global glass style) if (TactileSmallButton("Copy Address##hero", schema::UI().resolveFont("button"))) { ImGui::SetClipboardText(addr.address.c_str()); Notifications::instance().info("Address copied to clipboard"); } ImGui::SameLine(); // Explorer ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15))); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight())); if (TactileSmallButton("Explorer##hero", schema::UI().resolveFont("button"))) { OpenExplorerURL(addr.address); } ImGui::PopStyleColor(3); // Send From if (addr.balance > 0) { ImGui::SameLine(); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, 15))); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium())); if (TactileSmallButton("Send \xe2\x86\x97##hero", schema::UI().resolveFont("button"))) { SetSendFromAddress(addr.address); app->setCurrentPage(NavPage::Send); } ImGui::PopStyleColor(3); } // Label editor (inline) ImGui::SameLine(0, Layout::spacingXl()); auto lblIt = s_address_labels.find(addr.address); std::string currentLabel = (lblIt != s_address_labels.end()) ? lblIt->second : ""; snprintf(s_label_edit_buf, sizeof(s_label_edit_buf), "%s", currentLabel.c_str()); ImGui::SetNextItemWidth(std::min(200.0f, addrStripW * 0.3f)); if (ImGui::InputTextWithHint("##LabelHero", "Add label...", s_label_edit_buf, sizeof(s_label_edit_buf))) { s_address_labels[addr.address] = std::string(s_label_edit_buf); } } ImGui::PopStyleVar(); // Update hero height based on actual content float actualBottom = btnRowY + 24; heroH = actualBottom - heroMin.y + Layout::spacingMd(); heroMax.y = heroMin.y + heroH; ImGui::SetCursorScreenPos(ImVec2(heroMin.x, heroMax.y)); ImGui::Dummy(ImVec2(width, 0)); } // ============================================================================ // Payment request section (below QR hero) // ============================================================================ static void RenderPaymentRequest(ImDrawList* dl, const AddressInfo& addr, float innerW, const GlassPanelSpec& glassSpec, const char* suffix) { auto& S = schema::UI(); const float kLabelPos = S.label("tabs.receive", "label-column").position; bool hasMemo = (addr.type == "shielded"); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PAYMENT REQUEST"); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // Compute card height float prCardH = 16.0f + 24.0f + 8.0f + 12.0f; if (hasMemo) prCardH += 24.0f; if (s_request_amount > 0 && !s_cached_qr_data.empty()) { ImFont* capF = Type().caption(); ImVec2 uriSz = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, innerW - 24, s_cached_qr_data.c_str()); prCardH += uriSz.y + 8.0f; } if (s_request_amount > 0) prCardH += 32.0f; if (s_request_amount > 0 || s_request_memo[0]) prCardH += 4.0f; ImVec2 prMin = ImGui::GetCursorScreenPos(); ImVec2 prMax(prMin.x + innerW, prMin.y + prCardH); DrawGlassPanel(dl, prMin, prMax, glassSpec); ImGui::SetCursorScreenPos(ImVec2(prMin.x + Layout::spacingLg(), prMin.y + Layout::spacingMd())); ImGui::Dummy(ImVec2(0, 0)); ImGui::Text("Amount:"); ImGui::SameLine(kLabelPos); ImGui::SetNextItemWidth(std::max(S.input("tabs.receive", "amount-input").width, innerW * 0.4f)); char amtId[32]; snprintf(amtId, sizeof(amtId), "##RequestAmount%s", suffix); ImGui::InputDouble(amtId, &s_request_amount, 0.01, 1.0, "%.8f"); ImGui::SameLine(); ImGui::Text("%s", DRAGONX_TICKER); if (hasMemo) { ImGui::Text("Memo:"); ImGui::SameLine(kLabelPos); ImGui::SetNextItemWidth(innerW - kLabelPos - Layout::spacingXxl()); char memoId[32]; snprintf(memoId, sizeof(memoId), "##RequestMemo%s", suffix); ImGui::InputText(memoId, s_request_memo, sizeof(s_request_memo)); } // Live URI preview if (s_request_amount > 0 && !s_cached_qr_data.empty()) { ImGui::Spacing(); ImFont* capF = Type().caption(); ImVec2 uriPos = ImGui::GetCursorScreenPos(); float uriWrapW = innerW - Layout::spacingXxl(); ImVec2 uriSz = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, uriWrapW, s_cached_qr_data.c_str()); dl->AddText(capF, capF->LegacySize, uriPos, OnSurfaceDisabled(), s_cached_qr_data.c_str(), nullptr, uriWrapW); ImGui::Dummy(ImVec2(uriWrapW, uriSz.y + Layout::spacingSm())); } ImGui::Spacing(); if (s_request_amount > 0) { ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm()); char copyUriId[64]; snprintf(copyUriId, sizeof(copyUriId), "Copy Payment URI%s", suffix); if (TactileButton(copyUriId, ImVec2(innerW - Layout::spacingXxl(), 0), S.resolveFont("button"))) { ImGui::SetClipboardText(s_cached_qr_data.c_str()); Notifications::instance().info("Payment URI copied to clipboard"); } ImGui::PopStyleVar(); // Share as text char shareId[32]; snprintf(shareId, sizeof(shareId), "Share as Text%s", suffix); if (TactileSmallButton(shareId, S.resolveFont("button"))) { char shareBuf[1024]; snprintf(shareBuf, sizeof(shareBuf), "Payment Request\nAmount: %.8f %s\nAddress: %s\nURI: %s", s_request_amount, DRAGONX_TICKER, addr.address.c_str(), s_cached_qr_data.c_str()); ImGui::SetClipboardText(shareBuf); Notifications::instance().info("Payment request copied to clipboard"); } } if (s_request_amount > 0 || s_request_memo[0]) { ImGui::SameLine(); char clearId[32]; snprintf(clearId, sizeof(clearId), "Clear%s", suffix); if (TactileSmallButton(clearId, S.resolveFont("button"))) { s_request_amount = 0.0; s_request_memo[0] = '\0'; } } ImGui::SetCursorScreenPos(ImVec2(prMin.x, prMax.y)); ImGui::Dummy(ImVec2(innerW, 0)); } // ============================================================================ // Recent received transactions for selected address // ============================================================================ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& addr, const WalletState& state, float width, ImFont* capFont) { char buf[128]; int recvCount = 0; for (const auto& tx : state.transactions) { if (tx.address == addr.address && tx.type == "receive") recvCount++; } if (recvCount == 0) return; ImGui::Dummy(ImVec2(0, Layout::spacingMd())); snprintf(buf, sizeof(buf), "RECENT RECEIVED (%d)", std::min(recvCount, 3)); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), buf); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); int shown = 0; for (const auto& tx : state.transactions) { if (tx.address != addr.address || tx.type != "receive") continue; if (shown >= 3) break; ImVec2 rMin = ImGui::GetCursorScreenPos(); float rH = 22.0f; ImVec2 rMax(rMin.x + width, rMin.y + rH); GlassPanelSpec rsGlass; rsGlass.rounding = Layout::glassRounding() * 0.5f; rsGlass.fillAlpha = 8; DrawGlassPanel(dl, rMin, rMax, rsGlass); float rx = rMin.x + Layout::spacingMd(); float ry = rMin.y + (rH - capFont->LegacySize) * 0.5f; // Arrow indicator dl->AddText(capFont, capFont->LegacySize, ImVec2(rx, ry), Success(), "\xe2\x86\x90"); snprintf(buf, sizeof(buf), "+%.8f %s %s %s", tx.amount, DRAGONX_TICKER, tx.getTimeString().c_str(), tx.confirmations < 1 ? "(unconfirmed)" : ""); dl->AddText(capFont, capFont->LegacySize, ImVec2(rx + 16, ry), tx.confirmations >= 1 ? Success() : Warning(), buf); ImGui::Dummy(ImVec2(width, rH)); ImGui::Dummy(ImVec2(0, 2)); shown++; } } // ============================================================================ // Horizontal Address Strip — bottom switching bar (Layout G signature) // ============================================================================ static void RenderAddressStrip(App* app, ImDrawList* dl, const WalletState& state, float width, float hs, ImFont* /*sub1*/, ImFont* capFont) { char buf[128]; // Header row with filter and + New button Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "YOUR ADDRESSES"); float btnW = std::max(70.0f, 85.0f * hs); float comboW = std::max(48.0f, 58.0f * hs); ImGui::SameLine(width - btnW - comboW - Layout::spacingMd()); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, Layout::spacingSm()); const char* types[] = { "All", "Z", "T" }; ImGui::SetNextItemWidth(comboW); ImGui::Combo("##AddrTypeStrip", &s_addr_type_filter, types, 3); ImGui::SameLine(); ImGui::BeginDisabled(!app->isConnected()); if (TactileButton("+ New##strip", ImVec2(btnW, 0), schema::UI().resolveFont("button"))) { if (s_addr_type_filter != 2) { app->createNewZAddress([](const std::string& addr) { if (addr.empty()) Notifications::instance().error("Failed to create new shielded address"); else Notifications::instance().success("New shielded address created"); }); } else { app->createNewTAddress([](const std::string& addr) { if (addr.empty()) Notifications::instance().error("Failed to create new transparent address"); else Notifications::instance().success("New transparent address created"); }); } } ImGui::EndDisabled(); ImGui::PopStyleVar(); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); if (!app->isConnected()) { Type().textColored(TypeStyle::Caption, Warning(), "Waiting for connection..."); return; } if (state.addresses.empty()) { // Loading skeleton ImVec2 skelPos = ImGui::GetCursorScreenPos(); float alpha = (float)(0.3 + 0.15 * std::sin(ImGui::GetTime() * 2.0)); ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255)); for (int sk = 0; sk < 3; sk++) { dl->AddRectFilled( ImVec2(skelPos.x + sk * (130 + 8), skelPos.y), ImVec2(skelPos.x + sk * (130 + 8) + 120, skelPos.y + 56), skelCol, 6.0f); } ImGui::Dummy(ImVec2(width, 60)); return; } TrackNewAddresses(state); AddressGroups groups = BuildSortedAddressGroups(state); // Build filtered list std::vector filteredIdxs; if (s_addr_type_filter != 2) for (int idx : groups.shielded) filteredIdxs.push_back(idx); if (s_addr_type_filter != 1) for (int idx : groups.transparent) filteredIdxs.push_back(idx); // Horizontal scrolling strip float cardW = std::max(140.0f, std::min(200.0f, width * 0.22f)); float cardH = std::max(52.0f, 64.0f * hs); float stripH = cardH + 8; ImGui::BeginChild("##AddrStrip", ImVec2(width, stripH), false, ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoBackground); ImDrawList* sdl = ImGui::GetWindowDrawList(); for (size_t fi = 0; fi < filteredIdxs.size(); fi++) { int i = filteredIdxs[fi]; const auto& addr = state.addresses[i]; bool isCurrent = (i == s_selected_address_idx); bool isZ = addr.type == "shielded"; ImU32 typeCol = isZ ? IM_COL32(77, 204, 77, 255) : IM_COL32(204, 170, 51, 255); bool hasBalance = addr.balance > 0; ImVec2 cardMin = ImGui::GetCursorScreenPos(); ImVec2 cardMax(cardMin.x + cardW, cardMin.y + cardH); // Card background GlassPanelSpec cardGlass; cardGlass.rounding = Layout::glassRounding() * 0.75f; cardGlass.fillAlpha = isCurrent ? 28 : 14; cardGlass.borderAlpha = isCurrent ? 50 : 25; DrawGlassPanel(sdl, cardMin, cardMax, cardGlass); // Selected indicator — top accent bar if (isCurrent) { sdl->AddRectFilled(cardMin, ImVec2(cardMax.x, cardMin.y + 3), Primary(), cardGlass.rounding); } float ix = cardMin.x + Layout::spacingMd(); float iy = cardMin.y + Layout::spacingSm() + (isCurrent ? 4 : 0); // Type dot sdl->AddCircleFilled(ImVec2(ix + 4, iy + 6), 3.5f, typeCol); // Address label or truncated address auto lblIt = s_address_labels.find(addr.address); bool hasLabel = (lblIt != s_address_labels.end() && !lblIt->second.empty()); size_t addrTruncLen = static_cast(std::max(8.0f, (cardW - 30) / 9.0f)); if (hasLabel) { sdl->AddText(capFont, capFont->LegacySize, ImVec2(ix + 14, iy), isCurrent ? PrimaryLight() : OnSurfaceMedium(), lblIt->second.c_str()); std::string shortAddr = TruncateAddress(addr.address, std::max((size_t)6, addrTruncLen / 2)); sdl->AddText(capFont, capFont->LegacySize, ImVec2(ix + 14, iy + capFont->LegacySize + 2), OnSurfaceDisabled(), shortAddr.c_str()); } else { std::string dispAddr = TruncateAddress(addr.address, addrTruncLen); sdl->AddText(capFont, capFont->LegacySize, ImVec2(ix + 14, iy), isCurrent ? OnSurface() : OnSurfaceDisabled(), dispAddr.c_str()); } // Balance snprintf(buf, sizeof(buf), "%.4f %s", addr.balance, DRAGONX_TICKER); ImVec2 balSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); float balY = cardMax.y - balSz.y - Layout::spacingSm(); sdl->AddText(capFont, capFont->LegacySize, ImVec2(ix + 14, balY), hasBalance ? typeCol : OnSurfaceDisabled(), buf); // NEW badge double now = ImGui::GetTime(); auto newIt = s_new_address_timestamps.find(addr.address); if (newIt != s_new_address_timestamps.end() && newIt->second > 0.0) { double age = now - newIt->second; if (age < 10.0) { float alpha = (float)std::max(0.0, 1.0 - age / 10.0); int a = (int)(alpha * 220); ImVec2 badgePos(cardMax.x - 32, cardMin.y + 4); sdl->AddRectFilled(badgePos, ImVec2(badgePos.x + 28, badgePos.y + 14), IM_COL32(77, 204, 255, a / 4), 3.0f); sdl->AddText(capFont, capFont->LegacySize, ImVec2(badgePos.x + 4, badgePos.y + 1), IM_COL32(77, 204, 255, a), "NEW"); } } // Click interaction ImGui::SetCursorScreenPos(cardMin); ImGui::PushID(i); ImGui::InvisibleButton("##addrCard", ImVec2(cardW, cardH)); if (ImGui::IsItemHovered()) { if (!isCurrent) sdl->AddRectFilled(cardMin, cardMax, IM_COL32(255, 255, 255, 10), cardGlass.rounding); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); ImGui::SetTooltip("%s\nBalance: %.8f %s%s\nDouble-click to copy | Right-click for options", addr.address.c_str(), addr.balance, DRAGONX_TICKER, isCurrent ? " (selected)" : ""); } if (ImGui::IsItemClicked(ImGuiMouseButton_Left)) { s_selected_address_idx = i; s_cached_qr_data.clear(); } if (ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { ImGui::SetClipboardText(addr.address.c_str()); Notifications::instance().info("Address copied to clipboard"); } // Context menu if (ImGui::BeginPopupContextItem("##addrStripCtx")) { if (ImGui::MenuItem("Copy Address")) { ImGui::SetClipboardText(addr.address.c_str()); Notifications::instance().info("Address copied to clipboard"); } if (ImGui::MenuItem("View on Explorer")) { OpenExplorerURL(addr.address); } if (addr.balance > 0) { if (ImGui::MenuItem("Send From This Address")) { SetSendFromAddress(addr.address); app->setCurrentPage(NavPage::Send); } } ImGui::EndPopup(); } ImGui::PopID(); ImGui::SameLine(0, Layout::spacingSm()); } // Total balance at end of strip { double totalBal = 0; for (const auto& a : state.addresses) totalBal += a.balance; ImVec2 totPos = ImGui::GetCursorScreenPos(); float totCardW = std::max(100.0f, cardW * 0.6f); ImVec2 totMax(totPos.x + totCardW, totPos.y + cardH); GlassPanelSpec totGlass; totGlass.rounding = Layout::glassRounding() * 0.75f; totGlass.fillAlpha = 8; totGlass.borderAlpha = 15; DrawGlassPanel(sdl, totPos, totMax, totGlass); sdl->AddText(capFont, capFont->LegacySize, ImVec2(totPos.x + Layout::spacingMd(), totPos.y + Layout::spacingSm()), OnSurfaceMedium(), "TOTAL"); snprintf(buf, sizeof(buf), "%.8f", totalBal); ImVec2 totSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); sdl->AddText(capFont, capFont->LegacySize, ImVec2(totPos.x + Layout::spacingMd(), totMax.y - totSz.y - Layout::spacingSm()), OnSurface(), buf); snprintf(buf, sizeof(buf), "%s", DRAGONX_TICKER); sdl->AddText(capFont, capFont->LegacySize, ImVec2(totPos.x + Layout::spacingMd(), totMax.y - totSz.y - Layout::spacingSm() - capFont->LegacySize - 2), OnSurfaceDisabled(), buf); ImGui::Dummy(ImVec2(totCardW, cardH)); } // Keyboard navigation if (ImGui::IsWindowFocused(ImGuiFocusedFlags_RootAndChildWindows)) { if (ImGui::IsKeyPressed(ImGuiKey_RightArrow)) { int next = s_selected_address_idx + 1; if (next < (int)state.addresses.size()) { s_selected_address_idx = next; s_cached_qr_data.clear(); } } if (ImGui::IsKeyPressed(ImGuiKey_LeftArrow)) { int prev = s_selected_address_idx - 1; if (prev >= 0) { s_selected_address_idx = prev; s_cached_qr_data.clear(); } } if (ImGui::IsKeyPressed(ImGuiKey_Enter) || ImGui::IsKeyPressed(ImGuiKey_KeypadEnter)) { if (s_selected_address_idx >= 0 && s_selected_address_idx < (int)state.addresses.size()) { ImGui::SetClipboardText(state.addresses[s_selected_address_idx].address.c_str()); Notifications::instance().info("Address copied to clipboard"); } } } ImGui::EndChild(); // ##AddrStrip } // ============================================================================ // MAIN: RenderReceiveTab — Layout G: QR-Centered Hero // ============================================================================ void RenderReceiveTab(App* app) { const auto& state = app->getWalletState(); RenderSyncBanner(state); ImVec2 recvAvail = ImGui::GetContentRegionAvail(); ImGui::BeginChild("##ReceiveScroll", recvAvail, false, ImGuiWindowFlags_NoBackground); float hs = Layout::hScale(recvAvail.x); float vScale = Layout::vScale(recvAvail.y); float glassRound = Layout::glassRounding(); float availWidth = ImGui::GetContentRegionAvail().x; float contentWidth = std::min(availWidth * 0.92f, 1200.0f * hs); float offsetX = (availWidth - contentWidth) * 0.5f; if (offsetX > 0) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + offsetX); float sectionGap = Layout::spacingXl() * vScale; ImGui::BeginGroup(); ImDrawList* dl = ImGui::GetWindowDrawList(); GlassPanelSpec glassSpec; glassSpec.rounding = glassRound; ImFont* capFont = Type().caption(); ImFont* sub1 = Type().subtitle1(); ImFont* body2 = Type().body2(); // Auto-select first address if (!state.addresses.empty() && (s_selected_address_idx < 0 || s_selected_address_idx >= (int)state.addresses.size())) { s_selected_address_idx = 0; } const AddressInfo* selected = nullptr; if (s_selected_address_idx >= 0 && s_selected_address_idx < (int)state.addresses.size()) { selected = &state.addresses[s_selected_address_idx]; } // Generate QR data std::string qr_data; if (selected) { qr_data = selected->address; if (s_request_amount > 0) { qr_data = std::string("dragonx:") + selected->address + "?amount=" + std::to_string(s_request_amount); if (s_request_memo[0] && selected->type == "shielded") { qr_data += "&memo=" + std::string(s_request_memo); } } if (qr_data != s_cached_qr_data) { if (s_qr_texture) { FreeQRTexture(s_qr_texture); s_qr_texture = 0; } int w, h; s_qr_texture = GenerateQRTexture(qr_data.c_str(), &w, &h); s_cached_qr_data = qr_data; } } // ================================================================ // Not connected / empty state // ================================================================ if (!app->isConnected()) { ImVec2 emptyMin = ImGui::GetCursorScreenPos(); float emptyH = 120.0f; ImVec2 emptyMax(emptyMin.x + contentWidth, emptyMin.y + emptyH); DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec); dl->AddText(sub1, sub1->LegacySize, ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl()), OnSurfaceDisabled(), "Waiting for daemon connection..."); dl->AddText(capFont, capFont->LegacySize, ImVec2(emptyMin.x + Layout::spacingXl(), emptyMin.y + Layout::spacingXl() + sub1->LegacySize + 8), OnSurfaceDisabled(), "Your receiving addresses will appear here once connected."); ImGui::Dummy(ImVec2(contentWidth, emptyH)); ImGui::EndGroup(); ImGui::EndChild(); return; } if (state.addresses.empty()) { ImVec2 emptyMin = ImGui::GetCursorScreenPos(); float emptyH = 100.0f; ImVec2 emptyMax(emptyMin.x + contentWidth, emptyMin.y + emptyH); DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec); float alpha = (float)(0.3 + 0.15 * std::sin(ImGui::GetTime() * 2.0)); ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255)); dl->AddRectFilled( ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg()), ImVec2(emptyMin.x + contentWidth * 0.6f, emptyMin.y + Layout::spacingLg() + 16), skelCol, 4.0f); dl->AddRectFilled( ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + 24), ImVec2(emptyMin.x + contentWidth * 0.4f, emptyMin.y + Layout::spacingLg() + 36), skelCol, 4.0f); dl->AddText(capFont, capFont->LegacySize, ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - 24), OnSurfaceDisabled(), "Loading addresses..."); ImGui::Dummy(ImVec2(contentWidth, emptyH)); ImGui::EndGroup(); ImGui::EndChild(); return; } // ================================================================ // QR HERO — dominates center (Layout G signature) // ================================================================ if (selected) { // Calculate QR size based on available space float maxQrForWidth = std::min(contentWidth * 0.6f, 400.0f); float maxQrForHeight = std::min(recvAvail.y * 0.45f, 400.0f); float qrSize = std::max(140.0f, std::min(maxQrForWidth, maxQrForHeight)); // Center the hero horizontally float heroW = std::min(contentWidth, 700.0f * hs); float heroOffsetX = (contentWidth - heroW) * 0.5f; if (heroOffsetX > 4) { ImGui::SetCursorPosX(ImGui::GetCursorPosX() + heroOffsetX); } RenderQRHero(app, dl, *selected, heroW, qrSize, qr_data, glassSpec, state, sub1, body2, capFont); ImGui::Dummy(ImVec2(0, sectionGap)); // ---- PAYMENT REQUEST (collapsible on narrow) ---- constexpr float kTwoColumnThreshold = 800.0f; bool isNarrow = contentWidth < kTwoColumnThreshold; if (isNarrow) { ImGui::PushStyleColor(ImGuiCol_Header, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_HeaderHovered, ImVec4(1, 1, 1, 0.05f)); ImGui::PushStyleColor(ImGuiCol_HeaderActive, ImVec4(1, 1, 1, 0.08f)); ImGui::PushFont(Type().overline()); s_payment_request_open = ImGui::CollapsingHeader( "PAYMENT REQUEST (OPTIONAL)", s_payment_request_open ? ImGuiTreeNodeFlags_DefaultOpen : 0); ImGui::PopFont(); ImGui::PopStyleColor(3); if (s_payment_request_open) { float prW = std::min(contentWidth, 600.0f * hs); float prOffX = (contentWidth - prW) * 0.5f; if (prOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + prOffX); RenderPaymentRequest(dl, *selected, prW, glassSpec, "##hero"); } } else { float prW = std::min(contentWidth, 600.0f * hs); float prOffX = (contentWidth - prW) * 0.5f; if (prOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + prOffX); RenderPaymentRequest(dl, *selected, prW, glassSpec, "##hero"); } ImGui::Dummy(ImVec2(0, sectionGap)); // ---- RECENT RECEIVED ---- { float rcvW = std::min(contentWidth, 600.0f * hs); float rcvOffX = (contentWidth - rcvW) * 0.5f; if (rcvOffX > 4) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + rcvOffX); RenderRecentReceived(dl, *selected, state, rcvW, capFont); } ImGui::Dummy(ImVec2(0, sectionGap)); } // ================================================================ // ADDRESS STRIP — horizontal switching bar at bottom // ================================================================ RenderAddressStrip(app, dl, state, contentWidth, hs, sub1, capFont); ImGui::EndGroup(); ImGui::EndChild(); // ##ReceiveScroll } } // namespace ui } // namespace dragonx