feat(addresses): improve address labeling and view-only handling

- Add expanded address icon picker with search, bottom-aligned actions, and improved modal sizing
- Embed a pickaxe icon font subset and wire it into typography/address icon rendering
- Track view-only shielded addresses and prevent sends from non-spendable z-addresses
- Improve address transfer dialog sizing, max amount handling, and text clipping
- Tune main header layout values in ui.toml
- Update README, codebase overview, and third-party license documentation
This commit is contained in:
dan_s
2026-04-27 13:54:28 -05:00
parent 55a36e0d06
commit 9e1b1397ad
18 changed files with 567 additions and 90 deletions

View File

@@ -5,10 +5,14 @@
#pragma once
#include <string>
#include <vector>
#include <cctype>
#include <cstring>
#include <cstddef>
#include "../../app.h"
#include "../../util/i18n.h"
#include "../material/draw_helpers.h"
#include "../material/typography.h"
#include "../theme.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "imgui.h"
@@ -18,12 +22,41 @@ namespace ui {
class AddressLabelDialog {
public:
static constexpr const char* kPickaxeGlyph = "\xEE\x80\x81";
static bool drawIconByName(ImDrawList* dl,
const std::string& name,
ImVec2 center,
float /*emSize*/,
ImU32 color,
ImFont* iconFont,
float iconFontSize) {
if (name == "pickaxe") {
ImFont* pickaxeFont = material::Typography::instance().pickaxeFontForSize(iconFontSize);
if (!pickaxeFont) return false;
ImVec2 iSz = pickaxeFont->CalcTextSizeA(iconFontSize, 1000.0f, 0.0f, kPickaxeGlyph);
dl->AddText(pickaxeFont, iconFontSize,
ImVec2(center.x - iSz.x * 0.5f, center.y - iSz.y * 0.5f), color, kPickaxeGlyph);
return true;
}
const char* glyph = iconGlyphForName(name);
if (!glyph || !iconFont) return false;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFontSize, 1000.0f, 0.0f, glyph);
dl->AddText(iconFont, iconFontSize,
ImVec2(center.x - iSz.x * 0.5f, center.y - iSz.y * 0.5f), color, glyph);
return true;
}
static void show(App* app, const std::string& address, bool isZ) {
s_open = true;
s_app = app;
s_address = address;
s_isZ = isZ;
s_selectedIcon = -1;
s_iconSearch[0] = '\0';
// Pre-fill from existing metadata
std::string existing = app->getAddressLabel(address);
@@ -44,7 +77,9 @@ public:
using namespace material;
if (BeginOverlayDialog(TR("set_label"), &s_open, 420.0f, 0.92f)) {
constexpr float cardTopViewportRatio = 0.15f;
constexpr float cardBottomViewportRatio = 0.80f;
if (BeginOverlayDialog(TR("set_label"), &s_open, 660.0f, 0.92f, cardBottomViewportRatio)) {
float dp = Layout::dpiScale();
// Address preview
@@ -70,16 +105,64 @@ public:
Type().text(TypeStyle::Subtitle2, TR("choose_icon"));
ImGui::Spacing();
// Search bar
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##IconSearch", TR("search_icons"), s_iconSearch, sizeof(s_iconSearch));
ImGui::Spacing();
ImFont* iconFont = Type().iconMed();
float iconFsz = ScaledFontSize(iconFont);
float cellSz = iconFsz + 16.0f * dp;
float avail = ImGui::GetContentRegionAvail().x;
int cols = std::max(1, (int)(avail / (cellSz + 4.0f * dp)));
const char* cancelLabel = TR("cancel");
const char* saveLabel = TR("save");
ImFont* buttonFont = Type().button();
float buttonFontSize = ScaledFontSize(buttonFont);
const ImGuiStyle& style = ImGui::GetStyle();
const float cardHeight = ImGui::GetMainViewport()->Size.y * (cardBottomViewportRatio - cardTopViewportRatio);
const float bottomPadding = style.WindowPadding.y;
const bool showClearIcon = (s_selectedIcon >= 0);
const float clearRowH = showClearIcon ? ImGui::GetTextLineHeight() : 0.0f;
const float buttonRowH = ImGui::GetFrameHeight();
const float separatorH = 1.0f;
const float preButtonReserve =
(showClearIcon ? (style.ItemSpacing.y + clearRowH) : 0.0f) +
style.ItemSpacing.y * 3.0f + separatorH;
const float buttonY = cardHeight - bottomPadding - buttonRowH;
// Build filtered index list
std::vector<int> visible;
visible.reserve(kIconCount);
{
// Simple case-insensitive substring match on icon name
std::string needle(s_iconSearch);
for (char& c : needle) c = (char)std::tolower((unsigned char)c);
for (int i = 0; i < kIconCount; ++i) {
if (needle.empty() || std::strstr(kIconNames[i], needle.c_str()) != nullptr)
visible.push_back(i);
}
}
// The grid owns all vertical space above the bottom action band.
const float gridStartY = ImGui::GetCursorPosY();
const float controlsTopY = std::max(gridStartY + cellSz * 2.0f, buttonY - preButtonReserve);
const float gridMaxH = std::max(cellSz * 2.0f, controlsTopY - gridStartY);
ImGui::PushStyleColor(ImGuiCol_ChildBg, IM_COL32(0, 0, 0, 0));
ImGui::BeginChild("##IconGrid", ImVec2(avail, gridMaxH), ImGuiChildFlags_None,
ImGuiWindowFlags_NoScrollbar);
ImDrawList* dl = ImGui::GetWindowDrawList();
for (int i = 0; i < kIconCount; ++i) {
if (i % cols != 0) ImGui::SameLine(0, 4.0f * dp);
if (visible.empty()) {
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("no_icons_found"));
}
int col = 0;
for (int vi = 0; vi < (int)visible.size(); ++vi) {
int i = visible[vi];
if (col != 0) ImGui::SameLine(0, 4.0f * dp);
ImVec2 pos = ImGui::GetCursorScreenPos();
ImVec2 mn = pos;
ImVec2 mx(pos.x + cellSz, pos.y + cellSz);
@@ -95,22 +178,33 @@ public:
}
// Icon centered in cell
ImVec2 iSz = iconFont->CalcTextSizeA(iconFsz, 1000.0f, 0.0f, kIconGlyphs[i]);
dl->AddText(iconFont, iconFsz,
ImVec2(mn.x + (cellSz - iSz.x) * 0.5f, mn.y + (cellSz - iSz.y) * 0.5f),
sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()),
kIconGlyphs[i]);
drawIconByName(dl,
kIconNames[i],
ImVec2(mn.x + cellSz * 0.5f, mn.y + cellSz * 0.5f),
iconFsz,
sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium()),
iconFont,
iconFsz);
ImGui::PushID(i);
ImGui::InvisibleButton("##icon", ImVec2(cellSz, cellSz));
if (ImGui::IsItemClicked()) s_selectedIcon = i;
if (hov) ImGui::SetTooltip("%s", kIconNames[i]);
ImGui::PopID();
col = (col + 1) % cols;
}
ImGui::EndChild();
ImGui::PopStyleColor();
if (ImGui::GetCursorPosY() < controlsTopY) {
ImGui::SetCursorPosY(controlsTopY);
}
// "No icon" option
ImGui::Spacing();
if (s_selectedIcon >= 0) {
if (showClearIcon) {
ImGui::Spacing();
if (ImGui::SmallButton(TR("clear_icon"))) {
s_selectedIcon = -1;
}
@@ -121,16 +215,27 @@ public:
ImGui::Separator();
ImGui::Spacing();
// Buttons
float btnW = 120.0f;
float totalW = btnW * 2 + Layout::spacingMd();
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - totalW) * 0.5f);
if (ImGui::GetCursorPosY() < buttonY) {
ImGui::SetCursorPosY(buttonY);
}
if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
// Buttons
float minBtnW = 120.0f * dp;
float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp;
float cancelW = std::max(minBtnW,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, cancelLabel).x + buttonPadW);
float saveW = std::max(minBtnW,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, saveLabel).x + buttonPadW);
float totalW = cancelW + saveW + Layout::spacingMd();
float rowStartX = ImGui::GetCursorPosX();
float contentW = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
if (TactileButton(cancelLabel, ImVec2(cancelW, 0), buttonFont)) {
s_open = false;
}
ImGui::SameLine(0, Layout::spacingMd());
if (TactileButton(TR("save"), ImVec2(btnW, 0))) {
if (TactileButton(saveLabel, ImVec2(saveW, 0), buttonFont)) {
// Apply changes
s_app->setAddressLabel(s_address, s_label);
if (s_selectedIcon >= 0)
@@ -153,23 +258,118 @@ private:
static inline bool s_isZ = false;
static inline char s_label[128] = {};
static inline int s_selectedIcon = -1;
static inline char s_iconSearch[64] = {};
// Icon palette — wallet-relevant Material Design icons
static constexpr int kIconCount = 20;
static inline const char* kIconNames[kIconCount] = {
"savings", "account_balance", "wallet", "payments",
"diamond", "shield", "lock", "swap_horiz",
"store", "home", "work", "rocket_launch",
"favorite", "bolt", "token", "category",
"label", "coffee", "volunteer", "star"
static inline const char* kIconNames[] = {
// Finance / Crypto
"savings", "account_balance", "account_balance_wallet", "wallet",
"payments", "credit_card", "local_atm", "diamond",
"attach_money", "currency_bitcoin", "currency_exchange", "balance",
"calculate", "trending_up", "euro", "leaderboard",
"paid", "sell", "receipt", "percent",
"price_change", "price_check", "toll", "money",
// Charts / Analytics
"show_chart", "candlestick_chart", "bar_chart", "pie_chart",
"area_chart", "stacked_bar_chart", "waterfall_chart", "scatter_plot",
"query_stats", "speed", "donut_large",
// Mining / Tools
"pickaxe",
"hardware", "construction", "handyman", "build",
"carpenter", "plumbing", "home_repair_service", "precision_manufacturing",
"factory", "warehouse", "inventory", "recycling",
"oil_barrel", "offline_bolt", "thunderstorm", "terminal",
"storage", "memory", "developer_board",
// Security / Auth
"shield", "security", "lock", "swap_horiz",
"verified", "verified_user", "key", "badge",
// Commerce / Business
"store", "storefront", "shopping_bag", "business",
"work", "real_estate_agent", "gavel", "local_shipping",
// Home / Property
"home", "apartment", "cottage", "landscape",
// People / Identity
"account_circle", "face", "manage_accounts", "groups", "mood",
// Travel / Transport
"rocket_launch", "flight", "directions_car", "travel_explore",
"explore", "location_on", "map", "luggage", "anchor",
// Nature / Outdoors
"public", "language", "forest", "park",
"water_drop", "beach_access", "energy_savings_leaf", "solar_power",
// Social / Lifestyle
"favorite", "star", "celebration", "casino",
"auto_awesome", "emoji_events", "military_tech", "flag",
// Tech / Science
"bolt", "tungsten", "lightbulb", "insights",
"hub", "token", "electric_bolt", "science", "biotech",
// Organisation
"category", "label", "school", "local_hospital", "local_florist",
// Food / Drink
"coffee", "restaurant", "wine_bar", "liquor",
"outdoor_grill", "nightlife", "sports_bar",
// Recreation / Health
"pets", "fitness_center", "spa", "self_improvement",
"psychology", "sports_soccer", "sports_esports",
"hiking", "palette", "museum", "church", "surfing",
// Community
"redeem", "handshake", "healing", "volunteer",
"stadium", "temple_buddhist", "theater_comedy", "watch",
};
static inline const char* kIconGlyphs[kIconCount] = {
ICON_MD_SAVINGS, ICON_MD_ACCOUNT_BALANCE, ICON_MD_WALLET, ICON_MD_PAYMENTS,
ICON_MD_DIAMOND, ICON_MD_SHIELD, ICON_MD_LOCK, ICON_MD_SWAP_HORIZ,
ICON_MD_STORE, ICON_MD_HOME, ICON_MD_WORK, ICON_MD_ROCKET_LAUNCH,
ICON_MD_FAVORITE, ICON_MD_BOLT, ICON_MD_TOKEN, ICON_MD_CATEGORY,
ICON_MD_LABEL, ICON_MD_LOCAL_CAFE, ICON_MD_VOLUNTEER_ACTIVISM, ICON_MD_STAR
static inline const char* kIconGlyphs[] = {
// Finance / Crypto
ICON_MD_SAVINGS, ICON_MD_ACCOUNT_BALANCE, ICON_MD_ACCOUNT_BALANCE_WALLET, ICON_MD_WALLET,
ICON_MD_PAYMENTS, ICON_MD_CREDIT_CARD, ICON_MD_LOCAL_ATM, ICON_MD_DIAMOND,
ICON_MD_ATTACH_MONEY, ICON_MD_CURRENCY_BITCOIN, ICON_MD_CURRENCY_EXCHANGE, ICON_MD_BALANCE,
ICON_MD_CALCULATE, ICON_MD_TRENDING_UP, ICON_MD_EURO, ICON_MD_LEADERBOARD,
ICON_MD_PAID, ICON_MD_SELL, ICON_MD_RECEIPT, ICON_MD_PERCENT,
ICON_MD_PRICE_CHANGE, ICON_MD_PRICE_CHECK, ICON_MD_TOLL, ICON_MD_MONEY,
// Charts / Analytics
ICON_MD_SHOW_CHART, ICON_MD_CANDLESTICK_CHART, ICON_MD_BAR_CHART, ICON_MD_PIE_CHART,
ICON_MD_AREA_CHART, ICON_MD_STACKED_BAR_CHART, ICON_MD_WATERFALL_CHART, ICON_MD_SCATTER_PLOT,
ICON_MD_QUERY_STATS, ICON_MD_SPEED, ICON_MD_DONUT_LARGE,
// Mining / Tools
nullptr,
ICON_MD_HARDWARE, ICON_MD_CONSTRUCTION, ICON_MD_HANDYMAN, ICON_MD_BUILD,
ICON_MD_CARPENTER, ICON_MD_PLUMBING, ICON_MD_HOME_REPAIR_SERVICE, ICON_MD_PRECISION_MANUFACTURING,
ICON_MD_FACTORY, ICON_MD_WAREHOUSE, ICON_MD_INVENTORY, ICON_MD_RECYCLING,
ICON_MD_OIL_BARREL, ICON_MD_OFFLINE_BOLT, ICON_MD_THUNDERSTORM, ICON_MD_TERMINAL,
ICON_MD_STORAGE, ICON_MD_MEMORY, ICON_MD_DEVELOPER_BOARD,
// Security / Auth
ICON_MD_SHIELD, ICON_MD_SECURITY, ICON_MD_LOCK, ICON_MD_SWAP_HORIZ,
ICON_MD_VERIFIED, ICON_MD_VERIFIED_USER, ICON_MD_KEY, ICON_MD_BADGE,
// Commerce / Business
ICON_MD_STORE, ICON_MD_STOREFRONT, ICON_MD_SHOPPING_BAG, ICON_MD_BUSINESS,
ICON_MD_WORK, ICON_MD_REAL_ESTATE_AGENT, ICON_MD_GAVEL, ICON_MD_LOCAL_SHIPPING,
// Home / Property
ICON_MD_HOME, ICON_MD_APARTMENT, ICON_MD_COTTAGE, ICON_MD_LANDSCAPE,
// People / Identity
ICON_MD_ACCOUNT_CIRCLE, ICON_MD_FACE, ICON_MD_MANAGE_ACCOUNTS, ICON_MD_GROUPS, ICON_MD_MOOD,
// Travel / Transport
ICON_MD_ROCKET_LAUNCH, ICON_MD_FLIGHT, ICON_MD_DIRECTIONS_CAR, ICON_MD_TRAVEL_EXPLORE,
ICON_MD_EXPLORE, ICON_MD_LOCATION_ON, ICON_MD_MAP, ICON_MD_LUGGAGE, ICON_MD_ANCHOR,
// Nature / Outdoors
ICON_MD_PUBLIC, ICON_MD_LANGUAGE, ICON_MD_FOREST, ICON_MD_PARK,
ICON_MD_WATER_DROP, ICON_MD_BEACH_ACCESS, ICON_MD_ENERGY_SAVINGS_LEAF, ICON_MD_SOLAR_POWER,
// Social / Lifestyle
ICON_MD_FAVORITE, ICON_MD_STAR, ICON_MD_CELEBRATION, ICON_MD_CASINO,
ICON_MD_AUTO_AWESOME, ICON_MD_EMOJI_EVENTS, ICON_MD_MILITARY_TECH, ICON_MD_FLAG,
// Tech / Science
ICON_MD_BOLT, ICON_MD_TUNGSTEN, ICON_MD_LIGHTBULB, ICON_MD_INSIGHTS,
ICON_MD_HUB, ICON_MD_TOKEN, ICON_MD_ELECTRIC_BOLT, ICON_MD_SCIENCE, ICON_MD_BIOTECH,
// Organisation
ICON_MD_CATEGORY, ICON_MD_LABEL, ICON_MD_SCHOOL, ICON_MD_LOCAL_HOSPITAL, ICON_MD_LOCAL_FLORIST,
// Food / Drink
ICON_MD_LOCAL_CAFE, ICON_MD_RESTAURANT, ICON_MD_WINE_BAR, ICON_MD_LIQUOR,
ICON_MD_OUTDOOR_GRILL, ICON_MD_NIGHTLIFE, ICON_MD_SPORTS_BAR,
// Recreation / Health
ICON_MD_PETS, ICON_MD_FITNESS_CENTER, ICON_MD_SPA, ICON_MD_SELF_IMPROVEMENT,
ICON_MD_PSYCHOLOGY, ICON_MD_SPORTS_SOCCER, ICON_MD_SPORTS_ESPORTS,
ICON_MD_HIKING, ICON_MD_PALETTE, ICON_MD_MUSEUM, ICON_MD_CHURCH, ICON_MD_SURFING,
// Community
ICON_MD_REDEEM, ICON_MD_HANDSHAKE, ICON_MD_HEALING, ICON_MD_VOLUNTEER_ACTIVISM,
ICON_MD_STADIUM, ICON_MD_TEMPLE_BUDDHIST, ICON_MD_THEATER_COMEDY, ICON_MD_WATCH,
};
static constexpr int kIconCount = static_cast<int>(std::size(kIconNames));
public:
// Expose for the address list to look up icon glyphs by name

View File

@@ -45,11 +45,12 @@ public:
s_resultMsg.clear();
s_success = false;
// Pre-fill amount with full source balance
snprintf(s_amount, sizeof(s_amount), "%.8f", info.fromBalance);
// Default fee
s_fee = 0.0001;
// Pre-fill with the maximum sendable amount so the dialog is valid
// immediately without requiring the user to press Max.
snprintf(s_amount, sizeof(s_amount), "%.8f", maxSendableAmount(info.fromBalance, s_fee));
}
static void render() {
@@ -57,7 +58,7 @@ public:
using namespace material;
if (BeginOverlayDialog(TR("transfer_funds"), &s_open, 480.0f, 0.94f)) {
if (BeginOverlayDialog(TR("transfer_funds"), &s_open, 620.0f, 0.94f)) {
float dp = Layout::dpiScale();
ImDrawList* dl = ImGui::GetWindowDrawList();
@@ -104,18 +105,22 @@ public:
ImGui::Spacing();
ImGui::Spacing();
// Amount input
// Amount input + Max button on same row without overflow
Type().text(TypeStyle::Subtitle2, TR("amount"));
ImGui::SetNextItemWidth(-1);
{
float spacing = ImGui::GetStyle().ItemSpacing.x;
float maxBtnW = ImGui::CalcTextSize(TR("max")).x
+ ImGui::GetStyle().FramePadding.x * 2.0f;
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - maxBtnW - spacing);
}
ImGui::InputText("##TransferAmt", s_amount, sizeof(s_amount),
ImGuiInputTextFlags_CharsDecimal);
// Max button
ImGui::SameLine();
if (ImGui::SmallButton(TR("max"))) {
double maxAmt = s_info.fromBalance - s_fee;
if (maxAmt < 0) maxAmt = 0;
snprintf(s_amount, sizeof(s_amount), "%.8f", maxAmt);
snprintf(s_amount, sizeof(s_amount), "%.8f",
maxSendableAmount(s_info.fromBalance, s_fee));
}
ImGui::Spacing();
@@ -140,15 +145,15 @@ public:
ImGui::Spacing();
{
char buf[128];
snprintf(buf, sizeof(buf), "%s: %.8f DRGX → %.8f DRGX",
TR("sender_balance"), s_info.fromBalance, amountValid ? newFromBal : s_info.fromBalance);
snprintf(buf, sizeof(buf), TR("sender_balance"),
s_info.fromBalance, amountValid ? newFromBal : s_info.fromBalance);
Type().textColored(TypeStyle::Caption,
(amountValid && newFromBal < 1e-9) ? Warning() : OnSurfaceMedium(), buf);
}
{
char buf[128];
snprintf(buf, sizeof(buf), "%s: %.8f DRGX → %.8f DRGX",
TR("recipient_balance"), s_info.toBalance, amountValid ? newToBal : s_info.toBalance);
snprintf(buf, sizeof(buf), TR("recipient_balance"),
s_info.toBalance, amountValid ? newToBal : s_info.toBalance);
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), buf);
}
@@ -172,17 +177,32 @@ public:
ImGui::Spacing();
// Buttons
float btnW = 140.0f;
float totalW = btnW * 2 + Layout::spacingMd();
ImGui::SetCursorPosX((ImGui::GetContentRegionAvail().x - totalW) * 0.5f);
const char* cancelLabel = TR("cancel");
const char* confirmLabel = TR("confirm_transfer");
const char* sendingLabel = TR("sending");
ImFont* buttonFont = Type().button();
float buttonFontSize = ScaledFontSize(buttonFont);
float minBtnW = 120.0f * dp;
float confirmMinW = 160.0f * dp;
float buttonPadW = ImGui::GetStyle().FramePadding.x * 2.0f + 24.0f * dp;
float cancelW = std::max(minBtnW,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, cancelLabel).x + buttonPadW);
float confirmTextW = std::max(
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, confirmLabel).x,
buttonFont->CalcTextSizeA(buttonFontSize, 1000.0f, 0.0f, sendingLabel).x);
float confirmW = std::max(confirmMinW, confirmTextW + buttonPadW);
float totalW = cancelW + confirmW + Layout::spacingMd();
float rowStartX = ImGui::GetCursorPosX();
float contentW = ImGui::GetContentRegionAvail().x;
ImGui::SetCursorPosX(rowStartX + std::max(0.0f, (contentW - totalW) * 0.5f));
if (TactileButton(TR("cancel"), ImVec2(btnW, 0))) {
if (TactileButton(cancelLabel, ImVec2(cancelW, 0), buttonFont)) {
s_open = false;
}
ImGui::SameLine(0, Layout::spacingMd());
ImGui::BeginDisabled(!amountValid || s_sending);
if (TactileButton(s_sending ? TR("sending") : TR("confirm_transfer"), ImVec2(btnW, 0))) {
if (TactileButton(s_sending ? sendingLabel : confirmLabel, ImVec2(confirmW, 0), buttonFont)) {
s_sending = true;
s_app->sendTransaction(s_info.fromAddr, s_info.toAddr,
amount, s_fee, "",
@@ -205,6 +225,11 @@ public:
static void close() { s_open = false; }
private:
static double maxSendableAmount(double balance, double fee) {
double maxAmt = balance - fee;
return maxAmt > 0.0 ? maxAmt : 0.0;
}
static void renderAddressRow(const std::string& addr, double balance, bool isZ, float dp) {
using namespace material;
ImDrawList* dl = ImGui::GetWindowDrawList();
@@ -228,22 +253,25 @@ private:
dl->AddText(iconFont, iconFsz, ImVec2(mn.x + pad, mn.y + (h - iSz.y) * 0.5f), col, icon);
// Address (truncated)
float textX = mn.x + pad + iSz.x + 8.0f * dp;
std::string display = addr;
if (display.size() > 42) display = display.substr(0, 18) + "..." + display.substr(display.size() - 14);
ImFont* capFont = Type().caption();
float capFsz = ScaledFontSize(capFont);
dl->AddText(capFont, capFsz, ImVec2(textX, mn.y + (h * 0.5f - capFsz)), OnSurfaceMedium(), display.c_str());
// Balance (right-aligned)
// Balance (right-aligned) — computed first so we know how much space address gets
char balBuf[32];
snprintf(balBuf, sizeof(balBuf), "%.8f DRGX", balance);
ImFont* body = Type().body2();
float bodyFsz = ScaledFontSize(body);
ImVec2 balSz = body->CalcTextSizeA(bodyFsz, 1000.0f, 0.0f, balBuf);
dl->AddText(body, bodyFsz, ImVec2(mx.x - pad - balSz.x, mn.y + (h - balSz.y) * 0.5f),
float balX = mx.x - pad - balSz.x;
dl->AddText(body, bodyFsz, ImVec2(balX, mn.y + (h - balSz.y) * 0.5f),
balance > 0 ? OnSurface() : OnSurfaceDisabled(), balBuf);
float textX = mn.x + pad + iSz.x + 8.0f * dp;
ImFont* capFont = Type().caption();
float capFsz = ScaledFontSize(capFont);
// Clip address text so it never overlaps the balance
float addrMaxW = balX - textX - 8.0f * dp;
dl->PushClipRect(ImVec2(textX, mn.y), ImVec2(textX + addrMaxW, mn.y + h), true);
dl->AddText(capFont, capFsz, ImVec2(textX, mn.y + (h * 0.5f - capFsz)), OnSurfaceMedium(), addr.c_str());
dl->PopClipRect();
ImGui::Dummy(ImVec2(w, h));
}

View File

@@ -1027,8 +1027,9 @@ static void RenderBalanceClassic(App* app)
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)" : "";
char typeBuf[64];
snprintf(typeBuf, sizeof(typeBuf), "%s%s", typeLabel, hiddenTag);
snprintf(typeBuf, sizeof(typeBuf), "%s%s%s", typeLabel, hiddenTag, viewOnlyTag);
dl->AddText(capFont, capFont->LegacySize, ImVec2(labelX, cy), typeCol, typeBuf);
// Label (if present, next to type)
@@ -1739,13 +1740,19 @@ static void RenderSharedAddressList(App* app, float listH, float availW,
float iconCx = cx + rowIconSz;
float iconCy = cy + body2->LegacySize * 0.5f;
{
const char* customGlyph = row.icon.empty() ? nullptr : AddressLabelDialog::iconGlyphForName(row.icon);
ImFont* iconFont = Type().iconSmall();
const char* glyph = customGlyph ? customGlyph : (row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE);
ImU32 icCol = customGlyph ? OnSurfaceMedium() : typeCol;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, glyph);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), icCol, glyph);
bool drewCustom = false;
if (!row.icon.empty()) {
drewCustom = AddressLabelDialog::drawIconByName(
dl, row.icon, ImVec2(iconCx, iconCy), iconFont->LegacySize,
OnSurfaceMedium(), iconFont, iconFont->LegacySize);
}
if (!drewCustom) {
const char* glyph = row.isZ ? ICON_MD_SHIELD : ICON_MD_CIRCLE;
ImVec2 iSz = iconFont->CalcTextSizeA(iconFont->LegacySize, 1000.0f, 0.0f, glyph);
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(iconCx - iSz.x * 0.5f, iconCy - iSz.y * 0.5f), typeCol, glyph);
}
}
// ---- Type label (first line) ----

View File

@@ -222,7 +222,7 @@ static void RenderSourceDropdown(App* app, float width) {
int bestIdx = -1;
double bestBal = 0.0;
for (size_t i = 0; i < state.addresses.size(); i++) {
if (state.addresses[i].balance > bestBal) {
if (state.addresses[i].balance > bestBal && state.addresses[i].isSpendable()) {
bestBal = state.addresses[i].balance;
bestIdx = static_cast<int>(i);
}
@@ -259,11 +259,11 @@ static void RenderSourceDropdown(App* app, float width) {
if (!app->isConnected() || state.addresses.empty()) {
ImGui::TextDisabled("%s", TR("no_addresses_available"));
} else {
// Sort by balance descending, only show addresses with balance
// Sort by balance descending, only show spendable addresses with balance
std::vector<size_t> sortedIdx;
sortedIdx.reserve(state.addresses.size());
for (size_t i = 0; i < state.addresses.size(); i++) {
if (state.addresses[i].balance > 0)
if (state.addresses[i].balance > 0 && state.addresses[i].isSpendable())
sortedIdx.push_back(i);
}
std::sort(sortedIdx.begin(), sortedIdx.end(),