Files
ObsidianDragon/src/ui/windows/receive_tab.cpp
DanS 9205addf55 feat(ui): width-responsiveness — content max-width cap + per-surface form/input clamps
Wide/ultrawide (1440-3440px) responsiveness was unhealthy: no page/content-level
max-width cap existed, and every card/form/table derived width from raw
GetContentRegionAvail().x with floor-only clamps, so surfaces stretched edge-to-edge
(2000-3000px inputs, ballooning cards, giant grid cells, 2000px+ dead row-voids).

Root cause: adopt the (previously dead-code) clamp helpers.
- New Layout::kContentMaxWidth() (~1600dp, tunable via ui.toml [layout]
  content-max-width; <=0 disables). Cap ##ContentArea to it and center the column in
  wider windows — every tab derives from this child, so one change tames the app at
  wide widths. No-op below the cap (fills as before), so 1080p/1440p are unaffected.

Per-surface upper-clamps (std::min(cap*dp, expr), floors preserved) where a single
element is still too wide even within the capped column:
- Settings: Theme/Layout/Language combos, the font-scale slider (~3000px -> 360dp),
  the effect sliders, Explorer URL and RPC credential fields.
- Send / Receive: cap the compose / receive cards to a readable form width and center
  them (Indent(pad+offset) so the auto-layout fields align with the hand-drawn card);
  the recent-tx lists below keep the full column width.
- Chat message bubbles + composer, mining pool URL/payout inputs + stats left/right
  split, contacts search, and the lite-network add-server row / server cards / status
  panel (capped + centered).
- Wizard: vertically center the cards when they fit (was top-anchored, leaving a void
  on tall monitors), compensating the content-height measurement so it can't oscillate.

The 1600 cap also subsumes the fixed-4-column balance grids (~400px cards) and the
right-anchored row dead-gaps (voids shrink from ~2700px to ~800px), so those are left
to the cap rather than blind column/row redesigns.

Verified at 1024/1280 (and via a temporary 900dp cap to exercise the cap+center path,
since the test display clamps to 1280) across full-node + Lite + Windows (ctest green)
and an adversarial diff review (clean). The true wide/ultrawide look and the 1600dp cap
value still want eyes on a real wide monitor.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-08-18 23:15:39 -05:00

