diff --git a/CMakeLists.txt b/CMakeLists.txt index e3f78d9..70fc162 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -435,6 +435,7 @@ set(APP_SOURCES src/ui/notifications.cpp src/ui/windows/main_window.cpp src/ui/windows/balance_tab.cpp + src/ui/windows/balance_components.cpp src/ui/windows/balance_address_list.cpp src/ui/windows/balance_recent_tx.cpp src/ui/windows/balance_tab_helpers.cpp diff --git a/src/ui/windows/balance_components.cpp b/src/ui/windows/balance_components.cpp new file mode 100644 index 0000000..4a51b5c --- /dev/null +++ b/src/ui/windows/balance_components.cpp @@ -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 +#include +#include +#include +#include +#include +#include + +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 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 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(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 diff --git a/src/ui/windows/balance_components.h b/src/ui/windows/balance_components.h new file mode 100644 index 0000000..427f663 --- /dev/null +++ b/src/ui/windows/balance_components.h @@ -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 diff --git a/src/ui/windows/balance_tab.cpp b/src/ui/windows/balance_tab.cpp index 3bd6210..e96f0bc 100644 --- a/src/ui/windows/balance_tab.cpp +++ b/src/ui/windows/balance_tab.cpp @@ -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 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 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 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(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