feat: modernize address list with drag-transfer, labels, and UX polish

- Rewrite RenderSharedAddressList with two-pass layout architecture
- Add drag-to-transfer: drag address onto another to open transfer dialog
- Add AddressLabelDialog with custom label text and 20-icon picker
- Add AddressTransferDialog with amount input, fee, and balance preview
- Add AddressMeta persistence (label, icon, sortOrder) in settings.json
- Gold favorite border inset 2dp from container edge
- Show hide button on all addresses, not just zero-balance
- Smaller star/hide buttons to clear favorite border
- Semi-transparent dragged row with context-aware tooltip
- Copy-to-clipboard deferred to mouse-up (no copy on drag)
- Themed colors via resolveColor() with CSS variable fallbacks
- Keyboard nav (Up/Down/J/K, Enter to copy, F2 to edit label)
- Add i18n keys for all new UI strings
This commit is contained in:
dan_s
2026-04-12 17:29:56 -05:00
parent 79d8f0d809
commit 9f23b2781c
10 changed files with 1026 additions and 174 deletions

View File

@@ -35,6 +35,8 @@
#include "ui/windows/block_info_dialog.h"
#include "ui/windows/export_all_keys_dialog.h"
#include "ui/windows/export_transactions_dialog.h"
#include "ui/windows/address_label_dialog.h"
#include "ui/windows/address_transfer_dialog.h"
#include "ui/windows/console_tab.h"
#include "ui/pages/settings_page.h"
#include "ui/theme.h"
@@ -1347,7 +1349,13 @@ void App::render()
// Export transactions to CSV dialog (triggered from File menu)
ui::ExportTransactionsDialog::render(this);
// Address label/icon editor
ui::AddressLabelDialog::render();
// Address-to-address transfer confirmation
ui::AddressTransferDialog::render();
// Windows Defender antivirus help dialog
renderAntivirusHelpDialog();

View File

@@ -202,6 +202,16 @@ public:
void unfavoriteAddress(const std::string& addr);
bool isAddressFavorite(const std::string& addr) const;
// Address metadata (labels, icons, custom ordering)
void setAddressLabel(const std::string& addr, const std::string& label);
void setAddressIcon(const std::string& addr, const std::string& icon);
std::string getAddressLabel(const std::string& addr) const;
std::string getAddressIcon(const std::string& addr) const;
int getAddressSortOrder(const std::string& addr) const;
void setAddressSortOrder(const std::string& addr, int order);
int getNextSortOrder() const;
void swapAddressOrder(const std::string& a, const std::string& b);
// Key export/import
void exportPrivateKey(const std::string& address, std::function<void(const std::string&)> callback);
void exportAllKeys(std::function<void(const std::string&)> callback);

View File

@@ -1640,6 +1640,61 @@ bool App::isAddressFavorite(const std::string& addr) const
return settings_ && settings_->isAddressFavorite(addr);
}
void App::setAddressLabel(const std::string& addr, const std::string& label)
{
if (settings_) {
settings_->setAddressLabel(addr, label);
settings_->save();
}
}
void App::setAddressIcon(const std::string& addr, const std::string& icon)
{
if (settings_) {
settings_->setAddressIcon(addr, icon);
settings_->save();
}
}
std::string App::getAddressLabel(const std::string& addr) const
{
if (!settings_) return "";
return settings_->getAddressMeta(addr).label;
}
std::string App::getAddressIcon(const std::string& addr) const
{
if (!settings_) return "";
return settings_->getAddressMeta(addr).icon;
}
int App::getAddressSortOrder(const std::string& addr) const
{
if (!settings_) return -1;
return settings_->getAddressMeta(addr).sortOrder;
}
void App::setAddressSortOrder(const std::string& addr, int order)
{
if (settings_) {
settings_->setAddressSortOrder(addr, order);
settings_->save();
}
}
int App::getNextSortOrder() const
{
return settings_ ? settings_->getNextSortOrder() : 0;
}
void App::swapAddressOrder(const std::string& a, const std::string& b)
{
if (settings_) {
settings_->swapAddressOrder(a, b);
settings_->save();
}
}
// ============================================================================
// Key Export/Import Operations
// ============================================================================

View File

@@ -126,6 +126,19 @@ bool Settings::load(const std::string& path)
for (const auto& a : j["favorite_addresses"])
if (a.is_string()) favorite_addresses_.insert(a.get<std::string>());
}
if (j.contains("address_meta") && j["address_meta"].is_object()) {
address_meta_.clear();
for (auto& [addr, meta] : j["address_meta"].items()) {
AddressMeta m;
if (meta.contains("label") && meta["label"].is_string())
m.label = meta["label"].get<std::string>();
if (meta.contains("icon") && meta["icon"].is_string())
m.icon = meta["icon"].get<std::string>();
if (meta.contains("order") && meta["order"].is_number_integer())
m.sortOrder = meta["order"].get<int>();
address_meta_[addr] = m;
}
}
if (j.contains("wizard_completed")) wizard_completed_ = j["wizard_completed"].get<bool>();
if (j.contains("auto_lock_timeout")) auto_lock_timeout_ = j["auto_lock_timeout"].get<int>();
if (j.contains("unlock_duration")) unlock_duration_ = j["unlock_duration"].get<int>();
@@ -230,6 +243,18 @@ bool Settings::save(const std::string& path)
j["favorite_addresses"] = json::array();
for (const auto& addr : favorite_addresses_)
j["favorite_addresses"].push_back(addr);
{
json meta_obj = json::object();
for (const auto& [addr, m] : address_meta_) {
if (m.label.empty() && m.icon.empty() && m.sortOrder < 0) continue;
json entry = json::object();
if (!m.label.empty()) entry["label"] = m.label;
if (!m.icon.empty()) entry["icon"] = m.icon;
if (m.sortOrder >= 0) entry["order"] = m.sortOrder;
meta_obj[addr] = entry;
}
j["address_meta"] = meta_obj;
}
j["wizard_completed"] = wizard_completed_;
j["auto_lock_timeout"] = auto_lock_timeout_;
j["unlock_duration"] = unlock_duration_;

