// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "balance_tab.h" #include "key_export_dialog.h" #include "qr_popup_dialog.h" #include "send_tab.h" #include "../../app.h" #include "../../config/settings.h" #include "../../config/version.h" #include "../../util/i18n.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 "../../util/logger.h" namespace dragonx { namespace ui { // Case-insensitive substring search static bool containsIgnoreCase(const std::string& str, const std::string& search) { if (search.empty()) return true; std::string s = str, q = search; std::transform(s.begin(), s.end(), s.begin(), ::tolower); std::transform(q.begin(), q.end(), q.begin(), ::tolower); return s.find(q) != std::string::npos; } // Relative time string ("2m ago", "3h ago", etc.) static std::string timeAgo(int64_t timestamp) { if (timestamp <= 0) return ""; int64_t now = (int64_t)std::time(nullptr); int64_t diff = now - timestamp; if (diff < 0) diff = 0; if (diff < 60) return std::to_string(diff) + "s ago"; if (diff < 3600) return std::to_string(diff / 60) + "m ago"; if (diff < 86400) return std::to_string(diff / 3600) + "h ago"; return std::to_string(diff / 86400) + "d ago"; } // Draw a small transaction-type icon (send=up, receive=down, mined=construction) static void DrawTxIcon(ImDrawList* dl, const std::string& type, float cx, float cy, float /*s*/, ImU32 col) { using namespace material; ImFont* iconFont = Type().iconSmall(); const char* icon; if (type == "send") { icon = ICON_MD_CALL_MADE; } else if (type == "receive") { icon = ICON_MD_CALL_RECEIVED; } else { icon = ICON_MD_CONSTRUCTION; } ImVec2 sz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, icon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(cx - sz.x * 0.5f, cy - sz.y * 0.5f), col, icon); } // 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; // Helper to truncate address for display static std::string truncateAddress(const std::string& addr, int maxLen = 32) { if (addr.length() <= static_cast(maxLen)) return addr; int half = (maxLen - 3) / 2; return addr.substr(0, half) + "..." + addr.substr(addr.length() - half); } // Helper to draw a sparkline polyline within a bounding box static void DrawSparkline(ImDrawList* dl, const ImVec2& pMin, const ImVec2& pMax, const std::vector& data, ImU32 color, float thickness = 1.5f) { if (data.size() < 2) return; double lo = *std::min_element(data.begin(), data.end()); double hi = *std::max_element(data.begin(), data.end()); double range = hi - lo; if (range < 1e-12) range = 1.0; float w = pMax.x - pMin.x; float h = pMax.y - pMin.y; int n = (int)data.size(); std::vector pts; pts.reserve(n); for (int i = 0; i < n; i++) { float x = pMin.x + (float)i / (float)(n - 1) * w; float y = pMax.y - (float)((data[i] - lo) / range) * h; pts.push_back(ImVec2(x, y)); } dl->AddPolyline(pts.data(), n, color, ImDrawFlags_None, thickness); } // Forward declarations for all layout functions static void RenderBalanceClassic(App* app); static void RenderBalanceDonut(App* app); static void RenderBalanceConsolidated(App* app); static void RenderBalanceDashboard(App* app); static void RenderBalanceVerticalStack(App* app); static void RenderBalanceVertical2x2(App* app); static void RenderBalanceShield(App* app); static void RenderBalanceTimeline(App* app); static void RenderBalanceTwoRow(App* app); static void RenderBalanceMinimal(App* app); // ============================================================================ // Layout config — parsed from ui.toml [tabs.balance.layouts] // ============================================================================ // Legacy int→string ID mapping for old settings.json migration static const char* s_legacyLayoutIds[] = { "classic", "donut", "consolidated", "dashboard", "vertical-stack", "vertical-2x2", "shield", "timeline", "two-row", "minimal" }; static constexpr int s_legacyLayoutCount = 10; static std::vector s_balanceLayouts; static std::string s_defaultLayoutId = "classic"; static bool s_layoutConfigLoaded = false; static void LoadBalanceLayoutConfig() { s_balanceLayouts.clear(); const void* elem = schema::UI().findElement("tabs.balance", "layouts"); if (elem) { const auto& t = *static_cast(elem); if (auto selected = t["selected"].value()) s_defaultLayoutId = *selected; else if (auto def = t["default"].value()) s_defaultLayoutId = *def; if (auto* options = t["options"].as_array()) { for (auto& item : *options) { auto* opt = item.as_table(); if (!opt) continue; auto id = (*opt)["id"].value(); auto name = (*opt)["name"].value(); if (!id || !name) continue; BalanceLayoutEntry entry; entry.id = *id; entry.name = *name; entry.enabled = (*opt)["enabled"].value_or(true); s_balanceLayouts.push_back(std::move(entry)); } } } // Fallback if ui.toml had no layouts defined if (s_balanceLayouts.empty()) { for (int i = 0; i < s_legacyLayoutCount; i++) { BalanceLayoutEntry entry; entry.id = s_legacyLayoutIds[i]; // Capitalize first letter for display name entry.name = entry.id; if (!entry.name.empty()) entry.name[0] = (char)toupper((unsigned char)entry.name[0]); s_balanceLayouts.push_back(std::move(entry)); } } s_layoutConfigLoaded = true; } const std::vector& GetBalanceLayouts() { if (!s_layoutConfigLoaded) LoadBalanceLayoutConfig(); return s_balanceLayouts; } const std::string& GetDefaultBalanceLayout() { if (!s_layoutConfigLoaded) LoadBalanceLayoutConfig(); return s_defaultLayoutId; } void RefreshBalanceLayoutConfig() { s_layoutConfigLoaded = false; } std::string MigrateBalanceLayoutIndex(int index) { if (index >= 0 && index < s_legacyLayoutCount) return s_legacyLayoutIds[index]; return "classic"; } // Layout ID → render function dispatch using LayoutRenderFn = void(*)(App*); struct LayoutDispatchEntry { const char* id; LayoutRenderFn fn; }; static const LayoutDispatchEntry s_layoutDispatch[] = { { "classic", RenderBalanceClassic }, { "donut", RenderBalanceDonut }, { "consolidated", RenderBalanceConsolidated }, { "dashboard", RenderBalanceDashboard }, { "vertical-stack", RenderBalanceVerticalStack }, { "vertical-2x2", RenderBalanceVertical2x2 }, { "shield", RenderBalanceShield }, { "timeline", RenderBalanceTimeline }, { "two-row", RenderBalanceTwoRow }, { "minimal", RenderBalanceMinimal }, }; void RenderBalanceTab(App* app) { std::string layoutId = GetDefaultBalanceLayout(); if (app->settings()) { std::string saved = app->settings()->getBalanceLayout(); if (!saved.empty()) layoutId = saved; } // Left/Right arrows: cycle through enabled balance layouts // (skip when Ctrl is held — Ctrl+Arrow cycles themes instead) if (app->settings() && !ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl) { bool cycleUp = ImGui::IsKeyPressed(ImGuiKey_LeftArrow); bool cycleDown = ImGui::IsKeyPressed(ImGuiKey_RightArrow); if (cycleUp || cycleDown) { const auto& layouts = GetBalanceLayouts(); // Build list of enabled layout IDs std::vector enabled; for (const auto& l : layouts) if (l.enabled) enabled.push_back(l.id); if (!enabled.empty()) { int cur = 0; for (int i = 0; i < (int)enabled.size(); i++) { if (enabled[i] == layoutId) { cur = i; break; } } if (cycleUp) cur = (cur - 1 + (int)enabled.size()) % (int)enabled.size(); else cur = (cur + 1) % (int)enabled.size(); layoutId = enabled[cur]; app->settings()->setBalanceLayout(layoutId); // Show toast with layout name const auto& allLayouts = GetBalanceLayouts(); std::string displayName = layoutId; for (const auto& l : allLayouts) { if (l.id == layoutId) { displayName = l.name; break; } } Notifications::instance().info("Layout: " + displayName); } } } // Dispatch by string ID for (const auto& entry : s_layoutDispatch) { if (layoutId == entry.id) { entry.fn(app); return; } } // Fallback to Classic RenderBalanceClassic(app); } // ============================================================================ // Layout 0: Classic (original 3-card layout) // ============================================================================ static void RenderBalanceClassic(App* app) { using namespace material; const auto& S = schema::UISchema::instance(); 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 syncBar = S.drawElement("tabs.balance", "sync-bar"); // Read layout properties from schema const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f); const float kHeroPadTop = S.drawElement("tabs.balance", "hero-pad-top").sizeOr(12.0f); const float kRecentTxRowHeight = S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f); const float kButtonRowRightMargin = S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f); const auto& state = app->state(); // Responsive scale factors (recomputed every frame) ImVec2 contentAvail = ImGui::GetContentRegionAvail(); const float hs = Layout::hScale(contentAvail.x); const float vs = Layout::vScale(contentAvail.y); const auto tier = Layout::currentTier(contentAvail.x, contentAvail.y); const float glassRound = Layout::glassRounding(); const float cGap = Layout::cardGap(); const float dp = Layout::dpiScale(); // Responsive constants (scale with window size) const float kSparklineHeight = std::max(S.drawElement("tabs.balance", "sparkline-min-height").size, S.drawElement("tabs.balance", "sparkline-height").size * vs); const float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size, S.drawElement("tabs.balance", "buttons-position").size * hs); // Dynamic recent tx count — fit as many as space allows const float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs); // At minimum size ~601px avail, reserve ~300px for hero+cards+addr header, // rest for address list + recent txs. Show 3-5 rows depending on space. const int kRecentTxCount = std::clamp( (int)((contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f)) / scaledRowH), 2, 5); // Lerp displayed balances toward actual values { float dt = ImGui::GetIO().DeltaTime; float speed = 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); // Snap when very close 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); } // ================================================================ // Card row — Total Balance | Shielded | Transparent | Market // ================================================================ { float topMargin = S.drawElement("tabs.balance.classic", "top-margin").size; if (topMargin > 0.0f) ImGui::Dummy(ImVec2(0, topMargin)); else if (topMargin < 0.0f) { // auto: use hero-pad-top scaled by vertical factor float autoPad = kHeroPadTop * vs; if (autoPad > 0.0f) ImGui::Dummy(ImVec2(0, autoPad)); } // topMargin == 0 → no spacing at all const float cardGap = cGap; float availWidth = ImGui::GetContentRegionAvail().x; // Responsive card columns: 4 normally, 2 in compact, 1 if very narrow int numCols = (int)S.drawElement("tabs.balance.classic", "card-num-cols").sizeOr(4.0f); if (tier == Layout::LayoutTier::Compact) { if (availWidth < S.drawElement("tabs.balance.classic", "card-narrow-width").sizeOr(400.0f) * Layout::dpiScale()) numCols = (int)S.drawElement("tabs.balance.classic", "card-narrow-cols").sizeOr(1.0f); else numCols = (int)S.drawElement("tabs.balance.classic", "card-compact-cols").sizeOr(2.0f); } float cardWidth = (availWidth - (float)(numCols - 1) * cardGap) / (float)numCols; ImDrawList* dl = ImGui::GetWindowDrawList(); ImVec2 origin = ImGui::GetCursorScreenPos(); GlassPanelSpec cardSpec; cardSpec.rounding = glassRound; char buf[64]; ImU32 greenCol = Success(); ImU32 goldCol = Warning(); ImU32 amberCol = Warning(); ImFont* ovFont = Type().overline(); ImFont* sub1 = Type().subtitle1(); ImFont* capFont = Type().caption(); float classicPadOverride = S.drawElement("tabs.balance.classic", "card-padding").size; float cardPadLg = (classicPadOverride >= 0.0f) ? classicPadOverride : Layout::spacingLg(); // Card height: must fit the Market card's content (overline + price + 24h) const float ovGap = S.drawElement("tabs.balance", "overline-value-gap").sizeOr(6.0f); const float valGap = S.drawElement("tabs.balance", "value-caption-gap").sizeOr(4.0f); const float tickGap = S.drawElement("tabs.balance.classic", "ticker-gap").sizeOr(4.0f); float marketContentH = cardPadLg + ovFont->LegacySize + ovGap + sub1->LegacySize + 2.0f * dp + capFont->LegacySize + cardPadLg; float classicCardH = S.drawElement("tabs.balance.classic", "card-height").size; float cardH; if (classicCardH >= 0.0f) { cardH = classicCardH; // explicit override from ui.toml } else { float minH = S.drawElement("tabs.balance.classic", "card-min-height").sizeOr(70.0f); cardH = std::max(StatCardHeight(vs, minH), marketContentH); } // Helper: draw accent stripe on left edge, clipped to card rounded corners. // We draw a full-size rounded rect (left corners only) and clip it to the // stripe width so the shape itself follows the card rounding. const float accentW = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f); auto drawAccent = [&](const ImVec2& cMin, const ImVec2& cMax, ImU32 col) { dl->PushClipRect(cMin, ImVec2(cMin.x + accentW, cMax.y), true); dl->AddRectFilled(cMin, cMax, col, cardSpec.rounding, ImDrawFlags_RoundCornersLeft); dl->PopClipRect(); }; // Helper: compute card position given card index (0-3) auto cardPos = [&](int idx) -> ImVec2 { int col = idx % numCols; int row = idx / numCols; return ImVec2(origin.x + col * (cardWidth + cardGap), origin.y + row * (cardH + cardGap)); }; // ---- Total Balance card ---- { ImVec2 cMin = cardPos(0); ImVec2 cMax(cMin.x + cardWidth, cMin.y + cardH); DrawGlassPanel(dl, cMin, cMax, cardSpec); drawAccent(cMin, cMax, S.resolveColor("var(--accent-total)", OnSurface())); float cx = cMin.x + cardPadLg; float cy = cMin.y + cardPadLg; // Coin logo (small, top-right corner) ImTextureID logoTex = app->getCoinLogoTexture(); if (logoTex != 0) { float logoSz = ovFont->LegacySize + sub1->LegacySize + 4.0f * dp; float logoX = cMax.x - cardPadLg - logoSz; float logoY = cMin.y + cardPadLg; dl->AddImage(logoTex, ImVec2(logoX, logoY), ImVec2(logoX + logoSz, logoY + logoSz), ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.classic", "logo-opacity").sizeOr(180.0f))); } dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), "TOTAL BALANCE"); cy += ovFont->LegacySize + ovGap; snprintf(buf, sizeof(buf), "%.8f", s_dispTotal); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurface(), buf); ImVec2 balSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + balSz.x + tickGap, cy + sub1->LegacySize - capFont->LegacySize), OnSurfaceMedium(), DRAGONX_TICKER); cy += sub1->LegacySize + valGap; // USD value { double usd_value = state.getBalanceUSD(); if (usd_value > 0.0) snprintf(buf, sizeof(buf), "$%.2f USD", usd_value); else snprintf(buf, sizeof(buf), "$-.-- USD"); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf); } cy += capFont->LegacySize + 2 * dp; // Sync progress or mining indicator (whichever fits) if (state.sync.syncing && state.sync.headers > 0) { float pct = static_cast(state.sync.verification_progress) * 100.0f; snprintf(buf, sizeof(buf), "Syncing %.1f%%", pct); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), Warning(), buf); // Thin sync bar at card bottom — clipped to card rounded corners float barH = (syncBar.height >= 0) ? syncBar.height : 3.0f; float prog = static_cast(state.sync.verification_progress); if (prog > 1.0f) prog = 1.0f; float barTop = cMax.y - barH; // Clip to the bottom strip so the full-card rounded rect // curves exactly match the card's own rounded corners dl->PushClipRect(ImVec2(cMin.x, barTop), cMax, true); // Background track dl->AddRectFilled(cMin, cMax, IM_COL32(255, 255, 255, 15), cardSpec.rounding); // Progress fill — additional horizontal clip float progRight = cMin.x + (cMax.x - cMin.x) * prog; dl->PushClipRect(ImVec2(cMin.x, barTop), ImVec2(progRight, cMax.y), true); dl->AddRectFilled(cMin, cMax, WithAlpha(Warning(), 200), cardSpec.rounding); dl->PopClipRect(); dl->PopClipRect(); } else if (state.mining.generate) { float pulse = schema::UI().drawElement("animations", "pulse-base-normal").size + schema::UI().drawElement("animations", "pulse-amp-normal").size * (float)std::sin((double)ImGui::GetTime() * schema::UI().drawElement("animations", "pulse-speed-normal").size); ImU32 mineCol = WithAlpha(Success(), (int)(255.0f * pulse)); dl->AddCircleFilled(ImVec2(cx + 4 * dp, cy + capFont->LegacySize * 0.5f), S.drawElement("tabs.balance.classic", "mining-dot-radius").sizeOr(3.0f), mineCol); double hr = state.mining.localHashrate; if (hr >= 1000.0) snprintf(buf, sizeof(buf), " Mining %.1f KH/s", hr / 1000.0); else snprintf(buf, sizeof(buf), " Mining %.0f H/s", hr); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 12 * dp, cy), WithAlpha(Success(), 200), buf); } // Hover glow if (material::IsRectHovered(cMin, cMax)) { dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)), cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f)); } } // ---- Shielded card ---- { ImVec2 cMin = cardPos(1); ImVec2 cMax(cMin.x + cardWidth, cMin.y + cardH); DrawGlassPanel(dl, cMin, cMax, cardSpec); drawAccent(cMin, cMax, WithAlpha(S.resolveColor("var(--accent-shielded)", Success()), 200)); float cx = cMin.x + cardPadLg; float cy = cMin.y + cardPadLg; dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("shielded")); cy += ovFont->LegacySize + ovGap; snprintf(buf, sizeof(buf), "%.8f", s_dispShielded); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), greenCol, buf); ImVec2 balSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + balSz.x + tickGap, cy + sub1->LegacySize - capFont->LegacySize), OnSurfaceMedium(), DRAGONX_TICKER); cy += sub1->LegacySize + valGap; // Privacy ratio + address count { float privPct = (s_dispTotal > 1e-9) ? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f; snprintf(buf, sizeof(buf), "%.0f%% of total · %d Z-addr", privPct, (int)state.z_addresses.size()); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), WithAlpha(Success(), 160), buf); } // Unconfirmed badge (top-right corner) if (state.unconfirmed_balance > 0.0) { snprintf(buf, sizeof(buf), "+%.4f", state.unconfirmed_balance); ImVec2 ts = capFont->CalcTextSizeA( capFont->LegacySize, 10000, 0, buf); float bp = S.drawElement("tabs.balance.classic", "unconfirmed-badge-padding").sizeOr(4.0f); float br = S.drawElement("tabs.balance.classic", "unconfirmed-badge-rounding").sizeOr(4.0f); ImVec2 bMin(cMax.x - ts.x - bp * 3, cMin.y + cardPadLg); ImVec2 bMax(cMax.x - bp, bMin.y + ts.y + bp); dl->AddRectFilled(bMin, bMax, WithAlpha(Warning(), 40), br); dl->AddText(capFont, capFont->LegacySize, ImVec2(bMin.x + bp, bMin.y + bp * 0.5f), amberCol, buf); } // Hover glow + click to Receive if (material::IsRectHovered(cMin, cMax)) { dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)), cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f)); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (ImGui::IsMouseClicked(0)) app->setCurrentPage(NavPage::Receive); } } // ---- Transparent card ---- { ImVec2 cMin = cardPos(2); ImVec2 cMax(cMin.x + cardWidth, cMin.y + cardH); DrawGlassPanel(dl, cMin, cMax, cardSpec); drawAccent(cMin, cMax, WithAlpha(S.resolveColor("var(--accent-transparent)", Warning()), 200)); float cx = cMin.x + cardPadLg; float cy = cMin.y + cardPadLg; dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), TR("transparent")); cy += ovFont->LegacySize + ovGap; snprintf(buf, sizeof(buf), "%.8f", s_dispTransparent); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), goldCol, buf); ImVec2 balSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + balSz.x + tickGap, cy + sub1->LegacySize - capFont->LegacySize), OnSurfaceMedium(), DRAGONX_TICKER); cy += sub1->LegacySize + valGap; snprintf(buf, sizeof(buf), "%d T-addresses", (int)state.t_addresses.size()); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf); // Hover glow + click to Receive if (material::IsRectHovered(cMin, cMax)) { dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)), cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f)); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (ImGui::IsMouseClicked(0)) app->setCurrentPage(NavPage::Receive); } } // ---- Market card ---- { ImVec2 cMin = cardPos(3); ImVec2 cMax(cMin.x + cardWidth, cMin.y + cardH); DrawGlassPanel(dl, cMin, cMax, cardSpec); drawAccent(cMin, cMax, S.resolveColor("var(--accent-action)", Primary())); float cx = cMin.x + cardPadLg; float cy = cMin.y + cardPadLg; // Price string (compute early to measure text width) const auto& market = state.market; if (market.price_usd > 0) { if (market.price_usd >= 0.01) snprintf(buf, sizeof(buf), "$%.4f", market.price_usd); else if (market.price_usd >= 0.0001) snprintf(buf, sizeof(buf), "$%.6f", market.price_usd); else snprintf(buf, sizeof(buf), "$%.8f", market.price_usd); } else { snprintf(buf, sizeof(buf), "$--.--"); } ImVec2 pSz = sub1->CalcTextSizeA(sub1->LegacySize, 10000, 0, buf); ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, "USD"); // Measure widest text line to determine sparkline left edge float textW = std::max(pSz.x + tickGap + usdSz.x, ovFont->CalcTextSizeA(ovFont->LegacySize, 10000, 0, "MARKET").x); float sparkGap = S.drawElement("tabs.balance.classic", "sparkline-gap").sizeOr(12.0f); float sparkLeft = cx + textW + sparkGap; float sparkRight = cMax.x - cardPadLg; // Left side: label + price + 24h change dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), "MARKET"); cy += ovFont->LegacySize + ovGap; dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurface(), buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + pSz.x + tickGap, cy + sub1->LegacySize - capFont->LegacySize), OnSurfaceMedium(), "USD"); cy += sub1->LegacySize + valGap; // 24h change if (market.price_usd > 0) { bool pos = market.change_24h >= 0; ImU32 chgCol = pos ? Success() : Error(); snprintf(buf, sizeof(buf), "%s%.1f%% 24h", pos ? "+" : "", market.change_24h); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), chgCol, buf); } // Right side: sparkline fills remaining card space if (market.price_history.size() >= 2 && sparkLeft < sparkRight) { float spTop = cMin.y + cardPadLg; float spBot = cMax.y - cardPadLg; ImVec2 spMin(sparkLeft, spTop); ImVec2 spMax(sparkRight, spBot); ImU32 lineCol = market.change_24h >= 0 ? WithAlpha(Success(), 200) : WithAlpha(Error(), 200); DrawSparkline(dl, spMin, spMax, market.price_history, lineCol); } // Hover glow + click to Market if (material::IsRectHovered(cMin, cMax)) { dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)), cardSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f)); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (ImGui::IsMouseClicked(0)) app->setCurrentPage(NavPage::Market); } } // Advance cursor past the card row(s) { int totalCards = 4; int numRows = (totalCards + numCols - 1) / numCols; ImGui::Dummy(ImVec2(availWidth, cardH * numRows + cardGap * (numRows - 1))); } ImGui::Dummy(ImVec2(0, Layout::spacingMd())); } // ================================================================ // Address list — DrawList-based rows (matches recent tx style) // ================================================================ { // Header row: title only Type().text(TypeStyle::H6, TR("your_addresses")); ImGui::Spacing(); // Static filter state (declared here, UI rendered below with ADDRESSES overline) static char addr_search[128] = ""; static bool s_hideZeroBalances = true; static bool s_showHidden = false; // Build a merged + sorted list of all addresses struct AddrRow { const AddressInfo* info; bool isZ; bool hidden; bool favorite; }; std::vector rows; rows.reserve(state.z_addresses.size() + state.t_addresses.size()); for (const auto& a : state.z_addresses) { std::string filter(addr_search); if (!containsIgnoreCase(a.address, filter) && !containsIgnoreCase(a.label, filter)) continue; bool isHidden = app->isAddressHidden(a.address); if (isHidden && !s_showHidden) continue; bool isFav = app->isAddressFavorite(a.address); if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav) continue; rows.push_back({&a, true, isHidden, isFav}); } for (const auto& a : state.t_addresses) { std::string filter(addr_search); if (!containsIgnoreCase(a.address, filter) && !containsIgnoreCase(a.label, filter)) continue; bool isHidden = app->isAddressHidden(a.address); if (isHidden && !s_showHidden) continue; bool isFav = app->isAddressFavorite(a.address); if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav) continue; rows.push_back({&a, false, isHidden, isFav}); } // Sort: favorites first, then Z addresses, then by balance descending static int s_sortCol = 3; // default sort by balance static bool s_sortAsc = false; std::sort(rows.begin(), rows.end(), [](const AddrRow& a, const AddrRow& b) -> bool { if (a.favorite != b.favorite) return a.favorite > b.favorite; if (a.isZ != b.isZ) return a.isZ > b.isZ; // Z first if (s_sortAsc) return a.info->balance < b.info->balance; else return a.info->balance > b.info->balance; }); // Recent TX gets sizing priority — compute its reserve first, // then the address list gets whatever remains. float scaledTxRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs); // Recent TX section: header + kRecentTxCount rows + status label + gaps float recentTxReserve = S.drawElement("tabs.balance", "recent-tx-header-height").size * vs + kRecentTxCount * scaledTxRowH + Layout::spacingXl(); // Search + create buttons row float avail = ImGui::GetContentRegionAvail().x; float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f; float schemaRatio = (searchIn.widthRatio >= 0) ? searchIn.widthRatio : 0.30f; float searchW = std::min(schemaMaxW * hs, avail * schemaRatio); ImGui::SetNextItemWidth(searchW); ImGui::InputTextWithHint("##AddrSearch", "Filter...", addr_search, sizeof(addr_search)); ImGui::SameLine(0, Layout::spacingLg()); ImGui::Checkbox("Hide 0 Balances", &s_hideZeroBalances); { int hc = app->getHiddenAddressCount(); if (hc > 0) { ImGui::SameLine(0, Layout::spacingLg()); char hlbl[64]; snprintf(hlbl, sizeof(hlbl), "Show Hidden (%d)", 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; ImGui::SameLine(std::max(kMinButtonsPosition, ImGui::GetWindowWidth() - totalButtonsWidth - kButtonRowRightMargin)); bool addrSyncing = state.sync.syncing && !state.sync.isSynced(); ImGui::BeginDisabled(addrSyncing); if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { app->createNewZAddress([](const std::string& addr) { 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())); float availWidth = ImGui::GetContentRegionAvail().x; // Address list gets whatever height remains after recent TX reserve float classicAddrH = S.drawElement("tabs.balance.classic", "address-table-height").size; float addrListH; if (classicAddrH >= 0.0f) { addrListH = classicAddrH; // explicit override from ui.toml } else { addrListH = ImGui::GetContentRegionAvail().y - recentTxReserve; } // Keep address list at a reasonable minimum; if too tight, // shrink recent-tx reserve instead so both remain visible. float addrListMin = S.drawElement("tabs.balance", "addr-list-min-height").sizeOr(40.0f); if (addrListH < addrListMin) { addrListH = addrListMin; } // Glass panel wrapping the list area (matching tx list) ImDrawList* dlPanel = ImGui::GetWindowDrawList(); ImVec2 listPanelMin = ImGui::GetCursorScreenPos(); ImVec2 listPanelMax(listPanelMin.x + availWidth, listPanelMin.y + addrListH); GlassPanelSpec addrGlassSpec; addrGlassSpec.rounding = glassRound; DrawGlassPanel(dlPanel, listPanelMin, listPanelMax, addrGlassSpec); // Scroll-edge mask state 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(availWidth, addrListH), false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse); ApplySmoothScroll(); ImDrawList* addrChildDL = ImGui::GetWindowDrawList(); int addrChildVtx = addrChildDL->VtxBuffer.Size; ImGui::Dummy(ImVec2(0, Layout::spacingSm())); 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()) { // Empty state float cw = ImGui::GetContentRegionAvail().x; float ch = ImGui::GetContentRegionAvail().y; if (ch < 60) ch = 60; if (addr_search[0]) { ImVec2 textSz = ImGui::CalcTextSize("No matching addresses"); ImGui::SetCursorPosX((cw - textSz.x) * 0.5f); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f); ImGui::TextDisabled("No matching addresses"); } else { const char* msg = "No addresses yet"; ImVec2 msgSz = ImGui::CalcTextSize(msg); ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f); ImGui::TextDisabled("%s", msg); } } else { // DrawList-based address rows (matching transaction list style) ImDrawList* dl = ImGui::GetWindowDrawList(); ImFont* capFont = Type().caption(); ImFont* body2 = Type().body2(); float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + 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::spacingLg(); 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; for (int row_idx = 0; row_idx < (int)rows.size(); row_idx++) { const auto& row = rows[row_idx]; const auto& addr = *row.info; ImVec2 rowPos = ImGui::GetCursorScreenPos(); ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH); ImU32 typeCol = row.isZ ? greenCol : goldCol; if (row.hidden) typeCol = OnSurfaceDisabled(); // Golden border for favorites if (row.favorite) { ImU32 favBorder = IM_COL32(255, 200, 50, 120); dl->AddRect(rowPos, rowEnd, favBorder, 4.0f * dp, 0, 1.5f * dp); } // Selected indicator (left accent bar) if (selected_row == row_idx) { ImDrawFlags accentFlags = 0; float accentRound = 2.0f * dp; if (row_idx == 0) { accentFlags = ImDrawFlags_RoundCornersTopLeft; accentRound = glassRound; } if (row_idx == (int)rows.size() - 1) { accentFlags |= ImDrawFlags_RoundCornersBottomLeft; accentRound = glassRound; } dl->AddRectFilled(rowPos, ImVec2(rowPos.x + 3 * dp, rowEnd.y), typeCol, accentRound, accentFlags); dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), 4.0f * dp); } // Hover glow bool hovered = material::IsRectHovered(rowPos, rowEnd); if (hovered && selected_row != row_idx) { dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), 4.0f * dp); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } float cx = rowPos.x + rowPadLeft; float cy = rowPos.y + Layout::spacingMd(); // --- Button zone (right edge): [eye] [star] --- float btnH = rowH - Layout::spacingSm() * 2.0f; float btnW = btnH; float btnGap = Layout::spacingXs(); float btnY = rowPos.y + (rowH - btnH) * 0.5f; float rightEdge = rowPos.x + innerW; float starX = rightEdge - btnW - Layout::spacingSm(); float eyeX = starX - btnGap - btnW; float btnRound = 6.0f * dp; bool btnClicked = false; // Star button (always shown, rightmost) { ImVec2 bMin(starX, btnY), bMax(starX + btnW, btnY + btnH); bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax); ImU32 starFill = row.favorite ? IM_COL32(255, 200, 50, 40) : IM_COL32(255, 255, 255, bHov ? 25 : 12); ImU32 starBorder = row.favorite ? IM_COL32(255, 200, 50, 100) : IM_COL32(255, 255, 255, bHov ? 50 : 25); dl->AddRectFilled(bMin, bMax, starFill, btnRound); dl->AddRect(bMin, bMax, starBorder, btnRound, 0, 1.0f * dp); ImFont* iconFont = material::Type().iconSmall(); const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon); ImU32 starCol = row.favorite ? IM_COL32(255, 200, 50, 255) : (bHov ? OnSurface() : OnSurfaceDisabled()); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(starX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), starCol, starIcon); if (bHov && ImGui::IsMouseClicked(0)) { if (row.favorite) app->unfavoriteAddress(addr.address); else app->favoriteAddress(addr.address); btnClicked = true; } if (bHov) ImGui::SetTooltip("%s", row.favorite ? "Remove favorite" : "Favorite address"); } // Eye button (zero balance or hidden) if (addr.balance < 1e-9 || row.hidden) { ImVec2 bMin(eyeX, btnY), bMax(eyeX + btnW, btnY + btnH); bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax); ImU32 eyeFill = IM_COL32(255, 255, 255, bHov ? 25 : 12); ImU32 eyeBorder = IM_COL32(255, 255, 255, bHov ? 50 : 25); dl->AddRectFilled(bMin, bMax, eyeFill, btnRound); dl->AddRect(bMin, bMax, eyeBorder, btnRound, 0, 1.0f * dp); ImFont* iconFont = material::Type().iconSmall(); const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon); ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled(); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(eyeX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), iconCol, hideIcon); if (bHov && ImGui::IsMouseClicked(0)) { if (row.hidden) app->unhideAddress(addr.address); else app->hideAddress(addr.address); btnClicked = true; } if (bHov) ImGui::SetTooltip("%s", row.hidden ? "Restore address" : "Hide address"); } // Content zone ends before buttons float contentRight = eyeX - Layout::spacingSm(); // Type icon (shield for Z, circle for T) float iconCx = cx + rowIconSz; float iconCy = cy + body2->LegacySize * 0.5f; if (row.isZ) { ImFont* iconFont = material::Type().iconSmall(); const char* shieldIcon = ICON_MD_SHIELD; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, shieldIcon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, shieldIcon); } else { ImFont* iconFont = material::Type().iconSmall(); const char* circIcon = ICON_MD_CIRCLE; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, circIcon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, circIcon); } // Type label (first line, next to icon) float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm(); const char* typeLabel = row.isZ ? "Shielded" : "Transparent"; const char* hiddenTag = row.hidden ? " (hidden)" : ""; char typeBuf[64]; snprintf(typeBuf, sizeof(typeBuf), "%s%s", typeLabel, hiddenTag); dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf); // Label (if present, next to type) if (!addr.label.empty()) { float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x; dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy), OnSurfaceMedium(), addr.label.c_str()); } // Address (second line) — show full if it fits, otherwise truncate float addrAvailW = contentRight - labelX; ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str()); std::string display_addr; if (fullAddrSz.x <= addrAvailW) { display_addr = addr.address; } else { int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0) ? addrTable.columns.at("address").truncate : 32; display_addr = truncateAddress(addr.address, addrTruncLen); } dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()), OnSurfaceMedium(), display_addr.c_str()); // Balance (right-aligned within content zone) char balBuf[32]; snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance); ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf); float balX = contentRight - balSz.x; ImU32 balCol = addr.balance > 0.0 ? (row.isZ ? greenCol : OnSurface()) : OnSurfaceDisabled(); if (row.hidden) balCol = OnSurfaceDisabled(); DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf); // USD equivalent (right-aligned, second line) double priceUsd = state.market.price_usd; if (priceUsd > 0.0 && addr.balance > 0.0) { char usdBuf[32]; double usdVal = addr.balance * priceUsd; if (usdVal >= 1.0) snprintf(usdBuf, sizeof(usdBuf), "$%.2f", usdVal); else if (usdVal >= 0.01) snprintf(usdBuf, sizeof(usdBuf), "$%.4f", usdVal); else snprintf(usdBuf, sizeof(usdBuf), "$%.6f", usdVal); ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdBuf); dl->AddText(capFont, capFont->LegacySize, ImVec2(contentRight - usdSz.x, cy + body2->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), usdBuf); } // Click to copy + select if (hovered && ImGui::IsMouseClicked(0) && !btnClicked) { ImGui::SetClipboardText(addr.address.c_str()); selected_row = row_idx; } // Invisible button for context menu + tooltip ImGui::PushID(row_idx); ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH)); // Tooltip with full address if (ImGui::IsItemHovered() && !btnClicked) { ImGui::SetTooltip("%s", addr.address.c_str()); } // Right-click context menu const auto& acrTheme = GetCurrentAcrylicTheme(); if (effects::ImGuiAcrylic::BeginAcrylicContextItem("AddressContext", 0, acrTheme.menu)) { if (ImGui::MenuItem(TR("copy_address"))) { ImGui::SetClipboardText(addr.address.c_str()); } if (ImGui::MenuItem(TR("send_from_this_address"))) { SetSendFromAddress(addr.address); app->setCurrentPage(NavPage::Send); } ImGui::Separator(); 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 ? "Z-Address" : "T-Address"); } ImGui::Separator(); if (row.hidden) { if (ImGui::MenuItem("Restore Address")) app->unhideAddress(addr.address); } else if (addr.balance < 1e-9) { if (ImGui::MenuItem("Hide Address")) app->hideAddress(addr.address); } if (row.favorite) { if (ImGui::MenuItem("Remove Favorite")) app->unfavoriteAddress(addr.address); } else { if (ImGui::MenuItem("Favorite Address")) app->favoriteAddress(addr.address); } effects::ImGuiAcrylic::EndAcrylicPopup(); } ImGui::PopID(); // Subtle divider between rows (matching tx list) if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) { ImVec2 divStart = ImGui::GetCursorScreenPos(); dl->AddLine( ImVec2(divStart.x + rowPadLeft + rowIconSz * 2.0f, divStart.y), ImVec2(divStart.x + innerW - Layout::spacingLg(), divStart.y), IM_COL32(255, 255, 255, 15)); } } } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::EndChild(); ImGui::PopStyleVar(); // WindowPadding for address list // CSS-style clipping mask (same as history list) { 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); } // Status line (matching tx list) { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); char countBuf[128]; int totalAddrs = (int)(state.z_addresses.size() + state.t_addresses.size()); snprintf(countBuf, sizeof(countBuf), "Showing %d of %d addresses", (int)rows.size(), totalAddrs); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf); } } // ================================================================ // Recent Transactions // ================================================================ { 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(); const auto& txs = state.transactions; int maxTx = kRecentTxCount; int count = (int)txs.size(); if (count > maxTx) count = maxTx; 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; // Icon ImU32 iconCol; if (tx.type == "send") iconCol = Error(); else if (tx.type == "receive") iconCol = Success(); else iconCol = Warning(); DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, iconCol); // Type label 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(), tx.getTypeDisplay().c_str()); // Address (truncated) float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f); std::string trAddr = truncateAddress(tx.address, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f)); dl->AddText(capFont, capFont->LegacySize, ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), trAddr.c_str()); // Amount (right-aligned area) char amtBuf[32]; snprintf(amtBuf, sizeof(amtBuf), "%s%.4f %s", tx.type == "send" ? "-" : "+", std::abs(tx.amount), DRAGONX_TICKER); ImVec2 amtSz = capFont->CalcTextSizeA( capFont->LegacySize, 10000, 0, amtBuf); 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), tx.type == "send" ? Error() : Success(), amtBuf); // Time ago std::string ago = timeAgo(tx.timestamp); ImVec2 agoSz = capFont->CalcTextSizeA( capFont->LegacySize, 10000, 0, ago.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(), ago.c_str()); // Clickable row — hover highlight + navigate to History 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)); } } } } // ============================================================================ // 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 = 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(); static char addr_search[128] = ""; static bool s_hideZeroBalances = true; static bool s_showHidden = false; struct AddrRow { const AddressInfo* info; bool isZ; bool hidden; bool favorite; }; std::vector rows; rows.reserve(state.z_addresses.size() + state.t_addresses.size()); for (const auto& a : state.z_addresses) { std::string filter(addr_search); if (!containsIgnoreCase(a.address, filter) && !containsIgnoreCase(a.label, filter)) continue; bool isHidden = app->isAddressHidden(a.address); if (isHidden && !s_showHidden) continue; bool isFav = app->isAddressFavorite(a.address); if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav) continue; rows.push_back({&a, true, isHidden, isFav}); } for (const auto& a : state.t_addresses) { std::string filter(addr_search); if (!containsIgnoreCase(a.address, filter) && !containsIgnoreCase(a.label, filter)) continue; bool isHidden = app->isAddressHidden(a.address); if (isHidden && !s_showHidden) continue; bool isFav = app->isAddressFavorite(a.address); if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav) continue; rows.push_back({&a, false, isHidden, isFav}); } static int s_sortCol = 3; static bool s_sortAsc = false; std::sort(rows.begin(), rows.end(), [](const AddrRow& a, const AddrRow& b) -> bool { if (a.favorite != b.favorite) return a.favorite > b.favorite; if (a.isZ != b.isZ) return a.isZ > b.isZ; if (s_sortAsc) return a.info->balance < b.info->balance; else return a.info->balance > b.info->balance; }); // Search + create buttons row float avail = ImGui::GetContentRegionAvail().x; float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f; float schemaRatio = (searchIn.widthRatio >= 0) ? searchIn.widthRatio : 0.30f; float searchW = std::min(schemaMaxW * hs, avail * schemaRatio); ImGui::SetNextItemWidth(searchW); ImGui::InputTextWithHint("##AddrSearch", "Filter...", addr_search, sizeof(addr_search)); ImGui::SameLine(0, Layout::spacingLg()); ImGui::Checkbox("Hide 0 Balances", &s_hideZeroBalances); { int hc = app->getHiddenAddressCount(); if (hc > 0) { ImGui::SameLine(0, Layout::spacingLg()); char hlbl[64]; snprintf(hlbl, sizeof(hlbl), "Show Hidden (%d)", 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 (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) { app->createNewZAddress([](const std::string& addr) { 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())); 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())); 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; if (addr_search[0]) { ImVec2 textSz = ImGui::CalcTextSize("No matching addresses"); ImGui::SetCursorPosX((cw - textSz.x) * 0.5f); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f); ImGui::TextDisabled("No matching addresses"); } else { const char* msg = "No addresses yet"; ImVec2 msgSz = ImGui::CalcTextSize(msg); ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f); ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f); ImGui::TextDisabled("%s", msg); } } else { ImDrawList* dl = ImGui::GetWindowDrawList(); ImFont* capFont = Type().caption(); ImFont* body2 = Type().body2(); float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + 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::spacingLg(); 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; for (int row_idx = 0; row_idx < (int)rows.size(); row_idx++) { const auto& row = rows[row_idx]; const auto& addr = *row.info; ImVec2 rowPos = ImGui::GetCursorScreenPos(); ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH); ImU32 typeCol = row.isZ ? greenCol : goldCol; if (row.hidden) typeCol = OnSurfaceDisabled(); // Golden border for favorites if (row.favorite) { ImU32 favBorder = IM_COL32(255, 200, 50, 120); dl->AddRect(rowPos, rowEnd, favBorder, 4.0f * dp, 0, 1.5f * dp); } if (selected_row == row_idx) { ImDrawFlags accentFlags = 0; float accentRound = 2.0f * dp; if (row_idx == 0) { accentFlags = ImDrawFlags_RoundCornersTopLeft; accentRound = glassRound; } if (row_idx == (int)rows.size() - 1) { accentFlags |= ImDrawFlags_RoundCornersBottomLeft; accentRound = glassRound; } dl->AddRectFilled(rowPos, ImVec2(rowPos.x + 3 * dp, rowEnd.y), typeCol, accentRound, accentFlags); dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), 4.0f * dp); } bool hovered = material::IsRectHovered(rowPos, rowEnd); if (hovered && selected_row != row_idx) { dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), 4.0f * dp); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } float cx = rowPos.x + rowPadLeft; float cy = rowPos.y + Layout::spacingMd(); // --- Button zone (right edge): [eye] [star] --- float btnH = rowH - Layout::spacingSm() * 2.0f; float btnW = btnH; float btnGap = Layout::spacingXs(); float btnY = rowPos.y + (rowH - btnH) * 0.5f; float rightEdge = rowPos.x + innerW; float starX = rightEdge - btnW - Layout::spacingSm(); float eyeX = starX - btnGap - btnW; float btnRound = 6.0f * dp; bool btnClicked = false; // Star button (always shown, rightmost) { ImVec2 bMin(starX, btnY), bMax(starX + btnW, btnY + btnH); bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax); ImU32 starFill = row.favorite ? IM_COL32(255, 200, 50, 40) : IM_COL32(255, 255, 255, bHov ? 25 : 12); ImU32 starBorder = row.favorite ? IM_COL32(255, 200, 50, 100) : IM_COL32(255, 255, 255, bHov ? 50 : 25); dl->AddRectFilled(bMin, bMax, starFill, btnRound); dl->AddRect(bMin, bMax, starBorder, btnRound, 0, 1.0f * dp); ImFont* iconFont = material::Type().iconSmall(); const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon); ImU32 starCol = row.favorite ? IM_COL32(255, 200, 50, 255) : (bHov ? OnSurface() : OnSurfaceDisabled()); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(starX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), starCol, starIcon); if (bHov && ImGui::IsMouseClicked(0)) { if (row.favorite) app->unfavoriteAddress(addr.address); else app->favoriteAddress(addr.address); btnClicked = true; } if (bHov) ImGui::SetTooltip("%s", row.favorite ? "Remove favorite" : "Favorite address"); } // Eye button (zero balance or hidden) if (addr.balance < 1e-9 || row.hidden) { ImVec2 bMin(eyeX, btnY), bMax(eyeX + btnW, btnY + btnH); bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax); ImU32 eyeFill = IM_COL32(255, 255, 255, bHov ? 25 : 12); ImU32 eyeBorder = IM_COL32(255, 255, 255, bHov ? 50 : 25); dl->AddRectFilled(bMin, bMax, eyeFill, btnRound); dl->AddRect(bMin, bMax, eyeBorder, btnRound, 0, 1.0f * dp); ImFont* iconFont = material::Type().iconSmall(); const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon); ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled(); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(eyeX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), iconCol, hideIcon); if (bHov && ImGui::IsMouseClicked(0)) { if (row.hidden) app->unhideAddress(addr.address); else app->hideAddress(addr.address); btnClicked = true; } if (bHov) ImGui::SetTooltip("%s", row.hidden ? "Restore address" : "Hide address"); } // Content zone ends before buttons float contentRight = eyeX - Layout::spacingSm(); float iconCx = cx + rowIconSz; float iconCy = cy + body2->LegacySize * 0.5f; if (row.isZ) { ImFont* iconFont = material::Type().iconSmall(); const char* shieldIcon = ICON_MD_SHIELD; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, shieldIcon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, shieldIcon); } else { ImFont* iconFont = material::Type().iconSmall(); const char* circIcon = ICON_MD_CIRCLE; ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, circIcon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, circIcon); } float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm(); const char* typeLabel = row.isZ ? "Shielded" : "Transparent"; const char* hiddenTag = row.hidden ? " (hidden)" : ""; char typeBuf[64]; snprintf(typeBuf, sizeof(typeBuf), "%s%s", typeLabel, hiddenTag); dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf); if (!addr.label.empty()) { float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x; dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy), OnSurfaceMedium(), addr.label.c_str()); } // Address (second line) — show full if it fits, otherwise truncate float addrAvailW = contentRight - labelX; ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str()); std::string display_addr; if (fullAddrSz.x <= addrAvailW) { display_addr = addr.address; } else { int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0) ? addrTable.columns.at("address").truncate : 32; display_addr = truncateAddress(addr.address, addrTruncLen); } dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()), OnSurfaceMedium(), display_addr.c_str()); // Balance (right-aligned within content zone) char balBuf[32]; snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance); ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf); float balX = contentRight - balSz.x; ImU32 balCol = addr.balance > 0.0 ? (row.isZ ? greenCol : OnSurface()) : OnSurfaceDisabled(); if (row.hidden) balCol = OnSurfaceDisabled(); DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf); double priceUsd = state.market.price_usd; if (priceUsd > 0.0 && addr.balance > 0.0) { char usdBuf[32]; double usdVal = addr.balance * priceUsd; if (usdVal >= 1.0) snprintf(usdBuf, sizeof(usdBuf), "$%.2f", usdVal); else if (usdVal >= 0.01) snprintf(usdBuf, sizeof(usdBuf), "$%.4f", usdVal); else snprintf(usdBuf, sizeof(usdBuf), "$%.6f", usdVal); ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdBuf); dl->AddText(capFont, capFont->LegacySize, ImVec2(contentRight - usdSz.x, cy + body2->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), usdBuf); } if (hovered && ImGui::IsMouseClicked(0) && !btnClicked) { ImGui::SetClipboardText(addr.address.c_str()); selected_row = row_idx; } ImGui::PushID(row_idx); ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH)); if (ImGui::IsItemHovered() && !btnClicked) ImGui::SetTooltip("%s", addr.address.c_str()); const auto& acrTheme = GetCurrentAcrylicTheme(); if (effects::ImGuiAcrylic::BeginAcrylicContextItem("AddressContext", 0, acrTheme.menu)) { if (ImGui::MenuItem(TR("copy_address"))) ImGui::SetClipboardText(addr.address.c_str()); if (ImGui::MenuItem(TR("send_from_this_address"))) { SetSendFromAddress(addr.address); app->setCurrentPage(NavPage::Send); } ImGui::Separator(); 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 ? "Z-Address" : "T-Address"); ImGui::Separator(); if (row.hidden) { if (ImGui::MenuItem("Restore Address")) app->unhideAddress(addr.address); } else if (addr.balance < 1e-9) { if (ImGui::MenuItem("Hide Address")) app->hideAddress(addr.address); } if (row.favorite) { if (ImGui::MenuItem("Remove Favorite")) app->unfavoriteAddress(addr.address); } else { if (ImGui::MenuItem("Favorite Address")) app->favoriteAddress(addr.address); } effects::ImGuiAcrylic::EndAcrylicPopup(); } ImGui::PopID(); if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) { ImVec2 divStart = ImGui::GetCursorScreenPos(); dl->AddLine( ImVec2(divStart.x + rowPadLeft + rowIconSz * 2.0f, divStart.y), ImVec2(divStart.x + innerW - Layout::spacingLg(), divStart.y), IM_COL32(255, 255, 255, 15)); } } } ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::EndChild(); ImGui::PopStyleVar(); { 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); } { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); char countBuf[128]; int totalAddrs = (int)(state.z_addresses.size() + state.t_addresses.size()); snprintf(countBuf, sizeof(countBuf), "Showing %d of %d addresses", (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); int maxTx = std::clamp((int)(recentH / scaledRowH), 2, 5); const auto& txs = state.transactions; int count = (int)txs.size(); if (count > maxTx) count = maxTx; 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; ImU32 iconCol; if (tx.type == "send") iconCol = Error(); else if (tx.type == "receive") iconCol = Success(); else iconCol = Warning(); DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, iconCol); 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(), tx.getTypeDisplay().c_str()); float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f); std::string trAddr = truncateAddress(tx.address, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f)); dl->AddText(capFont, capFont->LegacySize, ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), trAddr.c_str()); char amtBuf[32]; snprintf(amtBuf, sizeof(amtBuf), "%s%.4f %s", tx.type == "send" ? "-" : "+", std::abs(tx.amount), DRAGONX_TICKER); ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, amtBuf); 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), tx.type == "send" ? Error() : Success(), amtBuf); std::string ago = timeAgo(tx.timestamp); ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, ago.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(), ago.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)); } } } // 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)); } } // ============================================================================ // Layout 1: Donut Chart // ============================================================================ static void RenderBalanceDonut(App* app) { using namespace material; const auto& S = schema::UISchema::instance(); const auto& state = app->state(); UpdateBalanceLerp(app); ImVec2 contentAvail = ImGui::GetContentRegionAvail(); float availW = contentAvail.x; float hs = Layout::hScale(availW); float vs = Layout::vScale(contentAvail.y); float glassRound = Layout::glassRounding(); float cGap = Layout::cardGap(); const float dp = Layout::dpiScale(); ImDrawList* dl = ImGui::GetWindowDrawList(); char buf[64]; // --- Hero: total balance --- float donutTopMargin = S.drawElement("tabs.balance.donut", "top-margin").size; if (donutTopMargin >= 0.0f) ImGui::Dummy(ImVec2(0, donutTopMargin)); else ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.donut", "hero-pad-ratio").sizeOr(8.0f) * vs)); { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE"); ImGui::Dummy(ImVec2(0, 2 * dp)); snprintf(buf, sizeof(buf), "%.8f", s_dispTotal); ImFont* heroFont = Type().h2(); ImVec2 pos = ImGui::GetCursorScreenPos(); DrawTextShadow(dl, heroFont, heroFont->LegacySize, pos, OnSurface(), buf); ImVec2 heroSize = heroFont->CalcTextSizeA(heroFont->LegacySize, 10000.0f, 0.0f, buf); ImGui::Dummy(heroSize); ImGui::SameLine(); ImFont* capFont = Type().caption(); float tickerY = pos.y + heroSize.y - capFont->LegacySize; dl->AddText(capFont, capFont->LegacySize, ImVec2(ImGui::GetCursorScreenPos().x, tickerY), OnSurfaceMedium(), DRAGONX_TICKER); ImGui::NewLine(); // USD value double usd_value = state.getBalanceUSD(); if (usd_value > 0.0) snprintf(buf, sizeof(buf), "$%.2f USD", usd_value); else snprintf(buf, sizeof(buf), "$-.-- USD"); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf); } RenderSyncBar(app, dl, vs); ImGui::Dummy(ImVec2(0, cGap)); // --- Donut + legend panel --- { float donutCardH = S.drawElement("tabs.balance.donut", "card-height").size; float panelH; if (donutCardH >= 0.0f) { panelH = donutCardH; // explicit override from ui.toml } else { panelH = std::max( S.drawElement("tabs.balance.donut", "panel-min-height").sizeOr(80.0f), contentAvail.y * S.drawElement("tabs.balance.donut", "panel-height-ratio").sizeOr(0.20f)); } ImVec2 panelMin = ImGui::GetCursorScreenPos(); ImVec2 panelMax(panelMin.x + availW, panelMin.y + panelH); GlassPanelSpec spec; spec.rounding = glassRound; DrawGlassPanel(dl, panelMin, panelMax, spec); float donutPadOverride = S.drawElement("tabs.balance.donut", "card-padding").size; float donutPad = (donutPadOverride >= 0.0f) ? donutPadOverride : Layout::spacingLg(); // Donut ring float cx = panelMin.x + panelH * 0.5f + donutPad; float cy = panelMin.y + panelH * 0.5f; float radius = std::min( panelH * S.drawElement("tabs.balance.donut", "outer-radius-ratio").sizeOr(0.40f), availW * S.drawElement("tabs.balance.donut", "max-radius-ratio").sizeOr(0.12f)); float innerRadius = radius * S.drawElement("tabs.balance.donut", "inner-radius-ratio").sizeOr(0.6f); float total = (float)s_dispTotal; float shielded = (float)s_dispShielded; float ratio = (total > 1e-9f) ? shielded / total : 0.5f; // Shielded arc (green) float startAngle = -IM_PI * 0.5f; // top float shieldEnd = startAngle + 2.0f * IM_PI * ratio; if (ratio > 0.01f) { dl->PathClear(); dl->PathArcTo(ImVec2(cx, cy), radius, startAngle, shieldEnd, 32); dl->PathArcTo(ImVec2(cx, cy), innerRadius, shieldEnd, startAngle, 32); dl->PathFillConvex(WithAlpha(Success(), 180)); } // Transparent arc (gold) if (ratio < 0.99f) { dl->PathClear(); dl->PathArcTo(ImVec2(cx, cy), radius, shieldEnd, startAngle + 2.0f * IM_PI, 32); dl->PathArcTo(ImVec2(cx, cy), innerRadius, startAngle + 2.0f * IM_PI, shieldEnd, 32); dl->PathFillConvex(WithAlpha(Warning(), 180)); } // Center text: privacy % float privPct = ratio * 100.0f; snprintf(buf, sizeof(buf), "%.0f%%", privPct); ImFont* sub1 = Type().subtitle1(); ImVec2 pctSz = sub1->CalcTextSizeA(sub1->LegacySize, 1000, 0, buf); dl->AddText(sub1, sub1->LegacySize, ImVec2(cx - pctSz.x * 0.5f, cy - pctSz.y * 0.5f), OnSurface(), buf); // Legend (right side) float legendX = panelMin.x + panelH + donutPad * 2; float legendY = panelMin.y + donutPad; ImFont* capFont = Type().caption(); ImFont* body2 = Type().body2(); float legendDotR = S.drawElement("tabs.balance.donut", "legend-dot-radius").sizeOr(4.0f); float legendXOff = S.drawElement("tabs.balance.donut", "legend-x-offset").sizeOr(14.0f); float legendLineGap = S.drawElement("tabs.balance.donut", "legend-line-gap").sizeOr(6.0f); float legendSectionGap = S.drawElement("tabs.balance.donut", "legend-section-gap").sizeOr(10.0f); // Shielded legend dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Success()); snprintf(buf, sizeof(buf), "Shielded %.8f", s_dispShielded); dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Success(), buf); legendY += capFont->LegacySize + legendLineGap; // Transparent legend dl->AddCircleFilled(ImVec2(legendX + 5 * dp, legendY + capFont->LegacySize * 0.5f), legendDotR, Warning()); snprintf(buf, sizeof(buf), "Transparent %.8f", s_dispTransparent); dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), Warning(), buf); legendY += capFont->LegacySize + legendSectionGap; // Market price const auto& market = state.market; if (market.price_usd > 0) { if (market.price_usd >= 0.01) snprintf(buf, sizeof(buf), "Market: $%.4f", market.price_usd); else snprintf(buf, sizeof(buf), "Market: $%.8f", market.price_usd); dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), OnSurfaceMedium(), buf); legendY += capFont->LegacySize + 4 * dp; bool pos = market.change_24h >= 0; snprintf(buf, sizeof(buf), "%s%.1f%% 24h", pos ? "+" : "", market.change_24h); dl->AddText(capFont, capFont->LegacySize, ImVec2(legendX + legendXOff, legendY), pos ? Success() : Error(), buf); } ImGui::Dummy(ImVec2(availW, panelH)); } ImGui::Dummy(ImVec2(0, cGap)); // --- Shared address list + recent tx --- float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f); float donutAddrOverride = S.drawElement("tabs.balance.donut", "address-table-height").size; float addrH = (donutAddrOverride >= 0.0f) ? donutAddrOverride : ImGui::GetContentRegionAvail().y - recentReserve - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd(); RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs); RenderSharedRecentTx(app, recentReserve, availW, hs, vs); } // ============================================================================ // Layout 2: Consolidated Card // ============================================================================ static void RenderBalanceConsolidated(App* app) { using namespace material; const auto& S = schema::UISchema::instance(); const auto& state = app->state(); UpdateBalanceLerp(app); ImVec2 contentAvail = ImGui::GetContentRegionAvail(); float availW = contentAvail.x; float hs = Layout::hScale(availW); float vs = Layout::vScale(contentAvail.y); float glassRound = Layout::glassRounding(); float cGap = Layout::cardGap(); const float dp = Layout::dpiScale(); ImDrawList* dl = ImGui::GetWindowDrawList(); char buf[64]; // Single consolidated card float consTopMargin = S.drawElement("tabs.balance.consolidated", "top-margin").size; if (consTopMargin >= 0.0f) ImGui::Dummy(ImVec2(0, consTopMargin)); float consCardH = S.drawElement("tabs.balance.consolidated", "card-height").size; float cardH; if (consCardH >= 0.0f) { cardH = consCardH; // explicit override from ui.toml } else { cardH = std::max( S.drawElement("tabs.balance.consolidated", "card-min-height").sizeOr(90.0f), contentAvail.y * S.drawElement("tabs.balance.consolidated", "card-height-ratio").sizeOr(0.22f)); } ImVec2 cardMin = ImGui::GetCursorScreenPos(); ImVec2 cardMax(cardMin.x + availW, cardMin.y + cardH); GlassPanelSpec spec; spec.rounding = glassRound; DrawGlassPanel(dl, cardMin, cardMax, spec); float consPadOverride = S.drawElement("tabs.balance.consolidated", "card-padding").size; float pad = (consPadOverride >= 0.0f) ? consPadOverride : Layout::spacingLg(); float cx = cardMin.x + pad; float cy = cardMin.y + pad; // Coin logo ImTextureID logoTex = app->getCoinLogoTexture(); ImFont* heroFont = Type().h2(); ImFont* sub1 = Type().subtitle1(); ImFont* capFont = Type().caption(); ImFont* ovFont = Type().overline(); float logoSz = heroFont->LegacySize + capFont->LegacySize + 4.0f * dp; if (logoTex != 0) { dl->AddImage(logoTex, ImVec2(cx, cy), ImVec2(cx + logoSz, cy + logoSz), ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255)); cx += logoSz + Layout::spacingMd(); } // Total balance snprintf(buf, sizeof(buf), "%.8f", s_dispTotal); DrawTextShadow(dl, heroFont, heroFont->LegacySize, ImVec2(cx, cy), OnSurface(), buf); ImVec2 heroSz = heroFont->CalcTextSizeA(heroFont->LegacySize, 10000, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + heroSz.x + 4 * dp, cy + heroSz.y - capFont->LegacySize), OnSurfaceMedium(), DRAGONX_TICKER); cy += heroSz.y + 2 * dp; // USD value double usd_value = state.getBalanceUSD(); if (usd_value > 0.0) snprintf(buf, sizeof(buf), "$%.2f USD", usd_value); else snprintf(buf, sizeof(buf), "$-.-- USD"); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf); // Market badge (top-right) { const auto& market = state.market; if (market.price_usd > 0) { float badgeX = cardMax.x - pad; float badgeY = cardMin.y + pad; if (market.price_usd >= 0.01) snprintf(buf, sizeof(buf), "$%.4f", market.price_usd); else snprintf(buf, sizeof(buf), "$%.8f", market.price_usd); ImVec2 pSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX - pSz.x, badgeY), OnSurfaceMedium(), buf); badgeY += capFont->LegacySize + 2 * dp; bool pos = market.change_24h >= 0; snprintf(buf, sizeof(buf), "%s%.1f%%", pos ? "+" : "", market.change_24h); ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(badgeX - chgSz.x, badgeY), pos ? Success() : Error(), buf); } } // Divider float divY = cardMin.y + cardH * S.drawElement("tabs.balance.consolidated", "divider-y-ratio").sizeOr(0.55f); dl->AddLine(ImVec2(cardMin.x + pad, divY), ImVec2(cardMax.x - pad, divY), IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.consolidated", "divider-alpha").sizeOr(20.0f)), S.drawElement("tabs.balance.consolidated", "divider-thickness").sizeOr(1.0f)); // Bottom half: proportion bars float barY = divY + Layout::spacingSm(); float barH = std::max( S.drawElement("tabs.balance.consolidated", "bar-min-height").sizeOr(6.0f), S.drawElement("tabs.balance.consolidated", "bar-base-height").sizeOr(10.0f) * vs); float halfW = (availW - pad * 3) * 0.5f; float total = (float)s_dispTotal; float shieldRatio = (total > 1e-9f) ? (float)(s_dispShielded / total) : 0.5f; float transRatio = 1.0f - shieldRatio; // Shielded bar float shieldX = cardMin.x + pad; dl->AddText(ovFont, ovFont->LegacySize, ImVec2(shieldX, barY), Success(), "SHIELDED"); barY += ovFont->LegacySize + 4 * dp; dl->AddRectFilled(ImVec2(shieldX, barY), ImVec2(shieldX + halfW, barY + barH), IM_COL32(255, 255, 255, 15), barH * 0.5f); dl->AddRectFilled(ImVec2(shieldX, barY), ImVec2(shieldX + halfW * shieldRatio, barY + barH), WithAlpha(Success(), 180), barH * 0.5f); barY += barH + 2 * dp; snprintf(buf, sizeof(buf), "%.8f (%.0f%%)", s_dispShielded, shieldRatio * 100.0f); dl->AddText(capFont, capFont->LegacySize, ImVec2(shieldX, barY), OnSurfaceMedium(), buf); // Transparent bar float transX = cardMin.x + pad * 2 + halfW; barY = divY + Layout::spacingSm(); dl->AddText(ovFont, ovFont->LegacySize, ImVec2(transX, barY), Warning(), "TRANSPARENT"); barY += ovFont->LegacySize + 4 * dp; dl->AddRectFilled(ImVec2(transX, barY), ImVec2(transX + halfW, barY + barH), IM_COL32(255, 255, 255, 15), barH * 0.5f); dl->AddRectFilled(ImVec2(transX, barY), ImVec2(transX + halfW * transRatio, barY + barH), WithAlpha(Warning(), 180), barH * 0.5f); barY += barH + 2 * dp; snprintf(buf, sizeof(buf), "%.8f (%.0f%%)", s_dispTransparent, transRatio * 100.0f); dl->AddText(capFont, capFont->LegacySize, ImVec2(transX, barY), OnSurfaceMedium(), buf); // Sync bar at card bottom — clipped to rounded corners if (state.sync.syncing && state.sync.headers > 0) { const auto syncBar = S.drawElement("tabs.balance", "sync-bar"); float syncBarH = (syncBar.height >= 0) ? syncBar.height : 3.0f; float prog = static_cast(state.sync.verification_progress); if (prog > 1.0f) prog = 1.0f; float syncBarTop = cardMax.y - syncBarH; // Clip to the bottom strip so the full-card rounded rect // curves exactly match the card's own rounded corners dl->PushClipRect(ImVec2(cardMin.x, syncBarTop), cardMax, true); // Background track dl->AddRectFilled(cardMin, cardMax, IM_COL32(255, 255, 255, 15), glassRound); // Progress fill — additional horizontal clip float progRight = cardMin.x + (cardMax.x - cardMin.x) * prog; dl->PushClipRect(ImVec2(cardMin.x, syncBarTop), ImVec2(progRight, cardMax.y), true); dl->AddRectFilled(cardMin, cardMax, WithAlpha(Warning(), 200), glassRound); dl->PopClipRect(); dl->PopClipRect(); } ImGui::Dummy(ImVec2(availW, cardH)); ImGui::Dummy(ImVec2(0, cGap)); float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f); float consAddrOverride = S.drawElement("tabs.balance.consolidated", "address-table-height").size; float addrH = (consAddrOverride >= 0.0f) ? consAddrOverride : ImGui::GetContentRegionAvail().y - recentReserve - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd(); RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs); RenderSharedRecentTx(app, recentReserve, availW, hs, vs); } // ============================================================================ // Layout 3: Dashboard Tiles // ============================================================================ static void RenderBalanceDashboard(App* app) { using namespace material; const auto& S = schema::UISchema::instance(); const auto& state = app->state(); UpdateBalanceLerp(app); ImVec2 contentAvail = ImGui::GetContentRegionAvail(); float availW = contentAvail.x; float hs = Layout::hScale(availW); float vs = Layout::vScale(contentAvail.y); auto tier = Layout::currentTier(availW, contentAvail.y); float glassRound = Layout::glassRounding(); float cGap = Layout::cardGap(); const float dp = Layout::dpiScale(); ImDrawList* dl = ImGui::GetWindowDrawList(); char buf[64]; // Compact hero line float dashTopMargin = S.drawElement("tabs.balance.dashboard", "top-margin").size; if (dashTopMargin >= 0.0f) ImGui::Dummy(ImVec2(0, dashTopMargin)); float dashHeroH = S.drawElement("tabs.balance.dashboard", "hero-height").size; RenderCompactHero(app, dl, availW, hs, vs, dashHeroH); RenderSyncBar(app, dl, vs); ImGui::Dummy(ImVec2(0, cGap)); // 4-tile grid int numCols = (tier == Layout::LayoutTier::Compact && availW < S.drawElement("tabs.balance.dashboard", "compact-cutoff").sizeOr(500.0f) * Layout::dpiScale()) ? (int)S.drawElement("tabs.balance.dashboard", "tile-compact-cols").sizeOr(2.0f) : (int)S.drawElement("tabs.balance.dashboard", "tile-num-cols").sizeOr(4.0f); int numRows = (numCols == 2) ? 2 : 1; float tileW = (availW - (numCols - 1) * cGap) / numCols; float dashCardH = S.drawElement("tabs.balance.dashboard", "card-height").size; float tileH; if (dashCardH >= 0.0f) { tileH = dashCardH; // explicit override from ui.toml } else { tileH = std::max( S.drawElement("tabs.balance.dashboard", "tile-min-height").sizeOr(70.0f), contentAvail.y * S.drawElement("tabs.balance.dashboard", "tile-height-ratio").sizeOr(0.16f) / numRows); } ImVec2 origin = ImGui::GetCursorScreenPos(); GlassPanelSpec tileSpec; tileSpec.rounding = glassRound; ImFont* ovFont = Type().overline(); ImFont* sub1 = Type().subtitle1(); ImFont* capFont = Type().caption(); struct TileInfo { const char* label; const char* value; ImU32 accent; const char* icon; NavPage nav; bool isAction; }; snprintf(buf, sizeof(buf), "%.8f", s_dispShielded); static char shBuf[64], trBuf[64]; snprintf(shBuf, sizeof(shBuf), "%.8f", s_dispShielded); snprintf(trBuf, sizeof(trBuf), "%.8f", s_dispTransparent); TileInfo tiles[4] = { {"SHIELDED", shBuf, S.resolveColor("var(--accent-shielded)", Success()), ICON_MD_SHIELD, NavPage::Receive, false}, {"TRANSPARENT", trBuf, S.resolveColor("var(--accent-transparent)", Warning()), ICON_MD_CIRCLE, NavPage::Receive, false}, {"QUICK SEND", "Send", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_MADE, NavPage::Send, true}, {"QUICK RECEIVE", "Receive", S.resolveColor("var(--accent-action)", Primary()), ICON_MD_CALL_RECEIVED, NavPage::Receive, true}, }; for (int i = 0; i < 4; i++) { int col = (numCols == 4) ? i : (i % 2); int row = (numCols == 4) ? 0 : (i / 2); float xOff = col * (tileW + cGap); float yOff = row * (tileH + cGap); ImVec2 tMin(origin.x + xOff, origin.y + yOff); ImVec2 tMax(tMin.x + tileW, tMin.y + tileH); DrawGlassPanel(dl, tMin, tMax, tileSpec); // Accent stripe — clipped to tile rounded corners { float aw = S.drawElement("tabs.balance", "accent-width").sizeOr(4.0f); dl->PushClipRect(tMin, ImVec2(tMin.x + aw, tMax.y), true); dl->AddRectFilled(tMin, tMax, tiles[i].accent, tileSpec.rounding, ImDrawFlags_RoundCornersLeft); dl->PopClipRect(); } float dashPadOverride = S.drawElement("tabs.balance.dashboard", "card-padding").size; float tilePad = (dashPadOverride >= 0.0f) ? dashPadOverride : Layout::spacingLg(); float tilePadV = (dashPadOverride >= 0.0f) ? dashPadOverride : Layout::spacingSm(); float px = tMin.x + tilePad; float py = tMin.y + tilePadV; // Icon ImFont* iconFont = Type().iconSmall(); ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, tiles[i].icon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(px, py), tiles[i].accent, tiles[i].icon); px += iSz.x + Layout::spacingSm(); // Label dl->AddText(ovFont, ovFont->LegacySize, ImVec2(px, py), OnSurfaceMedium(), tiles[i].label); py += ovFont->LegacySize + 4 * dp; // Value if (!tiles[i].isAction) { dl->AddText(capFont, capFont->LegacySize, ImVec2(tMin.x + tilePad, py), tiles[i].accent, tiles[i].value); } else { dl->AddText(capFont, capFont->LegacySize, ImVec2(tMin.x + tilePad, py), OnSurfaceMedium(), "Click to open"); } // Click if (material::IsRectHovered(tMin, tMax)) { dl->AddRect(tMin, tMax, IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance", "hover-glow-alpha").sizeOr(40.0f)), tileSpec.rounding, 0, S.drawElement("tabs.balance", "hover-glow-thickness").sizeOr(1.5f)); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (ImGui::IsMouseClicked(0)) app->setCurrentPage(tiles[i].nav); } } float totalTileH = numRows * tileH + (numRows - 1) * cGap; ImGui::Dummy(ImVec2(availW, totalTileH)); ImGui::Dummy(ImVec2(0, cGap)); float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f); float dashAddrOverride = S.drawElement("tabs.balance.dashboard", "address-table-height").size; float addrH = (dashAddrOverride >= 0.0f) ? dashAddrOverride : ImGui::GetContentRegionAvail().y - recentReserve - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd(); RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs); RenderSharedRecentTx(app, recentReserve, availW, hs, vs); } // ============================================================================ // Layout 4: Vertical Stack // ============================================================================ static void RenderBalanceVerticalStack(App* app) { using namespace material; const auto& S = schema::UISchema::instance(); const auto& state = app->state(); UpdateBalanceLerp(app); ImVec2 contentAvail = ImGui::GetContentRegionAvail(); float availW = contentAvail.x; float hs = Layout::hScale(availW); float vs = Layout::vScale(contentAvail.y); float glassRound = Layout::glassRounding(); float cGap = Layout::cardGap(); const float dp = Layout::dpiScale(); ImDrawList* dl = ImGui::GetWindowDrawList(); char buf[64]; float vstackTopMargin = S.drawElement("tabs.balance.vertical-stack", "top-margin").size; if (vstackTopMargin >= 0.0f) ImGui::Dummy(ImVec2(0, vstackTopMargin)); float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size; float stackH; if (vstackCardH >= 0.0f) { stackH = vstackCardH; // explicit override from ui.toml } else { stackH = std::max( S.drawElement("tabs.balance.vertical-stack", "stack-min-height").sizeOr(80.0f), contentAvail.y * S.drawElement("tabs.balance.vertical-stack", "stack-height-ratio").sizeOr(0.16f)); } float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f); float rowH = (stackH - 3 * rowGap) / 4.0f; float rowMinH = S.drawElement("tabs.balance.vertical-stack", "row-min-height").sizeOr(20.0f); if (rowH < rowMinH) rowH = rowMinH; ImFont* capFont = Type().caption(); ImFont* body2 = Type().body2(); ImFont* sub1 = Type().subtitle1(); float total = (float)s_dispTotal; float shieldRatio = (total > 1e-9f) ? (float)(s_dispShielded / total) : 0.5f; float transRatio = 1.0f - shieldRatio; struct RowInfo { const char* label; const char* icon; ImU32 accent; double amount; float ratio; }; RowInfo rowInfos[4] = { {"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f}, {"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio}, {"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio}, {"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f}, }; for (int i = 0; i < 4; i++) { ImVec2 rowPos = ImGui::GetCursorScreenPos(); ImVec2 rowEnd(rowPos.x + availW, rowPos.y + rowH); float vstackPadOverride = S.drawElement("tabs.balance.vertical-stack", "card-padding").size; float rowPad = (vstackPadOverride >= 0.0f) ? vstackPadOverride : Layout::spacingLg(); // Subtle background float rowBgAlpha = S.drawElement("tabs.balance.vertical-stack", "row-bg-alpha").sizeOr(8.0f); dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, (int)rowBgAlpha), 4.0f * dp); // Left accent — clipped to row rounding dl->PushClipRect(rowPos, rowEnd, true); dl->AddRectFilled(ImVec2(rowPos.x, rowPos.y), ImVec2(rowPos.x + 3 * dp, rowEnd.y), rowInfos[i].accent); dl->PopClipRect(); float px = rowPos.x + rowPad; float cy = rowPos.y + (rowH - capFont->LegacySize) * 0.5f; // Icon ImFont* iconFont = Type().iconSmall(); ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, rowInfos[i].icon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(px, rowPos.y + (rowH - iSz.y) * 0.5f), rowInfos[i].accent, rowInfos[i].icon); px += iSz.x + Layout::spacingSm(); // Label dl->AddText(capFont, capFont->LegacySize, ImVec2(px, cy), OnSurfaceMedium(), rowInfos[i].label); // Amount (right side) if (i < 3) { snprintf(buf, sizeof(buf), "%.8f %s", rowInfos[i].amount, DRAGONX_TICKER); } else { if (state.market.price_usd >= 0.01) snprintf(buf, sizeof(buf), "$%.4f", state.market.price_usd); else if (state.market.price_usd > 0) snprintf(buf, sizeof(buf), "$%.8f", state.market.price_usd); else snprintf(buf, sizeof(buf), "$--.--"); } ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(rowEnd.x - amtSz.x - rowPad, cy), i == 0 ? OnSurface() : rowInfos[i].accent, buf); // Proportion bar (for shielded/transparent rows — fills gap between label and amount) if (i == 1 || i == 2) { ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label); float barGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f); float barPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f); float barH = std::max( S.drawElement("tabs.balance.vertical-stack", "bar-min-height").sizeOr(3.0f), rowH * S.drawElement("tabs.balance.vertical-stack", "bar-height-ratio").sizeOr(0.15f)); float barLeft = px + labelSz.x + barGap; float barRight = rowEnd.x - amtSz.x - rowPad - barGap; if (barLeft < barRight) { float barW = barRight - barLeft; float barY = rowPos.y + (rowH - barH) * 0.5f; dl->AddRectFilled(ImVec2(barLeft, barY), ImVec2(barRight, barY + barH), IM_COL32(255, 255, 255, 15), barH * 0.5f); dl->AddRectFilled(ImVec2(barLeft, barY), ImVec2(barLeft + barW * rowInfos[i].ratio, barY + barH), WithAlpha(rowInfos[i].accent, 180), barH * 0.5f); } } // Market: 24h change + sparkline if (i == 3 && state.market.price_usd > 0) { bool pos = state.market.change_24h >= 0; snprintf(buf, sizeof(buf), "%s%.1f%%", pos ? "+" : "", state.market.change_24h); ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); float chgX = rowEnd.x - amtSz.x - Layout::spacingLg() - chgSz.x - Layout::spacingSm(); dl->AddText(capFont, capFont->LegacySize, ImVec2(chgX, cy), pos ? Success() : Error(), buf); // Sparkline in the gap between label and 24h change if (state.market.price_history.size() >= 2) { ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, rowInfos[i].label); float sparkGap = S.drawElement("tabs.balance.vertical-stack", "sparkline-gap").sizeOr(12.0f); float sparkPad = S.drawElement("tabs.balance.vertical-stack", "sparkline-pad").sizeOr(4.0f); float sparkLeft = px + labelSz.x + sparkGap; float sparkRight = chgX - sparkGap; if (sparkLeft < sparkRight) { ImVec2 spMin(sparkLeft, rowPos.y + sparkPad); ImVec2 spMax(sparkRight, rowEnd.y - sparkPad); ImU32 lineCol = pos ? WithAlpha(Success(), 200) : WithAlpha(Error(), 200); DrawSparkline(dl, spMin, spMax, state.market.price_history, lineCol); } } } ImGui::Dummy(ImVec2(availW, rowH)); if (i < 3) ImGui::Dummy(ImVec2(0, rowGap)); } RenderSyncBar(app, dl, vs); ImGui::Dummy(ImVec2(0, cGap)); float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f); float vstackAddrOverride = S.drawElement("tabs.balance.vertical-stack", "address-table-height").size; float addrH = (vstackAddrOverride >= 0.0f) ? vstackAddrOverride : ImGui::GetContentRegionAvail().y - recentReserve - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd(); RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs); RenderSharedRecentTx(app, recentReserve, availW, hs, vs); } // ============================================================================ // Layout 5b: Vertical 2×2 (Total+Market left, Shielded+Transparent right) // ============================================================================ static void RenderBalanceVertical2x2(App* app) { using namespace material; const auto& S = schema::UISchema::instance(); const auto& state = app->state(); UpdateBalanceLerp(app); ImVec2 contentAvail = ImGui::GetContentRegionAvail(); float availW = contentAvail.x; float hs = Layout::hScale(availW); float vs = Layout::vScale(contentAvail.y); float glassRound = Layout::glassRounding(); float cGap = Layout::cardGap(); const float dp = Layout::dpiScale(); ImDrawList* dl = ImGui::GetWindowDrawList(); char buf[64]; const char* cfgSec = "tabs.balance.vertical-2x2"; float topMargin = S.drawElement(cfgSec, "top-margin").size; if (topMargin >= 0.0f) ImGui::Dummy(ImVec2(0, topMargin)); float cardHOverride = S.drawElement(cfgSec, "card-height").size; float stackH; if (cardHOverride >= 0.0f) { stackH = cardHOverride; } else { stackH = std::max( S.drawElement(cfgSec, "stack-min-height").sizeOr(60.0f), contentAvail.y * S.drawElement(cfgSec, "stack-height-ratio").sizeOr(0.12f)); } float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f); float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f); float rowH = (stackH - rowGap) / 2.0f; float rowMinH = S.drawElement(cfgSec, "row-min-height").sizeOr(24.0f); if (rowH < rowMinH) rowH = rowMinH; float colW = (availW - colGap) / 2.0f; ImFont* capFont = Type().caption(); ImFont* iconFont = Type().iconSmall(); float padOverride = S.drawElement(cfgSec, "card-padding").size; float rowPad = (padOverride >= 0.0f) ? padOverride : Layout::spacingLg(); float rowBgAlpha = S.drawElement(cfgSec, "row-bg-alpha").sizeOr(8.0f); float total = (float)s_dispTotal; float shieldRatio = (total > 1e-9f) ? (float)(s_dispShielded / total) : 0.5f; float transRatio = 1.0f - shieldRatio; // Grid: [row][col] — row 0 top, row 1 bottom; col 0 left, col 1 right // Left col: Total Balance (row 0), Market (row 1) // Right col: Shielded (row 0), Transparent (row 1) struct CellInfo { const char* label; const char* icon; ImU32 accent; double amount; float ratio; bool isMarket; bool hasBar; }; CellInfo cells[2][2] = { // Row 0: Total Balance (left), Shielded (right) { {"Total Balance", ICON_MD_ACCOUNT_BALANCE_WALLET, S.resolveColor("var(--accent-total)", OnSurface()), s_dispTotal, 1.0f, false, false}, {"Shielded", ICON_MD_SHIELD, S.resolveColor("var(--accent-shielded)", Success()), s_dispShielded, shieldRatio, false, true}, }, // Row 1: Market (left), Transparent (right) { {"Market", ICON_MD_TRENDING_UP, S.resolveColor("var(--accent-action)", Primary()), state.market.price_usd, 0.0f, true, false}, {"Transparent", ICON_MD_CIRCLE, S.resolveColor("var(--accent-transparent)", Warning()), s_dispTransparent, transRatio, false, true}, }, }; ImVec2 gridOrigin = ImGui::GetCursorScreenPos(); for (int row = 0; row < 2; row++) { for (int col = 0; col < 2; col++) { const auto& cell = cells[row][col]; float cellX = gridOrigin.x + col * (colW + colGap); float cellY = gridOrigin.y + row * (rowH + rowGap); ImVec2 cellMin(cellX, cellY); ImVec2 cellMax(cellX + colW, cellY + rowH); // Background dl->AddRectFilled(cellMin, cellMax, IM_COL32(255, 255, 255, (int)rowBgAlpha), 4.0f * dp); // Left accent — clipped to cell rounding dl->PushClipRect(cellMin, cellMax, true); dl->AddRectFilled(ImVec2(cellMin.x, cellMin.y), ImVec2(cellMin.x + 3 * dp, cellMax.y), cell.accent); dl->PopClipRect(); float px = cellMin.x + rowPad; float cy = cellMin.y + (rowH - capFont->LegacySize) * 0.5f; // Icon ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, cell.icon); dl->AddText(iconFont, iconFont->LegacySize, ImVec2(px, cellMin.y + (rowH - iSz.y) * 0.5f), cell.accent, cell.icon); px += iSz.x + Layout::spacingSm(); // Label dl->AddText(capFont, capFont->LegacySize, ImVec2(px, cy), OnSurfaceMedium(), cell.label); // Amount (right-aligned) if (!cell.isMarket) { snprintf(buf, sizeof(buf), "%.8f %s", cell.amount, DRAGONX_TICKER); } else { if (state.market.price_usd >= 0.01) snprintf(buf, sizeof(buf), "$%.4f", state.market.price_usd); else if (state.market.price_usd > 0) snprintf(buf, sizeof(buf), "$%.8f", state.market.price_usd); else snprintf(buf, sizeof(buf), "$--.--"); } ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(cellMax.x - amtSz.x - rowPad, cy), cell.isMarket ? cell.accent : (row == 0 && col == 0 ? OnSurface() : cell.accent), buf); // Proportion bar (shielded/transparent) if (cell.hasBar) { float barW = colW * S.drawElement(cfgSec, "bar-width-ratio").sizeOr(0.12f); float barH = std::max( S.drawElement(cfgSec, "bar-min-height").sizeOr(3.0f), rowH * S.drawElement(cfgSec, "bar-height-ratio").sizeOr(0.15f)); float barX = cellMax.x - amtSz.x - rowPad - barW - Layout::spacingSm(); float barY = cellMin.y + (rowH - barH) * 0.5f; dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW, barY + barH), IM_COL32(255, 255, 255, 15), barH * 0.5f); dl->AddRectFilled(ImVec2(barX, barY), ImVec2(barX + barW * cell.ratio, barY + barH), WithAlpha(cell.accent, 180), barH * 0.5f); } // Market: 24h change + sparkline if (cell.isMarket && state.market.price_usd > 0) { bool pos = state.market.change_24h >= 0; snprintf(buf, sizeof(buf), "%s%.1f%%", pos ? "+" : "", state.market.change_24h); ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); float chgX = cellMax.x - amtSz.x - Layout::spacingLg() - chgSz.x - Layout::spacingSm(); dl->AddText(capFont, capFont->LegacySize, ImVec2(chgX, cy), pos ? Success() : Error(), buf); // Sparkline between label and 24h change if (state.market.price_history.size() >= 2) { ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, cell.label); float sparkGap = S.drawElement(cfgSec, "sparkline-gap").sizeOr(12.0f); float sparkPad = S.drawElement(cfgSec, "sparkline-pad").sizeOr(4.0f); float sparkLeft = px + labelSz.x + sparkGap; float sparkRight = chgX - sparkGap; if (sparkLeft < sparkRight) { ImVec2 spMin(sparkLeft, cellMin.y + sparkPad); ImVec2 spMax(sparkRight, cellMax.y - sparkPad); ImU32 lineCol = pos ? WithAlpha(Success(), 200) : WithAlpha(Error(), 200); DrawSparkline(dl, spMin, spMax, state.market.price_history, lineCol); } } } } } // Advance cursor past the 2×2 grid float totalGridH = 2.0f * rowH + rowGap; ImGui::Dummy(ImVec2(availW, totalGridH)); RenderSyncBar(app, dl, vs); ImGui::Dummy(ImVec2(0, cGap)); float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f); float addrOverride = S.drawElement(cfgSec, "address-table-height").size; float addrH = (addrOverride >= 0.0f) ? addrOverride : ImGui::GetContentRegionAvail().y - recentReserve - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd(); RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs); RenderSharedRecentTx(app, recentReserve, availW, hs, vs); } // ============================================================================ // Layout 5: Privacy Shield Meter // ============================================================================ static void RenderBalanceShield(App* app) { using namespace material; const auto& S = schema::UISchema::instance(); const auto& state = app->state(); UpdateBalanceLerp(app); ImVec2 contentAvail = ImGui::GetContentRegionAvail(); float availW = contentAvail.x; float hs = Layout::hScale(availW); float vs = Layout::vScale(contentAvail.y); float glassRound = Layout::glassRounding(); float cGap = Layout::cardGap(); const float dp = Layout::dpiScale(); ImDrawList* dl = ImGui::GetWindowDrawList(); char buf[64]; // Hero section float shieldTopMargin = S.drawElement("tabs.balance.shield", "top-margin").size; if (shieldTopMargin >= 0.0f) ImGui::Dummy(ImVec2(0, shieldTopMargin)); else ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance", "compact-hero-pad").sizeOr(8.0f) * vs)); { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE"); ImGui::Dummy(ImVec2(0, 2 * dp)); snprintf(buf, sizeof(buf), "%.8f", s_dispTotal); ImFont* heroFont = Type().h2(); ImVec2 pos = ImGui::GetCursorScreenPos(); DrawTextShadow(dl, heroFont, heroFont->LegacySize, pos, OnSurface(), buf); ImVec2 heroSz = heroFont->CalcTextSizeA(heroFont->LegacySize, 10000.0f, 0.0f, buf); ImGui::Dummy(heroSz); ImGui::SameLine(); ImFont* capFont = Type().caption(); dl->AddText(capFont, capFont->LegacySize, ImVec2(ImGui::GetCursorScreenPos().x, pos.y + heroSz.y - capFont->LegacySize), OnSurfaceMedium(), DRAGONX_TICKER); ImGui::NewLine(); double usd_value = state.getBalanceUSD(); if (usd_value > 0.0) snprintf(buf, sizeof(buf), "$%.2f USD", usd_value); else snprintf(buf, sizeof(buf), "$-.-- USD"); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf); } RenderSyncBar(app, dl, vs); ImGui::Dummy(ImVec2(0, cGap)); // Shield gauge panel { float shieldCardH = S.drawElement("tabs.balance.shield", "card-height").size; float gaugeH; if (shieldCardH >= 0.0f) { gaugeH = shieldCardH; // explicit override from ui.toml } else { gaugeH = std::max( S.drawElement("tabs.balance.shield", "gauge-min-height").sizeOr(80.0f), contentAvail.y * S.drawElement("tabs.balance.shield", "gauge-height-ratio").sizeOr(0.18f)); } ImVec2 panelMin = ImGui::GetCursorScreenPos(); ImVec2 panelMax(panelMin.x + availW, panelMin.y + gaugeH); GlassPanelSpec spec; spec.rounding = glassRound; DrawGlassPanel(dl, panelMin, panelMax, spec); float total = (float)s_dispTotal; float privacyRatio = (total > 1e-9f) ? (float)(s_dispShielded / total) : 0.0f; float privPct = privacyRatio * 100.0f; // Semicircle gauge float gaugeCx = panelMin.x + gaugeH; float gaugeCy = panelMin.y + gaugeH * S.drawElement("tabs.balance.shield", "gauge-center-y-ratio").sizeOr(0.7f); float gaugeR = std::min( gaugeH * S.drawElement("tabs.balance.shield", "gauge-radius-ratio").sizeOr(0.55f), availW * S.drawElement("tabs.balance.shield", "gauge-max-radius-ratio").sizeOr(0.15f)); float gaugeInnerR = gaugeR * S.drawElement("tabs.balance.shield", "gauge-inner-ratio").sizeOr(0.7f); // Background arc (gray) dl->PathClear(); dl->PathArcTo(ImVec2(gaugeCx, gaugeCy), gaugeR, IM_PI, 2.0f * IM_PI, 32); dl->PathArcTo(ImVec2(gaugeCx, gaugeCy), gaugeInnerR, 2.0f * IM_PI, IM_PI, 32); dl->PathFillConvex(IM_COL32(255, 255, 255, (int)S.drawElement("tabs.balance.shield", "gauge-bg-alpha").sizeOr(20.0f))); // Filled arc (colored by threshold) ImU32 gaugeCol; float goodThreshold = S.drawElement("tabs.balance.shield", "good-threshold").sizeOr(80.0f); float medThreshold = S.drawElement("tabs.balance.shield", "medium-threshold").sizeOr(50.0f); if (privPct >= goodThreshold) gaugeCol = WithAlpha(Success(), 200); else if (privPct >= medThreshold) gaugeCol = WithAlpha(Warning(), 200); else gaugeCol = WithAlpha(Error(), 200); float fillEnd = IM_PI + IM_PI * privacyRatio; if (privacyRatio > 0.01f) { dl->PathClear(); dl->PathArcTo(ImVec2(gaugeCx, gaugeCy), gaugeR, IM_PI, fillEnd, 32); dl->PathArcTo(ImVec2(gaugeCx, gaugeCy), gaugeInnerR, fillEnd, IM_PI, 32); dl->PathFillConvex(gaugeCol); } // Needle line float needleAngle = IM_PI + IM_PI * privacyRatio; float needleLen = gaugeR * 0.85f; ImVec2 needleTip(gaugeCx + cosf(needleAngle) * needleLen, gaugeCy + sinf(needleAngle) * needleLen); dl->AddLine(ImVec2(gaugeCx, gaugeCy), needleTip, gaugeCol, S.drawElement("tabs.balance.shield", "needle-thickness").sizeOr(2.0f)); // Center text: percentage ImFont* sub1 = Type().subtitle1(); snprintf(buf, sizeof(buf), "%.0f%%", privPct); ImVec2 pctSz = sub1->CalcTextSizeA(sub1->LegacySize, 1000, 0, buf); dl->AddText(sub1, sub1->LegacySize, ImVec2(gaugeCx - pctSz.x * 0.5f, gaugeCy - pctSz.y - 2 * dp), gaugeCol, buf); // Label below gauge ImFont* capFont = Type().caption(); const char* statusMsg; if (privPct >= 80.0f) statusMsg = "Great privacy!"; else if (privPct >= 50.0f) statusMsg = "Consider shielding more"; else statusMsg = "Low privacy — shield funds"; ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, 1000, 0, statusMsg); dl->AddText(capFont, capFont->LegacySize, ImVec2(gaugeCx - msgSz.x * 0.5f, gaugeCy + 4 * dp), OnSurfaceMedium(), statusMsg); // Right side: balances + market float shieldPadOverride = S.drawElement("tabs.balance.shield", "card-padding").size; float shieldPad = (shieldPadOverride >= 0.0f) ? shieldPadOverride : Layout::spacingLg(); float infoX = panelMin.x + gaugeH * 2 + shieldPad; float infoY = panelMin.y + shieldPad; ImFont* ovFont = Type().overline(); dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Success(), "SHIELDED"); infoY += ovFont->LegacySize + 2 * dp; snprintf(buf, sizeof(buf), "%.8f", s_dispShielded); dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), Success(), buf); infoY += capFont->LegacySize + 6 * dp; dl->AddText(ovFont, ovFont->LegacySize, ImVec2(infoX, infoY), Warning(), "TRANSPARENT"); infoY += ovFont->LegacySize + 2 * dp; snprintf(buf, sizeof(buf), "%.8f", s_dispTransparent); dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), Warning(), buf); infoY += capFont->LegacySize + 6 * dp; const auto& market = state.market; if (market.price_usd > 0) { if (market.price_usd >= 0.01) snprintf(buf, sizeof(buf), "$%.4f", market.price_usd); else snprintf(buf, sizeof(buf), "$%.8f", market.price_usd); dl->AddText(capFont, capFont->LegacySize, ImVec2(infoX, infoY), OnSurfaceMedium(), buf); } ImGui::Dummy(ImVec2(availW, gaugeH)); } ImGui::Dummy(ImVec2(0, cGap)); float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f); float shieldAddrOverride = S.drawElement("tabs.balance.shield", "address-table-height").size; float addrH = (shieldAddrOverride >= 0.0f) ? shieldAddrOverride : ImGui::GetContentRegionAvail().y - recentReserve - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd(); RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs); RenderSharedRecentTx(app, recentReserve, availW, hs, vs); } // ============================================================================ // Layout 6: Balance Timeline (placeholder — requires history tracking) // ============================================================================ static void RenderBalanceTimeline(App* app) { using namespace material; const auto& S = schema::UISchema::instance(); const auto& state = app->state(); UpdateBalanceLerp(app); ImVec2 contentAvail = ImGui::GetContentRegionAvail(); float availW = contentAvail.x; float hs = Layout::hScale(availW); float vs = Layout::vScale(contentAvail.y); float glassRound = Layout::glassRounding(); float cGap = Layout::cardGap(); const float dp = Layout::dpiScale(); ImDrawList* dl = ImGui::GetWindowDrawList(); char buf[64]; // Hero float tlTopMargin = S.drawElement("tabs.balance.timeline", "top-margin").size; if (tlTopMargin >= 0.0f) ImGui::Dummy(ImVec2(0, tlTopMargin)); else ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance", "compact-hero-pad").sizeOr(8.0f) * vs)); { Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "TOTAL BALANCE"); ImGui::Dummy(ImVec2(0, 2 * dp)); snprintf(buf, sizeof(buf), "%.8f", s_dispTotal); ImFont* heroFont = Type().h2(); ImVec2 pos = ImGui::GetCursorScreenPos(); DrawTextShadow(dl, heroFont, heroFont->LegacySize, pos, OnSurface(), buf); ImVec2 heroSz = heroFont->CalcTextSizeA(heroFont->LegacySize, 10000.0f, 0.0f, buf); ImGui::Dummy(heroSz); ImGui::SameLine(); ImFont* capFont = Type().caption(); dl->AddText(capFont, capFont->LegacySize, ImVec2(ImGui::GetCursorScreenPos().x, pos.y + heroSz.y - capFont->LegacySize), OnSurfaceMedium(), DRAGONX_TICKER); ImGui::NewLine(); double usd_value = state.getBalanceUSD(); if (usd_value > 0.0) snprintf(buf, sizeof(buf), "$%.2f USD", usd_value); else snprintf(buf, sizeof(buf), "$-.-- USD"); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf); } RenderSyncBar(app, dl, vs); ImGui::Dummy(ImVec2(0, cGap)); // Chart area placeholder { float tlChartH = S.drawElement("tabs.balance.timeline", "chart-height").size; float chartH; if (tlChartH >= 0.0f) { chartH = tlChartH; // explicit override from ui.toml } else { chartH = std::max( S.drawElement("tabs.balance.timeline", "chart-min-height").sizeOr(80.0f), contentAvail.y * S.drawElement("tabs.balance.timeline", "chart-height-ratio").sizeOr(0.20f)); } ImVec2 chartMin = ImGui::GetCursorScreenPos(); ImVec2 chartMax(chartMin.x + availW, chartMin.y + chartH); GlassPanelSpec spec; spec.rounding = glassRound; DrawGlassPanel(dl, chartMin, chartMax, spec); ImFont* capFont = Type().caption(); const char* msg = "Balance history — collecting data..."; ImVec2 msgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, msg); dl->AddText(capFont, capFont->LegacySize, ImVec2(chartMin.x + (availW - msgSz.x) * 0.5f, chartMin.y + (chartH - msgSz.y) * 0.5f), OnSurfaceDisabled(), msg); // If we have market sparkline data, use it as a preview const auto& market = state.market; if (market.price_history.size() >= 2) { float sparkPad = Layout::spacingLg(); ImVec2 spMin(chartMin.x + sparkPad, chartMin.y + capFont->LegacySize + sparkPad * 2); ImVec2 spMax(chartMax.x - sparkPad, chartMax.y - sparkPad); if (spMax.y > spMin.y && spMax.x > spMin.x) { ImU32 lineCol = market.change_24h >= 0 ? WithAlpha(Success(), (int)S.drawElement("tabs.balance.timeline", "sparkline-alpha").sizeOr(120.0f)) : WithAlpha(Error(), (int)S.drawElement("tabs.balance.timeline", "sparkline-alpha").sizeOr(120.0f)); DrawSparkline(dl, spMin, spMax, market.price_history, lineCol); } } ImGui::Dummy(ImVec2(availW, chartH)); } // Compact 3 summary cards ImGui::Dummy(ImVec2(0, cGap)); { float tlSummaryH = S.drawElement("tabs.balance.timeline", "summary-card-height").size; float cardH; if (tlSummaryH >= 0.0f) { cardH = tlSummaryH; // explicit override from ui.toml } else { cardH = std::max( S.drawElement("tabs.balance.timeline", "summary-min-height").sizeOr(44.0f), contentAvail.y * S.drawElement("tabs.balance.timeline", "summary-height-ratio").sizeOr(0.08f)); } float cardW = (availW - 2 * cGap) / 3.0f; ImVec2 origin = ImGui::GetCursorScreenPos(); GlassPanelSpec spec; spec.rounding = glassRound; ImFont* ovFont = Type().overline(); ImFont* capFont = Type().caption(); struct SumCard { const char* label; ImU32 col; double val; bool isMoney; }; SumCard cards[3] = { {"SHIELDED", Success(), s_dispShielded, false}, {"TRANSPARENT", Warning(), s_dispTransparent, false}, {"MARKET", Primary(), state.market.price_usd, true}, }; for (int i = 0; i < 3; i++) { ImVec2 cMin(origin.x + i * (cardW + cGap), origin.y); ImVec2 cMax(cMin.x + cardW, cMin.y + cardH); DrawGlassPanel(dl, cMin, cMax, spec); float tlPadOverride = S.drawElement("tabs.balance.timeline", "card-padding").size; float cx = cMin.x + ((tlPadOverride >= 0.0f) ? tlPadOverride : Layout::spacingSm()); float cy = cMin.y + ((tlPadOverride >= 0.0f) ? tlPadOverride : Layout::spacingXs()); dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), cards[i].label); cy += ovFont->LegacySize + 2 * dp; if (cards[i].isMoney) { if (cards[i].val >= 0.01) snprintf(buf, sizeof(buf), "$%.4f", cards[i].val); else if (cards[i].val > 0) snprintf(buf, sizeof(buf), "$%.8f", cards[i].val); else snprintf(buf, sizeof(buf), "$--.--"); } else { snprintf(buf, sizeof(buf), "%.8f", cards[i].val); } dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), cards[i].col, buf); } ImGui::Dummy(ImVec2(availW, cardH)); } ImGui::Dummy(ImVec2(0, cGap)); float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f); float tlAddrOverride = S.drawElement("tabs.balance.timeline", "address-table-height").size; float addrH = (tlAddrOverride >= 0.0f) ? tlAddrOverride : ImGui::GetContentRegionAvail().y - recentReserve - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd(); RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs); RenderSharedRecentTx(app, recentReserve, availW, hs, vs); } // ============================================================================ // Layout 7: Two-Row Hero // ============================================================================ static void RenderBalanceTwoRow(App* app) { using namespace material; const auto& S = schema::UISchema::instance(); const auto& state = app->state(); UpdateBalanceLerp(app); ImVec2 contentAvail = ImGui::GetContentRegionAvail(); float availW = contentAvail.x; float hs = Layout::hScale(availW); float vs = Layout::vScale(contentAvail.y); float glassRound = Layout::glassRounding(); float cGap = Layout::cardGap(); const float dp = Layout::dpiScale(); ImDrawList* dl = ImGui::GetWindowDrawList(); char buf[64]; // Top margin float twoRowTopMargin = S.drawElement("tabs.balance.two-row", "top-margin").size; if (twoRowTopMargin >= 0.0f) ImGui::Dummy(ImVec2(0, twoRowTopMargin)); // Row 1: logo + balance + USD + actions RenderCompactHero(app, dl, availW, hs, vs); // Sync + mining on same line { const auto& state = app->state(); ImFont* capFont = Type().caption(); if (state.sync.syncing && state.sync.headers > 0) { float pct = static_cast(state.sync.verification_progress) * 100.0f; snprintf(buf, sizeof(buf), "Syncing %.1f%%", pct); Type().textColored(TypeStyle::Caption, Warning(), buf); ImGui::SameLine(); } if (state.mining.generate) { double hr = state.mining.localHashrate; if (hr >= 1000.0) snprintf(buf, sizeof(buf), "Mining %.1f KH/s", hr / 1000.0); else snprintf(buf, sizeof(buf), "Mining %.0f H/s", hr); Type().textColored(TypeStyle::Caption, WithAlpha(Success(), 200), buf); ImGui::SameLine(); } // Action buttons right-aligned float btnW = S.drawElement("tabs.balance.two-row", "action-btn-width").sizeOr(80.0f); float rightEdge = ImGui::GetWindowWidth() - Layout::spacingLg(); ImGui::SameLine(rightEdge - btnW * 2 - Layout::spacingSm()); if (TactileButton("Send", ImVec2(btnW, 0), S.resolveFont("button"))) { app->setCurrentPage(NavPage::Send); } ImGui::SameLine(); if (TactileButton("Receive", ImVec2(btnW, 0), S.resolveFont("button"))) { app->setCurrentPage(NavPage::Receive); } } RenderSyncBar(app, dl, vs); ImGui::Dummy(ImVec2(0, S.drawElement("tabs.balance.two-row", "sync-gap").sizeOr(2.0f))); // Row 2: 3 mini-cards inline { float twoRowCardH = S.drawElement("tabs.balance.two-row", "card-height").size; float miniH; if (twoRowCardH >= 0.0f) { miniH = twoRowCardH; // explicit override from ui.toml } else { miniH = std::max( S.drawElement("tabs.balance.two-row", "mini-min-height").sizeOr(28.0f), S.drawElement("tabs.balance.two-row", "mini-base-height").sizeOr(36.0f) * vs); } float miniW = (availW - 2 * cGap) / 3.0f; ImVec2 origin = ImGui::GetCursorScreenPos(); GlassPanelSpec spec; spec.rounding = std::max( S.drawElement("tabs.balance.two-row", "mini-rounding-min").sizeOr(4.0f), glassRound * S.drawElement("tabs.balance.two-row", "mini-rounding-ratio").sizeOr(0.5f)); ImFont* capFont = Type().caption(); float indicatorR = S.drawElement("tabs.balance.two-row", "indicator-radius").sizeOr(3.0f); int balDecimals = (int)S.drawElement("tabs.balance.two-row", "balance-decimals").sizeOr(4.0f); float twoRowPadOverride = S.drawElement("tabs.balance.two-row", "card-padding").size; float miniPad = (twoRowPadOverride >= 0.0f) ? twoRowPadOverride : Layout::spacingSm(); // Shielded mini-card { ImVec2 cMin = origin; ImVec2 cMax(cMin.x + miniW, cMin.y + miniH); DrawGlassPanel(dl, cMin, cMax, spec); float cx = cMin.x + miniPad; float cy = cMin.y + (miniH - capFont->LegacySize) * 0.5f; dl->AddCircleFilled(ImVec2(cx + 4 * dp, cy + capFont->LegacySize * 0.5f), indicatorR, Success()); snprintf(buf, sizeof(buf), "%.*f", balDecimals, s_dispShielded); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 12 * dp, cy), Success(), buf); // Percentage of total (right-aligned) float shieldPct = (s_dispTotal > 1e-9) ? (float)(s_dispShielded / s_dispTotal * 100.0) : 0.0f; snprintf(buf, sizeof(buf), "%.0f%%", shieldPct); ImVec2 pctSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(cMax.x - pctSz.x - miniPad, cy), WithAlpha(Success(), 160), buf); } // Transparent mini-card { ImVec2 cMin(origin.x + miniW + cGap, origin.y); ImVec2 cMax(cMin.x + miniW, cMin.y + miniH); DrawGlassPanel(dl, cMin, cMax, spec); float cx = cMin.x + miniPad; float cy = cMin.y + (miniH - capFont->LegacySize) * 0.5f; dl->AddCircleFilled(ImVec2(cx + 4 * dp, cy + capFont->LegacySize * 0.5f), indicatorR, Warning()); snprintf(buf, sizeof(buf), "%.*f", balDecimals, s_dispTransparent); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 12 * dp, cy), Warning(), buf); // Percentage of total (right-aligned) float transPct = (s_dispTotal > 1e-9) ? (float)(s_dispTransparent / s_dispTotal * 100.0) : 0.0f; snprintf(buf, sizeof(buf), "%.0f%%", transPct); ImVec2 pctSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(cMax.x - pctSz.x - miniPad, cy), WithAlpha(Warning(), 160), buf); } // Market mini-card { ImVec2 cMin(origin.x + 2 * (miniW + cGap), origin.y); ImVec2 cMax(cMin.x + miniW, cMin.y + miniH); DrawGlassPanel(dl, cMin, cMax, spec); float cx = cMin.x + miniPad; float cy = cMin.y + (miniH - capFont->LegacySize) * 0.5f; const auto& market = state.market; if (market.price_usd >= 0.01) snprintf(buf, sizeof(buf), "$%.4f", market.price_usd); else if (market.price_usd > 0) snprintf(buf, sizeof(buf), "$%.8f", market.price_usd); else snprintf(buf, sizeof(buf), "$--.--"); ImVec2 priceSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceMedium(), buf); float sparkRight = cMax.x - miniPad; if (market.price_usd > 0) { bool pos = market.change_24h >= 0; snprintf(buf, sizeof(buf), "%s%.1f%%", pos ? "+" : "", market.change_24h); ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); float chgX = cMax.x - chgSz.x - miniPad; dl->AddText(capFont, capFont->LegacySize, ImVec2(chgX, cy), pos ? Success() : Error(), buf); sparkRight = chgX; // Sparkline between price and percentage if (market.price_history.size() >= 2) { float sparkGap = S.drawElement("tabs.balance.two-row", "sparkline-gap").sizeOr(6.0f); float sparkPad = S.drawElement("tabs.balance.two-row", "sparkline-pad").sizeOr(4.0f); float sparkLeft = cx + priceSz.x + sparkGap; float sparkRightEdge = sparkRight - sparkGap; if (sparkLeft < sparkRightEdge) { ImVec2 spMin(sparkLeft, cMin.y + sparkPad); ImVec2 spMax(sparkRightEdge, cMax.y - sparkPad); ImU32 lineCol = pos ? WithAlpha(Success(), 200) : WithAlpha(Error(), 200); DrawSparkline(dl, spMin, spMax, market.price_history, lineCol); } } } } ImGui::Dummy(ImVec2(availW, miniH)); } ImGui::Dummy(ImVec2(0, cGap)); float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f); float twoRowAddrOverride = S.drawElement("tabs.balance.two-row", "address-table-height").size; float addrH = (twoRowAddrOverride >= 0.0f) ? twoRowAddrOverride : ImGui::GetContentRegionAvail().y - recentReserve - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd(); RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs); RenderSharedRecentTx(app, recentReserve, availW, hs, vs); } // ============================================================================ // Layout 8: Minimal (card-less typography only) // ============================================================================ static void RenderBalanceMinimal(App* app) { using namespace material; const auto& S = schema::UISchema::instance(); const auto& state = app->state(); UpdateBalanceLerp(app); ImVec2 contentAvail = ImGui::GetContentRegionAvail(); float availW = contentAvail.x; float hs = Layout::hScale(availW); float vs = Layout::vScale(contentAvail.y); float glassRound = Layout::glassRounding(); ImDrawList* dl = ImGui::GetWindowDrawList(); char buf[64]; // Top margin float minTopMargin = S.drawElement("tabs.balance.minimal", "top-margin").size; if (minTopMargin >= 0.0f) ImGui::Dummy(ImVec2(0, minTopMargin)); ImFont* heroFont = Type().h2(); ImFont* sub1 = Type().subtitle1(); ImFont* capFont = Type().caption(); // Line 1: Big balance + market price snprintf(buf, sizeof(buf), "%.8f %s", s_dispTotal, DRAGONX_TICKER); ImVec2 pos = ImGui::GetCursorScreenPos(); DrawTextShadow(dl, heroFont, heroFont->LegacySize, pos, OnSurface(), buf); ImVec2 heroSz = heroFont->CalcTextSizeA(heroFont->LegacySize, 10000.0f, 0.0f, buf); // Market price right-aligned const auto& market = state.market; if (market.price_usd > 0) { if (market.price_usd >= 0.01) snprintf(buf, sizeof(buf), "$%.4f", market.price_usd); else snprintf(buf, sizeof(buf), "$%.8f", market.price_usd); ImVec2 pSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, buf); float rightEdge = pos.x + availW; dl->AddText(capFont, capFont->LegacySize, ImVec2(rightEdge - pSz.x, pos.y + heroSz.y - capFont->LegacySize), OnSurfaceMedium(), buf); } ImGui::Dummy(heroSz); // Line 2: Shielded + Transparent snprintf(buf, sizeof(buf), "Shielded: %.8f", s_dispShielded); Type().textColored(TypeStyle::Caption, Success(), buf); ImGui::SameLine(0, Layout::spacingLg()); snprintf(buf, sizeof(buf), "Transparent: %.8f", s_dispTransparent); Type().textColored(TypeStyle::Caption, Warning(), buf); // USD value double usd_value = state.getBalanceUSD(); if (usd_value > 0.0) snprintf(buf, sizeof(buf), "$%.2f USD", usd_value); else snprintf(buf, sizeof(buf), "$-.-- USD"); ImGui::SameLine(0, Layout::spacingLg()); Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), buf); RenderSyncBar(app, dl, vs); // Dashed separator { ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImVec2 sepPos = ImGui::GetCursorScreenPos(); float dashLen = S.drawElement("tabs.balance.minimal", "dash-length").sizeOr(6.0f); float gapLen = S.drawElement("tabs.balance.minimal", "dash-gap").sizeOr(4.0f); float sepAlpha = S.drawElement("tabs.balance.minimal", "separator-alpha").sizeOr(25.0f); float sepThick = S.drawElement("tabs.balance.minimal", "separator-thickness").sizeOr(1.0f); float x = sepPos.x; float endX = sepPos.x + availW; while (x < endX) { float x2 = std::min(x + dashLen, endX); dl->AddLine(ImVec2(x, sepPos.y), ImVec2(x2, sepPos.y), IM_COL32(255, 255, 255, (int)sepAlpha), sepThick); x += dashLen + gapLen; } ImGui::Dummy(ImVec2(availW, 1)); ImGui::Dummy(ImVec2(0, Layout::spacingSm())); } float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f); float minAddrOverride = S.drawElement("tabs.balance.minimal", "address-table-height").size; float addrH = (minAddrOverride >= 0.0f) ? minAddrOverride : ImGui::GetContentRegionAvail().y - recentReserve - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd(); RenderSharedAddressList(app, addrH, availW, glassRound, hs, vs); RenderSharedRecentTx(app, recentReserve, availW, hs, vs); } } // namespace ui } // namespace dragonx