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)
3274 lines
155 KiB
C++
3274 lines
155 KiB
C++
// 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
|