View File

@@ -5,6 +5,7 @@
#pragma once
#include <algorithm>
#include <map>
#include <string>
#include <set>
#include <vector>
@@ -141,6 +142,39 @@ public:
void unfavoriteAddress(const std::string& addr) { favorite_addresses_.erase(addr); }
int getFavoriteAddressCount() const { return (int)favorite_addresses_.size(); }
// Address metadata (labels, icons, custom ordering)
struct AddressMeta {
std::string label;
std::string icon; // material icon name, e.g. "savings"
int sortOrder = -1; // -1 = auto (use default sort)
};
const AddressMeta& getAddressMeta(const std::string& addr) const {
static const AddressMeta empty{};
auto it = address_meta_.find(addr);
return it != address_meta_.end() ? it->second : empty;
}
void setAddressLabel(const std::string& addr, const std::string& label) {
address_meta_[addr].label = label;
}
void setAddressIcon(const std::string& addr, const std::string& icon) {
address_meta_[addr].icon = icon;
}
void setAddressSortOrder(const std::string& addr, int order) {
address_meta_[addr].sortOrder = order;
}
int getNextSortOrder() const {
int mx = -1;
for (const auto& [k, v] : address_meta_)
if (v.sortOrder > mx) mx = v.sortOrder;
return mx + 1;
}
void swapAddressOrder(const std::string& a, const std::string& b) {
int oa = address_meta_[a].sortOrder;
int ob = address_meta_[b].sortOrder;
address_meta_[a].sortOrder = ob;
address_meta_[b].sortOrder = oa;
}
// First-run wizard
bool getWizardCompleted() const { return wizard_completed_; }
void setWizardCompleted(bool v) { wizard_completed_ = v; }
@@ -301,6 +335,7 @@ private:
bool scanline_enabled_ = true;
std::set<std::string> hidden_addresses_;
std::set<std::string> favorite_addresses_;
std::map<std::string, AddressMeta> address_meta_;
bool wizard_completed_ = false;
int auto_lock_timeout_ = 900; // 15 minutes
int unlock_duration_ = 600; // 10 minutes

View File

@@ -0,0 +1,184 @@
// DragonX Wallet - ImGui Edition
// Copyright 2024-2026 The Hush Developers
// Released under the GPLv3
#pragma once
#include <string>
#include <cstring>
#include "../../app.h"
#include "../../util/i18n.h"
#include "../material/draw_helpers.h"
#include "../theme.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
namespace dragonx {
namespace ui {
class AddressLabelDialog {
public:
static void show(App* app, const std::string& address, bool isZ) {
s_open = true;
s_app = app;
s_address = address;
s_isZ = isZ;
s_selectedIcon = -1;
// Pre-fill from existing metadata
std::string existing = app->getAddressLabel(address);
std::strncpy(s_label, existing.c_str(), sizeof(s_label) - 1);
s_label[sizeof(s_label) - 1] = '\0';
std::string existingIcon = app->getAddressIcon(address);
for (int i = 0; i < kIconCount; ++i) {
if (kIconNames[i] == existingIcon) {
s_selectedIcon = i;
break;
}
}
}
static void render() {
if (!s_open || !s_app) return;
using namespace material;
if (BeginOverlayDialog(TR("set_label"), &s_open, 420.0f, 0.92f)) {
float dp = Layout::dpiScale();
// Address preview
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(),
s_isZ ? TR("shielded_address") : TR("transparent_address"));
ImGui::Spacing();
{
std::string display = s_address;
if (display.size() > 42) display = display.substr(0, 20) + "..." + display.substr(display.size() - 16);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), display.c_str());
}
ImGui::Spacing();
ImGui::Spacing();
// Label input
Type().text(TypeStyle::Subtitle2, TR("label"));
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##AddrLabel", TR("label_placeholder"), s_label, sizeof(s_label));
ImGui::Spacing();
ImGui::Spacing();
// Icon picker grid
Type().text(TypeStyle::Subtitle2, TR("choose_icon"));
ImGui::Spacing();
ImFont* iconFont = Type().iconMed();
float iconFsz = ScaledFontSize(iconFont);
float cellSz = iconFsz + 16.0f * dp;
float avail = ImGui::GetContentRegionAvail().x;
int cols = std::max(1, (int)(avail / (cellSz + 4.0f * dp)));
ImDrawList* dl = ImGui::GetWindowDrawList();
for (int i = 0; i < kIconCount; ++i) {
if (i % cols != 0) ImGui::SameLine(0, 4.0f * dp);
ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 mn = pos;
ImVec2 mx(pos.x + cellSz, pos.y + cellSz);
bool hov = ImGui::IsMouseHoveringRect(mn, mx);
bool sel = (s_selectedIcon == i);
// Background
if (sel) {
dl->AddRectFilled(mn, mx, WithAlpha(Primary(), 40), 6.0f * dp);
dl->AddRect(mn, mx, WithAlpha(Primary(), 120), 6.0f * dp, 0, 1.5f * dp);
} else if (hov) {
dl->AddRectFilled(mn, mx, IM_COL32(255, 255, 255, 20), 6.0f * dp);
}
// Icon centered in cell
ImVec2 iSz = iconFont->CalcTextSizeA(iconFsz, 1000.0f, 0.0f, kIconGlyphs[i]);
dl->AddText(iconFont, iconFsz,
ImVec2(mn.x + (cellSz - iSz.x) * 0.5f, mn.y + (cellSz - iSz.y) * 0.5f),
sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()),
kIconGlyphs[i]);
ImGui::PushID(i);
ImGui::InvisibleButton("##icon", ImVec2(cellSz, cellSz));
if (ImGui::IsItemClicked()) s_selectedIcon = i;
if (hov) ImGui::SetTooltip("%s", kIconNames[i]);
ImGui::PopID();
}
// "No icon" option
ImGui::Spacing();
if (s_selectedIcon >= 0) {
if (ImGui::SmallButton(TR("clear_icon"))) {
s_selectedIcon = -1;
}
}
ImGui::Spacing();
ImGui::Spacing();
ImGui::Separator();
ImGui::Spacing();
// Buttons
float btnW = 120.0f;
float totalW = btnW * 2 + Layout::spacingMd();
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - totalW) * 0.5f);
if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
s_open = false;
}
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("save"), ImVec2(btnW, 0))) {
// Apply changes
s_app->setAddressLabel(s_address, s_label);
if (s_selectedIcon >= 0)
s_app->setAddressIcon(s_address, kIconNames[s_selectedIcon]);
else
s_app->setAddressIcon(s_address, "");
s_open = false;
}
EndOverlayDialog();
}
}
static bool isOpen() { return s_open; }
private:
static inline bool s_open = false;
static inline App* s_app = nullptr;
static inline std::string s_address;
static inline bool s_isZ = false;
static inline char s_label[128] = {};
static inline int s_selectedIcon = -1;
// Icon palette — wallet-relevant Material Design icons
static constexpr int kIconCount = 20;
static inline const char* kIconNames[kIconCount] = {
"savings", "account_balance", "wallet", "payments",
"diamond", "shield", "lock", "swap_horiz",
"store", "home", "work", "rocket_launch",
"favorite", "bolt", "token", "category",
"label", "coffee", "volunteer", "star"
};
static inline const char* kIconGlyphs[kIconCount] = {
ICON_MD_SAVINGS, ICON_MD_ACCOUNT_BALANCE, ICON_MD_WALLET, ICON_MD_PAYMENTS,
ICON_MD_DIAMOND, ICON_MD_SHIELD, ICON_MD_LOCK, ICON_MD_SWAP_HORIZ,
ICON_MD_STORE, ICON_MD_HOME, ICON_MD_WORK, ICON_MD_ROCKET_LAUNCH,
ICON_MD_FAVORITE, ICON_MD_BOLT, ICON_MD_TOKEN, ICON_MD_CATEGORY,
ICON_MD_LABEL, ICON_MD_LOCAL_CAFE, ICON_MD_VOLUNTEER_ACTIVISM, ICON_MD_STAR
};
public:
// Expose for the address list to look up icon glyphs by name
static const char* iconGlyphForName(const std::string& name) {
for (int i = 0; i < kIconCount; ++i)
if (kIconNames[i] == name) return kIconGlyphs[i];
return nullptr;
}
};
} // namespace ui
} // namespace dragonx

