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:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user