// 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 static int s_dropMode = 0; // 0 = transfer (released over a row's CENTER), 1 = reorder // (released over a row's top/bottom EDGE). Disambiguates the // two drag gestures by WHERE on the target row you release. // 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. Released over a row's CENTER (transfer mode) and that row has funds -> transfer; // released over an EDGE, in a gap, or on a zero-balance row -> reorder. if (s_dragActive) { bool didTransfer = false; if (s_dropMode == 0 && s_dropTargetIdx >= 0 && s_dropTargetIdx != s_dragIdx && s_dragIdx < (int)rows.size()) { const auto& srcRow = rows[s_dragIdx]; const auto& dstRow = rows[s_dropTargetIdx]; 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); didTransfer = true; } } if (!didTransfer) { // Reorder — 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() && s_dragIdx < (int)rows.size()) { app->swapAddressOrder(rows[s_dragIdx].info->address, rows[targetIdx].info->address); } } } } s_dragIdx = -1; s_dragActive = false; s_dropTargetIdx = -1; s_dropMode = 0; } // ---- 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 — reflect the gesture: centre of a row = transfer to it, // edge of a row = reorder (move here). if (s_dropTargetIdx >= 0 && s_dropTargetIdx < (int)rows.size() && s_dropMode == 0) { const auto& target = rows[s_dropTargetIdx]; material::Tooltip("%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 { material::Tooltip("%s\n%s\n\n%s", truncateAddress(addr.address, 32).c_str(), row.isZ ? TR("shielded") : TR("transparent"), TR("address_reorder_hint")); } } // 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 feedback when dragging another row over this one. The cursor's vertical // position WITHIN the row picks the gesture: top/bottom edge = reorder (show an insertion // line), centre = transfer (highlight the whole row). if (s_dragActive && s_dragIdx != row_idx && material::IsRectHovered(rowPos, rowEnd)) { s_dropTargetIdx = row_idx; const float relY = rowH > 0.0f ? (mousePos.y - rowPos.y) / rowH : 0.5f; s_dropMode = (relY < 0.30f || relY > 0.70f) ? 1 : 0; if (s_dropMode == 0) { // Transfer: highlight the whole target row. dl->AddRectFilled(rowPos, rowEnd, WithAlpha(Primary(), 40), 4.0f * dp); dl->AddRect(rowPos, rowEnd, WithAlpha(Primary(), 140), 4.0f * dp, 0, 2.0f * dp); } else { // Reorder: draw an insertion line at the near (top or bottom) edge. const float lineY = (relY < 0.5f) ? rowPos.y : rowEnd.y; dl->AddLine(ImVec2(rowPos.x, lineY), ImVec2(rowEnd.x, lineY), WithAlpha(Primary(), 230), 2.5f * dp); dl->AddCircleFilled(ImVec2(rowPos.x, lineY), 3.0f * dp, WithAlpha(Primary(), 230)); } } 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) material::Tooltip("%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) material::Tooltip("%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) { material::Tooltip("%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