View File

@@ -0,0 +1,293 @@
// 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;
// Pre-fill amount with full source balance
snprintf(s_amount, sizeof(s_amount), "%.8f", info.fromBalance);
// Default fee
s_fee = 0.0001;
}
static void render() {
if (!s_open || !s_app) return;
using namespace material;
if (BeginOverlayDialog(TR("transfer_funds"), &s_open, 480.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
Type().text(TypeStyle::Subtitle2, TR("amount"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##TransferAmt", s_amount, sizeof(s_amount),
ImGuiInputTextFlags_CharsDecimal);
// Max button
ImGui::SameLine();
if (ImGui::SmallButton(TR("max"))) {
double maxAmt = s_info.fromBalance - s_fee;
if (maxAmt < 0) maxAmt = 0;
snprintf(s_amount, sizeof(s_amount), "%.8f", maxAmt);
}
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), "%s: %.8f DRGX → %.8f DRGX",
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), "%s: %.8f DRGX → %.8f DRGX",
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
float btnW = 140.0f;
float totalW = btnW * 2 + Layout::spacingMd();
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - totalW) * 0.5f);
if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
s_open = false;
}
ImGui::SameLine(0, Layout::spacingMd());
ImGui::BeginDisabled(!amountValid || s_sending);
if (TactileButton(s_sending ? TR("sending") : TR("confirm_transfer"), ImVec2(btnW, 0))) {
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 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)
float textX = mn.x + pad + iSz.x + 8.0f * dp;
std::string display = addr;
if (display.size() > 42) display = display.substr(0, 18) + "..." + display.substr(display.size() - 14);
ImFont* capFont = Type().caption();
float capFsz = ScaledFontSize(capFont);
dl->AddText(capFont, capFsz, ImVec2(textX, mn.y + (h * 0.5f - capFsz)), OnSurfaceMedium(), display.c_str());
// Balance (right-aligned)
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);
dl->AddText(body, bodyFsz, ImVec2(mx.x - pad - balSz.x, mn.y + (h - balSz.y) * 0.5f),
balance > 0 ? OnSurface() : OnSurfaceDisabled(), balBuf);
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

View File

