Files
ObsidianDragon/src/ui/windows/balance_tab.cpp
DanS a605e35409 fix(ui): consistent hashrate units, full-address tooltips, drop dead vars
- Balance card hashrate now uses the shared FormatHashrate() (TH/GH/MH/KH/H)
  instead of a bespoke two-tier KH/s formatter.
- Recent-tx rows show the full untruncated address on hover — two z-addresses can
  truncate to the same first/last window — and the truncate helpers guard maxLen<=3.
- Remove the unused viewTop/viewBot "viewport culling" locals in the tx list
  (pagination already bounds per-frame work).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-07 14:25:43 -05:00

3450 lines
166 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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 "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 "../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 <toml++/toml.hpp>
#include <algorithm>
#include <cstring>
#include <ctime>
#include <cmath>
#include "../../util/logger.h"
namespace dragonx {
namespace ui {
// Animated balance state — lerps smoothly toward target
static double s_dispTotal = 0.0;
static double s_dispShielded = 0.0;
static double s_dispTransparent = 0.0;
static double s_dispUnconfirmed = 0.0;
// 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<BalanceLayoutEntry> s_balanceLayouts;
static std::string s_defaultLayoutId = "classic";
static bool s_layoutConfigLoaded = false;
static bool s_generating_z_address = false;
static void LoadBalanceLayoutConfig()
{
s_balanceLayouts.clear();
const void* elem = schema::UI().findElement("tabs.balance", "layouts");
if (elem) {
const auto& t = *static_cast<const toml::table*>(elem);
if (auto selected = t["selected"].value<std::string>())
s_defaultLayoutId = *selected;
else if (auto def = t["default"].value<std::string>())
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<std::string>();
auto name = (*opt)["name"].value<std::string>();
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<BalanceLayoutEntry>& 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<std::string> 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 = (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<float>(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<float>(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);
}
// 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;
bool mining;
};
std::vector<AddrRow> 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, app->isMiningAddress(a.address)});
}
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, app->isMiningAddress(a.address)});
}
// 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(TrId("hide_zero_balances", "hide0").c_str(), &s_hideZeroBalances);
{
int hc = app->getHiddenAddressCount();
if (hc > 0) {
ImGui::SameLine(0, Layout::spacingLg());
char hlbl[64];
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
ImGui::Checkbox(hlbl, &s_showHidden);
} else {
s_showHidden = false;
}
}
float buttonWidth = (addrBtn.width > 0) ? addrBtn.width : 140.0f;
float spacing = (addrBtn.gap > 0) ? addrBtn.gap : 8.0f;
float totalButtonsWidth = buttonWidth * 2 + spacing;
ImGui::SameLine(std::max(kMinButtonsPosition,
ImGui::GetWindowWidth() - totalButtonsWidth - kButtonRowRightMargin));
bool addrSyncing = state.sync.syncing && !state.sync.isSynced();
ImGui::BeginDisabled(addrSyncing);
if (s_generating_z_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##bal_z", TR("generating"), dotStr[dots]);
TactileButton(genLabel, ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
s_generating_z_address = true;
app->createNewZAddress([](const std::string& addr) {
s_generating_z_address = false;
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
});
}
ImGui::SameLine();
if (TactileButton(TR("new_t_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
app->createNewTAddress([](const std::string& addr) {
DEBUG_LOGF("Created new t-address: %s\n", addr.c_str());
});
}
ImGui::EndDisabled();
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
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 * dp; // scaled for DPI + font scale
} 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::cardInnerPadding();
float rowPadRight = Layout::cardInnerPadding();
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size, S.drawElement("tabs.balance", "address-icon-size").size * hs);
float innerW = ImGui::GetContentRegionAvail().x;
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::spacingSm();
float btnY = rowPos.y + (rowH - btnH) * 0.5f;
float rightEdge = rowPos.x + innerW;
float starX = rightEdge - btnW - rowPadRight;
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 ? TR("remove_favorite") : TR("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 ? TR("restore_address") : TR("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)" : "";
const char* viewOnlyTag = (!addr.has_spending_key) ? " (view-only)" : "";
const char* miningTag = row.mining ? TR("mining_tag") : "";
char typeBuf[64];
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s%s", typeLabel, hiddenTag, viewOnlyTag, miningTag);
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(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
app->setMiningAddress(addr.address, !row.mining);
}
if (ImGui::MenuItem(TR("export_private_key"))) {
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
}
if (row.isZ) {
if (ImGui::MenuItem(TR("export_viewing_key"))) {
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Viewing);
}
}
if (ImGui::MenuItem(TR("show_qr_code"))) {
QRPopupDialog::show(addr.address,
row.isZ ? "Z-Address" : "T-Address");
}
ImGui::Separator();
if (row.hidden) {
if (ImGui::MenuItem(TR("restore_address")))
app->unhideAddress(addr.address);
} else if (addr.balance < 1e-9) {
if (ImGui::MenuItem(TR("hide_address")))
app->hideAddress(addr.address);
}
if (row.favorite) {
if (ImGui::MenuItem(TR("remove_favorite")))
app->unfavoriteAddress(addr.address);
} else {
if (ImGui::MenuItem(TR("favorite_address")))
app->favoriteAddress(addr.address);
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
ImGui::PopID();
// 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();
ImFont* capFont = Type().caption();
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
ImGui::BeginChild("##BalanceClassicRecentRows", ImVec2(0, listH), false,
ImGuiWindowFlags_NoBackground);
const auto& txs = state.transactions;
int count = (int)txs.size();
if (count == 0) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
"No transactions yet");
} else {
ImDrawList* dl = ImGui::GetWindowDrawList();
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size, S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
for (int i = 0; i < count; i++) {
const auto& tx = txs[i];
ImVec2 rowPos = ImGui::GetCursorScreenPos();
float rowY = rowPos.y + rowH * 0.5f;
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
// 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(), display.typeText.c_str());
// Address (truncated)
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(addrX, rowPos.y + 2 * dp),
OnSurfaceDisabled(), display.addressText.c_str());
// Amount (right-aligned area)
ImVec2 amtSz = capFont->CalcTextSizeA(
capFont->LegacySize, 10000, 0, display.amountText.c_str());
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size, S.drawElement("tabs.balance", "amount-right-margin").size * hs);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(amtX, rowPos.y + 2 * dp),
recentTxAmountColor(tx.type),
display.amountText.c_str());
// Time ago
ImVec2 agoSz = capFont->CalcTextSizeA(
capFont->LegacySize, 10000, 0, display.timeText.c_str());
dl->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
OnSurfaceDisabled(), display.timeText.c_str());
// 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);
// Show the full, untruncated address — two z-addresses can truncate to the
// same first/last window, so the truncated text alone can't disambiguate.
if (!tx.address.empty())
ImGui::SetTooltip("%s", tx.address.c_str());
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::History);
}
ImGui::Dummy(ImVec2(0, rowH));
}
}
ImGui::EndChild();
}
}
// ============================================================================
// Shared helpers used by multiple layouts
// ============================================================================
// Update animated lerp balances — called at the top of every layout
static void UpdateBalanceLerp(App* app) {
using namespace material;
const auto& S = schema::UISchema::instance();
const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
const auto& state = app->state();
float dt = ImGui::GetIO().DeltaTime;
float speed = (app->settings() && app->settings()->getReduceMotion()) ? 999.0f : kBalanceLerpSpeed;
auto lerp = [](double& disp, double target, float dt, float spd) {
double diff = target - disp;
if (std::abs(diff) < 1e-9) { disp = target; return; }
disp += diff * (double)(dt * spd);
if (std::abs(target - disp) < 1e-9) disp = target;
};
lerp(s_dispTotal, app->getTotalBalance(), dt, speed);
lerp(s_dispShielded, app->getShieldedBalance(), dt, speed);
lerp(s_dispTransparent, app->getTransparentBalance(), dt, speed);
lerp(s_dispUnconfirmed, state.unconfirmed_balance, dt, speed);
}
// Render compact hero line: logo + balance + USD + mining on one line
static void RenderCompactHero(App* app, ImDrawList* dl, float availW, float hs, float vs, float heroHeightOverride = -1.0f) {
using namespace material;
char buf[64];
const float dp = Layout::dpiScale();
// Coin logo
ImTextureID logoTex = app->getCoinLogoTexture();
ImFont* sub1 = Type().subtitle1();
float logoSz = sub1->LegacySize + 4.0f * dp;
float lineH = (heroHeightOverride >= 0.0f) ? heroHeightOverride : logoSz;
if (logoTex != 0) {
ImVec2 pos = ImGui::GetCursorScreenPos();
dl->AddImage(logoTex,
ImVec2(pos.x, pos.y),
ImVec2(pos.x + lineH, pos.y + lineH),
ImVec2(0, 0), ImVec2(1, 1), IM_COL32(255, 255, 255, 255));
ImGui::Dummy(ImVec2(lineH + Layout::spacingSm(), lineH));
ImGui::SameLine();
}
// Total balance
snprintf(buf, sizeof(buf), "%.8f", s_dispTotal);
ImVec2 pos = ImGui::GetCursorScreenPos();
float fontSize = (heroHeightOverride >= 0.0f) ? heroHeightOverride : sub1->LegacySize;
DrawTextShadow(dl, sub1, fontSize, pos, OnSurface(), buf);
ImVec2 sz = sub1->CalcTextSizeA(fontSize, 10000.0f, 0.0f, buf);
ImGui::Dummy(ImVec2(sz.x, lineH));
ImGui::SameLine();
// Ticker
ImFont* capFont = Type().caption();
float tickerFontSize = (heroHeightOverride >= 0.0f) ? heroHeightOverride * 0.6f : capFont->LegacySize;
float tickerY = pos.y + lineH - tickerFontSize;
dl->AddText(capFont, tickerFontSize,
ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
OnSurfaceMedium(), DRAGONX_TICKER);
ImGui::Dummy(ImVec2(capFont->CalcTextSizeA(tickerFontSize, 10000, 0, DRAGONX_TICKER).x + Layout::spacingLg(), lineH));
ImGui::SameLine();
// USD value
const auto& state = app->state();
double usd_value = state.getBalanceUSD();
if (usd_value > 0.0)
snprintf(buf, sizeof(buf), "$%.2f", usd_value);
else
snprintf(buf, sizeof(buf), "$--.--");
dl->AddText(capFont, tickerFontSize,
ImVec2(ImGui::GetCursorScreenPos().x, tickerY),
OnSurfaceDisabled(), buf);
ImGui::NewLine();
}
// Render the shared address list section (used by all layouts)
static void RenderSharedAddressList(App* app, float listH, float availW,
float glassRound, float hs, float vs) {
using namespace material;
const auto& S = schema::UISchema::instance();
const float dp = Layout::dpiScale();
const auto addrBtn = S.button("tabs.balance", "address-button");
const auto actionBtn = S.button("tabs.balance", "action-button");
const auto searchIn = S.input("tabs.balance", "search-input");
const auto addrTable = S.table("tabs.balance", "address-table");
const auto& state = app->state();
Type().text(TypeStyle::H6, TR("your_addresses"));
ImGui::Spacing();
// ---- Persistent state ----
static char addr_search[128] = "";
static bool s_hideZeroBalances = true;
static bool s_showHidden = false;
// Drag state
static int s_dragIdx = -1; // row being dragged (-1 = none)
static float s_dragOffsetY = 0.0f; // mouse offset from row top
static float s_dragStartY = 0.0f; // mouse Y at drag start
static bool s_dragActive = false; // drag distance threshold passed
static int s_dropTargetIdx = -1; // row hovered during drag
// Copy feedback
static int s_copiedRow = -1;
static float s_copiedTime = 0.0f;
// ---- Build and filter address rows ----
std::vector<AddressListInput> rowInputs;
rowInputs.reserve(state.z_addresses.size() + state.t_addresses.size());
auto addRows = [&](const auto& addrs, bool isZ) {
for (const auto& a : addrs) {
std::string addrLabel = app->getAddressLabel(a.address);
bool isHidden = app->isAddressHidden(a.address);
bool isFav = app->isAddressFavorite(a.address);
bool isMining = app->isMiningAddress(a.address);
rowInputs.push_back({&a, isZ, isHidden, isFav, isMining,
addrLabel, app->getAddressIcon(a.address),
app->getAddressSortOrder(a.address)});
}
};
addRows(state.z_addresses, true);
addRows(state.t_addresses, false);
auto rows = BuildAddressListRows(rowInputs, addr_search, s_hideZeroBalances, s_showHidden);
// ---- Toolbar: search + checkboxes + create buttons ----
float avail = ImGui::GetContentRegionAvail().x;
float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f;
float schemaRatio = (searchIn.widthRatio >= 0) ? searchIn.widthRatio : 0.30f;
float searchW = std::min(schemaMaxW * hs, avail * schemaRatio);
ImGui::SetNextItemWidth(searchW);
ImGui::InputTextWithHint("##AddrSearch", TR("filter"), addr_search, sizeof(addr_search));
ImGui::SameLine(0, Layout::spacingLg());
ImGui::Checkbox(TrId("hide_zero_balances", "hide0_v2").c_str(), &s_hideZeroBalances);
{
int hc = app->getHiddenAddressCount();
if (hc > 0) {
ImGui::SameLine(0, Layout::spacingLg());
char hlbl[64];
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
ImGui::Checkbox(hlbl, &s_showHidden);
} else {
s_showHidden = false;
}
}
float buttonWidth = (addrBtn.width > 0) ? addrBtn.width : 140.0f;
float spacing = (addrBtn.gap > 0) ? addrBtn.gap : 8.0f;
float totalButtonsWidth = buttonWidth * 2 + spacing;
float kMinButtonsPosition = std::max(S.drawElement("tabs.balance", "min-buttons-position").size,
S.drawElement("tabs.balance", "buttons-position").size * hs);
ImGui::SameLine(std::max(kMinButtonsPosition,
ImGui::GetWindowWidth() - totalButtonsWidth -
S.drawElement("tabs.balance", "button-row-right-margin").sizeOr(16.0f)));
bool sharedAddrSyncing = state.sync.syncing && !state.sync.isSynced();
ImGui::BeginDisabled(sharedAddrSyncing);
if (s_generating_z_address) {
int dots = ((int)(ImGui::GetTime() * 3.0f)) % 4;
const char* dotStr[] = {"", ".", "..", "..."};
char genLabel[64];
snprintf(genLabel, sizeof(genLabel), "%s%s##shared_z", TR("generating"), dotStr[dots]);
TactileButton(genLabel, ImVec2(buttonWidth, 0),
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0),
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
s_generating_z_address = true;
app->createNewZAddress([](const std::string& addr) {
s_generating_z_address = false;
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
});
}
ImGui::SameLine();
if (TactileButton(TR("new_t_address"), ImVec2(buttonWidth, 0),
S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
app->createNewTAddress([](const std::string& addr) {
DEBUG_LOGF("Created new t-address: %s\n", addr.c_str());
});
}
ImGui::EndDisabled();
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// ---- Glass panel container ----
float addrListH = listH;
if (addrListH < 40.0f * dp) addrListH = 40.0f * dp;
ImDrawList* dlPanel = ImGui::GetWindowDrawList();
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
ImVec2 listPanelMax(listPanelMin.x + availW, listPanelMin.y + addrListH);
GlassPanelSpec addrGlassSpec;
addrGlassSpec.rounding = glassRound;
DrawGlassPanel(dlPanel, listPanelMin, listPanelMax, addrGlassSpec);
float addrScrollY = 0.0f, addrScrollMaxY = 0.0f;
int addrParentVtx = dlPanel->VtxBuffer.Size;
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingSm()));
ImGui::BeginChild("AddressList", ImVec2(availW, addrListH), false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
ApplySmoothScroll();
ImDrawList* addrChildDL = ImGui::GetWindowDrawList();
int addrChildVtx = addrChildDL->VtxBuffer.Size;
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// ---- Empty states ----
if (!app->isConnected()) {
ImGui::Dummy(ImVec2(0, 16 * dp));
float cw = ImGui::GetContentRegionAvail().x;
ImVec2 ts = ImGui::CalcTextSize(TR("not_connected"));
ImGui::SetCursorPosX((cw - ts.x) * 0.5f);
ImGui::TextDisabled("%s", TR("not_connected"));
} else if (rows.empty()) {
float cw = ImGui::GetContentRegionAvail().x;
float ch = ImGui::GetContentRegionAvail().y;
if (ch < 60) ch = 60;
const char* emptyMsg = addr_search[0] ? TR("no_addresses_match") : TR("no_addresses_yet");
ImVec2 msgSz = ImGui::CalcTextSize(emptyMsg);
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
ImGui::TextDisabled("%s", emptyMsg);
} else {
// ---- PASS 1: Compute row layout ----
ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* capFont = Type().caption();
ImFont* body2 = Type().body2();
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd() + Layout::spacingMd();
static int selected_row = -1;
addrScrollY = ImGui::GetScrollY();
addrScrollMaxY = ImGui::GetScrollMaxY();
ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning());
float rowPadLeft = Layout::cardInnerPadding();
float rowPadRight = Layout::cardInnerPadding();
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size,
S.drawElement("tabs.balance", "address-icon-size").size * hs);
float innerW = ImGui::GetContentRegionAvail().x;
// Theme colors for buttons (resolved once)
ImU32 favGoldFill = S.resolveColor("var(--favorite-fill)", IM_COL32(255, 200, 50, 40));
ImU32 favGoldBorder = S.resolveColor("var(--favorite-border)", IM_COL32(255, 200, 50, 100));
ImU32 favGoldIcon = S.resolveColor("var(--favorite-icon)", IM_COL32(255, 200, 50, 255));
ImU32 favGoldOutline= S.resolveColor("var(--favorite-outline)", IM_COL32(255, 200, 50, 120));
ImU32 btnFill = S.resolveColor("var(--row-button-fill)", IM_COL32(255, 255, 255, 12));
ImU32 btnFillHov = S.resolveColor("var(--row-button-fill-hover)", IM_COL32(255, 255, 255, 25));
ImU32 btnBorder = S.resolveColor("var(--row-button-border)", IM_COL32(255, 255, 255, 25));
ImU32 btnBorderHov = S.resolveColor("var(--row-button-border-hover)", IM_COL32(255, 255, 255, 50));
ImU32 rowHoverCol = S.resolveColor("var(--row-hover)", IM_COL32(255, 255, 255, 15));
ImU32 rowSelectCol = S.resolveColor("var(--row-select)", IM_COL32(255, 255, 255, 20));
ImU32 dividerCol = S.resolveColor("var(--sidebar-divider)", IM_COL32(255, 255, 255, 15));
// Compute Y positions for all rows (pass 1)
std::vector<float> rowY(rows.size());
float cursorStartY = ImGui::GetCursorScreenPos().y;
for (int i = 0; i < (int)rows.size(); ++i)
rowY[i] = cursorStartY + i * rowH;
// Drag logic — detect drag start, compute drag position
ImVec2 mousePos = ImGui::GetMousePos();
bool mouseDown = ImGui::IsMouseDown(ImGuiMouseButton_Left);
bool mouseClicked = ImGui::IsMouseClicked(ImGuiMouseButton_Left);
// Reset drop target each frame — it gets set only if mouse is over a row.
// Preserve the drop target on the release frame so the drop handler can use it.
if (s_dragActive && mouseDown) s_dropTargetIdx = -1;
if (s_dragIdx >= 0 && !mouseDown) {
// Mouse released — copy if it was a click (no drag activated)
if (!s_dragActive && s_dragIdx >= 0 && s_dragIdx < (int)rows.size()) {
const auto& clickRow = rows[s_dragIdx];
ImGui::SetClipboardText(clickRow.info->address.c_str());
selected_row = s_dragIdx;
s_copiedRow = s_dragIdx;
s_copiedTime = (float)ImGui::GetTime();
}
// Drop
if (s_dragActive && s_dropTargetIdx >= 0 && s_dropTargetIdx != s_dragIdx) {
const auto& srcRow = rows[s_dragIdx];
const auto& dstRow = rows[s_dropTargetIdx];
// Transfer: if dropped on another row and drag was active
if (srcRow.info->balance > 1e-9) {
AddressTransferDialog::TransferInfo ti;
ti.fromAddr = srcRow.info->address;
ti.toAddr = dstRow.info->address;
ti.fromBalance = srcRow.info->balance;
ti.toBalance = dstRow.info->balance;
ti.fromIsZ = srcRow.isZ;
ti.toIsZ = dstRow.isZ;
AddressTransferDialog::show(app, ti);
}
} else if (s_dragActive && s_dropTargetIdx < 0) {
// Reorder: dropped in gap — compute insert position from mouseY
int insertIdx = 0;
for (int i = 0; i < (int)rows.size(); ++i) {
if (mousePos.y > rowY[i] + rowH * 0.5f) insertIdx = i + 1;
}
if (insertIdx != s_dragIdx && insertIdx != s_dragIdx + 1) {
int targetIdx = (insertIdx > s_dragIdx) ? insertIdx - 1 : insertIdx;
if (targetIdx >= 0 && targetIdx < (int)rows.size()) {
app->swapAddressOrder(rows[s_dragIdx].info->address,
rows[targetIdx].info->address);
}
}
}
s_dragIdx = -1;
s_dragActive = false;
s_dropTargetIdx = -1;
}
// ---- PASS 2: Render rows ----
for (int row_idx = 0; row_idx < (int)rows.size(); row_idx++) {
const auto& row = rows[row_idx];
const auto& addr = *row.info;
bool isDragged = (s_dragIdx == row_idx && s_dragActive);
// For dragged row, offset Y to follow mouse
float drawY = rowY[row_idx];
if (isDragged) {
drawY = mousePos.y - s_dragOffsetY;
}
ImVec2 rowPos(ImGui::GetCursorScreenPos().x, drawY);
ImVec2 rowEnd(rowPos.x + innerW, drawY + rowH);
ImU32 typeCol = row.isZ ? greenCol : goldCol;
if (row.hidden) typeCol = OnSurfaceDisabled();
// Dragged row: draw with semi-transparent elevation
if (isDragged) {
dl->AddRectFilled(ImVec2(rowPos.x - 2*dp, rowPos.y - 2*dp),
ImVec2(rowEnd.x + 2*dp, rowEnd.y + 2*dp),
IM_COL32(0, 0, 0, 30), 6.0f * dp);
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(30, 30, 40, 120), 4.0f * dp);
// Tooltip following cursor — show transfer intent if over a target row
if (s_dropTargetIdx >= 0 && s_dropTargetIdx < (int)rows.size()) {
const auto& target = rows[s_dropTargetIdx];
ImGui::SetTooltip("%s\n%s\n\n%s %s",
truncateAddress(addr.address, 32).c_str(),
row.isZ ? TR("shielded") : TR("transparent"),
TR("transfer_to"),
truncateAddress(target.info->address, 32).c_str());
} else {
ImGui::SetTooltip("%s\n%s",
truncateAddress(addr.address, 32).c_str(),
row.isZ ? TR("shielded") : TR("transparent"));
}
}
// Golden border for favorites — inset so it doesn't touch container
if (row.favorite) {
float inset = 2.0f * dp;
dl->AddRect(ImVec2(rowPos.x + inset, rowPos.y + inset),
ImVec2(rowEnd.x - inset, rowEnd.y - inset),
favGoldOutline, 4.0f * dp, 0, 1.5f * dp);
}
// Selection highlight
if (selected_row == row_idx && !isDragged) {
ImDrawFlags accentFlags = 0;
float accentRound = 2.0f * dp;
if (row_idx == 0) { accentFlags = ImDrawFlags_RoundCornersTopLeft; accentRound = glassRound; }
if (row_idx == (int)rows.size() - 1) { accentFlags |= ImDrawFlags_RoundCornersBottomLeft; accentRound = glassRound; }
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + 3 * dp, rowEnd.y), typeCol, accentRound, accentFlags);
dl->AddRectFilled(rowPos, rowEnd, rowSelectCol, 4.0f * dp);
}
// Hover effects
bool hovered = !isDragged && material::IsRectHovered(rowPos, rowEnd);
// Drop target highlight when dragging another row over this one
if (s_dragActive && s_dragIdx != row_idx && material::IsRectHovered(rowPos, rowEnd)) {
s_dropTargetIdx = row_idx;
ImU32 dropCol = WithAlpha(Primary(), 40);
dl->AddRectFilled(rowPos, rowEnd, dropCol, 4.0f * dp);
dl->AddRect(rowPos, rowEnd, WithAlpha(Primary(), 140), 4.0f * dp, 0, 2.0f * dp);
}
if (hovered && selected_row != row_idx && !s_dragActive) {
dl->AddRectFilled(rowPos, rowEnd, rowHoverCol, 4.0f * dp);
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
}
auto rowLayout = ComputeAddressRowLayout(
rowPos.x, rowPos.y, innerW, rowH, rowPadLeft, rowIconSz,
Layout::spacingMd(), Layout::spacingSm(), Layout::spacingXs());
float cx = rowLayout.contentStartX;
float cy = rowLayout.contentStartY;
// ---- Button zone (right edge): [eye] [star] ----
float btnRound = 6.0f * dp;
bool btnClicked = false;
if (!isDragged) {
// Star button
{
const auto& starRect = rowLayout.favoriteButton;
ImVec2 bMin(starRect.x, starRect.y), bMax(starRect.x + starRect.width, starRect.y + starRect.height);
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
dl->AddRectFilled(bMin, bMax, row.favorite ? favGoldFill : (bHov ? btnFillHov : btnFill), btnRound);
dl->AddRect(bMin, bMax, row.favorite ? favGoldBorder : (bHov ? btnBorderHov : btnBorder), btnRound, 0, 1.0f * dp);
ImFont* iconFont = Type().iconSmall();
const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
ImU32 starCol = row.favorite ? favGoldIcon : (bHov ? OnSurface() : OnSurfaceDisabled());
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(starRect.x + (starRect.width - iSz.x) * 0.5f,
starRect.y + (starRect.height - iSz.y) * 0.5f), starCol, starIcon);
if (bHov && mouseClicked) {
if (row.favorite) app->unfavoriteAddress(addr.address);
else app->favoriteAddress(addr.address);
btnClicked = true;
}
if (bHov) ImGui::SetTooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
}
// Eye button (zero balance or hidden)
bool showEye = true;
// Always reserve space for both buttons so content doesn't shift
float contentRight = rowLayout.contentRight;
if (showEye) {
const auto& eyeRect = rowLayout.visibilityButton;
ImVec2 bMin(eyeRect.x, eyeRect.y), bMax(eyeRect.x + eyeRect.width, eyeRect.y + eyeRect.height);
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
dl->AddRectFilled(bMin, bMax, bHov ? btnFillHov : btnFill, btnRound);
dl->AddRect(bMin, bMax, bHov ? btnBorderHov : btnBorder, btnRound, 0, 1.0f * dp);
ImFont* iconFont = Type().iconSmall();
const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(eyeRect.x + (eyeRect.width - iSz.x) * 0.5f,
eyeRect.y + (eyeRect.height - iSz.y) * 0.5f), iconCol, hideIcon);
if (bHov && mouseClicked) {
if (row.hidden) app->unhideAddress(addr.address);
else app->hideAddress(addr.address);
btnClicked = true;
}
if (bHov) ImGui::SetTooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
}
// ---- Type icon or custom icon ----
float iconCx = cx + rowIconSz;
float iconCy = cy + body2->LegacySize * 0.5f;
{
ImFont* iconFont = Type().iconSmall();
bool drewCustom = false;
if (!row.icon.empty()) {
drewCustom = AddressLabelDialog::drawIconByName(
dl, row.icon, ImVec2(iconCx, iconCy), iconFont->LegacySize,
OnSurfaceMedium(), iconFont, iconFont->LegacySize);
}
if (!drewCustom) {
const char* glyph = row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, glyph);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, glyph);
}
}
// ---- Type label (first line) ----
float labelX = rowLayout.labelX;
{
const char* typeLabel = row.isZ ? TR("shielded") : TR("transparent");
const char* hiddenTag = row.hidden ? TR("hidden_tag") : "";
const char* miningTag = row.mining ? TR("mining_tag") : "";
char typeBuf[64];
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, miningTag);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
// User label next to type
if (!row.label.empty()) {
float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
dl->AddText(capFont, capFont->LegacySize,
ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy),
OnSurfaceMedium(), row.label.c_str());
}
}
// ---- Address (second line) ----
{
float addrAvailW = contentRight - labelX;
ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str());
std::string display_addr;
if (fullAddrSz.x <= addrAvailW) {
display_addr = addr.address;
} else {
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0)
? addrTable.columns.at("address").truncate : 32;
display_addr = truncateAddress(addr.address, addrTruncLen);
}
dl->AddText(capFont, capFont->LegacySize,
ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceMedium(), display_addr.c_str());
}
// ---- Balance (right-aligned, first line) ----
{
char balBuf[32];
snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance);
ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf);
float balX = contentRight - balSz.x;
ImU32 balCol = addr.balance > 0.0
? (row.isZ ? greenCol : OnSurface())
: OnSurfaceDisabled();
if (row.hidden) balCol = OnSurfaceDisabled();
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf);
}
// ---- USD value (second line, right-aligned) ----
{
std::string usdText = FormatAddressUsdValue(addr.balance, state.market.price_usd);
if (!usdText.empty()) {
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdText.c_str());
dl->AddText(capFont, capFont->LegacySize,
ImVec2(contentRight - usdSz.x,
cy + body2->LegacySize + Layout::spacingXs()),
OnSurfaceDisabled(), usdText.c_str());
}
}
// ---- Copy feedback flash ----
if (s_copiedRow == row_idx) {
float elapsed = (float)ImGui::GetTime() - s_copiedTime;
if (elapsed < 1.0f) {
float alpha = 1.0f - elapsed;
ImFont* capF = Type().caption();
const char* copiedTxt = TR("copied");
ImVec2 sz = capF->CalcTextSizeA(capF->LegacySize, 1000.0f, 0.0f, copiedTxt);
ImVec4 fc = ImGui::ColorConvertU32ToFloat4(Success());
fc.w = alpha;
float tx = rowPos.x + (innerW - sz.x) * 0.5f;
float ty = rowPos.y + (rowH - sz.y) * 0.5f;
dl->AddText(capF, capF->LegacySize, ImVec2(tx, ty),
ImGui::ColorConvertFloat4ToU32(fc), copiedTxt);
} else {
s_copiedRow = -1;
}
}
// ---- Click: begin drag tracking (copy deferred to mouse-up) ----
if (hovered && mouseClicked && !btnClicked) {
s_dragIdx = row_idx;
s_dragOffsetY = mousePos.y - rowPos.y;
s_dragStartY = mousePos.y;
s_dragActive = false;
}
// Activate drag after a small threshold
if (s_dragIdx == row_idx && !s_dragActive && mouseDown) {
if (std::abs(mousePos.y - s_dragStartY) > 6.0f * dp) {
s_dragActive = true;
s_dropTargetIdx = -1;
}
}
} // end !isDragged
// Advance cursor for interaction area
ImGui::PushID(row_idx);
ImGui::SetCursorScreenPos(ImVec2(rowPos.x, rowY[row_idx]));
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
if (ImGui::IsItemHovered() && !s_dragActive) {
ImGui::SetTooltip("%s", addr.address.c_str());
}
// Context menu
const auto& acrTheme = GetCurrentAcrylicTheme();
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("AddressContext", 0, acrTheme.menu)) {
if (ImGui::MenuItem(TR("copy_address"))) {
ImGui::SetClipboardText(addr.address.c_str());
s_copiedRow = row_idx;
s_copiedTime = (float)ImGui::GetTime();
}
if (ImGui::MenuItem(TR("send_from_this_address"))) {
SetSendFromAddress(addr.address);
app->setCurrentPage(NavPage::Send);
}
ImGui::Separator();
if (ImGui::MenuItem(TR("set_label"))) {
AddressLabelDialog::show(app, addr.address, row.isZ);
}
if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
app->setMiningAddress(addr.address, !row.mining);
}
if (ImGui::MenuItem(TR("export_private_key")))
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
if (row.isZ) {
if (ImGui::MenuItem(TR("export_viewing_key")))
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Viewing);
}
if (ImGui::MenuItem(TR("show_qr_code")))
QRPopupDialog::show(addr.address, row.isZ ? TR("z_address") : TR("t_address"));
ImGui::Separator();
if (row.hidden) {
if (ImGui::MenuItem(TR("restore_address")))
app->unhideAddress(addr.address);
} else if (addr.balance < 1e-9) {
if (ImGui::MenuItem(TR("hide_address")))
app->hideAddress(addr.address);
}
if (row.favorite) {
if (ImGui::MenuItem(TR("remove_favorite")))
app->unfavoriteAddress(addr.address);
} else {
if (ImGui::MenuItem(TR("favorite_address")))
app->favoriteAddress(addr.address);
}
effects::ImGuiAcrylic::EndAcrylicPopup();
}
ImGui::PopID();
// Separator between rows
if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) {
float sepY = rowY[row_idx] + rowH;
dl->AddLine(
ImVec2(rowPos.x + rowPadLeft + rowIconSz * 2.0f, sepY),
ImVec2(rowPos.x + innerW - Layout::spacingLg(), sepY),
dividerCol);
}
}
// Advance cursor past all rows
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x,
cursorStartY + (float)rows.size() * rowH));
// ---- Keyboard navigation ----
if (!ImGui::GetIO().WantTextInput && !ImGui::GetIO().KeyCtrl && !s_dragActive) {
bool nav = false;
if (ImGui::IsKeyPressed(ImGuiKey_UpArrow) || ImGui::IsKeyPressed(ImGuiKey_K)) {
if (selected_row > 0) { selected_row--; nav = true; }
else if (selected_row < 0 && !rows.empty()) { selected_row = 0; nav = true; }
}
if (ImGui::IsKeyPressed(ImGuiKey_DownArrow) || ImGui::IsKeyPressed(ImGuiKey_J)) {
if (selected_row < (int)rows.size() - 1) { selected_row++; nav = true; }
else if (selected_row < 0 && !rows.empty()) { selected_row = 0; nav = true; }
}
if (ImGui::IsKeyPressed(ImGuiKey_Enter) && selected_row >= 0 && selected_row < (int)rows.size()) {
ImGui::SetClipboardText(rows[selected_row].info->address.c_str());
s_copiedRow = selected_row;
s_copiedTime = (float)ImGui::GetTime();
}
if (ImGui::IsKeyPressed(ImGuiKey_F2) && selected_row >= 0 && selected_row < (int)rows.size()) {
const auto& r = rows[selected_row];
AddressLabelDialog::show(app, r.info->address, r.isZ);
}
(void)nav;
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::EndChild();
ImGui::PopStyleVar();
// Scroll fade
{
float fadeZone = std::min(
(Type().body2()->LegacySize + Type().caption()->LegacySize +
Layout::spacingLg() + Layout::spacingMd()) * 1.2f,
addrListH * 0.18f);
ApplyScrollEdgeMask(dlPanel, addrParentVtx, addrChildDL, addrChildVtx,
listPanelMin.y, listPanelMax.y, fadeZone, addrScrollY, addrScrollMaxY);
}
// Address count
{
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
char countBuf[128];
int totalAddrs = (int)(state.z_addresses.size() + state.t_addresses.size());
snprintf(countBuf, sizeof(countBuf), TR("showing_x_of_y"),
(int)rows.size(), totalAddrs);
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf);
}
}
// Render the shared recent transactions section (used by all layouts)
static void RenderSharedRecentTx(App* app, float recentH, float availW, float hs, float vs) {
using namespace material;
const auto& S = schema::UISchema::instance();
const float dp = Layout::dpiScale();
const auto actionBtn = S.button("tabs.balance", "action-button");
const float kRecentTxRowHeight = S.drawElement("tabs.balance", "recent-tx-row-height").sizeOr(22.0f);
const auto& state = app->state();
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS");
ImGui::SameLine();
if (TactileSmallButton("View All", S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) {
app->setCurrentPage(NavPage::History);
}
ImGui::Spacing();
float scaledRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
float availableListH = ImGui::GetContentRegionAvail().y;
float listH = std::max({scaledRowH, recentH, availableListH});
ImGui::BeginChild("##BalanceSharedRecentRows", ImVec2(0, listH), false,
ImGuiWindowFlags_NoBackground);
const auto& txs = state.transactions;
int count = (int)txs.size();
if (count == 0) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), "No transactions yet");
} else {
ImDrawList* dl = ImGui::GetWindowDrawList();
ImFont* capFont = Type().caption();
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size,
S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
for (int i = 0; i < count; i++) {
const auto& tx = txs[i];
ImVec2 rowPos = ImGui::GetCursorScreenPos();
float rowY = rowPos.y + rowH * 0.5f;
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
dl->AddText(capFont, capFont->LegacySize,
ImVec2(tx_x, rowPos.y + 2 * dp), OnSurfaceMedium(), display.typeText.c_str());
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(addrX, rowPos.y + 2 * dp), OnSurfaceDisabled(), display.addressText.c_str());
ImVec2 amtSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.amountText.c_str());
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
float amtX = rightEdge - amtSz.x - std::max(
S.drawElement("tabs.balance", "amount-right-min-margin").size,
S.drawElement("tabs.balance", "amount-right-margin").size * hs);
dl->AddText(capFont, capFont->LegacySize,
ImVec2(amtX, rowPos.y + 2 * dp),
recentTxAmountColor(tx.type), display.amountText.c_str());
ImVec2 agoSz = capFont->CalcTextSizeA(capFont->LegacySize, 10000, 0, display.timeText.c_str());
dl->AddText(capFont, capFont->LegacySize,
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
OnSurfaceDisabled(), display.timeText.c_str());
float rowW = ImGui::GetContentRegionAvail().x;
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
if (material::IsRectHovered(rowPos, rowEnd)) {
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
if (ImGui::IsMouseClicked(0))
app->setCurrentPage(NavPage::History);
}
ImGui::Dummy(ImVec2(0, rowH));
}
}
ImGui::EndChild();
}
// Render sync progress bar (used by multiple layouts)
static void RenderSyncBar(App* app, ImDrawList* dl, float vs) {
using namespace material;
const auto& S = schema::UISchema::instance();
const auto syncBar = S.drawElement("tabs.balance", "sync-bar");
const auto& state = app->state();
const float dp = Layout::dpiScale();
if (state.sync.syncing && state.sync.headers > 0) {
float prog = static_cast<float>(state.sync.verification_progress);
if (prog > 1.0f) prog = 1.0f;
float barH = (syncBar.height >= 0) ? syncBar.height : 3.0f;
float barW = ImGui::GetContentRegionAvail().x;
ImVec2 barPos = ImGui::GetCursorScreenPos();
dl->AddRectFilled(barPos,
ImVec2(barPos.x + barW, barPos.y + barH),
IM_COL32(255, 255, 255, 15), 1.0f * dp);
dl->AddRectFilled(barPos,
ImVec2(barPos.x + barW * prog, barPos.y + barH),
WithAlpha(Warning(), 200), 1.0f * dp);
ImGui::Dummy(ImVec2(barW, barH));
}
}
// ============================================================================
// 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<float>(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 = "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 * 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 = "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));
{
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<float>(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), "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 * 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