From d838243caaac391f545836c094f24bd3eea046bf Mon Sep 17 00:00:00 2001 From: DanS Date: Thu, 2 Jul 2026 23:44:16 -0500 Subject: [PATCH] feat(market): redesign the Manage-portfolio editor as a two-column layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/ui/windows/market_tab.cpp | 396 ++++++++++++++++++++++++---------- src/util/i18n.cpp | 8 + 2 files changed, 293 insertions(+), 111 deletions(-) diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index f8b8b6a..f800a67 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -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 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 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"))) { diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 74a7b85..912ca23 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -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";