Files
ObsidianDragon/src/ui/windows/balance_tab.cpp
DanS 3aee55b49c ObsidianDragon - DragonX ImGui Wallet
Full-node GUI wallet for DragonX cryptocurrency.
Built with Dear ImGui, SDL3, and OpenGL3/DX11.

Features:
- Send/receive shielded and transparent transactions
- Autoshield with merged transaction display
- Built-in CPU mining (xmrig)
- Peer management and network monitoring
- Wallet encryption with PIN lock
- QR code generation for receive addresses
- Transaction history with pagination
- Console for direct RPC commands
- Cross-platform (Linux, Windows)
2026-02-27 00:26:01 -06:00

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