refactor(balance): extract shared rendering components into balance_components.{h,cpp} (audit #10)
First slice of decomposing balance_tab.cpp (3449 lines). The five rendering helpers used by every balance layout — UpdateBalanceLerp, RenderCompactHero, RenderSharedAddressList (599 lines, the drag-reorderable address list), RenderSharedRecentTx, RenderSyncBar — are moved verbatim into balance_components.cpp. balance_tab.cpp is now 2680 lines. Clean extraction: the helpers' interactive statics (drag/copy/hide/show) are function-local and move WITH them; the only file-scope state they share is the balance-lerp animation values (s_dispTotal/Shielded/Transparent/Unconfirmed) and s_generating_z_address, now non-static and declared `extern` in balance_components.h (defined once in balance_tab.cpp, so both TUs share the same objects). RenderCompactHero's default arg moved to the header declaration. The layouts (still in balance_tab.cpp) call the helpers via the new header. Verified: full-node + Windows + lite build (links cleanly -> extern state resolves), tests, hygiene. This touches every layout's address list / recent-tx / hero / sync bar, so needs a hands-on pass across the balance layouts before the next slice. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
818
src/ui/windows/balance_components.cpp
Normal file
818
src/ui/windows/balance_components.cpp
Normal file
@@ -0,0 +1,818 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
|
||||
#include "balance_components.h"
|
||||
#include "balance_tab_helpers.h"
|
||||
#include "balance_address_list.h"
|
||||
#include "balance_recent_tx.h"
|
||||
#include "key_export_dialog.h"
|
||||
#include "qr_popup_dialog.h"
|
||||
#include "address_label_dialog.h"
|
||||
#include "address_transfer_dialog.h"
|
||||
#include "mining_tab_helpers.h"
|
||||
#include "send_tab.h"
|
||||
|
||||
#include "../../app.h"
|
||||
#include "../../config/settings.h"
|
||||
#include "../../config/version.h"
|
||||
#include "../../util/i18n.h"
|
||||
#include "../../util/text_format.h"
|
||||
#include "../theme.h"
|
||||
#include "../layout.h"
|
||||
#include "../schema/ui_schema.h"
|
||||
#include "../material/type.h"
|
||||
#include "../material/draw_helpers.h"
|
||||
#include "../effects/imgui_acrylic.h"
|
||||
#include "../sidebar.h"
|
||||
#include "../notifications.h"
|
||||
#include "../../embedded/IconsMaterialDesign.h"
|
||||
#include "imgui.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstring>
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
void UpdateBalanceLerp(App* app) {
|
||||
using namespace material;
|
||||
const auto& S = schema::UISchema::instance();
|
||||
const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
|
||||
const auto& state = app->state();
|
||||
float dt = ImGui::GetIO().DeltaTime;
|
||||
float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed;
|
||||
auto lerp = [](double& disp, double target, float dt, float spd) {
|
||||
double diff = target - disp;
|
||||
if (std::abs(diff) < 1e-9) { disp = target; return; }
|
||||
disp += diff * (double)(dt * spd);
|
||||
if (std::abs(target - disp) < 1e-9) disp = target;
|
||||
};
|
||||
lerp(s_dispTotal, app->getTotalBalance(), dt, speed);
|
||||
lerp(s_dispShielded, app->getShieldedBalance(), dt, speed);
|
||||
lerp(s_dispTransparent, app->getTransparentBalance(), dt, speed);
|
||||
lerp(s_dispUnconfirmed, state.unconfirmed_balance, dt, speed);
|
||||
}
|
||||
|
||||
// Render compact hero line: logo + balance + USD + mining on one line
|
||||
void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs, float heroHeightOverride) {
|
||||
using namespace material;
|
||||
char buf[64];
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
// Coin logo
|
||||
ImTextureID logoTex = app->getCoinLogoTexture();
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
float logoSz = sub1->LegacySize + 4.0f * dp;
|
||||
float lineH = (heroHeightOverride >= 0.0f) ? heroHeightOverride : logoSz;
|
||||
if (logoTex != 0) {
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
dl->AddImage(logoTex,
|
||||
ImVec2(pos.x, pos.y),
|
||||
ImVec2(pos.x + lineH, pos.y + lineH),
|
||||
ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255));
|
||||
ImGui::Dummy(ImVec2(lineH + Layout::spacingSm(), lineH));
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
// Total balance
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
float fontSize = (heroHeightOverride >= 0.0f) ? heroHeightOverride : sub1->LegacySize;
|
||||
DrawTextShadow(dl, sub1, fontSize, pos, OnSurface(), buf);
|
||||
ImVec2 sz = sub1->CalcTextSizeA(fontSize, 10000.0f, 0.0f, buf);
|
||||
ImGui::Dummy(ImVec2(sz.x, lineH));
|
||||
ImGui::SameLine();
|
||||
|
||||
// Ticker
|
||||
ImFont* capFont = Type().caption();
|
||||
float tickerFontSize = (heroHeightOverride >= 0.0f) ? heroHeightOverride * 0.6f : capFont->LegacySize;
|
||||
float tickerY = pos.y + lineH - tickerFontSize;
|
||||
dl->AddText(capFont, tickerFontSize,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
|
||||
OnSurfaceMedium(), DRAGONX_TICKER);
|
||||
ImGui::Dummy(ImVec2(capFont->CalcTextSizeA(tickerFontSize, 10000, 0, DRAGONX_TICKER).x + Layout::spacingLg(), lineH));
|
||||
ImGui::SameLine();
|
||||
|
||||
// USD value
|
||||
const auto& state = app->state();
|
||||
double usd_value = state.getBalanceUSD();
|
||||
if (usd_value > 0.0)
|
||||
snprintf(buf, sizeof(buf), "$%.2f", usd_value);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "$--.--");
|
||||
dl->AddText(capFont, tickerFontSize,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
|
||||
OnSurfaceDisabled(), buf);
|
||||
|
||||
ImGui::NewLine();
|
||||
}
|
||||
|
||||
// Render the shared address list section (used by all layouts)
|
||||
void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
float glassRound, float hs, float vs) {
|
||||
using namespace material;
|
||||
const auto& S = schema::UISchema::instance();
|
||||
const float dp = Layout::dpiScale();
|
||||
const auto addrBtn = S.button("tabs.balance", "address-button");
|
||||
const auto actionBtn = S.button("tabs.balance", "action-button");
|
||||
const auto searchIn = S.input("tabs.balance", "search-input");
|
||||
const auto addrTable = S.table("tabs.balance", "address-table");
|
||||
const auto& state = app->state();
|
||||
|
||||
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;
|
||||
|
||||
// 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 ----
|
||||
std::vector<AddressListInput> rowInputs;
|
||||
rowInputs.reserve(state.z_addresses.size() + state.t_addresses.size());
|
||||
|
||||
auto addRows = [&](const auto& addrs, bool isZ) {
|
||||
for (const auto& a : addrs) {
|
||||
std::string addrLabel = app->getAddressLabel(a.address);
|
||||
bool isHidden = app->isAddressHidden(a.address);
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
bool isMining = app->isMiningAddress(a.address);
|
||||
rowInputs.push_back({&a, isZ, isHidden, isFav, isMining,
|
||||
addrLabel, app->getAddressIcon(a.address),
|
||||
app->getAddressSortOrder(a.address)});
|
||||
}
|
||||
};
|
||||
addRows(state.z_addresses, true);
|
||||
addRows(state.t_addresses, false);
|
||||
auto rows = BuildAddressListRows(rowInputs, addr_search, s_hideZeroBalances, s_showHidden);
|
||||
|
||||
// ---- 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", TR("filter"), addr_search, sizeof(addr_search));
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
ImGui::Checkbox(TrId("hide_zero_balances", "hide0_v2").c_str(), &s_hideZeroBalances);
|
||||
{
|
||||
int hc = app->getHiddenAddressCount();
|
||||
if (hc > 0) {
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
char hlbl[64];
|
||||
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
|
||||
ImGui::Checkbox(hlbl, &s_showHidden);
|
||||
} else {
|
||||
s_showHidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
float buttonWidth = (addrBtn.width > 0) ? addrBtn.width : 140.0f;
|
||||
float spacing = (addrBtn.gap > 0) ? addrBtn.gap : 8.0f;
|
||||
float totalButtonsWidth = buttonWidth * 2 + spacing;
|
||||
float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size,
|
||||
S.drawElement("tabs.balance", "buttons-position").size * hs);
|
||||
ImGui::SameLine(std::max(kMinButtonsPosition,
|
||||
ImGui::GetWindowWidth() - totalButtonsWidth -
|
||||
S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f)));
|
||||
|
||||
bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced();
|
||||
ImGui::BeginDisabled(sharedAddrSyncing);
|
||||
if (s_generating_z_address) {
|
||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||
const char* dotStr[] = {"", ".", "..", "..."};
|
||||
char genLabel[64];
|
||||
snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), dotStr[dots]);
|
||||
TactileButton(genLabel, ImVec2(buttonWidth, 0),
|
||||
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
|
||||
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
|
||||
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
||||
s_generating_z_address = true;
|
||||
app->createNewZAddress([](const std::string& addr) {
|
||||
s_generating_z_address = false;
|
||||
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
|
||||
});
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton(TR("new_t_address"), ImVec2(buttonWidth, 0),
|
||||
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
||||
app->createNewTAddress([](const std::string& addr) {
|
||||
DEBUG_LOGF("Created new t-address: %s\n", addr.c_str());
|
||||
});
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// ---- Glass panel container ----
|
||||
float addrListH = listH;
|
||||
if (addrListH < 40.0f * dp) addrListH = 40.0f * dp;
|
||||
|
||||
ImDrawList* dlPanel = ImGui::GetWindowDrawList();
|
||||
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 listPanelMax(listPanelMin.x + availW, listPanelMin.y + addrListH);
|
||||
GlassPanelSpec addrGlassSpec;
|
||||
addrGlassSpec.rounding = glassRound;
|
||||
DrawGlassPanel(dlPanel, listPanelMin, listPanelMax, addrGlassSpec);
|
||||
|
||||
float addrScrollY = 0.0f, addrScrollMaxY = 0.0f;
|
||||
int addrParentVtx = dlPanel->VtxBuffer.Size;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingSm()));
|
||||
ImGui::BeginChild("AddressList", ImVec2(availW, addrListH), false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ApplySmoothScroll();
|
||||
ImDrawList* addrChildDL = ImGui::GetWindowDrawList();
|
||||
int addrChildVtx = addrChildDL->VtxBuffer.Size;
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// ---- Empty states ----
|
||||
if (!app->isConnected()) {
|
||||
ImGui::Dummy(ImVec2(0, 16 * dp));
|
||||
float cw = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 ts = ImGui::CalcTextSize(TR("not_connected"));
|
||||
ImGui::SetCursorPosX((cw - ts.x) * 0.5f);
|
||||
ImGui::TextDisabled("%s", TR("not_connected"));
|
||||
} else if (rows.empty()) {
|
||||
float cw = ImGui::GetContentRegionAvail().x;
|
||||
float ch = ImGui::GetContentRegionAvail().y;
|
||||
if (ch < 60) ch = 60;
|
||||
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() + Layout::spacingMd();
|
||||
static int selected_row = -1;
|
||||
addrScrollY = ImGui::GetScrollY();
|
||||
addrScrollMaxY = ImGui::GetScrollMaxY();
|
||||
ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
|
||||
ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning());
|
||||
float rowPadLeft = Layout::cardInnerPadding();
|
||||
float rowPadRight = Layout::cardInnerPadding();
|
||||
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size,
|
||||
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.
|
||||
// Preserve the drop target on the release frame so the drop handler can use it.
|
||||
if (s_dragActive && mouseDown) 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;
|
||||
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();
|
||||
|
||||
// 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"));
|
||||
}
|
||||
}
|
||||
|
||||
// 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, rowSelectCol, 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);
|
||||
}
|
||||
|
||||
auto rowLayout = ComputeAddressRowLayout(
|
||||
rowPos.x, rowPos.y, innerW, rowH, rowPadLeft, rowIconSz,
|
||||
Layout::spacingMd(), Layout::spacingSm(), Layout::spacingXs());
|
||||
float cx = rowLayout.contentStartX;
|
||||
float cy = rowLayout.contentStartY;
|
||||
|
||||
// ---- Button zone (right edge): [eye] [star] ----
|
||||
float btnRound = 6.0f * dp;
|
||||
bool btnClicked = false;
|
||||
|
||||
if (!isDragged) {
|
||||
// Star button
|
||||
{
|
||||
const auto& starRect = rowLayout.favoriteButton;
|
||||
ImVec2 bMin(starRect.x, starRect.y), bMax(starRect.x + starRect.width, starRect.y + starRect.height);
|
||||
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(starRect.x + (starRect.width - iSz.x) * 0.5f,
|
||||
starRect.y + (starRect.height - 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"));
|
||||
}
|
||||
|
||||
// Eye button (zero balance or hidden)
|
||||
bool showEye = true;
|
||||
// Always reserve space for both buttons so content doesn't shift
|
||||
float contentRight = rowLayout.contentRight;
|
||||
|
||||
if (showEye) {
|
||||
const auto& eyeRect = rowLayout.visibilityButton;
|
||||
ImVec2 bMin(eyeRect.x, eyeRect.y), bMax(eyeRect.x + eyeRect.width, eyeRect.y + eyeRect.height);
|
||||
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(eyeRect.x + (eyeRect.width - iSz.x) * 0.5f,
|
||||
eyeRect.y + (eyeRect.height - 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"));
|
||||
}
|
||||
|
||||
// ---- Type icon or custom icon ----
|
||||
float iconCx = cx + rowIconSz;
|
||||
float iconCy = cy + body2->LegacySize * 0.5f;
|
||||
{
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
bool drewCustom = false;
|
||||
if (!row.icon.empty()) {
|
||||
drewCustom = AddressLabelDialog::drawIconByName(
|
||||
dl, row.icon, ImVec2(iconCx, iconCy), iconFont->LegacySize,
|
||||
OnSurfaceMedium(), iconFont, iconFont->LegacySize);
|
||||
}
|
||||
if (!drewCustom) {
|
||||
const char* glyph = row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE;
|
||||
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), typeCol, glyph);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Type label (first line) ----
|
||||
float labelX = rowLayout.labelX;
|
||||
{
|
||||
const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
|
||||
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
|
||||
const char* miningTag = row.mining ? TR("mining_tag") : "";
|
||||
char typeBuf[64];
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||
|
||||
// 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) ----
|
||||
{
|
||||
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, 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);
|
||||
}
|
||||
|
||||
// ---- USD value (second line, right-aligned) ----
|
||||
{
|
||||
std::string usdText = FormatAddressUsdValue(addr.balance, state.market.price_usd);
|
||||
if (!usdText.empty()) {
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdText.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(contentRight - usdSz.x,
|
||||
cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), usdText.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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() && !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());
|
||||
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(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
|
||||
app->setMiningAddress(addr.address, !row.mining);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("export_private_key")))
|
||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
|
||||
if (row.isZ) {
|
||||
if (ImGui::MenuItem(TR("export_viewing_key")))
|
||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Viewing);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("show_qr_code")))
|
||||
QRPopupDialog::show(addr.address, row.isZ ? TR("z_address") : TR("t_address"));
|
||||
ImGui::Separator();
|
||||
if (row.hidden) {
|
||||
if (ImGui::MenuItem(TR("restore_address")))
|
||||
app->unhideAddress(addr.address);
|
||||
} else if (addr.balance < 1e-9) {
|
||||
if (ImGui::MenuItem(TR("hide_address")))
|
||||
app->hideAddress(addr.address);
|
||||
}
|
||||
if (row.favorite) {
|
||||
if (ImGui::MenuItem(TR("remove_favorite")))
|
||||
app->unfavoriteAddress(addr.address);
|
||||
} else {
|
||||
if (ImGui::MenuItem(TR("favorite_address")))
|
||||
app->favoriteAddress(addr.address);
|
||||
}
|
||||
effects::ImGuiAcrylic::EndAcrylicPopup();
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
// Separator between rows
|
||||
if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) {
|
||||
float sepY = rowY[row_idx] + rowH;
|
||||
dl->AddLine(
|
||||
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 +
|
||||
Layout::spacingLg() + Layout::spacingMd()) * 1.2f,
|
||||
addrListH * 0.18f);
|
||||
ApplyScrollEdgeMask(dlPanel, addrParentVtx, addrChildDL, addrChildVtx,
|
||||
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), TR("showing_x_of_y"),
|
||||
(int)rows.size(), totalAddrs);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf);
|
||||
}
|
||||
}
|
||||
|
||||
// Render the shared recent transactions section (used by all layouts)
|
||||
void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs) {
|
||||
using namespace material;
|
||||
const auto& S = schema::UISchema::instance();
|
||||
const float dp = Layout::dpiScale();
|
||||
const auto actionBtn = S.button("tabs.balance", "action-button");
|
||||
const float kRecentTxRowHeight = S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f);
|
||||
const auto& state = app->state();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS");
|
||||
ImGui::SameLine();
|
||||
if (TactileSmallButton("View All", S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) {
|
||||
app->setCurrentPage(NavPage::History);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
|
||||
float availableListH = ImGui::GetContentRegionAvail().y;
|
||||
float listH = std::max({scaledRowH, recentH, availableListH});
|
||||
ImGui::BeginChild("##BalanceSharedRecentRows", ImVec2(0, listH), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
|
||||
const auto& txs = state.transactions;
|
||||
int count = (int)txs.size();
|
||||
|
||||
if (count == 0) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No transactions yet");
|
||||
} else {
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImFont* capFont = Type().caption();
|
||||
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
|
||||
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
|
||||
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
const auto& tx = txs[i];
|
||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
||||
float rowY = rowPos.y + rowH * 0.5f;
|
||||
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
|
||||
|
||||
float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), display.typeText.c_str());
|
||||
|
||||
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), display.addressText.c_str());
|
||||
|
||||
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.amountText.c_str());
|
||||
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
|
||||
float amtX = rightEdge - amtSz.x - std::max(
|
||||
S.drawElement("tabs.balance", "amount-right-min-margin").size,
|
||||
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(amtX, rowPos.y + 2 * dp),
|
||||
recentTxAmountColor(tx.type), display.amountText.c_str());
|
||||
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.timeText.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
|
||||
OnSurfaceDisabled(), display.timeText.c_str());
|
||||
|
||||
float rowW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
|
||||
if (material::IsRectHovered(rowPos, rowEnd)) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::History);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, rowH));
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
// Render sync progress bar (used by multiple layouts)
|
||||
void RenderSyncBar(App* app, ImDrawList* dl, float vs) {
|
||||
using namespace material;
|
||||
const auto& S = schema::UISchema::instance();
|
||||
const auto syncBar = S.drawElement("tabs.balance", "sync-bar");
|
||||
const auto& state = app->state();
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
if (state.sync.syncing && state.sync.headers > 0) {
|
||||
float prog = static_cast<float>(state.sync.verification_progress);
|
||||
if (prog > 1.0f) prog = 1.0f;
|
||||
float barH = (syncBar.height >= 0) ? syncBar.height : 3.0f;
|
||||
float barW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 barPos = ImGui::GetCursorScreenPos();
|
||||
dl->AddRectFilled(barPos,
|
||||
ImVec2(barPos.x + barW, barPos.y + barH),
|
||||
IM_COL32(255, 255, 255, 15), 1.0f * dp);
|
||||
dl->AddRectFilled(barPos,
|
||||
ImVec2(barPos.x + barW * prog, barPos.y + barH),
|
||||
WithAlpha(Warning(), 200), 1.0f * dp);
|
||||
ImGui::Dummy(ImVec2(barW, barH));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
34
src/ui/windows/balance_components.h
Normal file
34
src/ui/windows/balance_components.h
Normal file
@@ -0,0 +1,34 @@
|
||||
// DragonX Wallet - ImGui Edition
|
||||
// Copyright 2024-2026 The Hush Developers
|
||||
// Released under the GPLv3
|
||||
//
|
||||
// Shared rendering components used by every balance-tab layout (the animated hero, the address
|
||||
// list, the recent-transactions strip, the sync bar) plus the balance-lerp animation state.
|
||||
// Extracted from balance_tab.cpp so the layouts can be split into their own files later.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "imgui.h"
|
||||
|
||||
namespace dragonx {
|
||||
class App;
|
||||
namespace ui {
|
||||
|
||||
// Animated balance display values — lerp toward the wallet's real balances each frame. Defined in
|
||||
// balance_tab.cpp (the lerp is advanced by UpdateBalanceLerp), shared with the components.
|
||||
extern double s_dispTotal;
|
||||
extern double s_dispShielded;
|
||||
extern double s_dispTransparent;
|
||||
extern double s_dispUnconfirmed;
|
||||
// True while an async new-address generation is in flight.
|
||||
extern bool s_generating_z_address;
|
||||
|
||||
void UpdateBalanceLerp(App* app);
|
||||
void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs,
|
||||
float heroHeightOverride = -1.0f);
|
||||
void RenderSharedAddressList(App* app, float listH, float availW, float glassRound, float hs, float vs);
|
||||
void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs);
|
||||
void RenderSyncBar(App* app, ImDrawList* dl, float vs);
|
||||
|
||||
} // namespace ui
|
||||
} // namespace dragonx
|
||||
@@ -6,6 +6,7 @@
|
||||
#include "balance_address_list.h"
|
||||
#include "balance_tab_helpers.h"
|
||||
#include "balance_recent_tx.h"
|
||||
#include "balance_components.h"
|
||||
#include "mining_tab_helpers.h" // FormatHashrate (consistent MH/GH/.. scaling)
|
||||
#include "key_export_dialog.h"
|
||||
#include "qr_popup_dialog.h"
|
||||
@@ -37,11 +38,12 @@
|
||||
namespace dragonx {
|
||||
namespace ui {
|
||||
|
||||
// Animated balance state — lerps smoothly toward target
|
||||
static double s_dispTotal = 0.0;
|
||||
static double s_dispShielded = 0.0;
|
||||
static double s_dispTransparent = 0.0;
|
||||
static double s_dispUnconfirmed = 0.0;
|
||||
// Animated balance state — lerps smoothly toward target. Non-static (declared extern in
|
||||
// balance_components.h) so the shared components in balance_components.cpp share the same values.
|
||||
double s_dispTotal = 0.0;
|
||||
double s_dispShielded = 0.0;
|
||||
double s_dispTransparent = 0.0;
|
||||
double s_dispUnconfirmed = 0.0;
|
||||
|
||||
// Forward declarations for all layout functions
|
||||
static void RenderBalanceClassic(App* app);
|
||||
@@ -69,7 +71,7 @@ static constexpr int s_legacyLayoutCount = 10;
|
||||
static std::vector<BalanceLayoutEntry> s_balanceLayouts;
|
||||
static std::string s_defaultLayoutId = "classic";
|
||||
static bool s_layoutConfigLoaded = false;
|
||||
static bool s_generating_z_address = false;
|
||||
bool s_generating_z_address = false; // external linkage — shared with balance_components.cpp
|
||||
|
||||
static void LoadBalanceLayoutConfig()
|
||||
{
|
||||
@@ -1190,784 +1192,8 @@ static void RenderBalanceClassic(App* app)
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Shared helpers used by multiple layouts
|
||||
// ============================================================================
|
||||
|
||||
// Update animated lerp balances — called at the top of every layout
|
||||
static void UpdateBalanceLerp(App* app) {
|
||||
using namespace material;
|
||||
const auto& S = schema::UISchema::instance();
|
||||
const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
|
||||
const auto& state = app->state();
|
||||
float dt = ImGui::GetIO().DeltaTime;
|
||||
float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed;
|
||||
auto lerp = [](double& disp, double target, float dt, float spd) {
|
||||
double diff = target - disp;
|
||||
if (std::abs(diff) < 1e-9) { disp = target; return; }
|
||||
disp += diff * (double)(dt * spd);
|
||||
if (std::abs(target - disp) < 1e-9) disp = target;
|
||||
};
|
||||
lerp(s_dispTotal, app->getTotalBalance(), dt, speed);
|
||||
lerp(s_dispShielded, app->getShieldedBalance(), dt, speed);
|
||||
lerp(s_dispTransparent, app->getTransparentBalance(), dt, speed);
|
||||
lerp(s_dispUnconfirmed, state.unconfirmed_balance, dt, speed);
|
||||
}
|
||||
|
||||
// Render compact hero line: logo + balance + USD + mining on one line
|
||||
static void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs, float heroHeightOverride = -1.0f) {
|
||||
using namespace material;
|
||||
char buf[64];
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
// Coin logo
|
||||
ImTextureID logoTex = app->getCoinLogoTexture();
|
||||
ImFont* sub1 = Type().subtitle1();
|
||||
float logoSz = sub1->LegacySize + 4.0f * dp;
|
||||
float lineH = (heroHeightOverride >= 0.0f) ? heroHeightOverride : logoSz;
|
||||
if (logoTex != 0) {
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
dl->AddImage(logoTex,
|
||||
ImVec2(pos.x, pos.y),
|
||||
ImVec2(pos.x + lineH, pos.y + lineH),
|
||||
ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255));
|
||||
ImGui::Dummy(ImVec2(lineH + Layout::spacingSm(), lineH));
|
||||
ImGui::SameLine();
|
||||
}
|
||||
|
||||
// Total balance
|
||||
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
|
||||
ImVec2 pos = ImGui::GetCursorScreenPos();
|
||||
float fontSize = (heroHeightOverride >= 0.0f) ? heroHeightOverride : sub1->LegacySize;
|
||||
DrawTextShadow(dl, sub1, fontSize, pos, OnSurface(), buf);
|
||||
ImVec2 sz = sub1->CalcTextSizeA(fontSize, 10000.0f, 0.0f, buf);
|
||||
ImGui::Dummy(ImVec2(sz.x, lineH));
|
||||
ImGui::SameLine();
|
||||
|
||||
// Ticker
|
||||
ImFont* capFont = Type().caption();
|
||||
float tickerFontSize = (heroHeightOverride >= 0.0f) ? heroHeightOverride * 0.6f : capFont->LegacySize;
|
||||
float tickerY = pos.y + lineH - tickerFontSize;
|
||||
dl->AddText(capFont, tickerFontSize,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
|
||||
OnSurfaceMedium(), DRAGONX_TICKER);
|
||||
ImGui::Dummy(ImVec2(capFont->CalcTextSizeA(tickerFontSize, 10000, 0, DRAGONX_TICKER).x + Layout::spacingLg(), lineH));
|
||||
ImGui::SameLine();
|
||||
|
||||
// USD value
|
||||
const auto& state = app->state();
|
||||
double usd_value = state.getBalanceUSD();
|
||||
if (usd_value > 0.0)
|
||||
snprintf(buf, sizeof(buf), "$%.2f", usd_value);
|
||||
else
|
||||
snprintf(buf, sizeof(buf), "$--.--");
|
||||
dl->AddText(capFont, tickerFontSize,
|
||||
ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
|
||||
OnSurfaceDisabled(), buf);
|
||||
|
||||
ImGui::NewLine();
|
||||
}
|
||||
|
||||
// Render the shared address list section (used by all layouts)
|
||||
static void RenderSharedAddressList(App* app, float listH, float availW,
|
||||
float glassRound, float hs, float vs) {
|
||||
using namespace material;
|
||||
const auto& S = schema::UISchema::instance();
|
||||
const float dp = Layout::dpiScale();
|
||||
const auto addrBtn = S.button("tabs.balance", "address-button");
|
||||
const auto actionBtn = S.button("tabs.balance", "action-button");
|
||||
const auto searchIn = S.input("tabs.balance", "search-input");
|
||||
const auto addrTable = S.table("tabs.balance", "address-table");
|
||||
const auto& state = app->state();
|
||||
|
||||
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;
|
||||
|
||||
// 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 ----
|
||||
std::vector<AddressListInput> rowInputs;
|
||||
rowInputs.reserve(state.z_addresses.size() + state.t_addresses.size());
|
||||
|
||||
auto addRows = [&](const auto& addrs, bool isZ) {
|
||||
for (const auto& a : addrs) {
|
||||
std::string addrLabel = app->getAddressLabel(a.address);
|
||||
bool isHidden = app->isAddressHidden(a.address);
|
||||
bool isFav = app->isAddressFavorite(a.address);
|
||||
bool isMining = app->isMiningAddress(a.address);
|
||||
rowInputs.push_back({&a, isZ, isHidden, isFav, isMining,
|
||||
addrLabel, app->getAddressIcon(a.address),
|
||||
app->getAddressSortOrder(a.address)});
|
||||
}
|
||||
};
|
||||
addRows(state.z_addresses, true);
|
||||
addRows(state.t_addresses, false);
|
||||
auto rows = BuildAddressListRows(rowInputs, addr_search, s_hideZeroBalances, s_showHidden);
|
||||
|
||||
// ---- 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", TR("filter"), addr_search, sizeof(addr_search));
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
ImGui::Checkbox(TrId("hide_zero_balances", "hide0_v2").c_str(), &s_hideZeroBalances);
|
||||
{
|
||||
int hc = app->getHiddenAddressCount();
|
||||
if (hc > 0) {
|
||||
ImGui::SameLine(0, Layout::spacingLg());
|
||||
char hlbl[64];
|
||||
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
|
||||
ImGui::Checkbox(hlbl, &s_showHidden);
|
||||
} else {
|
||||
s_showHidden = false;
|
||||
}
|
||||
}
|
||||
|
||||
float buttonWidth = (addrBtn.width > 0) ? addrBtn.width : 140.0f;
|
||||
float spacing = (addrBtn.gap > 0) ? addrBtn.gap : 8.0f;
|
||||
float totalButtonsWidth = buttonWidth * 2 + spacing;
|
||||
float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size,
|
||||
S.drawElement("tabs.balance", "buttons-position").size * hs);
|
||||
ImGui::SameLine(std::max(kMinButtonsPosition,
|
||||
ImGui::GetWindowWidth() - totalButtonsWidth -
|
||||
S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f)));
|
||||
|
||||
bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced();
|
||||
ImGui::BeginDisabled(sharedAddrSyncing);
|
||||
if (s_generating_z_address) {
|
||||
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
|
||||
const char* dotStr[] = {"", ".", "..", "..."};
|
||||
char genLabel[64];
|
||||
snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), dotStr[dots]);
|
||||
TactileButton(genLabel, ImVec2(buttonWidth, 0),
|
||||
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
|
||||
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
|
||||
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
||||
s_generating_z_address = true;
|
||||
app->createNewZAddress([](const std::string& addr) {
|
||||
s_generating_z_address = false;
|
||||
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
|
||||
});
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (TactileButton(TR("new_t_address"), ImVec2(buttonWidth, 0),
|
||||
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
||||
app->createNewTAddress([](const std::string& addr) {
|
||||
DEBUG_LOGF("Created new t-address: %s\n", addr.c_str());
|
||||
});
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
|
||||
// ---- Glass panel container ----
|
||||
float addrListH = listH;
|
||||
if (addrListH < 40.0f * dp) addrListH = 40.0f * dp;
|
||||
|
||||
ImDrawList* dlPanel = ImGui::GetWindowDrawList();
|
||||
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
|
||||
ImVec2 listPanelMax(listPanelMin.x + availW, listPanelMin.y + addrListH);
|
||||
GlassPanelSpec addrGlassSpec;
|
||||
addrGlassSpec.rounding = glassRound;
|
||||
DrawGlassPanel(dlPanel, listPanelMin, listPanelMax, addrGlassSpec);
|
||||
|
||||
float addrScrollY = 0.0f, addrScrollMaxY = 0.0f;
|
||||
int addrParentVtx = dlPanel->VtxBuffer.Size;
|
||||
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingSm()));
|
||||
ImGui::BeginChild("AddressList", ImVec2(availW, addrListH), false,
|
||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
ApplySmoothScroll();
|
||||
ImDrawList* addrChildDL = ImGui::GetWindowDrawList();
|
||||
int addrChildVtx = addrChildDL->VtxBuffer.Size;
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
|
||||
// ---- Empty states ----
|
||||
if (!app->isConnected()) {
|
||||
ImGui::Dummy(ImVec2(0, 16 * dp));
|
||||
float cw = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 ts = ImGui::CalcTextSize(TR("not_connected"));
|
||||
ImGui::SetCursorPosX((cw - ts.x) * 0.5f);
|
||||
ImGui::TextDisabled("%s", TR("not_connected"));
|
||||
} else if (rows.empty()) {
|
||||
float cw = ImGui::GetContentRegionAvail().x;
|
||||
float ch = ImGui::GetContentRegionAvail().y;
|
||||
if (ch < 60) ch = 60;
|
||||
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() + Layout::spacingMd();
|
||||
static int selected_row = -1;
|
||||
addrScrollY = ImGui::GetScrollY();
|
||||
addrScrollMaxY = ImGui::GetScrollMaxY();
|
||||
ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
|
||||
ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning());
|
||||
float rowPadLeft = Layout::cardInnerPadding();
|
||||
float rowPadRight = Layout::cardInnerPadding();
|
||||
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size,
|
||||
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.
|
||||
// Preserve the drop target on the release frame so the drop handler can use it.
|
||||
if (s_dragActive && mouseDown) 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;
|
||||
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();
|
||||
|
||||
// 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"));
|
||||
}
|
||||
}
|
||||
|
||||
// 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, rowSelectCol, 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);
|
||||
}
|
||||
|
||||
auto rowLayout = ComputeAddressRowLayout(
|
||||
rowPos.x, rowPos.y, innerW, rowH, rowPadLeft, rowIconSz,
|
||||
Layout::spacingMd(), Layout::spacingSm(), Layout::spacingXs());
|
||||
float cx = rowLayout.contentStartX;
|
||||
float cy = rowLayout.contentStartY;
|
||||
|
||||
// ---- Button zone (right edge): [eye] [star] ----
|
||||
float btnRound = 6.0f * dp;
|
||||
bool btnClicked = false;
|
||||
|
||||
if (!isDragged) {
|
||||
// Star button
|
||||
{
|
||||
const auto& starRect = rowLayout.favoriteButton;
|
||||
ImVec2 bMin(starRect.x, starRect.y), bMax(starRect.x + starRect.width, starRect.y + starRect.height);
|
||||
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(starRect.x + (starRect.width - iSz.x) * 0.5f,
|
||||
starRect.y + (starRect.height - 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"));
|
||||
}
|
||||
|
||||
// Eye button (zero balance or hidden)
|
||||
bool showEye = true;
|
||||
// Always reserve space for both buttons so content doesn't shift
|
||||
float contentRight = rowLayout.contentRight;
|
||||
|
||||
if (showEye) {
|
||||
const auto& eyeRect = rowLayout.visibilityButton;
|
||||
ImVec2 bMin(eyeRect.x, eyeRect.y), bMax(eyeRect.x + eyeRect.width, eyeRect.y + eyeRect.height);
|
||||
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(eyeRect.x + (eyeRect.width - iSz.x) * 0.5f,
|
||||
eyeRect.y + (eyeRect.height - 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"));
|
||||
}
|
||||
|
||||
// ---- Type icon or custom icon ----
|
||||
float iconCx = cx + rowIconSz;
|
||||
float iconCy = cy + body2->LegacySize * 0.5f;
|
||||
{
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
bool drewCustom = false;
|
||||
if (!row.icon.empty()) {
|
||||
drewCustom = AddressLabelDialog::drawIconByName(
|
||||
dl, row.icon, ImVec2(iconCx, iconCy), iconFont->LegacySize,
|
||||
OnSurfaceMedium(), iconFont, iconFont->LegacySize);
|
||||
}
|
||||
if (!drewCustom) {
|
||||
const char* glyph = row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE;
|
||||
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), typeCol, glyph);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Type label (first line) ----
|
||||
float labelX = rowLayout.labelX;
|
||||
{
|
||||
const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
|
||||
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
|
||||
const char* miningTag = row.mining ? TR("mining_tag") : "";
|
||||
char typeBuf[64];
|
||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
|
||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
||||
|
||||
// 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) ----
|
||||
{
|
||||
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, 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);
|
||||
}
|
||||
|
||||
// ---- USD value (second line, right-aligned) ----
|
||||
{
|
||||
std::string usdText = FormatAddressUsdValue(addr.balance, state.market.price_usd);
|
||||
if (!usdText.empty()) {
|
||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdText.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(contentRight - usdSz.x,
|
||||
cy + body2->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), usdText.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
// ---- 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() && !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());
|
||||
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(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
|
||||
app->setMiningAddress(addr.address, !row.mining);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("export_private_key")))
|
||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
|
||||
if (row.isZ) {
|
||||
if (ImGui::MenuItem(TR("export_viewing_key")))
|
||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Viewing);
|
||||
}
|
||||
if (ImGui::MenuItem(TR("show_qr_code")))
|
||||
QRPopupDialog::show(addr.address, row.isZ ? TR("z_address") : TR("t_address"));
|
||||
ImGui::Separator();
|
||||
if (row.hidden) {
|
||||
if (ImGui::MenuItem(TR("restore_address")))
|
||||
app->unhideAddress(addr.address);
|
||||
} else if (addr.balance < 1e-9) {
|
||||
if (ImGui::MenuItem(TR("hide_address")))
|
||||
app->hideAddress(addr.address);
|
||||
}
|
||||
if (row.favorite) {
|
||||
if (ImGui::MenuItem(TR("remove_favorite")))
|
||||
app->unfavoriteAddress(addr.address);
|
||||
} else {
|
||||
if (ImGui::MenuItem(TR("favorite_address")))
|
||||
app->favoriteAddress(addr.address);
|
||||
}
|
||||
effects::ImGuiAcrylic::EndAcrylicPopup();
|
||||
}
|
||||
ImGui::PopID();
|
||||
|
||||
// Separator between rows
|
||||
if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) {
|
||||
float sepY = rowY[row_idx] + rowH;
|
||||
dl->AddLine(
|
||||
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 +
|
||||
Layout::spacingLg() + Layout::spacingMd()) * 1.2f,
|
||||
addrListH * 0.18f);
|
||||
ApplyScrollEdgeMask(dlPanel, addrParentVtx, addrChildDL, addrChildVtx,
|
||||
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), TR("showing_x_of_y"),
|
||||
(int)rows.size(), totalAddrs);
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf);
|
||||
}
|
||||
}
|
||||
|
||||
// Render the shared recent transactions section (used by all layouts)
|
||||
static void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs) {
|
||||
using namespace material;
|
||||
const auto& S = schema::UISchema::instance();
|
||||
const float dp = Layout::dpiScale();
|
||||
const auto actionBtn = S.button("tabs.balance", "action-button");
|
||||
const float kRecentTxRowHeight = S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f);
|
||||
const auto& state = app->state();
|
||||
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS");
|
||||
ImGui::SameLine();
|
||||
if (TactileSmallButton("View All", S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) {
|
||||
app->setCurrentPage(NavPage::History);
|
||||
}
|
||||
ImGui::Spacing();
|
||||
|
||||
float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
|
||||
float availableListH = ImGui::GetContentRegionAvail().y;
|
||||
float listH = std::max({scaledRowH, recentH, availableListH});
|
||||
ImGui::BeginChild("##BalanceSharedRecentRows", ImVec2(0, listH), false,
|
||||
ImGuiWindowFlags_NoBackground);
|
||||
|
||||
const auto& txs = state.transactions;
|
||||
int count = (int)txs.size();
|
||||
|
||||
if (count == 0) {
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No transactions yet");
|
||||
} else {
|
||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
||||
ImFont* capFont = Type().caption();
|
||||
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
|
||||
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
|
||||
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
||||
|
||||
for (int i = 0; i < count; i++) {
|
||||
const auto& tx = txs[i];
|
||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
||||
float rowY = rowPos.y + rowH * 0.5f;
|
||||
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
||||
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
|
||||
|
||||
float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), display.typeText.c_str());
|
||||
|
||||
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), display.addressText.c_str());
|
||||
|
||||
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.amountText.c_str());
|
||||
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
|
||||
float amtX = rightEdge - amtSz.x - std::max(
|
||||
S.drawElement("tabs.balance", "amount-right-min-margin").size,
|
||||
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(amtX, rowPos.y + 2 * dp),
|
||||
recentTxAmountColor(tx.type), display.amountText.c_str());
|
||||
|
||||
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.timeText.c_str());
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
|
||||
OnSurfaceDisabled(), display.timeText.c_str());
|
||||
|
||||
float rowW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
|
||||
if (material::IsRectHovered(rowPos, rowEnd)) {
|
||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||
if (ImGui::IsMouseClicked(0))
|
||||
app->setCurrentPage(NavPage::History);
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, rowH));
|
||||
}
|
||||
}
|
||||
ImGui::EndChild();
|
||||
}
|
||||
|
||||
// Render sync progress bar (used by multiple layouts)
|
||||
static void RenderSyncBar(App* app, ImDrawList* dl, float vs) {
|
||||
using namespace material;
|
||||
const auto& S = schema::UISchema::instance();
|
||||
const auto syncBar = S.drawElement("tabs.balance", "sync-bar");
|
||||
const auto& state = app->state();
|
||||
const float dp = Layout::dpiScale();
|
||||
|
||||
if (state.sync.syncing && state.sync.headers > 0) {
|
||||
float prog = static_cast<float>(state.sync.verification_progress);
|
||||
if (prog > 1.0f) prog = 1.0f;
|
||||
float barH = (syncBar.height >= 0) ? syncBar.height : 3.0f;
|
||||
float barW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 barPos = ImGui::GetCursorScreenPos();
|
||||
dl->AddRectFilled(barPos,
|
||||
ImVec2(barPos.x + barW, barPos.y + barH),
|
||||
IM_COL32(255, 255, 255, 15), 1.0f * dp);
|
||||
dl->AddRectFilled(barPos,
|
||||
ImVec2(barPos.x + barW * prog, barPos.y + barH),
|
||||
WithAlpha(Warning(), 200), 1.0f * dp);
|
||||
ImGui::Dummy(ImVec2(barW, barH));
|
||||
}
|
||||
}
|
||||
// Shared helpers (UpdateBalanceLerp / RenderCompactHero / RenderSharedAddressList /
|
||||
// RenderSharedRecentTx / RenderSyncBar) now live in balance_components.{h,cpp}.
|
||||
|
||||
// ============================================================================
|
||||
// Layout 1: Donut Chart
|
||||
|
||||
Reference in New Issue
Block a user