1020 lines
51 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 "../../app.h"
#include "../../config/settings.h"
#include "../../util/i18n.h"
#include "../../util/payment_uri.h"
#include "../../util/platform.h"
#include "../../util/text_format.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;
static std::string TrId(const char* key, const char* id) {
return std::string(TR(key)) + "##" + id;
}
// ============================================================================
// State
// ============================================================================
static int s_selected_address_idx = -1;
static double s_request_amount = 0.0;
static char s_request_memo[512] = "";
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;
static bool s_generating_address = false;
// ============================================================================
// Helpers
// ============================================================================
static void OpenExplorerURL(App* app, const std::string& address) {
std::string url = app->settings()->getAddressExplorerUrl() + address;
dragonx::util::Platform::openUrl(url);
}
// ============================================================================
// 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;
// Use the work-weighted verification_progress like every other surface (not the naive
// blocks/headers ratio, which over-reports during early sync).
double vp = state.sync.verification_progress;
if (vp < 0.0) vp = 0.0; else if (vp > 1.0) vp = 1.0;
float syncPct = (float)(vp * 100.0);
char syncBuf[128];
snprintf(syncBuf, sizeof(syncBuf),
TR("blockchain_syncing"), 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(), TR("address_upper"));
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;
float segH = ImGui::GetFrameHeight();
ImGui::SameLine(width - toggleTotalW);
ImVec2 segOrigin = ImGui::GetCursorScreenPos();
const char* filterLabels[] = { TR("all_filter"), "Z", "T" };
int segClk = material::SegmentedControl(ImGui::GetWindowDrawList(), segOrigin, toggleTotalW, segH,
filterLabels, 3, s_addr_type_filter, Type().caption(),
"##addrFilter", Layout::dpiScale());
if (segClk >= 0) s_addr_type_filter = segClk;
ImGui::Dummy(ImVec2(toggleTotalW, segH));
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 = TR(app->isLiteBuild() ? "lite_no_wallet_short" : "not_connected");
} 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 = util::truncateMiddle(addr.address,
static_cast<int>(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 = TR("select_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("%s", TR("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("%s", TR("no_addresses_match"));
} 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 = util::truncateMiddle(addr.address, (int)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()) {
material::Tooltip("%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(TrId("copy", "recvAddr").c_str(), 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(TR("address_copied"));
}
}
ImGui::EndDisabled();
// New address button on same line
ImGui::SameLine(0, Layout::spacingSm());
ImGui::BeginDisabled(!app->isConnected() || s_generating_address);
if (s_generating_address) {
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##recv", TR("generating"), material::LoadingDots());
TactileButton(genLabel, ImVec2(newBtnW, 0), schema::UI().resolveFont("button"));
} else if (TactileButton(TrId("new", "recv").c_str(), ImVec2(newBtnW, 0), schema::UI().resolveFont("button"))) {
// createNewAddress can early-return without ever invoking the callback (e.g. connection
// dropped between this button being enabled and the click landing), which would leave
// s_generating_address stuck. Only enter the "generating…" state when a dispatch is possible.
if (!app->isConnected()) {
s_generating_address = false;
} else {
s_generating_address = true;
if (s_addr_type_filter != 2) {
app->createNewZAddress([](const std::string& addr) {
s_generating_address = false;
if (addr.empty())
Notifications::instance().error(TR("failed_create_shielded"));
else {
s_pending_select_address = addr;
Notifications::instance().success(TR("new_shielded_created"));
}
});
} else {
app->createNewTAddress([](const std::string& addr) {
s_generating_address = false;
if (addr.empty())
Notifications::instance().error(TR("failed_create_transparent"));
else {
s_pending_select_address = addr;
Notifications::instance().success(TR("new_transparent_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;
char buf[32];
if (diff < 60) { snprintf(buf, sizeof(buf), TR("time_seconds_ago"), (long long)diff); return buf; }
if (diff < 3600) { snprintf(buf, sizeof(buf), TR("time_minutes_ago"), (long long)(diff / 60)); return buf; }
if (diff < 86400) { snprintf(buf, sizeof(buf), TR("time_hours_ago"), (long long)(diff / 3600)); return buf; }
snprintf(buf, sizeof(buf), TR("time_days_ago"), (long long)(diff / 86400)); return buf;
}
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(const AddressInfo& /* addr */,
const WalletState& state, float width,
ImFont* capFont, App* app) {
auto& S = schema::UI();
ImGui::Dummy(ImVec2(0, Layout::spacingLg()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("recent_received"));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
float hs = Layout::hScale(width);
float vs = Layout::vScale(std::max(1.0f, ImGui::GetContentRegionAvail().y));
float dp = Layout::dpiScale();
float rowH = std::max(18.0f * dp, S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f) * vs);
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
ImU32 recvCol = Success();
// Collect matching transactions
std::vector<const TransactionInfo*> recvs;
for (const auto& tx : state.transactions) {
if (tx.type != "receive" && tx.type != "mined") continue;
recvs.push_back(&tx);
}
if (recvs.size() > 4) recvs.resize(4); // show only the 4 most recent (newest-first)
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
ImGui::BeginChild("##RecentReceivedRows", ImVec2(width, listH), false,
ImGuiWindowFlags_NoBackground);
ImDrawList* rowDL = ImGui::GetWindowDrawList();
char buf[64];
if (recvs.empty()) {
ImGui::SetCursorPosY(Layout::spacingMd());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_recent_receives"));
ImGui::EndChild();
return;
}
for (size_t ri = 0; ri < recvs.size(); ri++) {
const auto& tx = *recvs[ri];
ImVec2 rowPos = ImGui::GetCursorScreenPos();
float rowY = rowPos.y + rowH * 0.5f;
// Icon
DrawRecvIcon(rowDL, rowPos.x + Layout::spacingMd(), rowY, iconSz * 0.5f, recvCol);
// Type label (first line)
float txX = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
const char* typeText = tx.type == "mined" ? TR("mined_type") : TR("received_label");
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(txX, rowPos.y + 2.0f * dp), OnSurfaceMedium(), typeText);
// Address (second line)
float addrX = txX + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f) * hs;
std::string addrDisplay = util::truncateMiddle(tx.address,
(int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(addrX, rowPos.y + 2.0f * dp), OnSurfaceDisabled(), addrDisplay.c_str());
// Amount (right-aligned, first line)
snprintf(buf, sizeof(buf), "+%.4f %s", std::abs(tx.amount), DRAGONX_TICKER);
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, buf);
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size,
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(amtX, rowPos.y + 2.0f * dp), recvCol, buf);
// Time ago
std::string ago = recvTimeAgo(tx.timestamp);
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000.0f, 0.0f, ago.c_str());
rowDL->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f) * hs,
rowPos.y + 2.0f * dp),
OnSurfaceDisabled(), ago.c_str());
// Clickable row — hover highlight + navigate to History
float rowW = ImGui::GetContentRegionAvail().x;
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
if (material::IsRectHovered(rowPos, rowEnd)) {
rowDL->AddRectFilled(rowPos, rowEnd,
IM_COL32(255, 255, 255, 15),
S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0)) {
app->setCurrentPage(ui::NavPage::History);
}
}
ImGui::Dummy(ImVec2(0, rowH));
}
ImGui::EndChild();
}
// ============================================================================
// 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();
// Reserve a slice of the available height for RECENT RECEIVED (ratio — mirrors
// balance_tab's recent-tx-reserve). The main card's target height is capped so it can
// never grow past (available - reserve): a no-op at 1.0x (mainCardTargetH already fits
// well within scrollAvailH there), but at HiDPI it prevents the card — whose QR (280*dp)
// and pads scale with dp while scrollAvailH is physical px — from eating the whole child
// and evicting the list.
float recvReserveRatio = S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
float recvRecentReserve = std::max(0.0f, scrollAvailH) * recvReserveRatio;
float recvCardCapH = std::max(0.0f, scrollAvailH - recvRecentReserve);
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(), TR(app->isLiteBuild() ? "lite_no_wallet" : "waiting_for_daemon"));
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(), TR("addresses_appear_here"));
ImGui::Dummy(ImVec2(formW, emptyH));
ImGui::EndGroup();
ImGui::EndChild();
return;
}
if (state.addresses.empty()) {
ImVec2 emptyMin = ImGui::GetCursorScreenPos();
// Hand-drawn skeleton geometry is absolute px — scale heights/offsets/rounding by
// dpiScale() so it doesn't render native-thin at >100% (width ratios multiply formW, leave as-is).
float dp = Layout::dpiScale();
float emptyH = S.drawElement("tabs.receive", "skeleton-height").size * dp;
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));
float skelRound = schema::UI().drawElement("tabs.receive", "skeleton-rounding").size * dp;
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 * dp),
skelCol, skelRound);
dl->AddRectFilled(
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + Layout::spacingLg() + S.drawElement("tabs.receive", "skeleton-bar2-top").size * dp),
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 * dp),
skelCol, skelRound);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(emptyMin.x + Layout::spacingLg(), emptyMin.y + emptyH - S.drawElement("tabs.receive", "skeleton-text-bottom-offset").size * dp),
OnSurfaceDisabled(), TR("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) {
// Canonical, URL-encoded "drgx:" URI via the shared builder. The old inline code emitted an
// unparseable "dragonx:" scheme and appended the memo raw (spaces/&/= corrupted the QR).
qr_data = util::buildPaymentUri(selected.address, s_request_amount, "",
(s_request_memo[0] && isZ) ? std::string(s_request_memo) : std::string());
}
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)
// ================================================================
{
// Cap + center the card so the address/amount column stops stretching while the
// QR plateaus. cardW never exceeds formW (only shrinks); leftover becomes margin.
// The RECENT RECEIVED list below stays on the uncapped formW (handled separately).
float cardDp = Layout::dpiScale();
float cardW = std::min(formW, 860.0f * cardDp);
float cardLeftX = ImGui::GetCursorScreenPos().x;
float cardOffsetX = std::max(0.0f, (formW - cardW) * 0.5f);
if (cardOffsetX > 0.0f)
ImGui::SetCursorPosX(ImGui::GetCursorPosX() + cardOffsetX);
ImVec2 containerMin = ImGui::GetCursorScreenPos();
float pad = Layout::spacingLg();
float innerW = cardW - pad * 2;
float innerGap = Layout::spacingLg();
// Channel split: content on ch1, glass background on ch0
dl->ChannelsSplit(2);
dl->ChannelsSetCurrent(1);
// Indent carries the centering offset too, so ImGui auto-layout content (the address
// dropdown/inputs/chips) lands at containerMin.x + pad — matching the hand-drawn geometry
// (glass panel / QR column) that keys off the offset containerMin.x.
ImGui::Indent(pad + cardOffsetX);
ImGui::Dummy(ImVec2(0, pad)); // top padding
// ---- ADDRESS DROPDOWN + QR CODE — side by side ----
{
float qrColW = std::min(innerW * schema::UI().drawElement("tabs.receive", "qr-col-width-ratio").size, 340.0f * cardDp);
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(), TR("payment_request"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Amount input with currency toggle
float toggleW = S.drawElement("tabs.receive", "currency-toggle-width").size * Layout::dpiScale();
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")) {
if (s_request_usd_amount < 0) s_request_usd_amount = 0;
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 (s_request_amount < 0) s_request_amount = 0;
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 * Layout::dpiScale();
float iconGap2 = schema::UI().drawElement("tabs.receive", "currency-icon-gap").size * Layout::dpiScale();
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(), TR("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();
// The Sapling memo limit is in BYTES and matches the enforced buffer size
// (InputTextMultiline caps at sizeof(s_request_memo)); count against that, not the
// old display-only 256 value, and warn in colour once the field is at its cap.
size_t memo_len = strlen(s_request_memo);
size_t memoMax = (size_t)S.drawElement("business", "memo-max-length").size;
bool memoAtCap = memo_len + 1 >= memoMax;
snprintf(buf, sizeof(buf), TR("byte_count_fmt"), memo_len, memoMax);
Type().textColored(TypeStyle::Caption, memoAtCap ? Warning() : 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()));
}
}
float leftBottom = ImGui::GetCursorScreenPos().y;
// RIGHT: QR CODE
{
float qrAvailW = qrColW;
float rx = qrColX;
float ry = sectionTop.y;
float qrPadding = Layout::spacingMd();
float qrWidthBound = std::max(0.0f, qrAvailW - qrPadding * 2.0f);
float qrHeightBound = std::max(0.0f, leftBottom - sectionTop.y - qrPadding * 2.0f);
float qrMaxSize = qrHeightBound > 0.0f
? std::min(qrWidthBound, qrHeightBound)
: qrWidthBound;
float qrMinSize = std::min(S.drawElement("tabs.receive", "qr-min-size").size, qrWidthBound);
float qrSize = std::max(qrMinSize, qrMaxSize);
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(), TR("qr_unavailable"));
}
ImGui::SetCursorScreenPos(qrPanelMin);
ImGui::InvisibleButton("##QRClickCopy", ImVec2(totalQrSize, totalQrSize));
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip("%s",
s_request_amount > 0 ? TR("click_copy_uri") : TR("click_copy_address"));
}
if (ImGui::IsItemClicked()) {
ImGui::SetClipboardText(qr_data.c_str());
Notifications::instance().info(s_request_amount > 0
? TR("payment_uri_copied") : TR("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
{
float actionBtnH = std::max(S.drawElement("tabs.receive", "action-btn-min-height").size,
S.drawElement("tabs.receive", "action-btn-height").size * vScale);
float footerH = innerGap + actionBtnH + pad;
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
float targetCardH = std::min(Layout::mainCardTargetH(cardW, vScale), recvCardCapH);
float footerTopH = targetCardH - footerH;
if (currentCardH < footerTopH) {
ImGui::Dummy(ImVec2(0, footerTopH - currentCardH));
}
}
ImGui::Dummy(ImVec2(0, innerGap * 0.5f));
{
ImVec2 divPos = ImGui::GetCursorScreenPos();
dl->AddLine(ImVec2(containerMin.x + pad, divPos.y),
ImVec2(containerMin.x + cardW - 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(TrId("copy_uri", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
ImGui::SetClipboardText(s_cached_qr_data.c_str());
Notifications::instance().info(TR("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(TrId("share", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
char shareBuf[1024];
snprintf(shareBuf, sizeof(shareBuf),
"%s\n%s: %.8f %s\n%s: %s\nURI: %s",
TR("payment_request"), TR("amount"),
s_request_amount, DRAGONX_TICKER,
TR("address"), selected.address.c_str(),
s_cached_qr_data.c_str());
ImGui::SetClipboardText(shareBuf);
Notifications::instance().info(TR("payment_request_copied"));
}
ImGui::PopStyleColor(3);
}
{
bool hasData = (s_request_amount > 0 || s_request_memo[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", "clear-btn-hover-alpha").size)));
ImGui::PushStyleColor(ImGuiCol_Text,
ImGui::ColorConvertU32ToFloat4(hasData ? OnSurfaceMedium() : OnSurfaceDisabled()));
ImGui::BeginDisabled(!hasData);
if (TactileButton(TrId("clear_request", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
s_request_amount = 0.0;
s_request_usd_amount = 0.0;
s_request_memo[0] = '\0';
}
ImGui::EndDisabled();
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(TrId("explorer", "recv").c_str(), ImVec2(otherBtnW, btnH), S.resolveFont("button"))) {
OpenExplorerURL(app, selected.address);
}
ImGui::PopStyleVar(); // FrameBorderSize
ImGui::PopStyleColor(4);
}
// Bottom padding
ImGui::Dummy(ImVec2(0, pad));
ImGui::Unindent(pad + cardOffsetX);
// Enforce shared card height (matches QR-driven target), capped so the reserved
// RECENT RECEIVED slice below stays on-screen at HiDPI.
{
float currentCardH = ImGui::GetCursorScreenPos().y - containerMin.y;
float targetCardH = std::min(Layout::mainCardTargetH(cardW, vScale), recvCardCapH);
if (currentCardH < targetCardH)
ImGui::Dummy(ImVec2(0, targetCardH - currentCardH));
}
// Draw glass panel background on channel 0
ImVec2 containerMax(containerMin.x + cardW, ImGui::GetCursorScreenPos().y);
dl->ChannelsSetCurrent(0);
DrawGlassPanel(dl, containerMin, containerMax, glassSpec);
dl->ChannelsMerge();
// Restore the original (uncapped) left edge so RECENT RECEIVED below renders
// full-width as-is, unaffected by the card's centering offset.
ImGui::SetCursorScreenPos(ImVec2(cardLeftX, containerMax.y));
ImGui::Dummy(ImVec2(formW, 0));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
}
// ================================================================
// RECENT RECEIVED
// ================================================================
RenderRecentReceived(selected, state, formW, capFont, app);
ImGui::EndGroup();
float measuredH = ImGui::GetCursorPosY() - contentStartY;
s_recvContentH = std::round(measuredH);
ImGui::EndChild(); // ##ReceiveScroll
}
} // namespace ui
} // namespace dragonx