Files
ObsidianDragon/src/ui/windows/address_transfer_dialog.h
DanS ee8a08e569 feat(addresses): improve address labeling and view-only handling
- Add expanded address icon picker with search, bottom-aligned actions, and improved modal sizing
- Embed a pickaxe icon font subset and wire it into typography/address icon rendering
- Track view-only shielded addresses and prevent sends from non-spendable z-addresses
- Improve address transfer dialog sizing, max amount handling, and text clipping
- Tune main header layout values in ui.toml
- Update README, codebase overview, and third-party license documentation
2026-04-27 13:54:28 -05:00

322 lines
12 KiB
C++

// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <cstdio>
#include <cstring>
#include <cmath>
#include "../../app.h"
#include "../../util/i18n.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
/**
* @brief Modal dialog for transferring funds between two addresses.
*
* Shown when a user drags one address row onto another in the address list.
* Displays from/to addresses, balances, amount input, fee, and a preview of
* the resulting balances after transfer.
*/
struct AddressTransferInfo {
std::string fromAddr;
std::string toAddr;
double fromBalance = 0.0;
double toBalance = 0.0;
bool fromIsZ = false;
bool toIsZ = false;
};
class AddressTransferDialog {
public:
using TransferInfo = AddressTransferInfo;
static void show(App* app, const TransferInfo& info) {
s_open = true;
s_app = app;
s_info = info;
s_sending = false;
s_resultMsg.clear();
s_success = false;
// Default fee
s_fee = 0.0001;
// Pre-fill with the maximum sendable amount so the dialog is valid
// immediately without requiring the user to press Max.
snprintf(s_amount, sizeof(s_amount), "%.8f", maxSendableAmount(info.fromBalance, s_fee));
}
static void render() {
if (!s_open || !s_app) return;
using namespace material;
if (BeginOverlayDialog(TR("transfer_funds"), &s_open, 620.0f, 0.94f)) {
float dp = Layout::dpiScale();
ImDrawList* dl = ImGui::GetWindowDrawList();
// If we got a result, show it
if (!s_resultMsg.empty()) {
renderResult();
EndOverlayDialog();
return;
}
// Shielding / deshielding warning
if (s_info.fromIsZ && !s_info.toIsZ) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
ImGui::TextWrapped("%s", TR("deshielding_warning"));
ImGui::PopStyleColor();
ImGui::Spacing();
} else if (!s_info.fromIsZ && s_info.toIsZ) {
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Success()));
ImGui::TextWrapped("%s", TR("shielding_notice"));
ImGui::PopStyleColor();
ImGui::Spacing();
}
// From address
Type().text(TypeStyle::Overline, TR("from"));
renderAddressRow(s_info.fromAddr, s_info.fromBalance, s_info.fromIsZ, dp);
ImGui::Spacing();
// Arrow
{
float arrowCX = ImGui::GetContentRegionAvail().x * 0.5f;
ImGui::SetCursorPosX(arrowCX - 8.0f);
ImFont* iconFont = Type().iconMed();
float fsz = ScaledFontSize(iconFont);
ImVec2 pos = ImGui::GetCursorScreenPos();
dl->AddText(iconFont, fsz, pos, OnSurfaceMedium(), ICON_MD_ARROW_DOWNWARD);
ImGui::Dummy(ImVec2(fsz, fsz));
}
ImGui::Spacing();
// To address
Type().text(TypeStyle::Overline, TR("to"));
renderAddressRow(s_info.toAddr, s_info.toBalance, s_info.toIsZ, dp);
ImGui::Spacing();
ImGui::Spacing();
// Amount input + Max button on same row without overflow
Type().text(TypeStyle::Subtitle2, TR("amount"));
{
float spacing = ImGui::GetStyle().ItemSpacing.x;
float maxBtnW = ImGui::CalcTextSize(TR("max")).x
+ ImGui::GetStyle().FramePadding.x * 2.0f;
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - maxBtnW - spacing);
}
ImGui::InputText("##TransferAmt", s_amount, sizeof(s_amount),
ImGuiInputTextFlags_CharsDecimal);
// Max button
ImGui::SameLine();
if (ImGui::SmallButton(TR("max"))) {
snprintf(s_amount, sizeof(s_amount), "%.8f",
maxSendableAmount(s_info.fromBalance, s_fee));
}
ImGui::Spacing();
// Fee display
{
char feeBuf[64];
snprintf(feeBuf, sizeof(feeBuf), "%s: %.4f DRGX", TR("fee"), s_fee);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), feeBuf);
}
ImGui::Spacing();
// Preview after transfer
double amount = atof(s_amount);
double totalDeduct = amount + s_fee;
double newFromBal = s_info.fromBalance - totalDeduct;
double newToBal = s_info.toBalance + amount;
bool amountValid = amount > 0 && totalDeduct <= s_info.fromBalance;
ImGui::Separator();
ImGui::Spacing();
Type().text(TypeStyle::Subtitle2, TR("result_preview"));
ImGui::Spacing();
{
char buf[128];
snprintf(buf, sizeof(buf), TR("sender_balance"),
s_info.fromBalance, amountValid ? newFromBal : s_info.fromBalance);
Type().textColored(TypeStyle::Caption,
(amountValid && newFromBal < 1e-9) ? Warning() : OnSurfaceMedium(), buf);
}
{
char buf[128];
snprintf(buf, sizeof(buf), TR("recipient_balance"),
s_info.toBalance, amountValid ? newToBal : s_info.toBalance);
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), buf);
}
// Warnings
if (!amountValid && amount > 0) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Error()));
ImGui::TextWrapped("%s", TR("insufficient_funds"));
ImGui::PopStyleColor();
}
if (amountValid && newFromBal < 1e-9) {
ImGui::Spacing();
ImGui::PushStyleColor(ImGuiCol_Text, ImGui::ColorConvertU32ToFloat4(Warning()));
ImGui::TextWrapped("%s", TR("sends_full_balance_warning"));
ImGui::PopStyleColor();
}
ImGui::Spacing();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Buttons
const char* cancelLabel = TR("cancel");
const char* confirmLabel = TR("confirm_transfer");
const char* sendingLabel = TR("sending");
ImFont* buttonFont = Type().button();
float buttonFontSize = ScaledFontSize(buttonFont);
float minBtnW = 120.0f * dp;
float confirmMinW = 160.0f * dp;
float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp;
float cancelW = std::max(minBtnW,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, cancelLabel).x + buttonPadW);
float confirmTextW = std::max(
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, confirmLabel).x,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x);
float confirmW = std::max(confirmMinW, confirmTextW + buttonPadW);
float totalW = cancelW + confirmW + Layout::spacingMd();
float rowStartX = ImGui::GetCursorPosX();
float contentW = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
if (TactileButton(cancelLabel, ImVec2(cancelW, 0), buttonFont)) {
s_open = false;
}
ImGui::SameLine(0, Layout::spacingMd());
ImGui::BeginDisabled(!amountValid || s_sending);
if (TactileButton(s_sending ? sendingLabel : confirmLabel, ImVec2(confirmW, 0), buttonFont)) {
s_sending = true;
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
amount, s_fee, "",
[](bool ok, const std::string& result) {
s_sending = false;
s_success = ok;
if (ok)
s_resultMsg = result; // opid
else
s_resultMsg = result; // error message
});
}
ImGui::EndDisabled();
EndOverlayDialog();
}
}
static bool isOpen() { return s_open; }
static void close() { s_open = false; }
private:
static double maxSendableAmount(double balance, double fee) {
double maxAmt = balance - fee;
return maxAmt > 0.0 ? maxAmt : 0.0;
}
static void renderAddressRow(const std::string& addr, double balance, bool isZ, float dp) {
using namespace material;
ImDrawList* dl = ImGui::GetWindowDrawList();
ImVec2 pos = ImGui::GetCursorScreenPos();
float w = ImGui::GetContentRegionAvail().x;
float h = 40.0f * dp;
ImVec2 mn = pos, mx(pos.x + w, pos.y + h);
GlassPanelSpec g;
g.rounding = 8.0f * dp;
g.fillAlpha = 18;
DrawGlassPanel(dl, mn, mx, g);
float pad = 12.0f * dp;
// Type icon
ImFont* iconFont = Type().iconSmall();
const char* icon = isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE;
ImU32 col = isZ ? Success() : Warning();
float iconFsz = ScaledFontSize(iconFont);
ImVec2 iSz = iconFont->CalcTextSizeA(iconFsz, 1000.0f, 0.0f, icon);
dl->AddText(iconFont, iconFsz, ImVec2(mn.x + pad, mn.y + (h - iSz.y) * 0.5f), col, icon);
// Address (truncated)
// Balance (right-aligned) — computed first so we know how much space address gets
char balBuf[32];
snprintf(balBuf, sizeof(balBuf), "%.8f DRGX", balance);
ImFont* body = Type().body2();
float bodyFsz = ScaledFontSize(body);
ImVec2 balSz = body->CalcTextSizeA(bodyFsz, 1000.0f, 0.0f, balBuf);
float balX = mx.x - pad - balSz.x;
dl->AddText(body, bodyFsz, ImVec2(balX, mn.y + (h - balSz.y) * 0.5f),
balance > 0 ? OnSurface() : OnSurfaceDisabled(), balBuf);
float textX = mn.x + pad + iSz.x + 8.0f * dp;
ImFont* capFont = Type().caption();
float capFsz = ScaledFontSize(capFont);
// Clip address text so it never overlaps the balance
float addrMaxW = balX - textX - 8.0f * dp;
dl->PushClipRect(ImVec2(textX, mn.y), ImVec2(textX + addrMaxW, mn.y + h), true);
dl->AddText(capFont, capFsz, ImVec2(textX, mn.y + (h * 0.5f - capFsz)), OnSurfaceMedium(), addr.c_str());
dl->PopClipRect();
ImGui::Dummy(ImVec2(w, h));
}
static void renderResult() {
using namespace material;
if (s_success) {
ImGui::Spacing();
Type().text(TypeStyle::H6, TR("transfer_sent"));
ImGui::Spacing();
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), TR("transfer_sent_desc"));
ImGui::Spacing();
{
char buf[128];
snprintf(buf, sizeof(buf), "Operation ID: %s",
s_resultMsg.size() > 40
? (s_resultMsg.substr(0, 20) + "..." + s_resultMsg.substr(s_resultMsg.size() - 10)).c_str()
: s_resultMsg.c_str());
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf);
}
} else {
ImGui::Spacing();
Type().textColored(TypeStyle::H6, Error(), TR("transfer_failed"));
ImGui::Spacing();
ImGui::TextWrapped("%s", s_resultMsg.c_str());
}
ImGui::Spacing();
ImGui::Spacing();
float btnW = 120.0f;
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - btnW) * 0.5f);
if (TactileButton(TR("close"), ImVec2(btnW, 0))) {
s_open = false;
}
}
static inline bool s_open = false;
static inline App* s_app = nullptr;
static inline TransferInfo s_info;
static inline char s_amount[64] = {};
static inline double s_fee = 0.0001;
static inline bool s_sending = false;
static inline bool s_success = false;
static inline std::string s_resultMsg;
};
} // namespace ui
} // namespace dragonx