// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 // // Receive Tab — redesigned to match Send tab layout // - Address dropdown at top (like Send's source selector) // - Single glass card containing QR, address, payment request // - Action buttons below the card // - Recent received at bottom #include "receive_tab.h" #include "send_tab.h" #include "../../app.h" #include "../../config/version.h" #include "../../data/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 #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 double s_request_usd_amount = 0.0; static bool s_request_usd_mode = false; static std::string s_cached_qr_data; static uintptr_t s_qr_texture = 0; static bool s_auto_selected = false; // Address type filter static int s_addr_type_filter = 0; // 0=All, 1=Z, 2=T // Source dropdown preview string static std::string s_source_preview; // 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 std::string s_pending_select_address; // ============================================================================ // Helpers // ============================================================================ static std::string TruncateAddress(const std::string& addr, size_t maxLen = 40) { 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()); } // ============================================================================ // 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(); } // ============================================================================ // 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, ImGui::ColorConvertU32ToFloat4(schema::UI().resolveColor(schema::UI().drawElement("tabs.receive", "sync-banner-bg-color").color))); float syncH = std::max(schema::UI().drawElement("tabs.receive", "sync-banner-min-height").size, schema::UI().drawElement("tabs.receive", "sync-banner-height").size * Layout::vScale()); ImGui::BeginChild("##SyncBannerRecv", ImVec2(ImGui::GetContentRegionAvail().x, syncH), false, ImGuiWindowFlags_NoScrollbar); ImGui::SetCursorPos(ImVec2(Layout::spacingLg(), (syncH - Type().caption()->LegacySize) * 0.5f)); Type().textColored(TypeStyle::Caption, Warning(), syncBuf); ImGui::EndChild(); ImGui::PopStyleColor(); } // ============================================================================ // Address Dropdown — matches Send tab's source selector style // ============================================================================ static void RenderAddressDropdown(App* app, float width) { const auto& state = app->getWalletState(); char buf[256]; // Header row: label + address type toggle buttons Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "ADDRESS"); float toggleBtnW = std::max(schema::UI().drawElement("tabs.receive", "toggle-btn-min-width").size, schema::UI().drawElement("tabs.receive", "toggle-btn-width").size * Layout::hScale(width)); float toggleGap = schema::UI().drawElement("tabs.receive", "toggle-gap").size; float toggleTotalW = toggleBtnW * 3 + toggleGap * 2; ImGui::SameLine(width - toggleTotalW); const char* filterLabels[] = { "All", "Z", "T" }; for (int i = 0; i < 3; i++) { bool isActive = (s_addr_type_filter == i); if (isActive) { ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(PrimaryVariant())); ImGui::PushStyleColor(ImGuiCol_Text, ImVec4(1, 1, 1, 1)); } else { ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(1, 1, 1, 0.06f)); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium())); } ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, schema::UI().drawElement("tabs.receive", "toggle-rounding").size); char toggleId[32]; snprintf(toggleId, sizeof(toggleId), "%s##addrFilter", filterLabels[i]); if (ImGui::Button(toggleId, ImVec2(toggleBtnW, 0))) { s_addr_type_filter = i; } ImGui::PopStyleVar(); ImGui::PopStyleColor(2); if (i < 2) ImGui::SameLine(0, toggleGap); } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); TrackNewAddresses(state); // Auto-select address with the largest balance on first load if (!s_auto_selected && app->isConnected() && !state.addresses.empty()) { int bestIdx = -1; double bestBal = -1.0; for (size_t i = 0; i < state.addresses.size(); i++) { if (state.addresses[i].balance > bestBal) { bestBal = state.addresses[i].balance; bestIdx = static_cast(i); } } if (bestIdx >= 0) { s_selected_address_idx = bestIdx; } s_auto_selected = true; } // Auto-select pending new address if (!s_pending_select_address.empty()) { for (size_t i = 0; i < state.addresses.size(); i++) { if (state.addresses[i].address == s_pending_select_address) { s_selected_address_idx = static_cast(i); s_cached_qr_data.clear(); s_pending_select_address.clear(); break; } } } // Build preview string if (!app->isConnected()) { s_source_preview = "Not connected to daemon"; } else if (s_selected_address_idx >= 0 && s_selected_address_idx < (int)state.addresses.size()) { const auto& addr = state.addresses[s_selected_address_idx]; bool isZ = addr.type == "shielded"; const char* tag = isZ ? "[Z]" : "[T]"; std::string trunc = TruncateAddress(addr.address, static_cast(std::max(schema::UI().drawElement("tabs.receive", "addr-preview-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-preview-trunc-divisor").size))); snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s", tag, trunc.c_str(), addr.balance, DRAGONX_TICKER); s_source_preview = buf; } else { s_source_preview = "Select a receiving address..."; } float copyBtnW = std::max(schema::UI().drawElement("tabs.receive", "copy-btn-min-width").size, schema::UI().drawElement("tabs.receive", "copy-btn-width").size * Layout::hScale(width)); float newBtnW = std::max(schema::UI().drawElement("tabs.receive", "new-btn-min-width").size, schema::UI().drawElement("tabs.receive", "new-btn-width").size * Layout::hScale(width)); float dropdownW = width - copyBtnW - newBtnW - Layout::spacingSm() * 2; ImGui::SetNextItemWidth(dropdownW); ImGui::PushFont(Type().getFont(TypeStyle::Body2)); if (ImGui::BeginCombo("##RecvAddr", s_source_preview.c_str())) { if (!app->isConnected() || state.addresses.empty()) { ImGui::TextDisabled("No addresses available"); } else { // Build filtered and sorted list std::vector sortedIdx; sortedIdx.reserve(state.addresses.size()); for (size_t i = 0; i < state.addresses.size(); i++) { bool isZ = state.addresses[i].type == "shielded"; if (s_addr_type_filter == 1 && !isZ) continue; if (s_addr_type_filter == 2 && isZ) continue; sortedIdx.push_back(i); } std::sort(sortedIdx.begin(), sortedIdx.end(), [&](size_t a, size_t b) { return state.addresses[a].balance > state.addresses[b].balance; }); if (sortedIdx.empty()) { ImGui::TextDisabled("No addresses match filter"); } else { size_t addrTruncLen = static_cast(std::max(schema::UI().drawElement("tabs.receive", "addr-dropdown-trunc-min").size, width / schema::UI().drawElement("tabs.receive", "addr-dropdown-trunc-divisor").size)); double now = ImGui::GetTime(); for (size_t si = 0; si < sortedIdx.size(); si++) { size_t i = sortedIdx[si]; const auto& addr = state.addresses[i]; bool isCurrent = (s_selected_address_idx == static_cast(i)); bool isZ = addr.type == "shielded"; const char* tag = isZ ? "[Z]" : "[T]"; // Check for NEW badge bool isNew = false; 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 < schema::UI().drawElement("tabs.receive", "new-badge-timeout").size) isNew = true; } // Check for label auto lblIt = s_address_labels.find(addr.address); bool hasLabel = (lblIt != s_address_labels.end() && !lblIt->second.empty()); std::string trunc = TruncateAddress(addr.address, addrTruncLen); if (hasLabel) { snprintf(buf, sizeof(buf), "%s %s (%s) \xe2\x80\x94 %.8f %s%s", tag, lblIt->second.c_str(), trunc.c_str(), addr.balance, DRAGONX_TICKER, isNew ? " [NEW]" : ""); } else { snprintf(buf, sizeof(buf), "%s %s \xe2\x80\x94 %.8f %s%s", tag, trunc.c_str(), addr.balance, DRAGONX_TICKER, isNew ? " [NEW]" : ""); } ImGui::PushID(static_cast(i)); if (ImGui::Selectable(buf, isCurrent)) { s_selected_address_idx = static_cast(i); s_cached_qr_data.clear(); // Force QR regeneration } if (ImGui::IsItemHovered()) { ImGui::SetTooltip("%s\nBalance: %.8f %s%s", addr.address.c_str(), addr.balance, DRAGONX_TICKER, isCurrent ? "\n(selected)" : ""); } ImGui::PopID(); } } } ImGui::EndCombo(); } ImGui::PopFont(); // Copy address button ImGui::SameLine(0, Layout::spacingSm()); ImGui::BeginDisabled(!app->isConnected() || s_selected_address_idx < 0 || s_selected_address_idx >= (int)state.addresses.size()); if (TactileButton("Copy##recvAddr", ImVec2(copyBtnW, 0), schema::UI().resolveFont("button"))) { 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::EndDisabled(); // New address button on same line ImGui::SameLine(0, Layout::spacingSm()); ImGui::BeginDisabled(!app->isConnected()); if (TactileButton("+ New##recv", ImVec2(newBtnW, 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 { s_pending_select_address = addr; 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 { s_pending_select_address = addr; Notifications::instance().success("New transparent address created"); } }); } } ImGui::EndDisabled(); } // ============================================================================ // Helpers: timeAgo / DrawRecvIcon (local copies — originals are static in send_tab) // ============================================================================ static std::string recvTimeAgo(int64_t timestamp) { if (timestamp <= 0) return ""; int64_t now = (int64_t)std::time(nullptr); int64_t diff = now - timestamp; if (diff < 0) diff = 0; if (diff < 60) return std::to_string(diff) + "s ago"; if (diff < 3600) return std::to_string(diff / 60) + "m ago"; if (diff < 86400) return std::to_string(diff / 3600) + "h ago"; return std::to_string(diff / 86400) + "d ago"; } static void DrawRecvIcon(ImDrawList* dl, float cx, float cy, float s, ImU32 col) { dl->AddTriangleFilled( ImVec2(cx, cy + s), ImVec2(cx - s * 0.65f, cy - s * 0.3f), ImVec2(cx + s * 0.65f, cy - s * 0.3f), col); dl->AddRectFilled( ImVec2(cx - s * 0.2f, cy - s * 0.8f), ImVec2(cx + s * 0.2f, cy - s * 0.3f), col); } // ============================================================================ // Recent received transactions — styled to match transactions list // ============================================================================ static void RenderRecentReceived(ImDrawList* dl, const AddressInfo& /* addr */, const WalletState& state, float width, ImFont* capFont, App* app) { ImGui::Dummy(ImVec2(0, Layout::spacingLg())); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT RECEIVED"); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); float hs = Layout::hScale(width); float glassRound = Layout::glassRounding(); ImFont* body2 = Type().body2(); float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd(); float iconSz = std::max(schema::UI().drawElement("tabs.receive", "recent-icon-min-size").size, schema::UI().drawElement("tabs.receive", "recent-icon-size").size * hs); ImU32 recvCol = Success(); ImU32 greenCol = WithAlpha(Success(), (int)schema::UI().drawElement("tabs.receive", "recent-green-alpha").size); float rowPadLeft = Layout::spacingLg(); // Collect matching transactions std::vector recvs; for (const auto& tx : state.transactions) { if (tx.type != "receive") continue; recvs.push_back(&tx); if (recvs.size() >= (size_t)schema::UI().drawElement("tabs.receive", "max-recent-receives").size) break; } if (recvs.empty()) { Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No recent receives"); return; } // Outer glass panel wrapping all rows float itemSpacingY = ImGui::GetStyle().ItemSpacing.y; float listH = rowH * (float)recvs.size() + itemSpacingY * (float)(recvs.size() - 1); ImVec2 listPanelMin = ImGui::GetCursorScreenPos(); ImVec2 listPanelMax(listPanelMin.x + width, listPanelMin.y + listH); GlassPanelSpec glassSpec; glassSpec.rounding = glassRound; DrawGlassPanel(dl, listPanelMin, listPanelMax, glassSpec); // Clip draw commands to panel bounds to prevent overflow dl->PushClipRect(listPanelMin, listPanelMax, true); char buf[64]; for (size_t ri = 0; ri < recvs.size(); ri++) { const auto& tx = *recvs[ri]; ImVec2 rowPos = ImGui::GetCursorScreenPos(); ImVec2 rowEnd(rowPos.x + width, rowPos.y + rowH); // Hover glow bool hovered = material::IsRectHovered(rowPos, rowEnd); if (hovered) { dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.receive", "row-hover-alpha").size), schema::UI().drawElement("tabs.receive", "row-hover-rounding").size); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (ImGui::IsMouseClicked(0)) { app->setCurrentPage(ui::NavPage::History); } } float cx = rowPos.x + rowPadLeft; float cy = rowPos.y + Layout::spacingMd(); // Icon DrawRecvIcon(dl, cx + iconSz, cy + body2->LegacySize * 0.5f, iconSz, recvCol); // Type label (first line) float labelX = cx + iconSz * 2.0f + Layout::spacingSm(); dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), recvCol, "Received"); // Time (next to type) std::string ago = recvTimeAgo(tx.timestamp); float typeW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, "Received").x; dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeW + Layout::spacingLg(), cy), OnSurfaceDisabled(), ago.c_str()); // Address (second line) std::string addr_display = TruncateAddress(tx.address, (int)schema::UI().drawElement("tabs.receive", "recent-addr-trunc-len").size); dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()), OnSurfaceMedium(), addr_display.c_str()); // Amount (right-aligned, first line) snprintf(buf, sizeof(buf), "+%.8f", tx.amount); ImVec2 amtSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, buf); float amtX = rowPos.x + width - amtSz.x - Layout::spacingLg(); DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(amtX, cy), recvCol, buf, schema::UI().drawElement("tabs.receive", "text-shadow-offset-x").size, schema::UI().drawElement("tabs.receive", "text-shadow-offset-y").size, IM_COL32(0, 0, 0, (int)schema::UI().drawElement("tabs.receive", "text-shadow-alpha").size)); // USD equivalent (right-aligned, second line) double priceUsd = state.market.price_usd; if (priceUsd > 0.0) { double usdVal = tx.amount * priceUsd; if (usdVal >= 1.0) snprintf(buf, sizeof(buf), "$%.2f", usdVal); else if (usdVal >= 0.01) snprintf(buf, sizeof(buf), "$%.4f", usdVal); else snprintf(buf, sizeof(buf), "$%.6f", usdVal); ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(rowPos.x + width - usdSz.x - Layout::spacingLg(), cy + body2->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), buf); } // Status badge { const char* statusStr; ImU32 statusCol; if (tx.confirmations == 0) { statusStr = "Pending"; statusCol = Warning(); } else if (tx.confirmations < (int)schema::UI().drawElement("tabs.receive", "confirmed-threshold").size) { snprintf(buf, sizeof(buf), "%d conf", tx.confirmations); statusStr = buf; statusCol = Warning(); } else { statusStr = "Confirmed"; statusCol = greenCol; } ImVec2 sSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, statusStr); float statusX = amtX - sSz.x - Layout::spacingXxl(); float minStatusX = cx + width * schema::UI().drawElement("tabs.receive", "status-min-x-ratio").size; if (statusX < minStatusX) statusX = minStatusX; ImU32 pillBg = (statusCol & 0x00FFFFFFu) | (static_cast((int)schema::UI().drawElement("tabs.receive", "status-pill-bg-alpha").size) << 24); ImVec2 pillMin(statusX - Layout::spacingSm(), cy + body2->LegacySize + (int)schema::UI().drawElement("tabs.receive", "status-pill-y-offset").size); ImVec2 pillMax(statusX + sSz.x + Layout::spacingSm(), pillMin.y + capFont->LegacySize + Layout::spacingXs()); dl->AddRectFilled(pillMin, pillMax, pillBg, schema::UI().drawElement("tabs.receive", "status-pill-rounding").size); dl->AddText(capFont, capFont->LegacySize, ImVec2(statusX, cy + body2->LegacySize + Layout::spacingXs()), statusCol, statusStr); } ImGui::Dummy(ImVec2(0, rowH)); // Subtle divider between rows if (ri < recvs.size() - 1) { ImVec2 divStart = ImGui::GetCursorScreenPos(); dl->AddLine(ImVec2(divStart.x + rowPadLeft + iconSz * 2.0f, divStart.y), ImVec2(divStart.x + width - Layout::spacingLg(), divStart.y), IM_COL32(255, 255, 255, (int)schema::UI().drawElement("tabs.receive", "row-divider-alpha").size)); } } dl->PopClipRect(); } // ============================================================================ // MAIN: RenderReceiveTab // ============================================================================ void RenderReceiveTab(App* app) { const auto& state = app->getWalletState(); auto& S = schema::UI(); RenderSyncBanner(state); ImVec2 recvAvail = ImGui::GetContentRegionAvail(); float hs = Layout::hScale(recvAvail.x); float vScale = Layout::vScale(recvAvail.y); float glassRound = Layout::glassRounding(); float availWidth = recvAvail.x; ImDrawList* dl = ImGui::GetWindowDrawList(); ImFont* capFont = Type().caption(); ImFont* sub1 = Type().subtitle1(); ImFont* body2 = Type().body2(); GlassPanelSpec glassSpec; glassSpec.rounding = glassRound; float sectionGap = Layout::spacingXl() * vScale; char buf[128]; // ================================================================ // NON-SCROLLING CONTENT — resizes to fit available height // ================================================================ ImVec2 formAvail = ImGui::GetContentRegionAvail(); ImGui::BeginChild("##ReceiveScroll", formAvail, false, ImGuiWindowFlags_NoBackground); dl = ImGui::GetWindowDrawList(); // Top-aligned content — consistent vertical position across all tabs static float s_recvContentH = 0; float scrollAvailH = ImGui::GetContentRegionAvail().y; float groupStartY = ImGui::GetCursorPosY(); float contentStartY = ImGui::GetCursorPosY(); float formAvailW = ImGui::GetContentRegionAvail().x; float formW = formAvailW; ImGui::BeginGroup(); // ================================================================ // Not connected / empty state // ================================================================ if (!app->isConnected()) { ImVec2 emptyMin = ImGui::GetCursorScreenPos(); float emptyH = std::max(schema::UI().drawElement("tabs.receive", "empty-state-min-height").size, schema::UI().drawElement("tabs.receive", "empty-state-height").size * Layout::vScale()); ImVec2 emptyMax(emptyMin.x + formW, 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 + S.drawElement("tabs.receive", "empty-state-subtitle-gap").size), OnSurfaceDisabled(), "Your receiving addresses will appear here once connected."); ImGui::Dummy(ImVec2(formW, emptyH)); ImGui::EndGroup(); ImGui::EndChild(); return; } if (state.addresses.empty()) { ImVec2 emptyMin = ImGui::GetCursorScreenPos(); float emptyH = S.drawElement("tabs.receive", "skeleton-height").size; ImVec2 emptyMax(emptyMin.x + formW, emptyMin.y + emptyH); DrawGlassPanel(dl, emptyMin, emptyMax, glassSpec); float alpha = (float)(schema::UI().drawElement("animations", "skeleton-base").size + schema::UI().drawElement("animations", "skeleton-amp").size * std::sin(ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-slow").size)); ImU32 skelCol = IM_COL32(255, 255, 255, (int)(alpha * 255)); dl->AddRectFilled( ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg()), ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar1-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar1-height").size), skelCol, schema::UI().drawElement("tabs.receive", "skeleton-rounding").size); dl->AddRectFilled( ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-top").size), ImVec2(emptyMin.x + formW * S.drawElement("tabs.receive", "skeleton-bar2-width-ratio").size, emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-bottom").size), skelCol, schema::UI().drawElement("tabs.receive", "skeleton-rounding").size); dl->AddText(capFont, capFont->LegacySize, ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - S.drawElement("tabs.receive", "skeleton-text-bottom-offset").size), OnSurfaceDisabled(), "Loading addresses..."); ImGui::Dummy(ImVec2(formW, emptyH)); ImGui::EndGroup(); ImGui::EndChild(); return; } // Ensure valid selection if (s_selected_address_idx < 0 || s_selected_address_idx >= (int)state.addresses.size()) { s_selected_address_idx = 0; } const AddressInfo& selected = state.addresses[s_selected_address_idx]; bool isZ = selected.type == "shielded"; // Generate QR data std::string 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] && isZ) { 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; } // ================================================================ // MAIN CARD — single glass panel (channel split like Send tab) // ================================================================ { ImVec2 containerMin = ImGui::GetCursorScreenPos(); float pad = Layout::spacingLg(); float innerW = formW - pad * 2; float innerGap = Layout::spacingLg(); // Channel split: content on ch1, glass background on ch0 dl->ChannelsSplit(2); dl->ChannelsSetCurrent(1); ImGui::Indent(pad); ImGui::Dummy(ImVec2(0, pad)); // top padding // ---- ADDRESS DROPDOWN + QR CODE — side by side ---- { float qrColW = innerW * schema::UI().drawElement("tabs.receive", "qr-col-width-ratio").size; float colGap = Layout::spacingLg(); float addrColW = innerW - qrColW - colGap; float qrColX = containerMin.x + pad + addrColW + colGap; ImVec2 sectionTop = ImGui::GetCursorScreenPos(); // LEFT: ADDRESS DROPDOWN + PAYMENT REQUEST { // Address dropdown replaces the old address display panel RenderAddressDropdown(app, addrColW); ImGui::Dummy(ImVec2(0, Layout::spacingMd())); // ---- PAYMENT REQUEST ---- Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "PAYMENT REQUEST"); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); // Amount input with currency toggle float toggleW = S.drawElement("tabs.receive", "currency-toggle-width").size; float amtInputW = addrColW - toggleW - Layout::spacingMd(); if (amtInputW < S.drawElement("tabs.receive", "amount-input-min-width").size) amtInputW = S.drawElement("tabs.receive", "amount-input-min-width").size; double usd_price = state.market.price_usd; ImGui::PushFont(body2); if (s_request_usd_mode && usd_price > 0) { ImGui::PushItemWidth(amtInputW); if (ImGui::InputDouble("##RequestAmountUSD", &s_request_usd_amount, 0, 0, "$%.2f")) { s_request_amount = s_request_usd_amount / usd_price; } { ImVec2 iMin = ImGui::GetItemRectMin(); ImVec2 iMax = ImGui::GetItemRectMax(); snprintf(buf, sizeof(buf), "\xe2\x89\x88 %.4f %s", s_request_amount, DRAGONX_TICKER); ImVec2 sz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); float tx = iMax.x - sz.x - ImGui::GetStyle().FramePadding.x; float ty = iMin.y + (iMax.y - iMin.y - sz.y) * 0.5f; dl->AddText(capFont, capFont->LegacySize, ImVec2(tx, ty), IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "input-overlay-text-alpha").size), buf); } ImGui::PopItemWidth(); } else { ImGui::PushItemWidth(amtInputW); if (ImGui::InputDouble("##RequestAmount", &s_request_amount, 0, 0, "%.8f")) { if (usd_price > 0) s_request_usd_amount = s_request_amount * usd_price; } if (usd_price > 0 && s_request_amount > 0) { ImVec2 iMin = ImGui::GetItemRectMin(); ImVec2 iMax = ImGui::GetItemRectMax(); double usd_value = s_request_amount * usd_price; if (usd_value >= 0.01) snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.2f", usd_value); else snprintf(buf, sizeof(buf), "\xe2\x89\x88 $%.6f", usd_value); ImVec2 sz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); float tx = iMax.x - sz.x - ImGui::GetStyle().FramePadding.x; float ty = iMin.y + (iMax.y - iMin.y - sz.y) * 0.5f; dl->AddText(capFont, capFont->LegacySize, ImVec2(tx, ty), IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "input-overlay-text-alpha").size), buf); } ImGui::PopItemWidth(); } // Toggle button ImGui::SameLine(0, Layout::spacingMd()); { const char* currLabel = s_request_usd_mode ? "DRGX" : "USD"; bool canToggle = (usd_price > 0); ImGui::BeginDisabled(!canToggle); if (TactileButton("##ToggleCurrencyRecv", ImVec2(toggleW, 0), S.resolveFont("button"))) { s_request_usd_mode = !s_request_usd_mode; if (s_request_usd_mode && usd_price > 0) s_request_usd_amount = s_request_amount * usd_price; } { ImVec2 bMin = ImGui::GetItemRectMin(); ImVec2 bMax = ImGui::GetItemRectMax(); float bH = bMax.y - bMin.y; ImFont* font = ImGui::GetFont(); ImVec2 textSz = font->CalcTextSizeA(font->LegacySize, 10000, 0, currLabel); float iconW = schema::UI().drawElement("tabs.receive", "currency-icon-width").size; float iconGap2 = schema::UI().drawElement("tabs.receive", "currency-icon-gap").size; float totalW2 = iconW + iconGap2 + textSz.x; float startX = bMin.x + ((bMax.x - bMin.x) - totalW2) * 0.5f; float cy = bMin.y + bH * 0.5f; ImU32 iconCol = ImGui::GetColorU32(ImGuiCol_Text); float ss = iconW * 0.5f; float cx = startX + ss; float ag = S.drawElement("tabs.receive", "swap-icon-arrow-gap").size; float al = ss * S.drawElement("tabs.receive", "swap-icon-arrow-length-ratio").size; float hss = S.drawElement("tabs.receive", "swap-icon-arrowhead-size").size; float ay1 = cy - ag; dl->AddLine(ImVec2(cx - al, ay1), ImVec2(cx + al, ay1), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size); dl->AddTriangleFilled( ImVec2(cx + al, ay1), ImVec2(cx + al - hss, ay1 - hss), ImVec2(cx + al - hss, ay1 + hss), iconCol); float ay2 = cy + ag; dl->AddLine(ImVec2(cx + al, ay2), ImVec2(cx - al, ay2), iconCol, S.drawElement("tabs.receive", "swap-icon-line-thickness").size); dl->AddTriangleFilled( ImVec2(cx - al, ay2), ImVec2(cx - al + hss, ay2 - hss), ImVec2(cx - al + hss, ay2 + hss), iconCol); float tx = startX + iconW + iconGap2; float ty = cy - textSz.y * 0.5f; dl->AddText(font, font->LegacySize, ImVec2(tx, ty), iconCol, currLabel); } ImGui::EndDisabled(); } ImGui::PopFont(); // Preset amount chips { ImGui::Dummy(ImVec2(0, Layout::spacingXs())); float chipRound = schema::UI().drawElement("tabs.receive", "chip-rounding").size; float chipGap = schema::UI().drawElement("tabs.receive", "chip-gap").size; float chipH = schema::UI().drawElement("tabs.receive", "chip-height").size; struct Preset { const char* label; double amount; }; Preset presets[] = { { "0.1", 0.1 }, { "1", 1.0 }, { "10", 10.0 }, { "100", 100.0 }, { "1K", 1000.0 }, { "10K", 10000.0 }, { "50K", 50000.0 }, { "100K", 100000.0 }, }; constexpr int presetCount = 8; // Calculate equal chip widths to span full addrColW float totalGap = chipGap * (presetCount - 1); float chipW = (addrColW - totalGap) / presetCount; ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, chipRound); ImFont* chipFont = S.resolveFont("button"); for (int ci = 0; ci < presetCount; ci++) { if (ci > 0) ImGui::SameLine(0, chipGap); bool active = false; if (s_request_usd_mode) { active = (usd_price > 0 && std::abs(s_request_usd_amount - presets[ci].amount) < 0.001); } else { active = (std::abs(s_request_amount - presets[ci].amount) < 1e-8); } if (active) { ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "chip-active-bg-alpha").size))); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Primary())); } char chipId[32]; snprintf(chipId, sizeof(chipId), "%s##presetAmt%d", presets[ci].label, ci); if (TactileButton(chipId, ImVec2(chipW, chipH), chipFont)) { if (s_request_usd_mode) { s_request_usd_amount = presets[ci].amount; if (usd_price > 0) s_request_amount = s_request_usd_amount / usd_price; } else { s_request_amount = presets[ci].amount; if (usd_price > 0) s_request_usd_amount = s_request_amount * usd_price; } } if (active) ImGui::PopStyleColor(2); } ImGui::PopStyleVar(); ImGui::PopStyleColor(); } // Memo (z-addresses only) if (isZ) { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "MEMO (OPTIONAL)"); ImGui::Dummy(ImVec2(0, S.drawElement("tabs.receive", "memo-label-gap").size)); float memoInputH = std::max(schema::UI().drawElement("tabs.receive", "memo-input-min-height").size, schema::UI().drawElement("tabs.receive", "memo-input-height").size * vScale); ImGui::PushItemWidth(addrColW); ImGui::InputTextMultiline("##RequestMemo", s_request_memo, sizeof(s_request_memo), ImVec2(addrColW, memoInputH)); ImGui::PopItemWidth(); size_t memo_len = strlen(s_request_memo); snprintf(buf, sizeof(buf), "%zu / %d", memo_len, (int)S.drawElement("tabs.receive", "memo-max-display-chars").size); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf); } // URI preview ImGui::Dummy(ImVec2(0, Layout::spacingSm())); if (s_request_amount > 0 && !s_cached_qr_data.empty()) { float uriWrapW = addrColW; dl->AddText(capFont, capFont->LegacySize, ImGui::GetCursorScreenPos(), OnSurfaceDisabled(), s_cached_qr_data.c_str(), nullptr, uriWrapW); ImVec2 uriSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, uriWrapW, s_cached_qr_data.c_str()); ImGui::Dummy(ImVec2(addrColW, uriSz.y + Layout::spacingSm())); } else { ImGui::Dummy(ImVec2(addrColW, capFont->LegacySize + Layout::spacingSm())); } // Clear button { bool hasData = (s_request_amount > 0 || s_request_memo[0]); ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "clear-btn-hover-alpha").size))); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(hasData ? OnSurfaceMedium() : OnSurfaceDisabled())); ImGui::BeginDisabled(!hasData); if (TactileSmallButton("Clear Request##recv", S.resolveFont("button"))) { s_request_amount = 0.0; s_request_usd_amount = 0.0; s_request_memo[0] = '\0'; } ImGui::EndDisabled(); ImGui::PopStyleColor(3); } } float leftBottom = ImGui::GetCursorScreenPos().y; // RIGHT: QR CODE { float qrAvailW = qrColW; float rx = qrColX; float ry = sectionTop.y; float maxQrSize = std::min(qrAvailW - Layout::spacingMd() * 2, S.drawElement("tabs.receive", "qr-max-size").size); float qrSize = std::max(S.drawElement("tabs.receive", "qr-min-size").size, maxQrSize); float qrPadding = Layout::spacingMd(); float totalQrSize = qrSize + qrPadding * 2; float qrOffsetX = std::max(0.0f, (qrAvailW - totalQrSize) * 0.5f); ImVec2 qrPanelMin(rx + qrOffsetX, ry); ImVec2 qrPanelMax(qrPanelMin.x + totalQrSize, qrPanelMin.y + totalQrSize); GlassPanelSpec qrGlass; qrGlass.rounding = glassRound * S.drawElement("tabs.receive", "qr-glass-rounding-ratio").size; qrGlass.fillAlpha = (int)S.drawElement("tabs.receive", "qr-glass-fill-alpha").size; qrGlass.borderAlpha = (int)S.drawElement("tabs.receive", "qr-glass-border-alpha").size; 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 - S.drawElement("tabs.receive", "qr-unavailable-text-offset").size, qrPanelMin.y + totalQrSize * 0.5f); dl->AddText(capFont, capFont->LegacySize, textPos, OnSurfaceDisabled(), "QR unavailable"); } 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" : "Address copied"); } ImGui::SetCursorScreenPos(ImVec2(rx, qrPanelMax.y)); ImGui::Dummy(ImVec2(qrAvailW, 0)); } float rightBottom = ImGui::GetCursorScreenPos().y; float sectionBottom = std::max(leftBottom, rightBottom); ImGui::SetCursorScreenPos(ImVec2(containerMin.x + pad, sectionBottom)); ImGui::Dummy(ImVec2(innerW, 0)); } // Divider before action buttons ImGui::Dummy(ImVec2(0, innerGap * 0.5f)); { ImVec2 divPos = ImGui::GetCursorScreenPos(); dl->AddLine(ImVec2(containerMin.x + pad, divPos.y), ImVec2(containerMin.x + formW - pad, divPos.y), ImGui::GetColorU32(Divider()), S.drawElement("tabs.receive", "divider-thickness").size); } ImGui::Dummy(ImVec2(0, innerGap * 0.5f)); // ---- ACTION BUTTONS (inside card) ---- { float btnGap = Layout::spacingMd(); float btnH = std::max(schema::UI().drawElement("tabs.receive", "action-btn-min-height").size, schema::UI().drawElement("tabs.receive", "action-btn-height").size * vScale); float otherBtnW = std::max(S.drawElement("tabs.receive", "action-btn-min-width").size, innerW * S.drawElement("tabs.receive", "action-btn-width-ratio").size); bool firstBtn = true; if (s_request_amount > 0) { if (!firstBtn) ImGui::SameLine(0, btnGap); firstBtn = false; if (TactileButton("Copy URI##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) { ImGui::SetClipboardText(s_cached_qr_data.c_str()); Notifications::instance().info("Payment URI copied"); } } if (s_request_amount > 0) { if (!firstBtn) ImGui::SameLine(0, btnGap); firstBtn = false; ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size))); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight())); if (TactileButton("Share##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) { char shareBuf[1024]; snprintf(shareBuf, sizeof(shareBuf), "Payment Request\nAmount: %.8f %s\nAddress: %s\nURI: %s", s_request_amount, DRAGONX_TICKER, selected.address.c_str(), s_cached_qr_data.c_str()); ImGui::SetClipboardText(shareBuf); Notifications::instance().info("Payment request copied"); } ImGui::PopStyleColor(3); } if (!firstBtn) ImGui::SameLine(0, btnGap); firstBtn = false; ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size))); ImGui::PushStyleColor(ImGuiCol_Border, ImGui::ColorConvertU32ToFloat4(OnSurfaceDisabled())); ImGui::PushStyleVar(ImGuiStyleVar_FrameBorderSize, S.drawElement("tabs.receive", "explorer-btn-border-size").size); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(PrimaryLight())); if (TactileButton("Explorer##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) { OpenExplorerURL(selected.address); } ImGui::PopStyleVar(); // FrameBorderSize ImGui::PopStyleColor(4); if (selected.balance > 0) { if (!firstBtn) ImGui::SameLine(0, btnGap); firstBtn = false; ImGui::PushStyleColor(ImGuiCol_Button, ImVec4(0, 0, 0, 0)); ImGui::PushStyleColor(ImGuiCol_ButtonHovered, ImGui::ColorConvertU32ToFloat4(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.receive", "btn-hover-alpha").size))); ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(OnSurfaceMedium())); if (TactileButton("Send \xe2\x86\x97##recv", ImVec2(otherBtnW, btnH), S.resolveFont("button"))) { SetSendFromAddress(selected.address); app->setCurrentPage(NavPage::Send); } ImGui::PopStyleColor(3); } } // Bottom padding ImGui::Dummy(ImVec2(0, pad)); ImGui::Unindent(pad); // Enforce shared card height (matches QR-driven target) { float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y; float targetCardH = Layout::mainCardTargetH(formW, vScale); if (currentCardH < targetCardH) ImGui::Dummy(ImVec2(0, targetCardH - currentCardH)); } // Draw glass panel background on channel 0 ImVec2 containerMax(containerMin.x + formW, ImGui::GetCursorScreenPos().y); dl->ChannelsSetCurrent(0); DrawGlassPanel(dl, containerMin, containerMax, glassSpec); dl->ChannelsMerge(); ImGui::SetCursorScreenPos(ImVec2(containerMin.x, containerMax.y)); ImGui::Dummy(ImVec2(formW, 0)); ImGui::Dummy(ImVec2(0, Layout::spacingMd())); } // ================================================================ // RECENT RECEIVED // ================================================================ RenderRecentReceived(dl, selected, state, formW, capFont, app); ImGui::EndGroup(); float measuredH = ImGui::GetCursorPosY() - contentStartY; s_recvContentH = std::round(measuredH); ImGui::EndChild(); // ##ReceiveScroll } } // namespace ui } // namespace dragonx