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)
1020 lines
50 KiB
C++
1020 lines
50 KiB
C++
// 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 <string>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <ctime>
|
|
#include <map>
|
|
#include <vector>
|
|
|
|
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<std::string, double> s_new_address_timestamps;
|
|
static size_t s_prev_address_count = 0;
|
|
|
|
// Address labels (in-memory until persistent config)
|
|
static std::map<std::string, std::string> 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<int>(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<int>(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<size_t>(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<size_t> 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<size_t>(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<int>(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<int>(i));
|
|
if (ImGui::Selectable(buf, isCurrent)) {
|
|
s_selected_address_idx = static_cast<int>(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<const TransactionInfo*> 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<ImU32>((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
|