// DragonX Wallet - ImGui Edition // Copyright 2024-2026 The Hush Developers // Released under the GPLv3 #include "balance_tab.h" #include "balance_address_list.h" #include "balance_tab_helpers.h" #include "balance_recent_tx.h" #include "balance_components.h" #include "mining_tab_helpers.h" // FormatHashrate (consistent MH/GH/.. scaling) #include "key_export_dialog.h" #include "qr_popup_dialog.h" #include "address_label_dialog.h" #include "address_transfer_dialog.h" #include "send_tab.h" #include "../../app.h" #include "../../config/settings.h" #include "../../config/version.h" #include "../../util/i18n.h" #include "../../util/text_format.h" #include "../theme.h" #include "../layout.h" #include "../schema/ui_schema.h" #include "../material/type.h" #include "../material/draw_helpers.h" #include "../effects/imgui_acrylic.h" #include "../sidebar.h" #include "../notifications.h" #include "../staleness_badge.h" #include "../../embedded/IconsMaterialDesign.h" #include "imgui.h" #include #include #include #include #include #include "../../util/logger.h" namespace dragonx { namespace ui { // Animated balance state — lerps smoothly toward target. Non-static (declared extern in // balance_components.h) so the shared components in balance_components.cpp share the same values. double s_dispTotal = 0.0; double s_dispShielded = 0.0; double s_dispTransparent = 0.0; double s_dispUnconfirmed = 0.0; // Forward declarations for all layout functions static void RenderBalanceClassic(App* app); 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; bool s_generating_z_address = false; // external linkage — shared with balance_components.cpp 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 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 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(); // Lerp displayed balances toward actual values { float dt = ImGui::GetIO().DeltaTime; float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed; auto lerp = [](double& disp, double target, float dt, float spd) { double diff = target - disp; if (std::abs(diff) < 1e-9) { disp = target; return; } disp += diff * (double)(dt * spd); // 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) { // TOML override scaled by dp so it grows with DPI + user font scale cardH = std::max(classicCardH * dp, marketContentH); } else { float minH = S.drawElement("tabs.balance.classic", "card-min-height").sizeOr(70.0f) * dp; 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; snprintf(buf, sizeof(buf), " Mining %s", FormatHashrate(hr).c_str()); dl->AddText(capFont, capFont->LegacySize, ImVec2(cx + 12 * dp, cy), WithAlpha(Success(), 200), buf); } else { // Refresh-staleness badge (W6-2): connected, not syncing/mining, but the balance // hasn't refreshed in a while (a busy daemon can fail z_gettotalbalance without // dropping the whole connection). The node banner only covers full disconnects, so // this pill is the sole signal that the shown number may be out of date. StalenessBadge badge = evaluateStalenessBadge( state.last_balance_update, std::time(nullptr), state.connected); if (badge.show) { const bool err = (badge.severity == StalenessSeverity::Error); ImU32 fg = err ? Error() : Warning(); ImU32 bg = WithAlpha(fg, 38); ImU32 bd = WithAlpha(fg, 90); snprintf(buf, sizeof(buf), "%s %s", TR("data_stale_prefix"), timeAgo(state.last_balance_update).c_str()); ImVec2 pillSz = DrawPill(dl, ImVec2(cx, cy), buf, capFont, fg, bg, bd); // Hover → explain what stale means and how old the data actually is. if (material::IsRectHovered(ImVec2(cx, cy), ImVec2(cx + pillSz.x, cy + pillSz.y))) Tooltip("%s", TR("data_stale_tooltip")); } } // 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 + Recent transactions — shared renderers (feature-complete: // set-label / add-to-portfolio context items, custom address icons, drag-to-reorder, // keyboard nav, copy-flash, i18n). Classic keeps its own address-table-height override // (see tabs.balance.classic) so the card sizing is unchanged. // ================================================================ { float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f); float classicAddrH = S.drawElement("tabs.balance.classic", "address-table-height").size; float addrH = (classicAddrH >= 0.0f) ? classicAddrH * dp : ImGui::GetContentRegionAvail().y - recentReserve - Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd(); RenderSharedAddressList(app, addrH, contentAvail.x, glassRound, hs, vs); RenderSharedRecentTx(app, recentReserve, contentAvail.x, hs, vs); } } // Shared helpers (UpdateBalanceLerp / RenderCompactHero / RenderSharedAddressList / // RenderSharedRecentTx / RenderSyncBar) now live in balance_components.{h,cpp}. // ============================================================================ // 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 --- { ImFont* donutOv = Type().overline(); ImFont* donutCap = Type().caption(); // Font-content floor: legend needs overline + 3 caption lines + spacing float donutFontFloor = Layout::spacingLg() * 2 + donutOv->LegacySize + Layout::spacingMd() + donutCap->LegacySize * 3 + Layout::spacingSm() * 3; float donutCardH = S.drawElement("tabs.balance.donut", "card-height").size; float panelH; if (donutCardH >= 0.0f) { panelH = std::max(donutCardH * dp, donutFontFloor); } else { panelH = std::max({donutFontFloor, S.drawElement("tabs.balance.donut", "panel-min-height").sizeOr(80.0f) * dp, 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 * dp : 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)); ImFont* heroFont = Type().h2(); ImFont* sub1 = Type().subtitle1(); ImFont* capFont = Type().caption(); ImFont* ovFont = Type().overline(); float consPadOverride = S.drawElement("tabs.balance.consolidated", "card-padding").size; float pad = (consPadOverride >= 0.0f) ? consPadOverride : Layout::spacingLg(); // Font-content floor: pad + overline + hero + caption + subtitle + pad float consFontFloor = pad + ovFont->LegacySize + Layout::spacingSm() + heroFont->LegacySize + Layout::spacingSm() + capFont->LegacySize + Layout::spacingSm() + sub1->LegacySize + pad; float consCardH = S.drawElement("tabs.balance.consolidated", "card-height").size; float cardH; if (consCardH >= 0.0f) { cardH = std::max(consCardH * dp, consFontFloor); } else { cardH = std::max({consFontFloor, S.drawElement("tabs.balance.consolidated", "card-min-height").sizeOr(90.0f) * dp, 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 cx = cardMin.x + pad; float cy = cardMin.y + pad; // Coin logo ImTextureID logoTex = app->getCoinLogoTexture(); 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 * dp : 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; ImFont* ovFont = Type().overline(); ImFont* sub1 = Type().subtitle1(); ImFont* capFont = Type().caption(); // Font-content floor: pad + overline + subtitle1 + caption + pad float dashFontFloor = Layout::spacingLg() + ovFont->LegacySize + Layout::spacingSm() + sub1->LegacySize + Layout::spacingSm() + capFont->LegacySize + Layout::spacingLg(); float dashCardH = S.drawElement("tabs.balance.dashboard", "card-height").size; float tileH; if (dashCardH >= 0.0f) { tileH = std::max(dashCardH * dp, dashFontFloor); } else { tileH = std::max({dashFontFloor, S.drawElement("tabs.balance.dashboard", "tile-min-height").sizeOr(70.0f) * dp, contentAvail.y * S.drawElement("tabs.balance.dashboard", "tile-height-ratio").sizeOr(0.16f) / numRows}); } ImVec2 origin = ImGui::GetCursorScreenPos(); GlassPanelSpec tileSpec; tileSpec.rounding = glassRound; 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 * dp : 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)); ImFont* capFont = Type().caption(); ImFont* body2 = Type().body2(); ImFont* sub1 = Type().subtitle1(); // Font-content floor per row: icon + label + value must fit float vstackRowFontFloor = std::max(body2->LegacySize, capFont->LegacySize) + Layout::spacingSm() * 2; float rowGap = S.drawElement("tabs.balance.vertical-stack", "row-gap").sizeOr(2.0f); float vstackFontFloor = vstackRowFontFloor * 4 + rowGap * 3; float vstackCardH = S.drawElement("tabs.balance.vertical-stack", "card-height").size; float stackH; if (vstackCardH >= 0.0f) { stackH = std::max(vstackCardH * dp, vstackFontFloor); } else { stackH = std::max({vstackFontFloor, S.drawElement("tabs.balance.vertical-stack", "stack-min-height").sizeOr(80.0f) * dp, contentAvail.y * S.drawElement("tabs.balance.vertical-stack", "stack-height-ratio").sizeOr(0.16f)}); } float rowH = (stackH - 3 * rowGap) / 4.0f; float rowMinH = std::max( S.drawElement("tabs.balance.vertical-stack", "row-min-height").sizeOr(20.0f) * dp, vstackRowFontFloor); if (rowH < rowMinH) rowH = rowMinH; 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 * dp : 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)); ImFont* capFont = Type().caption(); ImFont* iconFont = Type().iconSmall(); // Font-content floor per row: caption text + vertical padding float v2x2RowFontFloor = capFont->LegacySize + Layout::spacingSm() * 2; float rowGap = S.drawElement(cfgSec, "row-gap").sizeOr(2.0f); float colGap = S.drawElement(cfgSec, "col-gap").sizeOr(8.0f); float v2x2FontFloor = v2x2RowFontFloor * 2 + rowGap; float cardHOverride = S.drawElement(cfgSec, "card-height").size; float stackH; if (cardHOverride >= 0.0f) { stackH = std::max(cardHOverride * dp, v2x2FontFloor); } else { stackH = std::max({v2x2FontFloor, S.drawElement(cfgSec, "stack-min-height").sizeOr(60.0f) * dp, contentAvail.y * S.drawElement(cfgSec, "stack-height-ratio").sizeOr(0.12f)}); } float rowH = (stackH - rowGap) / 2.0f; float rowMinH = std::max( S.drawElement(cfgSec, "row-min-height").sizeOr(24.0f) * dp, v2x2RowFontFloor); if (rowH < rowMinH) rowH = rowMinH; float colW = (availW - colGap) / 2.0f; 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 * dp : 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 { ImFont* shieldCap = Type().caption(); ImFont* shieldSub1 = Type().subtitle1(); // Font-content floor: gauge area + legend text (subtitle + 2 captions) float shieldFontFloor = Layout::spacingLg() * 2 + shieldSub1->LegacySize + Layout::spacingSm() + shieldCap->LegacySize * 2 + Layout::spacingSm() * 2; float shieldCardH = S.drawElement("tabs.balance.shield", "card-height").size; float gaugeH; if (shieldCardH >= 0.0f) { gaugeH = std::max(shieldCardH * dp, shieldFontFloor); } else { gaugeH = std::max({shieldFontFloor, S.drawElement("tabs.balance.shield", "gauge-min-height").sizeOr(80.0f) * dp, 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 = TR("privacy_great"); else if (privPct >= 50.0f) statusMsg = TR("privacy_medium"); else statusMsg = TR("privacy_low"); 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 * dp : 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 * dp; // scaled by dp for DPI + font scale } else { chartH = std::max( S.drawElement("tabs.balance.timeline", "chart-min-height").sizeOr(80.0f) * dp, 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 = TR("balance_history_collecting"); 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)); { ImFont* ovFont = Type().overline(); ImFont* capFont = Type().caption(); // Font-content floor: pad + overline + gap + caption + pad float tlPadVal = S.drawElement("tabs.balance.timeline", "card-padding").size; float tlPad = (tlPadVal >= 0.0f) ? tlPadVal : Layout::spacingXs(); float tlFontFloor = tlPad + ovFont->LegacySize + 2.0f * dp + capFont->LegacySize + tlPad; float tlSummaryH = S.drawElement("tabs.balance.timeline", "summary-card-height").size; float cardH; if (tlSummaryH >= 0.0f) { cardH = std::max(tlSummaryH * dp, tlFontFloor); } else { cardH = std::max({tlFontFloor, S.drawElement("tabs.balance.timeline", "summary-min-height").sizeOr(44.0f) * dp, 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; 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 * dp : 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; snprintf(buf, sizeof(buf), "Mining %s", FormatHashrate(hr).c_str()); 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 { ImFont* twoRowCap = Type().caption(); // Font-content floor: caption centered + vertical padding float twoRowFontFloor = twoRowCap->LegacySize + Layout::spacingSm() * 2; float twoRowCardH = S.drawElement("tabs.balance.two-row", "card-height").size; float miniH; if (twoRowCardH >= 0.0f) { miniH = std::max(twoRowCardH * dp, twoRowFontFloor); } else { miniH = std::max({twoRowFontFloor, S.drawElement("tabs.balance.two-row", "mini-min-height").sizeOr(28.0f) * dp, 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 * dp : 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(); const float dp = Layout::dpiScale(); 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), TR("balance_shielded_fmt"), s_dispShielded); Type().textColored(TypeStyle::Caption, Success(), buf); ImGui::SameLine(0, Layout::spacingLg()); snprintf(buf, sizeof(buf), TR("balance_transparent_fmt"), 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 * dp : 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