@@ -5,6 +5,8 @@
#include "balance_tab.h"
#include "key_export_dialog.h"
#include "qr_popup_dialog.h"
#include "address_label_dialog.h"
#include "address_transfer_dialog.h"
#include "send_tab.h"
#include "../../app.h"
#include "../../config/settings.h"
@@ -1360,50 +1362,70 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
Type().text(TypeStyle::H6, TR("your_addresses"));
ImGui::Spacing();
// ---- Persistent state ----
static char addr_search[128] = "";
static bool s_hideZeroBalances = true;
static bool s_showHidden = false;
struct AddrRow { const AddressInfo* info; bool isZ; bool hidden; bool favorite; };
// Drag state
static int s_dragIdx = -1; // row being dragged (-1 = none)
static float s_dragOffsetY = 0.0f; // mouse offset from row top
static float s_dragStartY = 0.0f; // mouse Y at drag start
static bool s_dragActive = false; // drag distance threshold passed
static int s_dropTargetIdx = -1; // row hovered during drag
// Copy feedback
static int s_copiedRow = -1;
static float s_copiedTime = 0.0f;
// ---- Build and filter address rows ----
struct AddrRow {
const AddressInfo* info;
bool isZ, hidden, favorite;
std::string label, icon;
int sortOrder;
};
std::vector<AddrRow> rows;
rows.reserve(state.z_addresses.size() + state.t_addresses.size());
for (const auto& a : state.z_addresses) {
std::string filter(addr_search);
if (!containsIgnoreCase(a.address, filter) &&
!containsIgnoreCase(a.label, filter)) continue;
bool isHidden = app->isAddressHidden(a.address);
if (isHidden && !s_showHidden) continue;
bool isFav = app->isAddressFavorite(a.address);
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav) continue;
rows.push_back({&a, true, isHidden, isFav});
}
for (const auto& a : state.t_addresses) {
std::string filter(addr_search);
if (!containsIgnoreCase(a.address, filter) &&
!containsIgnoreCase(a.label, filter)) continue;
bool isHidden = app->isAddressHidden(a.address);
if (isHidden && !s_showHidden) continue;
bool isFav = app->isAddressFavorite(a.address);
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav) continue;
rows.push_back({&a, false, isHidden, isFav});
}
static int s_sortCol = 3;
static bool s_sortAsc = false;
auto addRows = [&](const auto& addrs, bool isZ) {
for (const auto& a : addrs) {
std::string filter(addr_search);
std::string addrLabel = app->getAddressLabel(a.address);
if (!containsIgnoreCase(a.address, filter) &&
!containsIgnoreCase(a.label, filter) &&
!containsIgnoreCase(addrLabel, filter)) continue;
bool isHidden = app->isAddressHidden(a.address);
if (isHidden && !s_showHidden) continue;
bool isFav = app->isAddressFavorite(a.address);
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav) continue;
rows.push_back({&a, isZ, isHidden, isFav,
addrLabel, app->getAddressIcon(a.address),
app->getAddressSortOrder(a.address)});
}
};
addRows(state.z_addresses, true);
addRows(state.t_addresses, false);
// Sort: custom order (if any) → favorites → Z first → balance desc
std::sort(rows.begin(), rows.end(),
[](const AddrRow& a, const AddrRow& b) -> bool {
bool aHasOrder = a.sortOrder >= 0;
bool bHasOrder = b.sortOrder >= 0;
if (aHasOrder && bHasOrder) return a.sortOrder < b.sortOrder;
if (aHasOrder != bHasOrder) return aHasOrder > bHasOrder;
if (a.favorite != b.favorite) return a.favorite > b.favorite;
if (a.isZ != b.isZ) return a.isZ > b.isZ;
if (s_sortAsc) return a.info->balance < b.info->balance;
else return a.info->balance > b.info->balance;
return a.info->balance > b.info->balance;
});
// Search + create buttons row
// ---- Toolbar: search + checkboxes + create buttons ----
float avail = ImGui::GetContentRegionAvail().x;
float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f;
float schemaRatio = (searchIn.widthRatio >= 0) ? searchIn.widthRatio : 0.30f;
float searchW = std::min(schemaMaxW * hs, avail * schemaRatio);
ImGui::SetNextItemWidth(searchW);
ImGui::InputTextWithHint("##AddrSearch", "Filter...", addr_search, sizeof(addr_search));
ImGui::InputTextWithHint("##AddrSearch", TR("filter"), addr_search, sizeof(addr_search));
ImGui::SameLine(0, Layout::spacingLg());
ImGui::Checkbox(TrId("hide_zero_balances", "hide0_v2").c_str(), &s_hideZeroBalances);
{
@@ -1455,6 +1477,7 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// ---- Glass panel container ----
float addrListH = listH;
if (addrListH < 40.0f * dp) addrListH = 40.0f * dp;
@@ -1477,6 +1500,7 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// ---- Empty states ----
if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 16 * dp));
float cw = ImGui::GetContentRegionAvail().x;
@@ -1487,23 +1511,17 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
float cw = ImGui::GetContentRegionAvail().x;
float ch = ImGui::GetContentRegionAvail().y;
if (ch < 60) ch = 60;
if (addr_search[0]) {
ImVec2 textSz = ImGui::CalcTextSize(TR("no_addresses_match"));
ImGui::SetCursorPosX((cw - textSz.x) * 0.5f);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
ImGui::TextDisabled("%s", TR("no_addresses_match"));
} else {
const char* msg = "No addresses yet";
ImVec2 msgSz = ImGui::CalcTextSize(msg);
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
ImGui::TextDisabled("%s", msg);
}
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
ImGui::TextDisabled("%s", emptyMsg);
} else {
// ---- PASS 1: Compute row layout ----
ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* capFont = Type().caption();
ImFont* body2 = Type().body2();
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd() + Layout::spacingMd();
static int selected_row = -1;
addrScrollY = ImGui::GetScrollY();
addrScrollMaxY = ImGui::GetScrollMaxY();
@@ -1515,181 +1533,345 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
S.drawElement("tabs.balance", "address-icon-size").size * hs);
float innerW = ImGui::GetContentRegionAvail().x;
// Theme colors for buttons (resolved once)
ImU32 favGoldFill = S.resolveColor("var(--favorite-fill)", IM_COL32(255, 200, 50, 40));
ImU32 favGoldBorder = S.resolveColor("var(--favorite-border)", IM_COL32(255, 200, 50, 100));
ImU32 favGoldIcon = S.resolveColor("var(--favorite-icon)", IM_COL32(255, 200, 50, 255));
ImU32 favGoldOutline= S.resolveColor("var(--favorite-outline)", IM_COL32(255, 200, 50, 120));
ImU32 btnFill = S.resolveColor("var(--row-button-fill)", IM_COL32(255, 255, 255, 12));
ImU32 btnFillHov = S.resolveColor("var(--row-button-fill-hover)", IM_COL32(255, 255, 255, 25));
ImU32 btnBorder = S.resolveColor("var(--row-button-border)", IM_COL32(255, 255, 255, 25));
ImU32 btnBorderHov = S.resolveColor("var(--row-button-border-hover)", IM_COL32(255, 255, 255, 50));
ImU32 rowHoverCol = S.resolveColor("var(--row-hover)", IM_COL32(255, 255, 255, 15));
ImU32 rowSelectCol = S.resolveColor("var(--row-select)", IM_COL32(255, 255, 255, 20));
ImU32 dividerCol = S.resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15));
// Compute Y positions for all rows (pass 1)
std::vector<float> rowY(rows.size());
float cursorStartY = ImGui::GetCursorScreenPos().y;
for (int i = 0; i < (int)rows.size(); ++i)
rowY[i] = cursorStartY + i * rowH;
// Drag logic — detect drag start, compute drag position
ImVec2 mousePos = ImGui::GetMousePos();
bool mouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
bool mouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
// Reset drop target each frame — it gets set only if mouse is over a row
if (s_dragActive) s_dropTargetIdx = -1;
if (s_dragIdx >= 0 && !mouseDown) {
// Mouse released — copy if it was a click (no drag activated)
if (!s_dragActive && s_dragIdx >= 0 && s_dragIdx < (int)rows.size()) {
const auto& clickRow = rows[s_dragIdx];
ImGui::SetClipboardText(clickRow.info->address.c_str());
selected_row = s_dragIdx;
s_copiedRow = s_dragIdx;
s_copiedTime = (float)ImGui::GetTime();
}
// Drop
if (s_dragActive && s_dropTargetIdx >= 0 && s_dropTargetIdx != s_dragIdx) {
const auto& srcRow = rows[s_dragIdx];
const auto& dstRow = rows[s_dropTargetIdx];
// Transfer: if dropped on another row and drag was active
if (srcRow.info->balance > 1e-9) {
AddressTransferDialog::TransferInfo ti;
ti.fromAddr = srcRow.info->address;
ti.toAddr = dstRow.info->address;
ti.fromBalance = srcRow.info->balance;
ti.toBalance = dstRow.info->balance;
ti.fromIsZ = srcRow.isZ;
ti.toIsZ = dstRow.isZ;
AddressTransferDialog::show(app, ti);
}
} else if (s_dragActive && s_dropTargetIdx < 0) {
// Reorder: dropped in gap — compute insert position from mouseY
int insertIdx = 0;
for (int i = 0; i < (int)rows.size(); ++i) {
if (mousePos.y > rowY[i] + rowH * 0.5f) insertIdx = i + 1;
}
if (insertIdx != s_dragIdx && insertIdx != s_dragIdx + 1) {
int targetIdx = (insertIdx > s_dragIdx) ? insertIdx - 1 : insertIdx;
if (targetIdx >= 0 && targetIdx < (int)rows.size()) {
app->swapAddressOrder(rows[s_dragIdx].info->address,
rows[targetIdx].info->address);
}
}
}
s_dragIdx = -1;
s_dragActive = false;
s_dropTargetIdx = -1;
}
// ---- PASS 2: Render rows ----
for (int row_idx = 0; row_idx < (int)rows.size(); row_idx++) {
const auto& row = rows[row_idx];
const auto& addr = *row.info;
ImVec2 rowPos = ImGui::GetCursorScreenPos();
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
bool isDragged = (s_dragIdx == row_idx && s_dragActive);
// For dragged row, offset Y to follow mouse
float drawY = rowY[row_idx];
if (isDragged) {
drawY = mousePos.y - s_dragOffsetY;
}
ImVec2 rowPos(ImGui::GetCursorScreenPos().x, drawY);
ImVec2 rowEnd(rowPos.x + innerW, drawY + rowH);
ImU32 typeCol = row.isZ ? greenCol : goldCol;
if (row.hidden) typeCol = OnSurfaceDisabled();
// Golden border for favorites
if (row.favorite) {
ImU32 favBorder = IM_COL32(255, 200, 50, 120);
dl->AddRect(rowPos, rowEnd, favBorder, 4.0f * dp, 0, 1.5f * dp);
// Dragged row: draw with semi-transparent elevation
if (isDragged) {
dl->AddRectFilled(ImVec2(rowPos.x - 2*dp, rowPos.y - 2*dp),
ImVec2(rowEnd.x + 2*dp, rowEnd.y + 2*dp),
IM_COL32(0, 0, 0, 30), 6.0f * dp);
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(30, 30, 40, 120), 4.0f * dp);
// Tooltip following cursor — show transfer intent if over a target row
if (s_dropTargetIdx >= 0 && s_dropTargetIdx < (int)rows.size()) {
const auto& target = rows[s_dropTargetIdx];
ImGui::SetTooltip("%s\n%s\n\n%s %s",
truncateAddress(addr.address, 32).c_str(),
row.isZ ? TR("shielded") : TR("transparent"),
TR("transfer_to"),
truncateAddress(target.info->address, 32).c_str());
} else {
ImGui::SetTooltip("%s\n%s",
truncateAddress(addr.address, 32).c_str(),
row.isZ ? TR("shielded") : TR("transparent"));
}
}
if (selected_row == row_idx) {
// Golden border for favorites — inset so it doesn't touch container
if (row.favorite) {
float inset = 2.0f * dp;
dl->AddRect(ImVec2(rowPos.x + inset, rowPos.y + inset),
ImVec2(rowEnd.x - inset, rowEnd.y - inset),
favGoldOutline, 4.0f * dp, 0, 1.5f * dp);
}
// Selection highlight
if (selected_row == row_idx && !isDragged) {
ImDrawFlags accentFlags = 0;
float accentRound = 2.0f * dp;
if (row_idx == 0) { accentFlags = ImDrawFlags_RoundCornersTopLeft; accentRound = glassRound; }
if (row_idx == (int)rows.size() - 1) { accentFlags |= ImDrawFlags_RoundCornersBottomLeft; accentRound = glassRound; }
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + 3 * dp, rowEnd.y), typeCol, accentRound, accentFlags);
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), 4.0f * dp);
dl->AddRectFilled(rowPos, rowEnd, rowSelectCol, 4.0f * dp);
}
bool hovered = material::IsRectHovered(rowPos, rowEnd);
if (hovered && selected_row != row_idx) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), 4.0f * dp);
// Hover effects
bool hovered = !isDragged && material::IsRectHovered(rowPos, rowEnd);
// Drop target highlight when dragging another row over this one
if (s_dragActive && s_dragIdx != row_idx && material::IsRectHovered(rowPos, rowEnd)) {
s_dropTargetIdx = row_idx;
ImU32 dropCol = WithAlpha(Primary(), 40);
dl->AddRectFilled(rowPos, rowEnd, dropCol, 4.0f * dp);
dl->AddRect(rowPos, rowEnd, WithAlpha(Primary(), 140), 4.0f * dp, 0, 2.0f * dp);
}
if (hovered && selected_row != row_idx && !s_dragActive) {
dl->AddRectFilled(rowPos, rowEnd, rowHoverCol, 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
float cx = rowPos.x + rowPadLeft;
float cy = rowPos.y + Layout::spacingMd();
// --- Button zone (right edge): [eye] [star] ---
float btnH = rowH - Layout::spacingSm() * 2.0f;
// ---- Button zone (right edge): [eye] [star] ----
float btnH = rowH - Layout::spacingMd() * 2.0f;
float btnW = btnH;
float btnGap = Layout::spacingSm();
float btnY = rowPos.y + (rowH - btnH) * 0.5f;
float rightEdge = rowPos.x + innerW;
float starX = rightEdge - btnW - rowPadRight;
float starX = rightEdge - btnW - Layout::spacingXs();
float eyeX = starX - btnGap - btnW;
float btnRound = 6.0f * dp;
bool btnClicked = false;
// Star button (always shown, rightmost)
{
ImVec2 bMin(starX, btnY), bMax(starX + btnW, btnY + btnH);
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
ImU32 starFill = row.favorite ? IM_COL32(255, 200, 50, 40) : IM_COL32(255, 255, 255, bHov ? 25 : 12);
ImU32 starBorder = row.favorite ? IM_COL32(255, 200, 50, 100) : IM_COL32(255, 255, 255, bHov ? 50 : 25);
dl->AddRectFilled(bMin, bMax, starFill, btnRound);
dl->AddRect(bMin, bMax, starBorder, btnRound, 0, 1.0f * dp);
ImFont* iconFont = material::Type().iconSmall();
const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
ImU32 starCol = row.favorite ? IM_COL32(255, 200, 50, 255) : (bHov ? OnSurface() : OnSurfaceDisabled());
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(starX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), starCol, starIcon);
if (bHov && ImGui::IsMouseClicked(0)) {
if (row.favorite) app->unfavoriteAddress(addr.address);
else app->favoriteAddress(addr.address);
btnClicked = true;
if (!isDragged) {
// Star button
{
ImVec2 bMin(starX, btnY), bMax(starX + btnW, btnY + btnH);
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
dl->AddRectFilled(bMin, bMax, row.favorite ? favGoldFill : (bHov ? btnFillHov : btnFill), btnRound);
dl->AddRect(bMin, bMax, row.favorite ? favGoldBorder : (bHov ? btnBorderHov : btnBorder), btnRound, 0, 1.0f * dp);
ImFont* iconFont = Type().iconSmall();
const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
ImU32 starCol = row.favorite ? favGoldIcon : (bHov ? OnSurface() : OnSurfaceDisabled());
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(starX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), starCol, starIcon);
if (bHov && mouseClicked) {
if (row.favorite) app->unfavoriteAddress(addr.address);
else app->favoriteAddress(addr.address);
btnClicked = true;
}
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
}
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
}
// Eye button (zero balance or hidden)
if (addr.balance < 1e-9 || row.hidden) {
ImVec2 bMin(eyeX, btnY), bMax(eyeX + btnW, btnY + btnH);
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
ImU32 eyeFill = IM_COL32(255, 255, 255, bHov ? 25 : 12);
ImU32 eyeBorder = IM_COL32(255, 255, 255, bHov ? 50 : 25);
dl->AddRectFilled(bMin, bMax, eyeFill, btnRound);
dl->AddRect(bMin, bMax, eyeBorder, btnRound, 0, 1.0f * dp);
ImFont* iconFont = material::Type().iconSmall();
const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(eyeX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), iconCol, hideIcon);
if (bHov && ImGui::IsMouseClicked(0)) {
if (row.hidden) app->unhideAddress(addr.address);
else app->hideAddress(addr.address);
btnClicked = true;
// Eye button (zero balance or hidden)
bool showEye = true;
// Always reserve space for both buttons so content doesn't shift
float contentRight = eyeX - Layout::spacingSm();
if (showEye) {
ImVec2 bMin(eyeX, btnY), bMax(eyeX + btnW, btnY + btnH);
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
dl->AddRectFilled(bMin, bMax, bHov ? btnFillHov : btnFill, btnRound);
dl->AddRect(bMin, bMax, bHov ? btnBorderHov : btnBorder, btnRound, 0, 1.0f * dp);
ImFont* iconFont = Type().iconSmall();
const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(eyeX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), iconCol, hideIcon);
if (bHov && mouseClicked) {
if (row.hidden) app->unhideAddress(addr.address);
else app->hideAddress(addr.address);
btnClicked = true;
}
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
}
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
}
// Content zone ends before buttons
float contentRight = eyeX - Layout::spacingSm();
// ---- Type icon or custom icon ----
float iconCx = cx + rowIconSz;
float iconCy = cy + body2->LegacySize * 0.5f;
{
const char* customGlyph = row.icon.empty() ? nullptr : AddressLabelDialog::iconGlyphForName(row.icon);
ImFont* iconFont = Type().iconSmall();
const char* glyph = customGlyph ? customGlyph : (row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE);
ImU32 icCol = customGlyph ? OnSurfaceMedium() : typeCol;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, glyph);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), icCol, glyph);
}
float iconCx = cx + rowIconSz;
float iconCy = cy + body2->LegacySize * 0.5f;
if (row.isZ) {
ImFont* iconFont = material::Type().iconSmall();
const char* shieldIcon = ICON_MD_SHIELD;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, shieldIcon);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, shieldIcon);
} else {
ImFont* iconFont = material::Type().iconSmall();
const char* circIcon = ICON_MD_CIRCLE;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, circIcon);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, circIcon);
}
// ---- Type label (first line) ----
float labelX = cx + rowIconSz * 2.0f + Layout::spacingMd();
{
const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
char typeBuf[64];
snprintf(typeBuf, sizeof(typeBuf), "%s%s", typeLabel, hiddenTag);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
const char* typeLabel = row.isZ ? "Shielded" : "Transparent";
const char* hiddenTag = row.hidden ? " (hidden)" : "";
char typeBuf[64];
snprintf(typeBuf, sizeof(typeBuf), "%s%s", typeLabel, hiddenTag);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
if (!addr.label.empty()) {
float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy),
OnSurfaceMedium(), addr.label.c_str());
}
// User label next to type
if (!row.label.empty()) {
float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy),
OnSurfaceMedium(), row.label.c_str());
}
}
// Address (second line) — show full if it fits, otherwise truncate
float addrAvailW = contentRight - labelX;
ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str());
std::string display_addr;
if (fullAddrSz.x <= addrAvailW) {
display_addr = addr.address;
} else {
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0)
? addrTable.columns.at("address").truncate : 32;
display_addr = truncateAddress(addr.address, addrTruncLen);
}
dl->AddText(capFont, capFont->LegacySize,
ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceMedium(), display_addr.c_str());
// ---- Address (second line) ----
{
float addrAvailW = contentRight - labelX;
ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str());
std::string display_addr;
if (fullAddrSz.x <= addrAvailW) {
display_addr = addr.address;
} else {
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0)
? addrTable.columns.at("address").truncate : 32;
display_addr = truncateAddress(addr.address, addrTruncLen);
}
dl->AddText(capFont, capFont->LegacySize,
ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceMedium(), display_addr.c_str());
}
// Balance (right-aligned within content zone)
char balBuf[32];
snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance);
ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf);
float balX = contentRight - balSz.x;
ImU32 balCol = addr.balance > 0.0
? (row.isZ ? greenCol : OnSurface())
: OnSurfaceDisabled();
if (row.hidden) balCol = OnSurfaceDisabled();
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf);
// ---- Balance (right-aligned, first line) ----
{
char balBuf[32];
snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance);
ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf);
float balX = contentRight - balSz.x;
ImU32 balCol = addr.balance > 0.0
? (row.isZ ? greenCol : OnSurface())
: OnSurfaceDisabled();
if (row.hidden) balCol = OnSurfaceDisabled();
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf);
}
double priceUsd = state.market.price_usd;
if (priceUsd > 0.0 && addr.balance > 0.0) {
char usdBuf[32];
double usdVal = addr.balance * priceUsd;
if (usdVal >= 1.0) snprintf(usdBuf, sizeof(usdBuf), "$%.2f", usdVal);
else if (usdVal >= 0.01) snprintf(usdBuf, sizeof(usdBuf), "$%.4f", usdVal);
else snprintf(usdBuf, sizeof(usdBuf), "$%.6f", usdVal);
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdBuf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(contentRight - usdSz.x,
cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceDisabled(), usdBuf);
}
// ---- USD value (second line, right-aligned) ----
{
double priceUsd = state.market.price_usd;
if (priceUsd > 0.0 && addr.balance > 0.0) {
char usdBuf[32];
double usdVal = addr.balance * priceUsd;
if (usdVal >= 1.0) snprintf(usdBuf, sizeof(usdBuf), "$%.2f", usdVal);
else if (usdVal >= 0.01) snprintf(usdBuf, sizeof(usdBuf), "$%.4f", usdVal);
else snprintf(usdBuf, sizeof(usdBuf), "$%.6f", usdVal);
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdBuf);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(contentRight - usdSz.x,
cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceDisabled(), usdBuf);
}
}
if (hovered && ImGui::IsMouseClicked(0) && !btnClicked) {
ImGui::SetClipboardText(addr.address.c_str());
selected_row = row_idx;
}
// ---- Copy feedback flash ----
if (s_copiedRow == row_idx) {
float elapsed = (float)ImGui::GetTime() - s_copiedTime;
if (elapsed < 1.0f) {
float alpha = 1.0f - elapsed;
ImFont* capF = Type().caption();
const char* copiedTxt = TR("copied");
ImVec2 sz = capF->CalcTextSizeA(capF->LegacySize, 1000.0f, 0.0f, copiedTxt);
ImVec4 fc = ImGui::ColorConvertU32ToFloat4(Success());
fc.w = alpha;
float tx = rowPos.x + (innerW - sz.x) * 0.5f;
float ty = rowPos.y + (rowH - sz.y) * 0.5f;
dl->AddText(capF, capF->LegacySize, ImVec2(tx, ty),
ImGui::ColorConvertFloat4ToU32(fc), copiedTxt);
} else {
s_copiedRow = -1;
}
}
// ---- Click: begin drag tracking (copy deferred to mouse-up) ----
if (hovered && mouseClicked && !btnClicked) {
s_dragIdx = row_idx;
s_dragOffsetY = mousePos.y - rowPos.y;
s_dragStartY = mousePos.y;
s_dragActive = false;
}
// Activate drag after a small threshold
if (s_dragIdx == row_idx && !s_dragActive && mouseDown) {
if (std::abs(mousePos.y - s_dragStartY) > 6.0f * dp) {
s_dragActive = true;
s_dropTargetIdx = -1;
}
}
} // end !isDragged
// Advance cursor for interaction area
ImGui::PushID(row_idx);
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowY[row_idx]));
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
if (ImGui::IsItemHovered() && !btnClicked) ImGui::SetTooltip("%s", addr.address.c_str());
if (ImGui::IsItemHovered() && !s_dragActive) {
ImGui::SetTooltip("%s", addr.address.c_str());
}
// Context menu
const auto& acrTheme = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("AddressContext", 0, acrTheme.menu)) {
if (ImGui::MenuItem(TR("copy_address"))) ImGui::SetClipboardText(addr.address.c_str());
if (ImGui::MenuItem(TR("copy_address"))) {
ImGui::SetClipboardText(addr.address.c_str());
s_copiedRow = row_idx;
s_copiedTime = (float)ImGui::GetTime();
}
if (ImGui::MenuItem(TR("send_from_this_address"))) {
SetSendFromAddress(addr.address);
app->setCurrentPage(NavPage::Send);
}
ImGui::Separator();
if (ImGui::MenuItem(TR("set_label"))) {
AddressLabelDialog::show(app, addr.address, row.isZ);
}
if (ImGui::MenuItem(TR("export_private_key")))
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
if (row.isZ) {
@@ -1697,7 +1879,7 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Viewing);
}
if (ImGui::MenuItem(TR("show_qr_code")))
QRPopupDialog::show(addr.address, row.isZ ? "Z-Address" : "T-Address");
QRPopupDialog::show(addr.address, row.isZ ? TR("z_address") : TR("t_address"));
ImGui::Separator();
if (row.hidden) {
if (ImGui::MenuItem(TR("restore_address")))
@@ -1717,20 +1899,49 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
}
ImGui::PopID();
// Separator between rows
if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) {
ImVec2 divStart = ImGui::GetCursorScreenPos();
float sepY = rowY[row_idx] + rowH;
dl->AddLine(
ImVec2(divStart.x + rowPadLeft + rowIconSz * 2.0f, divStart.y),
ImVec2(divStart.x + innerW - Layout::spacingLg(), divStart.y),
IM_COL32(255, 255, 255, 15));
ImVec2(rowPos.x + rowPadLeft + rowIconSz * 2.0f, sepY),
ImVec2(rowPos.x + innerW - Layout::spacingLg(), sepY),
dividerCol);
}
}
// Advance cursor past all rows
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x,
cursorStartY + (float)rows.size() * rowH));
// ---- Keyboard navigation ----
if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl && !s_dragActive) {
bool nav = false;
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) {
if (selected_row > 0) { selected_row--; nav = true; }
else if (selected_row < 0 && !rows.empty()) { selected_row = 0; nav = true; }
}
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) {
if (selected_row < (int)rows.size() - 1) { selected_row++; nav = true; }
else if (selected_row < 0 && !rows.empty()) { selected_row = 0; nav = true; }
}
if (ImGui::IsKeyPressed(ImGuiKey_Enter) && selected_row >= 0 && selected_row < (int)rows.size()) {
ImGui::SetClipboardText(rows[selected_row].info->address.c_str());
s_copiedRow = selected_row;
s_copiedTime = (float)ImGui::GetTime();
}
if (ImGui::IsKeyPressed(ImGuiKey_F2) && selected_row >= 0 && selected_row < (int)rows.size()) {
const auto& r = rows[selected_row];
AddressLabelDialog::show(app, r.info->address, r.isZ);
}
(void)nav;
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::EndChild();
ImGui::PopStyleVar();
// Scroll fade
{
float fadeZone = std::min(
(Type().body2()->LegacySize + Type().caption()->LegacySize +
@@ -1740,11 +1951,12 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
listPanelMin.y, listPanelMax.y, fadeZone, addrScrollY, addrScrollMaxY);
}
// Address count
{
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
char countBuf[128];
int totalAddrs = (int)(state.z_addresses.size() + state.t_addresses.size());
snprintf(countBuf, sizeof(countBuf), "Showing %d of %d addresses",
snprintf(countBuf, sizeof(countBuf), TR("showing_x_of_y"),
(int)rows.size(), totalAddrs);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf);
}

View File

@@ -550,9 +550,13 @@ void RenderPeersTab(App* app)
float iconSz = iconFont->LegacySize;
const char* label = isRefreshing ? TR("peers_refreshing") : TR("peers_refresh");
ImVec2 lblSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0, label);
// Use the wider label so button doesn't resize between states
ImVec2 altSz = ovFont->CalcTextSizeA(ovFont->LegacySize, FLT_MAX, 0,
isRefreshing ? TR("peers_refresh") : TR("peers_refreshing"));
float maxLblW = std::max(lblSz.x, altSz.x);
float padH = Layout::spacingSm();
float padV = Layout::spacingSm();
float btnW = padH + iconSz + Layout::spacingXs() + lblSz.x + padH;
float btnW = padH + iconSz + Layout::spacingXs() + maxLblW + padH;
float btnH = std::max(iconSz, lblSz.y) + padV * 2;
float btnX = ImGui::GetWindowPos().x + availWidth - btnW - Layout::spacingSm();
float btnY = toggleY + (toggleH - btnH) * 0.5f;

