refactor(audit): batch 9 — de-duplicate balance/peers/transactions UI cards
Three UI de-duplications (rendering changes; the test suite skips the GUI, so these need a manual screenshot check of the Balance/Peers/Transactions tabs). 1. Classic balance layout — the DEFAULT layout — hand-rolled its own ~550-line address-list + recent-tx renderers that had DIVERGED from the shared RenderSharedAddressList/RenderSharedRecentTx used by the other 9 layouts. Deleted the inline copies and called the shared renderers (as the minimal layout already does), so the default layout now gains set-label + add-to-portfolio context items, custom address icons, drag-to-reorder, keyboard nav, copy-flash, and fully TR()'d strings. Hero row and Classic sizing (tabs.balance.classic address-table-height=340) preserved; addrH is computed before the call exactly as before. This is a deliberate feature-parity behavior change for the default layout. 2. peers_tab info cards — extracted drawStatCell() (label + value/em-dash with the shared offset math) and one drawCardDivider() replacing two duplicate divider lambdas; plain cells drive off a per-card loop. Bespoke cells (Blocks "(X left)", copy-on-click Best Block, TLS check-icon) kept inline. 3. transactions_tab — one drawSummaryCard() replaces the three near-identical Received/Sent/Mined blocks (same glass panel, icon, hover outline, click-to-filter). Byte-equivalent. Full-node + Lite build clean (net -651 lines, no new warnings); ctest 1/1; hygiene clean. NEEDS SCREENSHOT VERIFICATION. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -218,17 +218,11 @@ static void RenderBalanceClassic(App* app)
|
|||||||
{
|
{
|
||||||
using namespace material;
|
using namespace material;
|
||||||
const auto& S = schema::UISchema::instance();
|
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");
|
const auto syncBar = S.drawElement("tabs.balance", "sync-bar");
|
||||||
|
|
||||||
// Read layout properties from schema
|
// Read layout properties from schema
|
||||||
const float kBalanceLerpSpeed = S.drawElement("tabs.balance", "balance-lerp-speed").sizeOr(8.0f);
|
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 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();
|
const auto& state = app->state();
|
||||||
|
|
||||||
@@ -241,17 +235,6 @@ static void RenderBalanceClassic(App* app)
|
|||||||
const float cGap = Layout::cardGap();
|
const float cGap = Layout::cardGap();
|
||||||
const float dp = Layout::dpiScale();
|
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
|
// Lerp displayed balances toward actual values
|
||||||
{
|
{
|
||||||
float dt = ImGui::GetIO().DeltaTime;
|
float dt = ImGui::GetIO().DeltaTime;
|
||||||
@@ -634,559 +617,19 @@ static void RenderBalanceClassic(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// Address list — DrawList-based rows (matches recent tx style)
|
// Address list + Recent transactions — shared renderers (feature-complete:
|
||||||
|
// set-label / add-to-portfolio context items, custom address icons, drag-to-reorder,
|
||||||
|
// keyboard nav, copy-flash, i18n). Classic keeps its own address-table-height override
|
||||||
|
// (see tabs.balance.classic) so the card sizing is unchanged.
|
||||||
// ================================================================
|
// ================================================================
|
||||||
{
|
{
|
||||||
// Header row: title only
|
float recentReserve = contentAvail.y * S.drawElement("tabs.balance", "recent-tx-reserve-ratio").sizeOr(0.18f);
|
||||||
Type().text(TypeStyle::H6, TR("your_addresses"));
|
|
||||||
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
// Static filter state (declared here, UI rendered below with ADDRESSES overline)
|
|
||||||
static char addr_search[128] = "";
|
|
||||||
static bool s_hideZeroBalances = true;
|
|
||||||
static bool s_showHidden = false;
|
|
||||||
|
|
||||||
// Build a merged + sorted list of all addresses
|
|
||||||
struct AddrRow {
|
|
||||||
const AddressInfo* info;
|
|
||||||
bool isZ;
|
|
||||||
bool hidden;
|
|
||||||
bool favorite;
|
|
||||||
bool mining;
|
|
||||||
};
|
|
||||||
std::vector<AddrRow> rows;
|
|
||||||
rows.reserve(state.z_addresses.size() + state.t_addresses.size());
|
|
||||||
// The search text is constant across the loop and containsIgnoreCase is now allocation-free,
|
|
||||||
// so build the filter view ONCE (was a per-address std::string + two tolower-copies/frame).
|
|
||||||
const std::string_view addrFilter(addr_search);
|
|
||||||
for (const auto& a : state.z_addresses) {
|
|
||||||
if (!util::containsIgnoreCase(a.address, addrFilter) &&
|
|
||||||
!util::containsIgnoreCase(a.label, addrFilter))
|
|
||||||
continue;
|
|
||||||
bool isHidden = app->isAddressHidden(a.address);
|
|
||||||
if (isHidden && !s_showHidden) continue;
|
|
||||||
bool isFav = app->isAddressFavorite(a.address);
|
|
||||||
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav)
|
|
||||||
continue;
|
|
||||||
rows.push_back({&a, true, isHidden, isFav, app->isMiningAddress(a.address)});
|
|
||||||
}
|
|
||||||
for (const auto& a : state.t_addresses) {
|
|
||||||
if (!util::containsIgnoreCase(a.address, addrFilter) &&
|
|
||||||
!util::containsIgnoreCase(a.label, addrFilter))
|
|
||||||
continue;
|
|
||||||
bool isHidden = app->isAddressHidden(a.address);
|
|
||||||
if (isHidden && !s_showHidden) continue;
|
|
||||||
bool isFav = app->isAddressFavorite(a.address);
|
|
||||||
if (s_hideZeroBalances && a.balance < 1e-9 && !isHidden && !isFav)
|
|
||||||
continue;
|
|
||||||
rows.push_back({&a, false, isHidden, isFav, app->isMiningAddress(a.address)});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sort: favorites first, then Z addresses, then by balance descending
|
|
||||||
static int s_sortCol = 3; // default sort by balance
|
|
||||||
static bool s_sortAsc = false;
|
|
||||||
std::sort(rows.begin(), rows.end(),
|
|
||||||
[](const AddrRow& a, const AddrRow& b) -> bool {
|
|
||||||
if (a.favorite != b.favorite) return a.favorite > b.favorite;
|
|
||||||
if (a.isZ != b.isZ) return a.isZ > b.isZ; // Z first
|
|
||||||
if (s_sortAsc)
|
|
||||||
return a.info->balance < b.info->balance;
|
|
||||||
else
|
|
||||||
return a.info->balance > b.info->balance;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Recent TX gets sizing priority — compute its reserve first,
|
|
||||||
// then the address list gets whatever remains.
|
|
||||||
float scaledTxRowH = std::max(S.drawElement("tabs.balance", "recent-tx-row-min-height").size, kRecentTxRowHeight * vs);
|
|
||||||
// Recent TX section: header + kRecentTxCount rows + status label + gaps
|
|
||||||
float recentTxReserve = S.drawElement("tabs.balance", "recent-tx-header-height").size * vs + kRecentTxCount * scaledTxRowH + Layout::spacingXl();
|
|
||||||
|
|
||||||
// Search + create buttons row
|
|
||||||
float avail = ImGui::GetContentRegionAvail().x;
|
|
||||||
float schemaMaxW = (searchIn.maxWidth >= 0) ? searchIn.maxWidth : 250.0f;
|
|
||||||
float schemaRatio = (searchIn.widthRatio >= 0) ? searchIn.widthRatio : 0.30f;
|
|
||||||
float searchW = std::min(schemaMaxW * hs, avail * schemaRatio);
|
|
||||||
ImGui::SetNextItemWidth(searchW);
|
|
||||||
ImGui::InputTextWithHint("##AddrSearch", "Filter...", addr_search, sizeof(addr_search));
|
|
||||||
|
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
|
||||||
ImGui::Checkbox(TrId("hide_zero_balances", "hide0").c_str(), &s_hideZeroBalances);
|
|
||||||
{
|
|
||||||
int hc = app->getHiddenAddressCount();
|
|
||||||
if (hc > 0) {
|
|
||||||
ImGui::SameLine(0, Layout::spacingLg());
|
|
||||||
char hlbl[64];
|
|
||||||
snprintf(hlbl, sizeof(hlbl), TR("show_hidden"), hc);
|
|
||||||
ImGui::Checkbox(hlbl, &s_showHidden);
|
|
||||||
} else {
|
|
||||||
s_showHidden = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
float buttonWidth = (addrBtn.width > 0) ? addrBtn.width : 140.0f;
|
|
||||||
float spacing = (addrBtn.gap > 0) ? addrBtn.gap : 8.0f;
|
|
||||||
float totalButtonsWidth = buttonWidth * 2 + spacing;
|
|
||||||
ImGui::SameLine(std::max(kMinButtonsPosition,
|
|
||||||
ImGui::GetWindowWidth() - totalButtonsWidth - kButtonRowRightMargin));
|
|
||||||
|
|
||||||
bool addrSyncing = state.sync.syncing && !state.sync.isSynced();
|
|
||||||
ImGui::BeginDisabled(addrSyncing);
|
|
||||||
if (s_generating_z_address) {
|
|
||||||
char genLabel[64];
|
|
||||||
snprintf(genLabel, sizeof(genLabel), "%s%s##bal_z", TR("generating"), material::LoadingDots());
|
|
||||||
TactileButton(genLabel, ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font));
|
|
||||||
} else if (TactileButton(TR("new_z_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
|
||||||
s_generating_z_address = true;
|
|
||||||
app->createNewZAddress([](const std::string& addr) {
|
|
||||||
s_generating_z_address = false;
|
|
||||||
DEBUG_LOGF("Created new z-address: %s\n", addr.c_str());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (TactileButton(TR("new_t_address"), ImVec2(buttonWidth, 0), S.resolveFont(addrBtn.font.empty() ? "button" : addrBtn.font))) {
|
|
||||||
app->createNewTAddress([](const std::string& addr) {
|
|
||||||
DEBUG_LOGF("Created new t-address: %s\n", addr.c_str());
|
|
||||||
});
|
|
||||||
}
|
|
||||||
ImGui::EndDisabled();
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
|
||||||
|
|
||||||
float availWidth = ImGui::GetContentRegionAvail().x;
|
|
||||||
// Address list gets whatever height remains after recent TX reserve
|
|
||||||
float classicAddrH = S.drawElement("tabs.balance.classic", "address-table-height").size;
|
float classicAddrH = S.drawElement("tabs.balance.classic", "address-table-height").size;
|
||||||
float addrListH;
|
float addrH = (classicAddrH >= 0.0f) ? classicAddrH * dp
|
||||||
if (classicAddrH >= 0.0f) {
|
: ImGui::GetContentRegionAvail().y - recentReserve
|
||||||
addrListH = classicAddrH * dp; // scaled for DPI + font scale
|
- Layout::spacingXl() - Type().h6()->LegacySize - Layout::spacingMd();
|
||||||
} else {
|
RenderSharedAddressList(app, addrH, contentAvail.x, glassRound, hs, vs);
|
||||||
addrListH = ImGui::GetContentRegionAvail().y - recentTxReserve;
|
RenderSharedRecentTx(app, recentReserve, contentAvail.x, hs, vs);
|
||||||
}
|
|
||||||
// Keep address list at a reasonable minimum; if too tight,
|
|
||||||
// shrink recent-tx reserve instead so both remain visible.
|
|
||||||
float addrListMin = S.drawElement("tabs.balance", "addr-list-min-height").sizeOr(40.0f);
|
|
||||||
if (addrListH < addrListMin) {
|
|
||||||
addrListH = addrListMin;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Glass panel wrapping the list area (matching tx list)
|
|
||||||
ImDrawList* dlPanel = ImGui::GetWindowDrawList();
|
|
||||||
ImVec2 listPanelMin = ImGui::GetCursorScreenPos();
|
|
||||||
ImVec2 listPanelMax(listPanelMin.x + availWidth, listPanelMin.y + addrListH);
|
|
||||||
GlassPanelSpec addrGlassSpec;
|
|
||||||
addrGlassSpec.rounding = glassRound;
|
|
||||||
DrawGlassPanel(dlPanel, listPanelMin, listPanelMax, addrGlassSpec);
|
|
||||||
|
|
||||||
// Scroll-edge mask state
|
|
||||||
float addrScrollY = 0.0f, addrScrollMaxY = 0.0f;
|
|
||||||
int addrParentVtx = dlPanel->VtxBuffer.Size;
|
|
||||||
|
|
||||||
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingLg(), Layout::spacingSm()));
|
|
||||||
ImGui::BeginChild("AddressList", ImVec2(availWidth, addrListH), false,
|
|
||||||
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollWithMouse);
|
|
||||||
ApplySmoothScroll();
|
|
||||||
ImDrawList* addrChildDL = ImGui::GetWindowDrawList();
|
|
||||||
int addrChildVtx = addrChildDL->VtxBuffer.Size;
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
||||||
|
|
||||||
if (!app->isConnected()) {
|
|
||||||
ImGui::Dummy(ImVec2(0, 16 * dp));
|
|
||||||
float cw = ImGui::GetContentRegionAvail().x;
|
|
||||||
ImVec2 ts = ImGui::CalcTextSize(TR("not_connected"));
|
|
||||||
ImGui::SetCursorPosX((cw - ts.x) * 0.5f);
|
|
||||||
ImGui::TextDisabled("%s", TR("not_connected"));
|
|
||||||
} else if (rows.empty()) {
|
|
||||||
// Empty state
|
|
||||||
float cw = ImGui::GetContentRegionAvail().x;
|
|
||||||
float ch = ImGui::GetContentRegionAvail().y;
|
|
||||||
if (ch < 60) ch = 60;
|
|
||||||
|
|
||||||
if (addr_search[0]) {
|
|
||||||
ImVec2 textSz = ImGui::CalcTextSize("No matching addresses");
|
|
||||||
ImGui::SetCursorPosX((cw - textSz.x) * 0.5f);
|
|
||||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
|
|
||||||
ImGui::TextDisabled("No matching addresses");
|
|
||||||
} else {
|
|
||||||
const char* msg = "No addresses yet";
|
|
||||||
ImVec2 msgSz = ImGui::CalcTextSize(msg);
|
|
||||||
ImGui::SetCursorPosX((cw - msgSz.x) * 0.5f);
|
|
||||||
ImGui::SetCursorPosY(ImGui::GetCursorPosY() + ch * 0.25f);
|
|
||||||
ImGui::TextDisabled("%s", msg);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// DrawList-based address rows (matching transaction list style)
|
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
||||||
ImFont* capFont = Type().caption();
|
|
||||||
ImFont* body2 = Type().body2();
|
|
||||||
float rowH = body2->LegacySize + capFont->LegacySize + Layout::spacingLg() + Layout::spacingMd();
|
|
||||||
static int selected_row = -1;
|
|
||||||
addrScrollY = ImGui::GetScrollY();
|
|
||||||
addrScrollMaxY = ImGui::GetScrollMaxY();
|
|
||||||
|
|
||||||
ImU32 greenCol = S.resolveColor("var(--accent-shielded)", Success());
|
|
||||||
ImU32 goldCol = S.resolveColor("var(--accent-transparent)", Warning());
|
|
||||||
float rowPadLeft = Layout::cardInnerPadding();
|
|
||||||
float rowPadRight = Layout::cardInnerPadding();
|
|
||||||
float rowIconSz = std::max(S.drawElement("tabs.balance", "address-icon-min-size").size, S.drawElement("tabs.balance", "address-icon-size").size * hs);
|
|
||||||
float innerW = ImGui::GetContentRegionAvail().x;
|
|
||||||
|
|
||||||
for (int row_idx = 0; row_idx < (int)rows.size(); row_idx++) {
|
|
||||||
const auto& row = rows[row_idx];
|
|
||||||
const auto& addr = *row.info;
|
|
||||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
|
||||||
ImVec2 rowEnd(rowPos.x + innerW, rowPos.y + rowH);
|
|
||||||
|
|
||||||
ImU32 typeCol = row.isZ ? greenCol : goldCol;
|
|
||||||
if (row.hidden) typeCol = OnSurfaceDisabled();
|
|
||||||
|
|
||||||
// Golden border for favorites
|
|
||||||
if (row.favorite) {
|
|
||||||
ImU32 favBorder = IM_COL32(255, 200, 50, 120);
|
|
||||||
dl->AddRect(rowPos, rowEnd, favBorder, 4.0f * dp, 0, 1.5f * dp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selected indicator (left accent bar)
|
|
||||||
if (selected_row == row_idx) {
|
|
||||||
ImDrawFlags accentFlags = 0;
|
|
||||||
float accentRound = 2.0f * dp;
|
|
||||||
if (row_idx == 0) {
|
|
||||||
accentFlags = ImDrawFlags_RoundCornersTopLeft;
|
|
||||||
accentRound = glassRound;
|
|
||||||
}
|
|
||||||
if (row_idx == (int)rows.size() - 1) {
|
|
||||||
accentFlags |= ImDrawFlags_RoundCornersBottomLeft;
|
|
||||||
accentRound = glassRound;
|
|
||||||
}
|
|
||||||
dl->AddRectFilled(rowPos, ImVec2(rowPos.x + 3 * dp, rowEnd.y), typeCol, accentRound, accentFlags);
|
|
||||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 20), 4.0f * dp);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hover glow
|
|
||||||
bool hovered = material::IsRectHovered(rowPos, rowEnd);
|
|
||||||
if (hovered && selected_row != row_idx) {
|
|
||||||
dl->AddRectFilled(rowPos, rowEnd, IM_COL32(255, 255, 255, 15), 4.0f * dp);
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
}
|
|
||||||
|
|
||||||
float cx = rowPos.x + rowPadLeft;
|
|
||||||
float cy = rowPos.y + Layout::spacingMd();
|
|
||||||
|
|
||||||
// --- Button zone (right edge): [eye] [star] ---
|
|
||||||
float btnH = rowH - Layout::spacingSm() * 2.0f;
|
|
||||||
float btnW = btnH;
|
|
||||||
float btnGap = Layout::spacingSm();
|
|
||||||
float btnY = rowPos.y + (rowH - btnH) * 0.5f;
|
|
||||||
float rightEdge = rowPos.x + innerW;
|
|
||||||
float starX = rightEdge - btnW - rowPadRight;
|
|
||||||
float eyeX = starX - btnGap - btnW;
|
|
||||||
float btnRound = 6.0f * dp;
|
|
||||||
bool btnClicked = false;
|
|
||||||
|
|
||||||
// Star button (always shown, rightmost)
|
|
||||||
{
|
|
||||||
ImVec2 bMin(starX, btnY), bMax(starX + btnW, btnY + btnH);
|
|
||||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
|
||||||
ImU32 starFill = row.favorite ? IM_COL32(255, 200, 50, 40) : IM_COL32(255, 255, 255, bHov ? 25 : 12);
|
|
||||||
ImU32 starBorder = row.favorite ? IM_COL32(255, 200, 50, 100) : IM_COL32(255, 255, 255, bHov ? 50 : 25);
|
|
||||||
dl->AddRectFilled(bMin, bMax, starFill, btnRound);
|
|
||||||
dl->AddRect(bMin, bMax, starBorder, btnRound, 0, 1.0f * dp);
|
|
||||||
ImFont* iconFont = material::Type().iconSmall();
|
|
||||||
const char* starIcon = row.favorite ? ICON_MD_STAR : ICON_MD_STAR_BORDER;
|
|
||||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, starIcon);
|
|
||||||
ImU32 starCol = row.favorite ? IM_COL32(255, 200, 50, 255) : (bHov ? OnSurface() : OnSurfaceDisabled());
|
|
||||||
dl->AddText(iconFont, iconFont->LegacySize,
|
|
||||||
ImVec2(starX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), starCol, starIcon);
|
|
||||||
if (bHov && ImGui::IsMouseClicked(0)) {
|
|
||||||
if (row.favorite) app->unfavoriteAddress(addr.address);
|
|
||||||
else app->favoriteAddress(addr.address);
|
|
||||||
btnClicked = true;
|
|
||||||
}
|
|
||||||
if (bHov) material::Tooltip("%s", row.favorite ? TR("remove_favorite") : TR("favorite_address"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Eye button (zero balance or hidden)
|
|
||||||
if (addr.balance < 1e-9 || row.hidden) {
|
|
||||||
ImVec2 bMin(eyeX, btnY), bMax(eyeX + btnW, btnY + btnH);
|
|
||||||
bool bHov = ImGui::IsMouseHoveringRect(bMin, bMax);
|
|
||||||
ImU32 eyeFill = IM_COL32(255, 255, 255, bHov ? 25 : 12);
|
|
||||||
ImU32 eyeBorder = IM_COL32(255, 255, 255, bHov ? 50 : 25);
|
|
||||||
dl->AddRectFilled(bMin, bMax, eyeFill, btnRound);
|
|
||||||
dl->AddRect(bMin, bMax, eyeBorder, btnRound, 0, 1.0f * dp);
|
|
||||||
ImFont* iconFont = material::Type().iconSmall();
|
|
||||||
const char* hideIcon = row.hidden ? ICON_MD_VISIBILITY : ICON_MD_VISIBILITY_OFF;
|
|
||||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, hideIcon);
|
|
||||||
ImU32 iconCol = bHov ? OnSurface() : OnSurfaceDisabled();
|
|
||||||
dl->AddText(iconFont, iconFont->LegacySize,
|
|
||||||
ImVec2(eyeX + (btnW - iSz.x) * 0.5f, btnY + (btnH - iSz.y) * 0.5f), iconCol, hideIcon);
|
|
||||||
if (bHov && ImGui::IsMouseClicked(0)) {
|
|
||||||
if (row.hidden) app->unhideAddress(addr.address);
|
|
||||||
else app->hideAddress(addr.address);
|
|
||||||
btnClicked = true;
|
|
||||||
}
|
|
||||||
if (bHov) material::Tooltip("%s", row.hidden ? TR("restore_address") : TR("hide_address"));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Content zone ends before buttons
|
|
||||||
float contentRight = eyeX - Layout::spacingSm();
|
|
||||||
|
|
||||||
// Type icon (shield for Z, circle for T)
|
|
||||||
float iconCx = cx + rowIconSz;
|
|
||||||
float iconCy = cy + body2->LegacySize * 0.5f;
|
|
||||||
if (row.isZ) {
|
|
||||||
ImFont* iconFont = material::Type().iconSmall();
|
|
||||||
const char* shieldIcon = ICON_MD_SHIELD;
|
|
||||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, shieldIcon);
|
|
||||||
dl->AddText(iconFont, iconFont->LegacySize,
|
|
||||||
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, shieldIcon);
|
|
||||||
} else {
|
|
||||||
ImFont* iconFont = material::Type().iconSmall();
|
|
||||||
const char* circIcon = ICON_MD_CIRCLE;
|
|
||||||
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, circIcon);
|
|
||||||
dl->AddText(iconFont, iconFont->LegacySize,
|
|
||||||
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, circIcon);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type label (first line, next to icon)
|
|
||||||
float labelX = cx + rowIconSz * 2.0f + Layout::spacingSm();
|
|
||||||
const char* typeLabel = row.isZ ? "Shielded" : "Transparent";
|
|
||||||
const char* hiddenTag = row.hidden ? " (hidden)" : "";
|
|
||||||
const char* viewOnlyTag = (!addr.has_spending_key) ? " (view-only)" : "";
|
|
||||||
const char* miningTag = row.mining ? TR("mining_tag") : "";
|
|
||||||
char typeBuf[64];
|
|
||||||
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s%s", typeLabel, hiddenTag, viewOnlyTag, miningTag);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
|
|
||||||
|
|
||||||
// Label (if present, next to type)
|
|
||||||
if (!addr.label.empty()) {
|
|
||||||
float typeLabelW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, typeBuf).x;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(labelX + typeLabelW + Layout::spacingLg(), cy),
|
|
||||||
OnSurfaceMedium(), addr.label.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Address (second line) — show full if it fits, otherwise truncate
|
|
||||||
float addrAvailW = contentRight - labelX;
|
|
||||||
ImVec2 fullAddrSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0.0f, addr.address.c_str());
|
|
||||||
std::string display_addr;
|
|
||||||
if (fullAddrSz.x <= addrAvailW) {
|
|
||||||
display_addr = addr.address;
|
|
||||||
} else {
|
|
||||||
int addrTruncLen = (addrTable.columns.count("address") && addrTable.columns.at("address").truncate > 0)
|
|
||||||
? addrTable.columns.at("address").truncate : 32;
|
|
||||||
display_addr = truncateAddress(addr.address, addrTruncLen);
|
|
||||||
}
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(labelX, cy + body2->LegacySize + Layout::spacingXs()),
|
|
||||||
OnSurfaceMedium(), display_addr.c_str());
|
|
||||||
|
|
||||||
// Balance (right-aligned within content zone)
|
|
||||||
char balBuf[32];
|
|
||||||
snprintf(balBuf, sizeof(balBuf), "%.8f", addr.balance);
|
|
||||||
ImVec2 balSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, balBuf);
|
|
||||||
float balX = contentRight - balSz.x;
|
|
||||||
ImU32 balCol = addr.balance > 0.0
|
|
||||||
? (row.isZ ? greenCol : OnSurface())
|
|
||||||
: OnSurfaceDisabled();
|
|
||||||
if (row.hidden) balCol = OnSurfaceDisabled();
|
|
||||||
DrawTextShadow(dl, body2, body2->LegacySize, ImVec2(balX, cy), balCol, balBuf);
|
|
||||||
|
|
||||||
// USD equivalent (right-aligned, second line)
|
|
||||||
double priceUsd = state.market.price_usd;
|
|
||||||
if (priceUsd > 0.0 && addr.balance > 0.0) {
|
|
||||||
char usdBuf[32];
|
|
||||||
double usdVal = addr.balance * priceUsd;
|
|
||||||
if (usdVal >= 1.0)
|
|
||||||
snprintf(usdBuf, sizeof(usdBuf), "$%.2f", usdVal);
|
|
||||||
else if (usdVal >= 0.01)
|
|
||||||
snprintf(usdBuf, sizeof(usdBuf), "$%.4f", usdVal);
|
|
||||||
else
|
|
||||||
snprintf(usdBuf, sizeof(usdBuf), "$%.6f", usdVal);
|
|
||||||
ImVec2 usdSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usdBuf);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(contentRight - usdSz.x,
|
|
||||||
cy + body2->LegacySize + Layout::spacingXs()),
|
|
||||||
OnSurfaceDisabled(), usdBuf);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Click to copy + select
|
|
||||||
if (hovered && ImGui::IsMouseClicked(0) && !btnClicked) {
|
|
||||||
ImGui::SetClipboardText(addr.address.c_str());
|
|
||||||
selected_row = row_idx;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Invisible button for context menu + tooltip
|
|
||||||
ImGui::PushID(row_idx);
|
|
||||||
ImGui::InvisibleButton("##addr", ImVec2(innerW, rowH));
|
|
||||||
|
|
||||||
// Tooltip with full address
|
|
||||||
if (ImGui::IsItemHovered() && !btnClicked) {
|
|
||||||
material::Tooltip("%s", addr.address.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
// Right-click context menu
|
|
||||||
const auto& acrTheme = GetCurrentAcrylicTheme();
|
|
||||||
if (effects::ImGuiAcrylic::BeginAcrylicContextItem("AddressContext", 0, acrTheme.menu)) {
|
|
||||||
if (ImGui::MenuItem(TR("copy_address"))) {
|
|
||||||
ImGui::SetClipboardText(addr.address.c_str());
|
|
||||||
}
|
|
||||||
if (ImGui::MenuItem(TR("send_from_this_address"))) {
|
|
||||||
SetSendFromAddress(addr.address);
|
|
||||||
app->setCurrentPage(NavPage::Send);
|
|
||||||
}
|
|
||||||
ImGui::Separator();
|
|
||||||
if (ImGui::MenuItem(row.mining ? TR("unmark_mining_address") : TR("mark_mining_address"))) {
|
|
||||||
app->setMiningAddress(addr.address, !row.mining);
|
|
||||||
}
|
|
||||||
if (ImGui::MenuItem(TR("export_private_key"))) {
|
|
||||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Private);
|
|
||||||
}
|
|
||||||
if (row.isZ) {
|
|
||||||
if (ImGui::MenuItem(TR("export_viewing_key"))) {
|
|
||||||
KeyExportDialog::show(addr.address, KeyExportDialog::KeyType::Viewing);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (ImGui::MenuItem(TR("show_qr_code"))) {
|
|
||||||
QRPopupDialog::show(addr.address,
|
|
||||||
row.isZ ? "Z-Address" : "T-Address");
|
|
||||||
}
|
|
||||||
ImGui::Separator();
|
|
||||||
if (row.hidden) {
|
|
||||||
if (ImGui::MenuItem(TR("restore_address")))
|
|
||||||
app->unhideAddress(addr.address);
|
|
||||||
} else if (addr.balance < 1e-9) {
|
|
||||||
if (ImGui::MenuItem(TR("hide_address")))
|
|
||||||
app->hideAddress(addr.address);
|
|
||||||
}
|
|
||||||
if (row.favorite) {
|
|
||||||
if (ImGui::MenuItem(TR("remove_favorite")))
|
|
||||||
app->unfavoriteAddress(addr.address);
|
|
||||||
} else {
|
|
||||||
if (ImGui::MenuItem(TR("favorite_address")))
|
|
||||||
app->favoriteAddress(addr.address);
|
|
||||||
}
|
|
||||||
effects::ImGuiAcrylic::EndAcrylicPopup();
|
|
||||||
}
|
|
||||||
ImGui::PopID();
|
|
||||||
|
|
||||||
// Subtle divider between rows (matching tx list)
|
|
||||||
if (row_idx < (int)rows.size() - 1 && selected_row != row_idx) {
|
|
||||||
ImVec2 divStart = ImGui::GetCursorScreenPos();
|
|
||||||
dl->AddLine(
|
|
||||||
ImVec2(divStart.x + rowPadLeft + rowIconSz * 2.0f, divStart.y),
|
|
||||||
ImVec2(divStart.x + innerW - Layout::spacingLg(), divStart.y),
|
|
||||||
IM_COL32(255, 255, 255, 15));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
||||||
ImGui::EndChild();
|
|
||||||
ImGui::PopStyleVar(); // WindowPadding for address list
|
|
||||||
|
|
||||||
// CSS-style clipping mask (same as history list)
|
|
||||||
{
|
|
||||||
float fadeZone = std::min(
|
|
||||||
(Type().body2()->LegacySize + Type().caption()->LegacySize + Layout::spacingLg() + Layout::spacingMd()) * 1.2f,
|
|
||||||
addrListH * 0.18f);
|
|
||||||
ApplyScrollEdgeMask(dlPanel, addrParentVtx, addrChildDL, addrChildVtx,
|
|
||||||
listPanelMin.y, listPanelMax.y, fadeZone, addrScrollY, addrScrollMaxY);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status line (matching tx list)
|
|
||||||
{
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
||||||
char countBuf[128];
|
|
||||||
int totalAddrs = (int)(state.z_addresses.size() + state.t_addresses.size());
|
|
||||||
snprintf(countBuf, sizeof(countBuf), "Showing %d of %d addresses",
|
|
||||||
(int)rows.size(), totalAddrs);
|
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), countBuf);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
|
||||||
// Recent Transactions
|
|
||||||
// ================================================================
|
|
||||||
{
|
|
||||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
|
||||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), "RECENT TRANSACTIONS");
|
|
||||||
ImGui::SameLine();
|
|
||||||
if (TactileSmallButton("View All", S.resolveFont(actionBtn.font.empty() ? "button" : actionBtn.font))) {
|
|
||||||
app->setCurrentPage(NavPage::History);
|
|
||||||
}
|
|
||||||
ImGui::Spacing();
|
|
||||||
|
|
||||||
ImFont* capFont = Type().caption();
|
|
||||||
float rowH = std::max(18.0f * dp, kRecentTxRowHeight * vs);
|
|
||||||
float listH = std::max(rowH, ImGui::GetContentRegionAvail().y);
|
|
||||||
ImGui::BeginChild("##BalanceClassicRecentRows", ImVec2(0, listH), false,
|
|
||||||
ImGuiWindowFlags_NoBackground);
|
|
||||||
|
|
||||||
const auto& txs = state.transactions;
|
|
||||||
int count = (int)txs.size();
|
|
||||||
if (count == 0) {
|
|
||||||
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(),
|
|
||||||
"No transactions yet");
|
|
||||||
} else {
|
|
||||||
ImDrawList* dl = ImGui::GetWindowDrawList();
|
|
||||||
float iconSz = std::max(S.drawElement("tabs.balance", "recent-tx-icon-min-size").size, S.drawElement("tabs.balance", "recent-tx-icon-size").size * hs);
|
|
||||||
|
|
||||||
for (int i = 0; i < count; i++) {
|
|
||||||
const auto& tx = txs[i];
|
|
||||||
ImVec2 rowPos = ImGui::GetCursorScreenPos();
|
|
||||||
float rowY = rowPos.y + rowH * 0.5f;
|
|
||||||
|
|
||||||
auto display = buildRecentTxDisplay(tx, (int)S.drawElement("tabs.balance", "recent-tx-addr-trunc").sizeOr(20.0f));
|
|
||||||
DrawTxIcon(dl, tx.type, rowPos.x + Layout::spacingMd(), rowY, iconSz, recentTxIconColor(tx.type));
|
|
||||||
|
|
||||||
// Type label
|
|
||||||
float tx_x = rowPos.x + Layout::spacingMd() + iconSz * 2.0f + Layout::spacingSm();
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(tx_x, rowPos.y + 2 * dp),
|
|
||||||
OnSurfaceMedium(), display.typeText.c_str());
|
|
||||||
|
|
||||||
// Address (truncated)
|
|
||||||
float addrX = tx_x + S.drawElement("tabs.balance", "recent-tx-addr-offset").sizeOr(65.0f);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(addrX, rowPos.y + 2 * dp),
|
|
||||||
OnSurfaceDisabled(), display.addressText.c_str());
|
|
||||||
|
|
||||||
// Amount (right-aligned area)
|
|
||||||
ImVec2 amtSz = capFont->CalcTextSizeA(
|
|
||||||
capFont->LegacySize, 10000, 0, display.amountText.c_str());
|
|
||||||
float rightEdge = rowPos.x + ImGui::GetContentRegionAvail().x;
|
|
||||||
float amtX = rightEdge - amtSz.x - std::max(S.drawElement("tabs.balance", "amount-right-min-margin").size, S.drawElement("tabs.balance", "amount-right-margin").size * hs);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(amtX, rowPos.y + 2 * dp),
|
|
||||||
recentTxAmountColor(tx.type),
|
|
||||||
display.amountText.c_str());
|
|
||||||
|
|
||||||
// Time ago
|
|
||||||
ImVec2 agoSz = capFont->CalcTextSizeA(
|
|
||||||
capFont->LegacySize, 10000, 0, display.timeText.c_str());
|
|
||||||
dl->AddText(capFont, capFont->LegacySize,
|
|
||||||
ImVec2(rightEdge - agoSz.x - S.drawElement("tabs.balance", "recent-tx-time-margin").sizeOr(4.0f), rowPos.y + 2 * dp),
|
|
||||||
OnSurfaceDisabled(), display.timeText.c_str());
|
|
||||||
|
|
||||||
// Clickable row — hover highlight + navigate to History
|
|
||||||
float rowW = ImGui::GetContentRegionAvail().x;
|
|
||||||
ImVec2 rowEnd(rowPos.x + rowW, rowPos.y + rowH);
|
|
||||||
if (material::IsRectHovered(rowPos, rowEnd)) {
|
|
||||||
dl->AddRectFilled(rowPos, rowEnd,
|
|
||||||
IM_COL32(255, 255, 255, 15), S.drawElement("tabs.balance", "row-hover-rounding").sizeOr(4.0f));
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
// Show the full, untruncated address — two z-addresses can truncate to the
|
|
||||||
// same first/last window, so the truncated text alone can't disambiguate.
|
|
||||||
if (!tx.address.empty())
|
|
||||||
material::Tooltip("%s", tx.address.c_str());
|
|
||||||
if (ImGui::IsMouseClicked(0))
|
|
||||||
app->setCurrentPage(NavPage::History);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImGui::Dummy(ImVec2(0, rowH));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImGui::EndChild();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -64,6 +64,30 @@ static bool IsSeedNode(const std::string& addr) {
|
|||||||
return seeds.count(ExtractIP(addr)) > 0;
|
return seeds.count(ExtractIP(addr)) > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw one label/value stat cell in an info card. The caption label sits on the first line
|
||||||
|
// (OnSurfaceMedium); the value sits below it (valueFont) — or an em-dash "—" in the disabled
|
||||||
|
// color when the value is null/empty. Matches the hand-positioned blocks it replaces exactly:
|
||||||
|
// value baseline is ry + capFont->LegacySize + spacingXs().
|
||||||
|
static void drawStatCell(ImDrawList* dl, float cx, float ry, ImFont* capFont, ImFont* valueFont,
|
||||||
|
const char* label, const char* valueText, ImU32 valueCol)
|
||||||
|
{
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), label);
|
||||||
|
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||||
|
if (valueText && valueText[0]) {
|
||||||
|
dl->AddText(valueFont, valueFont->LegacySize, ImVec2(cx, valY), valueCol, valueText);
|
||||||
|
} else {
|
||||||
|
dl->AddText(valueFont, valueFont->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw a subtle horizontal divider between info-card rows, inset to the card's rounded corners.
|
||||||
|
static void drawCardDivider(ImDrawList* dl, float leftX, float rightX, float y, float rounding, float dp)
|
||||||
|
{
|
||||||
|
dl->AddLine(ImVec2(leftX + rounding * 0.5f, y),
|
||||||
|
ImVec2(rightX - rounding * 0.5f, y),
|
||||||
|
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
||||||
|
}
|
||||||
|
|
||||||
void RenderPeersTab(App* app)
|
void RenderPeersTab(App* app)
|
||||||
{
|
{
|
||||||
auto& S = schema::UI();
|
auto& S = schema::UI();
|
||||||
@@ -153,20 +177,22 @@ void RenderPeersTab(App* app)
|
|||||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_blockchain"));
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_blockchain"));
|
||||||
|
|
||||||
float colW = (cardW - pad * 2) / 2.0f;
|
float colW = (cardW - pad * 2) / 2.0f;
|
||||||
|
float leftX = cardMin.x + pad;
|
||||||
|
float rightX = cardMin.x + pad + colW;
|
||||||
float ry = cardMin.y + pad * 0.5f + headerH;
|
float ry = cardMin.y + pad * 0.5f + headerH;
|
||||||
|
|
||||||
// Helper to draw a subtle horizontal divider
|
// Advance ry past a row and draw a divider before the next one. Keeps the row
|
||||||
auto drawDivider = [&](float y) {
|
// stepping + divider in one place (was a per-row copy + a duplicate divider lambda).
|
||||||
float rnd = glassSpec.rounding;
|
auto stepRow = [&]() {
|
||||||
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, y),
|
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||||
ImVec2(cardMax.x - rnd * 0.5f, y),
|
drawCardDivider(dl, cardMin.x, cardMax.x, ry, glassSpec.rounding, dp);
|
||||||
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||||
};
|
};
|
||||||
|
|
||||||
// Row 1: Blocks | Longest Chain
|
// Row 1: Blocks | Longest Chain
|
||||||
{
|
{
|
||||||
// Blocks
|
// Blocks — bespoke: block number + "(X left)" in a second color
|
||||||
float cx = cardMin.x + pad;
|
float cx = leftX;
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_blocks"));
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_blocks"));
|
||||||
int blocks = state.sync.blocks;
|
int blocks = state.sync.blocks;
|
||||||
if (blocks > 0) {
|
if (blocks > 0) {
|
||||||
@@ -174,7 +200,6 @@ void RenderPeersTab(App* app)
|
|||||||
int blocksLeft = chainTip - blocks;
|
int blocksLeft = chainTip - blocks;
|
||||||
if (blocksLeft < 0) blocksLeft = 0;
|
if (blocksLeft < 0) blocksLeft = 0;
|
||||||
if (blocksLeft > 0) {
|
if (blocksLeft > 0) {
|
||||||
snprintf(buf, sizeof(buf), "%d (%d left)", blocks, blocksLeft);
|
|
||||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||||
// Draw block number in normal color
|
// Draw block number in normal color
|
||||||
char blockStr[32];
|
char blockStr[32];
|
||||||
@@ -196,136 +221,85 @@ void RenderPeersTab(App* app)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Longest Chain
|
// Longest Chain
|
||||||
cx = cardMin.x + pad + colW;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_longest_chain"));
|
|
||||||
if (state.longestchain > 0) {
|
if (state.longestchain > 0) {
|
||||||
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
||||||
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
||||||
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
|
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), chainCol, buf);
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_longest_chain"), buf, chainCol);
|
||||||
} else {
|
} else {
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurfaceDisabled(), "\xE2\x80\x94");
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_longest_chain"), nullptr, OnSurface());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
stepRow();
|
||||||
drawDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 2: Hashrate | Difficulty
|
// Row 2: Hashrate | Difficulty
|
||||||
{
|
{
|
||||||
// Hashrate
|
std::string hr = mining.networkHashrate > 0 ? FormatHashrate(mining.networkHashrate) : std::string();
|
||||||
float cx = cardMin.x + pad;
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_hashrate"),
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_hashrate"));
|
hr.empty() ? nullptr : hr.c_str(), Success());
|
||||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
||||||
if (mining.networkHashrate > 0) {
|
|
||||||
std::string hr = FormatHashrate(mining.networkHashrate);
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), Success(), hr.c_str());
|
|
||||||
} else {
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Difficulty
|
if (mining.difficulty > 0) snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
|
||||||
cx = cardMin.x + pad + colW;
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("difficulty"),
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("difficulty"));
|
mining.difficulty > 0 ? buf : nullptr, OnSurface());
|
||||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
||||||
if (mining.difficulty > 0) {
|
|
||||||
snprintf(buf, sizeof(buf), "%.4f", mining.difficulty);
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
|
|
||||||
} else {
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
stepRow();
|
||||||
drawDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 3: Notarized | Protocol
|
// Row 3: Notarized | Protocol
|
||||||
{
|
{
|
||||||
// Notarized
|
if (state.notarized > 0) snprintf(buf, sizeof(buf), "%d", state.notarized);
|
||||||
float cx = cardMin.x + pad;
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_notarized"),
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_notarized"));
|
state.notarized > 0 ? buf : nullptr, OnSurface());
|
||||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
||||||
if (state.notarized > 0) {
|
|
||||||
snprintf(buf, sizeof(buf), "%d", state.notarized);
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
|
|
||||||
} else {
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Protocol
|
if (state.protocol_version > 0) snprintf(buf, sizeof(buf), "%d", state.protocol_version);
|
||||||
cx = cardMin.x + pad + colW;
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_protocol"),
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_protocol"));
|
state.protocol_version > 0 ? buf : nullptr, OnSurface());
|
||||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
||||||
if (state.protocol_version > 0) {
|
|
||||||
snprintf(buf, sizeof(buf), "%d", state.protocol_version);
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
|
|
||||||
} else {
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
stepRow();
|
||||||
drawDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 4: Version | Memory
|
// Row 4: Version | Memory
|
||||||
{
|
{
|
||||||
// Version
|
|
||||||
float cx = cardMin.x + pad;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_version"));
|
|
||||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
||||||
if (state.daemon_version > 0) {
|
if (state.daemon_version > 0) {
|
||||||
int major = state.daemon_version / 1000000;
|
int major = state.daemon_version / 1000000;
|
||||||
int minor = (state.daemon_version / 10000) % 100;
|
int minor = (state.daemon_version / 10000) % 100;
|
||||||
int patch = (state.daemon_version / 100) % 100;
|
int patch = (state.daemon_version / 100) % 100;
|
||||||
snprintf(buf, sizeof(buf), "%d.%d.%d", major, minor, patch);
|
snprintf(buf, sizeof(buf), "%d.%d.%d", major, minor, patch);
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
|
|
||||||
} else {
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
|
||||||
}
|
}
|
||||||
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_version"),
|
||||||
|
state.daemon_version > 0 ? buf : nullptr, OnSurface());
|
||||||
|
|
||||||
// Memory
|
|
||||||
cx = cardMin.x + pad + colW;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_memory"));
|
|
||||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
||||||
double memMb = state.mining.daemon_memory_mb;
|
double memMb = state.mining.daemon_memory_mb;
|
||||||
if (memMb > 0) {
|
if (memMb > 0) {
|
||||||
if (memMb >= 1024.0)
|
if (memMb >= 1024.0)
|
||||||
snprintf(buf, sizeof(buf), "%.1f GB", memMb / 1024.0);
|
snprintf(buf, sizeof(buf), "%.1f GB", memMb / 1024.0);
|
||||||
else
|
else
|
||||||
snprintf(buf, sizeof(buf), "%.0f MB", memMb);
|
snprintf(buf, sizeof(buf), "%.0f MB", memMb);
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
|
|
||||||
} else {
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
|
||||||
}
|
}
|
||||||
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_memory"),
|
||||||
|
memMb > 0 ? buf : nullptr, OnSurface());
|
||||||
}
|
}
|
||||||
|
|
||||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
stepRow();
|
||||||
drawDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 5: Longest Chain | Best Block
|
// Row 5: Longest Chain | Best Block
|
||||||
{
|
{
|
||||||
// Longest Chain
|
// Longest Chain
|
||||||
float cx = cardMin.x + pad;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_longest"));
|
|
||||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
||||||
if (state.longestchain > 0) {
|
if (state.longestchain > 0) {
|
||||||
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
snprintf(buf, sizeof(buf), "%d", state.longestchain);
|
||||||
// Color green if local matches longest, warning if behind
|
// Color green if local matches longest, warning if behind
|
||||||
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
int localHeight = mining.blocks > 0 ? mining.blocks : state.sync.blocks;
|
||||||
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
|
ImU32 chainCol = (localHeight >= state.longestchain) ? Success() : Warning();
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), chainCol, buf);
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_longest"), buf, chainCol);
|
||||||
} else {
|
} else {
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_longest"), nullptr, OnSurface());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best Block (truncated hash)
|
// Best Block — bespoke: truncated hash with click-to-copy + hover underline
|
||||||
cx = cardMin.x + pad + colW;
|
float cx = rightX;
|
||||||
|
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_best_block"));
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_best_block"));
|
||||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
||||||
if (!state.sync.best_blockhash.empty()) {
|
if (!state.sync.best_blockhash.empty()) {
|
||||||
// Truncate hash to fit: first 6 + "..." + last 6
|
// Truncate hash to fit: first 6 + "..." + last 6
|
||||||
std::string hash = state.sync.best_blockhash;
|
std::string hash = state.sync.best_blockhash;
|
||||||
@@ -372,39 +346,32 @@ void RenderPeersTab(App* app)
|
|||||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_upper"));
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cardMin.x + pad, cardMin.y + pad * 0.5f), Primary(), TR("peers_upper"));
|
||||||
|
|
||||||
float colW = (cardW - pad * 2) / 2.0f;
|
float colW = (cardW - pad * 2) / 2.0f;
|
||||||
|
float leftX = cardMin.x + pad;
|
||||||
|
float rightX = cardMin.x + pad + colW;
|
||||||
float ry = cardMin.y + pad * 0.5f + headerH;
|
float ry = cardMin.y + pad * 0.5f + headerH;
|
||||||
|
|
||||||
// Helper to draw a subtle horizontal divider
|
// Advance ry past a row and draw a divider before the next one (see blockchain card).
|
||||||
auto drawPeerDivider = [&](float y) {
|
auto stepRow = [&]() {
|
||||||
float rnd = glassSpec.rounding;
|
ry += rowH + Layout::spacingSm() * 0.5f;
|
||||||
dl->AddLine(ImVec2(cardMin.x + rnd * 0.5f, y),
|
drawCardDivider(dl, cardMin.x, cardMax.x, ry, glassSpec.rounding, dp);
|
||||||
ImVec2(cardMax.x - rnd * 0.5f, y),
|
ry += Layout::spacingSm() * 0.5f + dividerH;
|
||||||
WithAlpha(OnSurface(), 15), 1.0f * dp);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Row 1: Connected | In/Out
|
// Row 1: Connected | In/Out
|
||||||
{
|
{
|
||||||
// Connected
|
|
||||||
float cx = cardMin.x + pad;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_connected"));
|
|
||||||
snprintf(buf, sizeof(buf), "%d", totalPeers);
|
snprintf(buf, sizeof(buf), "%d", totalPeers);
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_connected"), buf, OnSurface());
|
||||||
|
|
||||||
// In / Out
|
|
||||||
cx = cardMin.x + pad + colW;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_in_out"));
|
|
||||||
snprintf(buf, sizeof(buf), "%d / %d", inboundCount, outboundCount);
|
snprintf(buf, sizeof(buf), "%d / %d", inboundCount, outboundCount);
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), buf);
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_in_out"), buf, OnSurface());
|
||||||
}
|
}
|
||||||
|
|
||||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
stepRow();
|
||||||
drawPeerDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 2: TLS | Avg Ping
|
// Row 2: TLS | Avg Ping
|
||||||
{
|
{
|
||||||
// TLS
|
// TLS — bespoke: appends a check icon when every peer is TLS
|
||||||
float cx = cardMin.x + pad;
|
float cx = leftX;
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "TLS");
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), "TLS");
|
||||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
||||||
if (totalPeers > 0) {
|
if (totalPeers > 0) {
|
||||||
@@ -420,60 +387,39 @@ void RenderPeersTab(App* app)
|
|||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Avg Ping
|
// Avg Ping — color by latency
|
||||||
cx = cardMin.x + pad + colW;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_avg_ping"));
|
|
||||||
ImU32 pingCol;
|
ImU32 pingCol;
|
||||||
if (avgPing < 100) pingCol = Success();
|
if (avgPing < 100) pingCol = Success();
|
||||||
else if (avgPing < 500) pingCol = Warning();
|
else if (avgPing < 500) pingCol = Warning();
|
||||||
else pingCol = Error();
|
else pingCol = Error();
|
||||||
snprintf(buf, sizeof(buf), "%.0f ms", avgPing);
|
snprintf(buf, sizeof(buf), "%.0f ms", avgPing);
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), pingCol, buf);
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_avg_ping"), buf, pingCol);
|
||||||
}
|
}
|
||||||
|
|
||||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
stepRow();
|
||||||
drawPeerDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 3: Received | Sent
|
// Row 3: Received | Sent
|
||||||
{
|
{
|
||||||
// Received
|
|
||||||
float cx = cardMin.x + pad;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_received"));
|
|
||||||
std::string recvStr = fmtBytes(totalBytesRecv);
|
std::string recvStr = fmtBytes(totalBytesRecv);
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), recvStr.c_str());
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_received"), recvStr.c_str(), OnSurface());
|
||||||
|
|
||||||
// Sent
|
|
||||||
cx = cardMin.x + pad + colW;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_sent"));
|
|
||||||
std::string sentStr = fmtBytes(totalBytesSent);
|
std::string sentStr = fmtBytes(totalBytesSent);
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, ry + capFont->LegacySize + Layout::spacingXs()), OnSurface(), sentStr.c_str());
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_sent"), sentStr.c_str(), OnSurface());
|
||||||
}
|
}
|
||||||
|
|
||||||
ry += rowH + Layout::spacingSm() * 0.5f;
|
stepRow();
|
||||||
drawPeerDivider(ry);
|
|
||||||
ry += Layout::spacingSm() * 0.5f + dividerH;
|
|
||||||
|
|
||||||
// Row 4: P2P Port | Banned
|
// Row 4: P2P Port | Banned
|
||||||
{
|
{
|
||||||
// P2P Port
|
if (state.p2p_port > 0) snprintf(buf, sizeof(buf), "%d", state.p2p_port);
|
||||||
float cx = cardMin.x + pad;
|
drawStatCell(dl, leftX, ry, capFont, sub1, TR("peers_p2p_port"),
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_p2p_port"));
|
state.p2p_port > 0 ? buf : nullptr, OnSurface());
|
||||||
float valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
||||||
if (state.p2p_port > 0) {
|
|
||||||
snprintf(buf, sizeof(buf), "%d", state.p2p_port);
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurface(), buf);
|
|
||||||
} else {
|
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), OnSurfaceDisabled(), "\xE2\x80\x94");
|
|
||||||
}
|
|
||||||
// Banned count
|
|
||||||
cx = cardMin.x + pad + colW;
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, ry), OnSurfaceMedium(), TR("peers_banned"));
|
|
||||||
valY = ry + capFont->LegacySize + Layout::spacingXs();
|
|
||||||
size_t bannedCount = state.bannedPeers.size();
|
size_t bannedCount = state.bannedPeers.size();
|
||||||
snprintf(buf, sizeof(buf), "%zu", bannedCount);
|
snprintf(buf, sizeof(buf), "%zu", bannedCount);
|
||||||
ImU32 bannedCol = (bannedCount > 0) ? Warning() : OnSurface();
|
ImU32 bannedCol = (bannedCount > 0) ? Warning() : OnSurface();
|
||||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, valY), bannedCol, buf); }
|
drawStatCell(dl, rightX, ry, capFont, sub1, TR("peers_banned"), buf, bannedCol);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ImGui::SetCursorScreenPos(basePos);
|
ImGui::SetCursorScreenPos(basePos);
|
||||||
|
|||||||
@@ -96,6 +96,44 @@ static void DrawTxIcon(ImDrawList* dl, const std::string& type,
|
|||||||
ImVec2(cx - sz.x * 0.5f, cy - sz.y * 0.5f), col, icon);
|
ImVec2(cx - sz.x * 0.5f, cy - sz.y * 0.5f), col, icon);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Draw one Received/Sent/Mined summary card: icon + label + count + signed total, with a
|
||||||
|
// hover outline and click-to-toggle type filter. All three cards are identical apart from the
|
||||||
|
// per-card data passed here. cMin is the card's top-left; filterValue is the type_filter value
|
||||||
|
// this card selects (clicking again clears it).
|
||||||
|
static void drawSummaryCard(ImDrawList* dl, ImVec2 cMin, float cardW, float cardH,
|
||||||
|
float innerPad, float iconSz, const GlassPanelSpec& glassSpec,
|
||||||
|
ImFont* ovFont, ImFont* capFont, ImFont* body2,
|
||||||
|
const char* iconType, ImU32 accentCol, const char* labelKey,
|
||||||
|
int count, double total, const char* signPrefix,
|
||||||
|
int filterValue, int& type_filter)
|
||||||
|
{
|
||||||
|
char buf[128];
|
||||||
|
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
|
||||||
|
DrawGlassPanel(dl, cMin, cMax, glassSpec);
|
||||||
|
|
||||||
|
float cx = cMin.x + innerPad;
|
||||||
|
float cy = cMin.y + Layout::spacingMd();
|
||||||
|
|
||||||
|
DrawTxIcon(dl, iconType, cx + iconSz, cy + iconSz * 1.33f, iconSz, accentCol);
|
||||||
|
|
||||||
|
float labelX = cx + iconSz * 3.0f;
|
||||||
|
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR(labelKey));
|
||||||
|
cy += ovFont->LegacySize + Layout::spacingSm();
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), TR("txs_count"), count);
|
||||||
|
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
|
||||||
|
cy += capFont->LegacySize + Layout::spacingXs();
|
||||||
|
|
||||||
|
snprintf(buf, sizeof(buf), "%s%.4f %s", signPrefix, total, DRAGONX_TICKER);
|
||||||
|
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), accentCol, buf);
|
||||||
|
|
||||||
|
if (material::IsRectHovered(cMin, cMax)) {
|
||||||
|
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
|
||||||
|
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
||||||
|
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == filterValue) ? 0 : filterValue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void RenderTransactionsTab(App* app)
|
void RenderTransactionsTab(App* app)
|
||||||
{
|
{
|
||||||
auto& S = schema::UISchema::instance();
|
auto& S = schema::UISchema::instance();
|
||||||
@@ -203,95 +241,17 @@ void RenderTransactionsTab(App* app)
|
|||||||
// Sort mode: 0 = newest first, 1 = oldest first, 2 = largest amount, 3 = smallest amount.
|
// Sort mode: 0 = newest first, 1 = oldest first, 2 = largest amount, 3 = smallest amount.
|
||||||
static int s_sort_mode = 0;
|
static int s_sort_mode = 0;
|
||||||
|
|
||||||
// --- Received card ---
|
// --- Received / Sent / Mined cards ---
|
||||||
{
|
// filter values: Received = 2, Sent = 1, Mined = 3 (see idx_map for the accent bar below)
|
||||||
ImVec2 cMin = origin;
|
drawSummaryCard(dl, origin, cardW, cardH, innerPad, iconSz, glassSpec,
|
||||||
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
|
ovFont, capFont, body2, "receive", greenCol, "received_upper",
|
||||||
DrawGlassPanel(dl, cMin, cMax, glassSpec);
|
recvCount, recvTotal, "+", 2, type_filter);
|
||||||
|
drawSummaryCard(dl, ImVec2(origin.x + (cardW + cardGap), origin.y), cardW, cardH,
|
||||||
float cx = cMin.x + innerPad;
|
innerPad, iconSz, glassSpec, ovFont, capFont, body2, "send", redCol, "sent_upper",
|
||||||
float cy = cMin.y + Layout::spacingMd();
|
sendCount, sendTotal, "-", 1, type_filter);
|
||||||
|
drawSummaryCard(dl, ImVec2(origin.x + 2 * (cardW + cardGap), origin.y), cardW, cardH,
|
||||||
// Icon
|
innerPad, iconSz, glassSpec, ovFont, capFont, body2, "mined", goldCol, "mined_upper",
|
||||||
DrawTxIcon(dl, "receive", cx + iconSz, cy + iconSz * 1.33f, iconSz, greenCol);
|
minedCount, minedTotal, "+", 3, type_filter);
|
||||||
|
|
||||||
float labelX = cx + iconSz * 3.0f;
|
|
||||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("received_upper"));
|
|
||||||
cy += ovFont->LegacySize + Layout::spacingSm();
|
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), TR("txs_count"), recvCount);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
|
|
||||||
cy += capFont->LegacySize + Layout::spacingXs();
|
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), "+%.4f %s", recvTotal, DRAGONX_TICKER);
|
|
||||||
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), greenCol, buf);
|
|
||||||
|
|
||||||
if (material::IsRectHovered(cMin, cMax)) {
|
|
||||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 2) ? 0 : 2;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Sent card ---
|
|
||||||
{
|
|
||||||
float xOff = cardW + cardGap;
|
|
||||||
ImVec2 cMin(origin.x + xOff, origin.y);
|
|
||||||
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
|
|
||||||
DrawGlassPanel(dl, cMin, cMax, glassSpec);
|
|
||||||
|
|
||||||
float cx = cMin.x + innerPad;
|
|
||||||
float cy = cMin.y + Layout::spacingMd();
|
|
||||||
|
|
||||||
DrawTxIcon(dl, "send", cx + iconSz, cy + iconSz * 1.33f, iconSz, redCol);
|
|
||||||
|
|
||||||
float labelX = cx + iconSz * 3.0f;
|
|
||||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("sent_upper"));
|
|
||||||
cy += ovFont->LegacySize + Layout::spacingSm();
|
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), TR("txs_count"), sendCount);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
|
|
||||||
cy += capFont->LegacySize + Layout::spacingXs();
|
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), "-%.4f %s", sendTotal, DRAGONX_TICKER);
|
|
||||||
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), redCol, buf);
|
|
||||||
|
|
||||||
if (material::IsRectHovered(cMin, cMax)) {
|
|
||||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 1) ? 0 : 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --- Mined card ---
|
|
||||||
{
|
|
||||||
float xOff = 2 * (cardW + cardGap);
|
|
||||||
ImVec2 cMin(origin.x + xOff, origin.y);
|
|
||||||
ImVec2 cMax(cMin.x + cardW, cMin.y + cardH);
|
|
||||||
DrawGlassPanel(dl, cMin, cMax, glassSpec);
|
|
||||||
|
|
||||||
float cx = cMin.x + innerPad;
|
|
||||||
float cy = cMin.y + Layout::spacingMd();
|
|
||||||
|
|
||||||
DrawTxIcon(dl, "mined", cx + iconSz, cy + iconSz * 1.33f, iconSz, goldCol);
|
|
||||||
|
|
||||||
float labelX = cx + iconSz * 3.0f;
|
|
||||||
dl->AddText(ovFont, ovFont->LegacySize, ImVec2(labelX, cy), OnSurfaceMedium(), TR("mined_upper"));
|
|
||||||
cy += ovFont->LegacySize + Layout::spacingSm();
|
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), TR("txs_count"), minedCount);
|
|
||||||
dl->AddText(capFont, capFont->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), buf);
|
|
||||||
cy += capFont->LegacySize + Layout::spacingXs();
|
|
||||||
|
|
||||||
snprintf(buf, sizeof(buf), "+%.4f %s", minedTotal, DRAGONX_TICKER);
|
|
||||||
dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), goldCol, buf);
|
|
||||||
|
|
||||||
if (material::IsRectHovered(cMin, cMax)) {
|
|
||||||
dl->AddRect(cMin, cMax, IM_COL32(255, 255, 255, 40), glassSpec.rounding, 0, 1.5f);
|
|
||||||
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
|
|
||||||
if (ImGui::IsMouseClicked(0)) type_filter = (type_filter == 3) ? 0 : 3;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Selected card accent
|
// Selected card accent
|
||||||
if (type_filter > 0) {
|
if (type_filter > 0) {
|
||||||
|
|||||||
Reference in New Issue
Block a user