feat(market): redesign the Manage-portfolio editor as a two-column layout

Full redesign of the group edit view (list mode unchanged):

- Two columns in a wider (780px) dialog:
  * LEFT "Appearance": a live preview of the group card (icon + color + name +
    summed DRGX/USD), then Label, the icon picker, and the color picker.
  * RIGHT "Addresses": a search box, Shielded/Transparent/All type filters, a
    "Funded" toggle, Select-shown / Clear, and the address list.
- Reworked address rows: a custom checkbox, a Z/T type chip, the address's own
  label + icon (from the address book), a middle-truncated address, and the
  balance right-aligned. Selected rows sort to the top (then by balance); the
  whole row toggles selection. Replaces the old wall of raw address strings.
- Search filters by address or label; empty-state hint when nothing matches.

New i18n keys (portfolio_appearance/addresses_hdr/search/funded/select_all/
select_shown/group_name/no_addr_match). Full-node + Lite build clean; ctest 1/1;
hygiene clean. Rendering change — needs a screenshot check.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-02 23:44:16 -05:00
parent 75f33e41eb
commit d838243caa
2 changed files with 293 additions and 111 deletions

View File

@@ -16,6 +16,7 @@
#include "../material/colors.h"
#include "../material/typography.h"
#include "../material/project_icons.h"
#include "../../util/text_format.h"
#include "../../embedded/IconsMaterialDesign.h"
#include "../../util/platform.h"
#include "../layout.h"
@@ -98,6 +99,9 @@ static char s_pf_label[64] = {0};
static std::vector<std::string> s_pf_addrs;
static std::string s_pf_icon; // selected wallet-icon name ("" = none)
static unsigned int s_pf_color = 0; // selected accent (packed IM_COL32; 0 = default)
static char s_pf_search[64] = {0}; // address-list filter text
static int s_pf_type_filter = 0; // 0 = all, 1 = shielded, 2 = transparent
static bool s_pf_funded_only = false;
// Preset accent palette for portfolio groups (0 slot = "default / no color").
static const ImU32 kPfPalette[] = {
@@ -127,6 +131,9 @@ static void PortfolioBeginEdit(App* app, int index)
s_pf_icon.clear();
s_pf_color = 0;
}
s_pf_search[0] = '\0';
s_pf_type_filter = 0;
s_pf_funded_only = false;
}
// The "Manage portfolio" modal: list/add/edit/delete entries, each a label tied to a group
@@ -134,7 +141,9 @@ static void PortfolioBeginEdit(App* app, int index)
static void RenderPortfolioEditor(App* app)
{
if (!s_portfolio_editor_open) return;
float dialogW = std::min(560.0f, ImGui::GetMainViewport()->Size.x * 0.9f);
// Edit mode is a wider two-column layout; list mode stays compact.
float dialogW = std::min((s_pf_editing == -1 ? 560.0f : 780.0f),
ImGui::GetMainViewport()->Size.x * 0.92f);
if (!material::BeginOverlayDialog(TR("portfolio_manage_title"), &s_portfolio_editor_open, dialogW)) {
return;
}
@@ -177,128 +186,293 @@ static void RenderPortfolioEditor(App* app)
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
if (ImGui::Button(TR("portfolio_add_entry"))) PortfolioBeginEdit(app, -2);
} else {
// ---- EDIT MODE ----
ImGui::TextUnformatted(TR("portfolio_label"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##pfLabel", s_pf_label, sizeof(s_pf_label));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// ---- EDIT MODE: two columns (Appearance | Addresses) ----
float dp = Layout::dpiScale();
ImFont* body2f = Type().body2();
ImFont* capF = Type().caption();
ImFont* sub1f = Type().subtitle1();
ImFont* iconFont = Type().iconSmall();
float iconFsz = iconFont->LegacySize;
// ---- Icon picker (same wallet icon set as the overview tab) ----
float contentW = ImGui::GetContentRegionAvail().x;
float colGap = Layout::spacingLg();
float leftW = std::max(220.0f, (contentW - colGap) * 0.40f);
float rightW = contentW - colGap - leftW;
float colH = std::min(ImGui::GetMainViewport()->Size.y * 0.52f, 460.0f * dp);
// ===================== LEFT: APPEARANCE =====================
ImGui::BeginChild("##pfLeft", ImVec2(leftW, colH), false);
{
float dp = Layout::dpiScale();
ImFont* iconFont = Type().iconSmall();
float iconFsz = iconFont->LegacySize;
float cell = 30.0f * dp, cellGap = 4.0f * dp;
ImGui::TextUnformatted(TR("portfolio_icon"));
ImGui::BeginChild("##pfIconGrid", ImVec2(0, cell * 2.0f + cellGap + Layout::spacingSm() * 2.0f), true);
ImDrawList* gdl = ImGui::GetWindowDrawList();
float availW = ImGui::GetContentRegionAvail().x;
int cols = std::max(1, (int)((availW + cellGap) / (cell + cellGap)));
int total = material::project_icons::walletIconCount() + 1; // slot 0 = "None"
int col = 0;
for (int k = 0; k < total; k++) {
if (col != 0) ImGui::SameLine(0, cellGap);
ImVec2 mn = ImGui::GetCursorScreenPos();
ImVec2 mx(mn.x + cell, mn.y + cell);
ImVec2 cc(mn.x + cell * 0.5f, mn.y + cell * 0.5f);
bool hov = ImGui::IsMouseHoveringRect(mn, mx);
bool isNone = (k == 0);
const char* nm = isNone ? "" : material::project_icons::walletIconName(k - 1);
bool sel = isNone ? s_pf_icon.empty() : (s_pf_icon == nm);
if (sel) {
gdl->AddRectFilled(mn, mx, WithAlpha(Primary(), 40), 6.0f * dp);
gdl->AddRect(mn, mx, WithAlpha(Primary(), 120), 6.0f * dp, 0, 1.5f * dp);
} else if (hov) {
gdl->AddRectFilled(mn, mx, IM_COL32(255, 255, 255, 20), 6.0f * dp);
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("portfolio_appearance"));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Live preview of the group card (icon + color + name + summed balance).
{
ImDrawList* pdl = ImGui::GetWindowDrawList();
float ppad = Layout::spacingMd();
float pw = ImGui::GetContentRegionAvail().x;
float ph = ppad * 2.0f + sub1f->LegacySize + Layout::spacingXs() + capF->LegacySize;
ImVec2 pMin = ImGui::GetCursorScreenPos();
ImVec2 pMax(pMin.x + pw, pMin.y + ph);
ImU32 accent = s_pf_color ? (ImU32)s_pf_color : 0;
GlassPanelSpec g; g.rounding = 10.0f; g.fillAlpha = 22; g.borderAlpha = 40;
DrawGlassPanel(pdl, pMin, pMax, g);
if (accent) {
float bx = pMin.x + 2.0f;
pdl->AddRectFilled(ImVec2(bx, pMin.y + ppad * 0.6f),
ImVec2(bx + 3.0f, pMax.y - ppad * 0.6f), accent, 2.0f);
}
ImU32 icol = sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium());
if (isNone) { // slashed circle = "no icon"
float r = cell * 0.26f;
gdl->AddCircle(cc, r, icol, 0, 1.5f * dp);
gdl->AddLine(ImVec2(cc.x - r * 0.7f, cc.y + r * 0.7f),
ImVec2(cc.x + r * 0.7f, cc.y - r * 0.7f), icol, 1.5f * dp);
} else {
material::project_icons::drawByName(gdl, nm, cc, icol, iconFont, iconFsz);
float rowY = pMin.y + ppad, nameX = pMin.x + ppad + (accent ? 6.0f : 0.0f);
if (!s_pf_icon.empty()) {
material::project_icons::drawByName(pdl, s_pf_icon,
ImVec2(nameX + sub1f->LegacySize * 0.5f, rowY + sub1f->LegacySize * 0.5f),
accent ? accent : OnSurface(), iconFont, sub1f->LegacySize);
nameX += sub1f->LegacySize + Layout::spacingSm();
}
ImGui::PushID(k);
ImGui::InvisibleButton("##pfic", ImVec2(cell, cell));
if (ImGui::IsItemClicked()) s_pf_icon = isNone ? std::string() : std::string(nm);
if (hov) material::Tooltip("%s", isNone ? TR("portfolio_no_icon") : nm);
ImGui::PopID();
col = (col + 1) % cols;
const char* nm = s_pf_label[0] ? s_pf_label : TR("portfolio_group_name");
pdl->AddText(sub1f, sub1f->LegacySize, ImVec2(nameX, rowY),
s_pf_label[0] ? OnSurface() : OnSurfaceDisabled(), nm);
double pbal = data::SumPortfolioBalance(s_pf_addrs, state.addresses);
char pbuf[64];
snprintf(pbuf, sizeof(pbuf), "%.4f %s", pbal, DRAGONX_TICKER);
float pbw = body2f->CalcTextSizeA(body2f->LegacySize, FLT_MAX, 0, pbuf).x;
pdl->AddText(body2f, body2f->LegacySize,
ImVec2(pMax.x - ppad - pbw, rowY + (sub1f->LegacySize - body2f->LegacySize) * 0.5f),
OnSurface(), pbuf);
if (state.market.price_usd > 0) {
snprintf(pbuf, sizeof(pbuf), "$%.2f", pbal * state.market.price_usd);
float uw = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, pbuf).x;
pdl->AddText(capF, capF->LegacySize,
ImVec2(pMax.x - ppad - uw, rowY + sub1f->LegacySize + Layout::spacingXs()),
OnSurfaceDisabled(), pbuf);
}
ImGui::Dummy(ImVec2(pw, ph));
}
ImGui::EndChild();
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
// ---- Color picker ----
{
float dp = Layout::dpiScale();
float sw = 24.0f * dp;
ImGui::TextUnformatted(TR("portfolio_color"));
ImDrawList* cdl = ImGui::GetWindowDrawList();
int nColors = (int)(sizeof(kPfPalette) / sizeof(kPfPalette[0]));
for (int c = 0; c <= nColors; c++) { // slot 0 = "default / no color"
if (c != 0) ImGui::SameLine(0, 6.0f * dp);
ImVec2 mn = ImGui::GetCursorScreenPos();
ImVec2 cc(mn.x + sw * 0.5f, mn.y + sw * 0.5f);
bool isDefault = (c == 0);
bool sel = isDefault ? (s_pf_color == 0u) : (s_pf_color == kPfPalette[c - 1]);
bool hov = ImGui::IsMouseHoveringRect(mn, ImVec2(mn.x + sw, mn.y + sw));
if (isDefault)
cdl->AddCircle(cc, sw * 0.40f, OnSurfaceMedium(), 0, 1.5f * dp);
else
cdl->AddCircleFilled(cc, sw * 0.40f, kPfPalette[c - 1]);
if (sel) cdl->AddCircle(cc, sw * 0.50f, OnSurface(), 0, 2.0f * dp);
else if (hov) cdl->AddCircle(cc, sw * 0.50f, WithAlpha(OnSurface(), 120), 0, 1.5f * dp);
ImGui::PushID(2000 + c);
ImGui::InvisibleButton("##pfcol", ImVec2(sw, sw));
if (ImGui::IsItemClicked()) s_pf_color = isDefault ? 0u : kPfPalette[c - 1];
ImGui::PopID();
// Label
ImGui::TextUnformatted(TR("portfolio_label"));
ImGui::SetNextItemWidth(-1);
ImGui::InputText("##pfLabel", s_pf_label, sizeof(s_pf_label));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Icon picker
{
float cell = 28.0f * dp, cellGap = 4.0f * dp;
ImGui::TextUnformatted(TR("portfolio_icon"));
ImGui::BeginChild("##pfIconGrid", ImVec2(0, cell * 3.0f + cellGap * 2.0f + Layout::spacingSm() * 2.0f), true);
ImDrawList* gdl = ImGui::GetWindowDrawList();
float availW = ImGui::GetContentRegionAvail().x;
int cols = std::max(1, (int)((availW + cellGap) / (cell + cellGap)));
int total = material::project_icons::walletIconCount() + 1; // slot 0 = "None"
int gcol = 0;
for (int k = 0; k < total; k++) {
if (gcol != 0) ImGui::SameLine(0, cellGap);
ImVec2 mn = ImGui::GetCursorScreenPos();
ImVec2 mx(mn.x + cell, mn.y + cell);
ImVec2 cc(mn.x + cell * 0.5f, mn.y + cell * 0.5f);
bool hov = ImGui::IsMouseHoveringRect(mn, mx);
bool isNone = (k == 0);
const char* nm2 = isNone ? "" : material::project_icons::walletIconName(k - 1);
bool sel = isNone ? s_pf_icon.empty() : (s_pf_icon == nm2);
if (sel) {
gdl->AddRectFilled(mn, mx, WithAlpha(Primary(), 40), 6.0f * dp);
gdl->AddRect(mn, mx, WithAlpha(Primary(), 120), 6.0f * dp, 0, 1.5f * dp);
} else if (hov) {
gdl->AddRectFilled(mn, mx, IM_COL32(255, 255, 255, 20), 6.0f * dp);
}
ImU32 icol = sel ? Primary() : (hov ? OnSurface() : OnSurfaceMedium());
if (isNone) {
float r = cell * 0.26f;
gdl->AddCircle(cc, r, icol, 0, 1.5f * dp);
gdl->AddLine(ImVec2(cc.x - r * 0.7f, cc.y + r * 0.7f),
ImVec2(cc.x + r * 0.7f, cc.y - r * 0.7f), icol, 1.5f * dp);
} else {
material::project_icons::drawByName(gdl, nm2, cc, icol, iconFont, iconFsz);
}
ImGui::PushID(k);
ImGui::InvisibleButton("##pfic", ImVec2(cell, cell));
if (ImGui::IsItemClicked()) s_pf_icon = isNone ? std::string() : std::string(nm2);
if (hov) material::Tooltip("%s", isNone ? TR("portfolio_no_icon") : nm2);
ImGui::PopID();
gcol = (gcol + 1) % cols;
}
ImGui::EndChild();
}
}
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Quick group selectors.
if (ImGui::Button(TR("portfolio_all_shielded"))) {
for (const auto& a : state.addresses)
if (a.type == "shielded") data::PortfolioEntryAdd(s_pf_addrs, a.address);
}
ImGui::SameLine();
if (ImGui::Button(TR("portfolio_all_transparent"))) {
for (const auto& a : state.addresses)
if (a.type == "transparent") data::PortfolioEntryAdd(s_pf_addrs, a.address);
}
ImGui::SameLine();
if (ImGui::Button(TR("portfolio_clear_sel"))) s_pf_addrs.clear();
char selc[48];
snprintf(selc, sizeof(selc), TR("portfolio_addresses_sel"), (int)s_pf_addrs.size());
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), selc);
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Address checklist. The overlay dialog auto-resizes to its content
// (ImGuiChildFlags_AutoResizeY), so a negative "fill-remaining" height collapses to ~0
// here — size the list to its rows instead, capped at 40% of the viewport so a large
// wallet scrolls rather than running off-screen.
float pfRowH = ImGui::GetFrameHeightWithSpacing();
int pfRows = std::max(1, (int)state.addresses.size());
float pfListH = std::min(pfRows * pfRowH, ImGui::GetMainViewport()->Size.y * 0.4f);
ImGui::BeginChild("##pfAddrList", ImVec2(0, pfListH), true);
for (const auto& a : state.addresses) {
bool inSet = data::PortfolioEntryContains(s_pf_addrs, a.address);
char line[192];
snprintf(line, sizeof(line), "%s (%.4f %s)", a.address.c_str(), a.balance, DRAGONX_TICKER);
ImGui::PushID(a.address.c_str());
if (ImGui::Checkbox(line, &inSet)) {
if (inSet) data::PortfolioEntryAdd(s_pf_addrs, a.address);
else data::PortfolioEntryRemove(s_pf_addrs, a.address);
// Color picker
{
float sw = 22.0f * dp;
ImGui::TextUnformatted(TR("portfolio_color"));
ImDrawList* cdl = ImGui::GetWindowDrawList();
int nColors = (int)(sizeof(kPfPalette) / sizeof(kPfPalette[0]));
for (int c = 0; c <= nColors; c++) { // slot 0 = default / no color
if (c != 0) ImGui::SameLine(0, 5.0f * dp);
ImVec2 mn = ImGui::GetCursorScreenPos();
ImVec2 cc(mn.x + sw * 0.5f, mn.y + sw * 0.5f);
bool isDefault = (c == 0);
bool sel = isDefault ? (s_pf_color == 0u) : (s_pf_color == kPfPalette[c - 1]);
bool hov = ImGui::IsMouseHoveringRect(mn, ImVec2(mn.x + sw, mn.y + sw));
if (isDefault) cdl->AddCircle(cc, sw * 0.40f, OnSurfaceMedium(), 0, 1.5f * dp);
else cdl->AddCircleFilled(cc, sw * 0.40f, kPfPalette[c - 1]);
if (sel) cdl->AddCircle(cc, sw * 0.50f, OnSurface(), 0, 2.0f * dp);
else if (hov) cdl->AddCircle(cc, sw * 0.50f, WithAlpha(OnSurface(), 120), 0, 1.5f * dp);
ImGui::PushID(2000 + c);
ImGui::InvisibleButton("##pfcol", ImVec2(sw, sw));
if (ImGui::IsItemClicked()) s_pf_color = isDefault ? 0u : kPfPalette[c - 1];
ImGui::PopID();
}
}
ImGui::PopID();
}
ImGui::EndChild();
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::SameLine(0, colGap);
// ===================== RIGHT: ADDRESSES =====================
ImGui::BeginChild("##pfRight", ImVec2(rightW, colH), false);
{
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("portfolio_addresses_hdr"));
ImGui::SameLine();
{
char selc[48];
snprintf(selc, sizeof(selc), TR("portfolio_addresses_sel"), (int)s_pf_addrs.size());
float w = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, selc).x;
float ax = ImGui::GetContentRegionAvail().x;
if (ax > w) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + ax - w);
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), selc);
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Search
ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pf_search, sizeof(s_pf_search));
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Type filter chips + funded toggle
{
const char* flabels[3] = { TR("portfolio_select_all"), TR("shielded"), TR("transparent") };
for (int f = 0; f < 3; f++) {
if (f) ImGui::SameLine(0, 4.0f * dp);
bool active = (s_pf_type_filter == f);
if (active) ImGui::PushStyleColor(ImGuiCol_Button, ImGui::ColorConvertU32ToFloat4(WithAlpha(Primary(), 200)));
if (ImGui::SmallButton((std::string(flabels[f]) + "##ptf" + std::to_string(f)).c_str()))
s_pf_type_filter = f;
if (active) ImGui::PopStyleColor();
}
ImGui::SameLine(0, Layout::spacingMd());
ImGui::Checkbox(TR("portfolio_funded"), &s_pf_funded_only);
}
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Build the filtered address list (selected first, then by balance desc).
std::string search = s_pf_search;
std::vector<const AddressInfo*> filtered;
for (const auto& a : state.addresses) {
if (s_pf_type_filter == 1 && a.type != "shielded") continue;
if (s_pf_type_filter == 2 && a.type != "transparent") continue;
if (s_pf_funded_only && a.balance <= 1e-9) continue;
if (!search.empty()) {
std::string hay = a.address + " " + app->getAddressLabel(a.address);
if (!util::containsIgnoreCase(hay, search)) continue;
}
filtered.push_back(&a);
}
std::sort(filtered.begin(), filtered.end(), [&](const AddressInfo* x, const AddressInfo* y) {
bool sx = data::PortfolioEntryContains(s_pf_addrs, x->address);
bool sy = data::PortfolioEntryContains(s_pf_addrs, y->address);
if (sx != sy) return sx;
return x->balance > y->balance;
});
// Select-shown / clear
if (ImGui::SmallButton(TR("portfolio_select_shown")))
for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pf_addrs, a->address);
ImGui::SameLine();
if (ImGui::SmallButton(TR("portfolio_clear_sel"))) s_pf_addrs.clear();
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Address list fills the remaining height in the right column.
float listH = std::max(60.0f, ImGui::GetContentRegionAvail().y);
ImGui::BeginChild("##pfAddrList", ImVec2(0, listH), true);
ImDrawList* ldl = ImGui::GetWindowDrawList();
float rowH = std::max(26.0f * dp, body2f->LegacySize + Layout::spacingSm());
float lw = ImGui::GetContentRegionAvail().x;
if (filtered.empty())
Type().textColored(TypeStyle::Caption, OnSurfaceDisabled(), TR("portfolio_no_addr_match"));
for (const AddressInfo* ap : filtered) {
const AddressInfo& a = *ap;
bool inSet = data::PortfolioEntryContains(s_pf_addrs, a.address);
ImVec2 rmn = ImGui::GetCursorScreenPos();
ImVec2 rmx(rmn.x + lw, rmn.y + rowH);
bool rhov = ImGui::IsMouseHoveringRect(rmn, rmx);
if (inSet) ldl->AddRectFilled(rmn, rmx, WithAlpha(Primary(), 22), 4.0f);
else if (rhov) ldl->AddRectFilled(rmn, rmx, IM_COL32(255, 255, 255, 14), 4.0f);
float rcy = rmn.y + rowH * 0.5f;
float rx = rmn.x + Layout::spacingSm();
// Checkbox indicator
float cb = 15.0f * dp;
ImVec2 cbMin(rx, rcy - cb * 0.5f), cbMax(rx + cb, rcy + cb * 0.5f);
if (inSet) {
ldl->AddRectFilled(cbMin, cbMax, Primary(), 3.0f * dp);
ldl->AddLine(ImVec2(cbMin.x + cb * 0.22f, rcy),
ImVec2(cbMin.x + cb * 0.42f, cbMax.y - cb * 0.25f), IM_COL32(255, 255, 255, 255), 1.6f * dp);
ldl->AddLine(ImVec2(cbMin.x + cb * 0.42f, cbMax.y - cb * 0.25f),
ImVec2(cbMin.x + cb * 0.80f, cbMin.y + cb * 0.22f), IM_COL32(255, 255, 255, 255), 1.6f * dp);
} else {
ldl->AddRect(cbMin, cbMax, WithAlpha(OnSurface(), 120), 3.0f * dp, 0, 1.2f * dp);
}
rx += cb + Layout::spacingSm();
// Type chip (Z / T)
bool isZ = (a.type == "shielded");
const char* chip = isZ ? "Z" : "T";
ImU32 chipCol = isZ ? Success() : Warning();
float chipW = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, chip).x + 8.0f * dp;
ldl->AddRectFilled(ImVec2(rx, rcy - capF->LegacySize * 0.5f - 2.0f),
ImVec2(rx + chipW, rcy + capF->LegacySize * 0.5f + 2.0f), WithAlpha(chipCol, 40), 4.0f * dp);
ldl->AddText(capF, capF->LegacySize, ImVec2(rx + 4.0f * dp, rcy - capF->LegacySize * 0.5f), chipCol, chip);
rx += chipW + Layout::spacingSm();
// Address's own icon (if set)
std::string aicon = app->getAddressIcon(a.address);
if (!aicon.empty()) {
material::project_icons::drawByName(ldl, aicon, ImVec2(rx + iconFsz * 0.5f, rcy), OnSurfaceMedium(), iconFont, iconFsz);
rx += iconFsz + Layout::spacingSm();
}
// Balance (right) then primary text (label or truncated address).
char balbuf[48];
snprintf(balbuf, sizeof(balbuf), "%.4f", a.balance);
float balW = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, balbuf).x;
float textRight = rmx.x - Layout::spacingSm() - balW - Layout::spacingSm();
std::string alabel = app->getAddressLabel(a.address);
std::string primary = alabel.empty() ? util::truncateMiddle(a.address, 22) : alabel;
float availTextW = textRight - rx;
while (primary.size() > 1 &&
body2f->CalcTextSizeA(body2f->LegacySize, FLT_MAX, 0, primary.c_str()).x > availTextW)
primary.pop_back();
ldl->AddText(body2f, body2f->LegacySize, ImVec2(rx, rcy - body2f->LegacySize * 0.5f),
inSet ? OnSurface() : OnSurfaceMedium(), primary.c_str());
ldl->AddText(capF, capF->LegacySize, ImVec2(rmx.x - Layout::spacingSm() - balW, rcy - capF->LegacySize * 0.5f),
a.balance > 1e-9 ? OnSurface() : OnSurfaceDisabled(), balbuf);
// Whole row toggles selection.
ImGui::PushID(a.address.c_str());
ImGui::InvisibleButton("##pfrow", ImVec2(lw, rowH));
if (ImGui::IsItemClicked()) {
if (inSet) data::PortfolioEntryRemove(s_pf_addrs, a.address);
else data::PortfolioEntryAdd(s_pf_addrs, a.address);
}
ImGui::PopID();
}
ImGui::EndChild();
}
ImGui::EndChild();
// ===================== ACTIONS =====================
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
bool canSave = s_pf_label[0] != '\0';
ImGui::BeginDisabled(!canSave);
if (ImGui::Button(TR("portfolio_save"))) {

View File

@@ -1123,6 +1123,14 @@ void I18n::loadBuiltinEnglish()
strings_["portfolio_icon"] = "Icon";
strings_["portfolio_color"] = "Color";
strings_["portfolio_no_icon"] = "None";
strings_["portfolio_appearance"] = "APPEARANCE";
strings_["portfolio_addresses_hdr"] = "ADDRESSES";
strings_["portfolio_search"] = "Search addresses\xE2\x80\xA6";
strings_["portfolio_funded"] = "Funded";
strings_["portfolio_select_all"] = "All";
strings_["portfolio_select_shown"] = "Select shown";
strings_["portfolio_group_name"] = "Group name";
strings_["portfolio_no_addr_match"] = "No addresses match";
strings_["portfolio_manage_title"] = "Manage portfolio";
strings_["portfolio_add_entry"] = "Add entry";
strings_["portfolio_new_entry"] = "New entry";