View File

@@ -401,6 +401,32 @@ void I18n::loadBuiltinEnglish()
strings_["remove_favorite"] = "Remove Favorite";
strings_["favorite_address"] = "Favorite Address";
strings_["show_hidden"] = "Show Hidden (%d)";
strings_["filter"] = "Filter...";
strings_["no_addresses_yet"] = "No addresses yet";
strings_["showing_x_of_y"] = "Showing %d of %d addresses";
strings_["set_label"] = "Set Label...";
strings_["copied"] = "Copied!";
strings_["hidden_tag"] = " (hidden)";
strings_["z_address"] = "Z-Address";
strings_["t_address"] = "T-Address";
strings_["shielded_address"] = "Shielded Address";
strings_["transparent_address"] = "Transparent Address";
strings_["label_placeholder"] = "e.g. Savings, Mining...";
strings_["choose_icon"] = "Choose Icon";
strings_["clear_icon"] = "Clear Icon";
strings_["transfer_funds"] = "Transfer Funds";
strings_["transfer_to"] = "Transfer to:";
strings_["deshielding_warning"] = "Warning: This will de-shield funds from a private (Z) address to a transparent (T) address.";
strings_["shielding_notice"] = "Note: This will shield funds from a transparent (T) address to a private (Z) address.";
strings_["result_preview"] = "Result Preview";
strings_["sender_balance"] = "Sender: %.8f \xe2\x86\x92 %.8f DRGX";
strings_["recipient_balance"] = "Recipient: %.8f \xe2\x86\x92 %.8f DRGX";
strings_["insufficient_funds"] = "Insufficient funds for this amount plus fee.";
strings_["sends_full_balance_warning"] = "This sends the full balance. The sending address will have a zero balance.";
strings_["confirm_transfer"] = "Confirm Transfer";
strings_["transfer_sent"] = "Transfer Sent";
strings_["transfer_sent_desc"] = "Your transfer has been submitted to the network.";
strings_["transfer_failed"] = "Transfer Failed";
// Misc dialog strings
strings_["shield_operation_id"] = "Operation ID: %s";