refactor(market): extract portfolio detail sections into helpers
Pull the three detail-pane section blocks out of RenderPortfolioEditor into pfDrawAppearanceSection / pfDrawPriceSection / pfDrawAddressSection (each recomputes its own dp/fonts/state; the section dispatch is a 3-line switch). Verbatim move — same widgets, IDs and drawing. RenderPortfolioEditor drops from ~760 to ~357 lines. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -431,6 +431,356 @@ static void pfCommitIfNeeded(config::Settings* settings)
|
||||
pfPersist(settings, entries);
|
||||
}
|
||||
|
||||
// ---- Detail-pane section renderers (drawn inside ##pfDetailBody; operate on s_pfEdit) ----
|
||||
|
||||
// Appearance: accent-color picker (+ custom), outline-opacity slider, icon grid.
|
||||
static void pfDrawAppearanceSection()
|
||||
{
|
||||
float dp = Layout::dpiScale();
|
||||
// Color picker: preset swatches in ONE row (overflow hidden) + a custom "+" cell that is
|
||||
// always visible at the right and opens a picker.
|
||||
{
|
||||
float sw = 22.0f * dp, gap = 5.0f * dp;
|
||||
ImGui::TextUnformatted(TR("portfolio_color"));
|
||||
ImDrawList* cdl = ImGui::GetWindowDrawList();
|
||||
int nColors = (int)(sizeof(kPfPalette) / sizeof(kPfPalette[0]));
|
||||
float availW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 rowStart = ImGui::GetCursorScreenPos();
|
||||
// Reserved "+" slot at the right; inset by a few px so its selection outline
|
||||
// (drawn at radius sw*0.5) stays inside the modal instead of clipping at the edge.
|
||||
float plusX = rowStart.x + std::max(sw, availW - sw - 4.0f * dp);
|
||||
bool isCustom = (s_pfEdit.color != 0u);
|
||||
for (int c = 0; c < nColors; c++) if (s_pfEdit.color == kPfPalette[c]) isCustom = false;
|
||||
bool openCustom = false;
|
||||
// Preset swatches (0 = default/no color, 1..N = palette); stop before the "+" slot.
|
||||
float x = rowStart.x;
|
||||
for (int idx = 0; idx <= nColors; idx++) {
|
||||
if (x + sw > plusX - gap) break; // would reach the "+" -> hide the remaining colors
|
||||
ImVec2 mn(x, rowStart.y);
|
||||
ImVec2 cc(mn.x + sw * 0.5f, mn.y + sw * 0.5f);
|
||||
bool isDefault = (idx == 0);
|
||||
ImU32 col32 = isDefault ? 0u : kPfPalette[idx - 1];
|
||||
bool sel = isDefault ? (s_pfEdit.color == 0u) : (s_pfEdit.color == col32);
|
||||
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, col32);
|
||||
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 + idx);
|
||||
ImGui::SetCursorScreenPos(mn);
|
||||
ImGui::InvisibleButton("##pfcol", ImVec2(sw, sw));
|
||||
if (ImGui::IsItemClicked()) s_pfEdit.color = isDefault ? 0u : col32;
|
||||
ImGui::PopID();
|
||||
x += sw + gap;
|
||||
}
|
||||
// Custom "+" cell — always visible at the reserved right slot.
|
||||
{
|
||||
ImVec2 mn(plusX, rowStart.y);
|
||||
ImVec2 cc(mn.x + sw * 0.5f, mn.y + sw * 0.5f);
|
||||
bool hov = ImGui::IsMouseHoveringRect(mn, ImVec2(mn.x + sw, mn.y + sw));
|
||||
if (isCustom) {
|
||||
cdl->AddCircleFilled(cc, sw * 0.40f, (ImU32)s_pfEdit.color);
|
||||
} else {
|
||||
cdl->AddCircle(cc, sw * 0.40f, OnSurfaceMedium(), 0, 1.5f * dp);
|
||||
float pr = sw * 0.18f;
|
||||
cdl->AddLine(ImVec2(cc.x - pr, cc.y), ImVec2(cc.x + pr, cc.y), OnSurfaceMedium(), 1.5f * dp);
|
||||
cdl->AddLine(ImVec2(cc.x, cc.y - pr), ImVec2(cc.x, cc.y + pr), OnSurfaceMedium(), 1.5f * dp);
|
||||
}
|
||||
if (isCustom) 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(2999);
|
||||
ImGui::SetCursorScreenPos(mn);
|
||||
ImGui::InvisibleButton("##pfcustomcol", ImVec2(sw, sw));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
ImU32 base = s_pfEdit.color ? (ImU32)s_pfEdit.color : IM_COL32(0x4F, 0x9D, 0xFF, 0xFF);
|
||||
s_pfEdit.customRgb[0] = ((base >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
|
||||
s_pfEdit.customRgb[1] = ((base >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
|
||||
s_pfEdit.customRgb[2] = ((base >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
|
||||
openCustom = true;
|
||||
}
|
||||
if (hov) material::Tooltip("%s", TR("portfolio_custom_color"));
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::SetCursorScreenPos(ImVec2(rowStart.x, rowStart.y + sw));
|
||||
ImGui::Dummy(ImVec2(availW, 0));
|
||||
if (openCustom) ImGui::OpenPopup("##pfCustomColor");
|
||||
if (ImGui::BeginPopup("##pfCustomColor")) {
|
||||
ImGui::ColorPicker3("##pfpick", s_pfEdit.customRgb,
|
||||
ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoLabel);
|
||||
s_pfEdit.color = IM_COL32((int)(s_pfEdit.customRgb[0] * 255.0f + 0.5f),
|
||||
(int)(s_pfEdit.customRgb[1] * 255.0f + 0.5f),
|
||||
(int)(s_pfEdit.customRgb[2] * 255.0f + 0.5f), 255);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
// Accent-outline opacity (only meaningful when an accent color is set).
|
||||
ImGui::BeginDisabled(s_pfEdit.color == 0u);
|
||||
ImGui::TextUnformatted(TR("portfolio_outline_opacity"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::SliderInt("##pfOutlineOpacity", &s_pfEdit.outlineOpacity, 0, 100, "%d%%");
|
||||
if (s_pfEdit.outlineOpacity < 0) s_pfEdit.outlineOpacity = 0;
|
||||
if (s_pfEdit.outlineOpacity > 100) s_pfEdit.outlineOpacity = 100;
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
// Icon picker: search on the heading line + a 12-per-row grid that scales to width,
|
||||
// smooth-scrolled with the Settings-tab edge fade.
|
||||
{
|
||||
float cellGap = 6.0f * dp;
|
||||
ImFont* gIconFont = Type().iconXL();
|
||||
ImGui::TextUnformatted(TR("portfolio_icon"));
|
||||
ImGui::SameLine();
|
||||
{
|
||||
float searchW = std::min(ImGui::GetContentRegionAvail().x, 200.0f * dp);
|
||||
float off = ImGui::GetContentRegionAvail().x - searchW; // right-align the field
|
||||
if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off);
|
||||
ImGui::SetNextItemWidth(searchW);
|
||||
ImGui::InputTextWithHint("##pfIconSearch", TR("portfolio_search_icons"),
|
||||
s_pfEdit.iconSearch, sizeof(s_pfEdit.iconSearch));
|
||||
}
|
||||
// Filtered icon list (-1 = "None", shown only when not searching).
|
||||
std::string isearch = s_pfEdit.iconSearch;
|
||||
std::vector<int> iconIdx;
|
||||
if (isearch.empty()) iconIdx.push_back(-1);
|
||||
int nIcons = material::project_icons::walletIconCount();
|
||||
for (int i = 0; i < nIcons; i++) {
|
||||
if (!isearch.empty() &&
|
||||
!util::containsIgnoreCase(material::project_icons::walletIconName(i), isearch))
|
||||
continue;
|
||||
iconIdx.push_back(i);
|
||||
}
|
||||
ImDrawList* gdl = pfBeginScrollChild("##pfIconGrid", s_pfEdit.iconFade,
|
||||
ImVec2(Layout::spacingSm(), Layout::spacingSm()), dp);
|
||||
const int cols = 12;
|
||||
float availW = ImGui::GetContentRegionAvail().x;
|
||||
float cell = std::max(14.0f * dp, (availW - cellGap * (cols - 1)) / (float)cols);
|
||||
float gIconSz = cell * 0.5f;
|
||||
float gridW = cell * cols + cellGap * (cols - 1);
|
||||
float indent = std::max(0.0f, (availW - gridW) * 0.5f);
|
||||
int gcol = 0;
|
||||
for (size_t n = 0; n < iconIdx.size(); n++) {
|
||||
int idx = iconIdx[n];
|
||||
if (gcol == 0) { if (indent > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + indent); }
|
||||
else 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 = (idx < 0);
|
||||
const char* nm2 = isNone ? "" : material::project_icons::walletIconName(idx);
|
||||
bool sel = isNone ? s_pfEdit.icon.empty() : (s_pfEdit.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, gIconFont, gIconSz);
|
||||
}
|
||||
ImGui::PushID(idx + 2);
|
||||
ImGui::InvisibleButton("##pfic", ImVec2(cell, cell));
|
||||
if (ImGui::IsItemClicked()) s_pfEdit.icon = isNone ? std::string() : std::string(nm2);
|
||||
if (hov) material::Tooltip("%s", isNone ? TR("portfolio_no_icon") : nm2);
|
||||
ImGui::PopID();
|
||||
gcol = (gcol + 1) % cols;
|
||||
}
|
||||
pfEndScrollChild(s_pfEdit.iconFade, gdl);
|
||||
}
|
||||
}
|
||||
|
||||
// Price: basis radios, optional manual price/currency, shown-field toggles, sparkline interval.
|
||||
static void pfDrawPriceSection()
|
||||
{
|
||||
// Price basis — radio group (all options visible; no dropdown).
|
||||
const char* basisItems[4] = { TR("portfolio_price_usd"), TR("portfolio_price_btc"),
|
||||
TR("portfolio_price_drgx"), TR("portfolio_price_manual") };
|
||||
for (int b = 0; b < 4; b++) { if (b) ImGui::SameLine(); ImGui::RadioButton(basisItems[b], &s_pfEdit.priceBasis, b); }
|
||||
if (s_pfEdit.priceBasis == 3) { // Manual: price-per-DRGX + currency label
|
||||
float half = (ImGui::GetContentRegionAvail().x - Layout::spacingSm()) * 0.62f;
|
||||
ImGui::SetNextItemWidth(half);
|
||||
ImGui::InputDouble("##pfManPrice", &s_pfEdit.manualPrice, 0.0, 0.0, "%.6f");
|
||||
if (s_pfEdit.manualPrice < 0.0) s_pfEdit.manualPrice = 0.0;
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##pfManCcy", TR("portfolio_currency"), s_pfEdit.manualCcy, sizeof(s_pfEdit.manualCcy));
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
// Shown fields.
|
||||
ImGui::TextUnformatted(TR("portfolio_show"));
|
||||
ImGui::Checkbox(DRAGONX_TICKER "##pfShowDrgx", &s_pfEdit.showDrgx);
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(s_pfEdit.priceBasis == 2); // DRGX-only -> no value field
|
||||
ImGui::Checkbox(TR("portfolio_show_value"), &s_pfEdit.showValue);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
// 24h + sparkline toggles share one line (both live-market only).
|
||||
ImGui::BeginDisabled(!(s_pfEdit.priceBasis == 0 || s_pfEdit.priceBasis == 1));
|
||||
ImGui::Checkbox(TR("portfolio_show_24h"), &s_pfEdit.show24h);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox(TR("portfolio_show_sparkline"), &s_pfEdit.showSparkline);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
// Sparkline interval — radio group (enabled only when the sparkline is shown).
|
||||
ImGui::BeginDisabled(!s_pfEdit.showSparkline || !(s_pfEdit.priceBasis == 0 || s_pfEdit.priceBasis == 1));
|
||||
const char* spItems[5] = { TR("portfolio_spark_min"), TR("portfolio_spark_hour"),
|
||||
TR("portfolio_spark_day"), TR("portfolio_spark_week"),
|
||||
TR("portfolio_spark_month") };
|
||||
for (int s = 0; s < 5; s++) { if (s) ImGui::SameLine(); ImGui::RadioButton(spItems[s], &s_pfEdit.sparkInterval, s); }
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
// Addresses: selection count, search, type-filter/select row, and the pickable address list.
|
||||
static void pfDrawAddressSection(App* app)
|
||||
{
|
||||
float dp = Layout::dpiScale();
|
||||
const auto& state = app->getWalletState();
|
||||
ImFont* body2f = Type().body2();
|
||||
ImFont* capF = Type().caption();
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
float iconFsz = iconFont->LegacySize;
|
||||
{
|
||||
char selc[48];
|
||||
snprintf(selc, sizeof(selc), TR("portfolio_addresses_sel"), (int)s_pfEdit.addrs.size());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), selc);
|
||||
}
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pfEdit.search, sizeof(s_pfEdit.search));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
// Filter + selection on one row: type-filter segmented (left) + Select all / Clear /
|
||||
// Funded (right). Select-all is deferred so it applies to the freshly-filtered set below.
|
||||
bool selAllClicked = false;
|
||||
{
|
||||
const char* flabels[3] = { TR("portfolio_select_all"), TR("shielded"), TR("transparent") };
|
||||
const char* selLbl = TR("portfolio_select_shown");
|
||||
const char* clrLbl = TR("portfolio_clear_sel");
|
||||
ImGuiStyle& st = ImGui::GetStyle();
|
||||
ImVec2 pillPad(Layout::spacingLg(), Layout::spacingSm()); // roomier text padding
|
||||
float defFrameH = ImGui::GetFrameHeight();
|
||||
float fundedW = defFrameH + st.ItemInnerSpacing.x + ImGui::CalcTextSize(TR("portfolio_funded")).x;
|
||||
float pillH = ImGui::GetTextLineHeight() + pillPad.y * 2.0f;
|
||||
float selW = ImGui::CalcTextSize(selLbl).x + pillPad.x * 2.0f;
|
||||
float clrW = ImGui::CalcTextSize(clrLbl).x + pillPad.x * 2.0f;
|
||||
float groupW = selW + clrW + fundedW + st.ItemSpacing.x * 2.0f;
|
||||
float segH = 26.0f * dp;
|
||||
float rowW = ImGui::GetContentRegionAvail().x;
|
||||
float segW = std::min(360.0f * dp, std::max(160.0f * dp, rowW - groupW - Layout::spacingMd()));
|
||||
float rowH = std::max(segH, pillH);
|
||||
ImVec2 rowStart = ImGui::GetCursorScreenPos();
|
||||
// Segmented type filter (left), vertically centered in the row.
|
||||
float segY = rowStart.y + (rowH - segH) * 0.5f;
|
||||
int tclk = pfSegmentedControl(ImGui::GetWindowDrawList(), ImVec2(rowStart.x, segY),
|
||||
segW, segH, flabels, 3, s_pfEdit.typeFilter, capF, "##pftf", dp);
|
||||
if (tclk >= 0) s_pfEdit.typeFilter = tclk;
|
||||
// Select all / Clear (rounded pills) + Funded toggle (right), vertically centered.
|
||||
float grpX = rowStart.x + std::max(0.0f, rowW - groupW);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 12.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, pillPad);
|
||||
ImGui::SetCursorScreenPos(ImVec2(grpX, rowStart.y + (rowH - pillH) * 0.5f));
|
||||
if (ImGui::Button(selLbl)) selAllClicked = true;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(clrLbl)) s_pfEdit.addrs.clear();
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x,
|
||||
rowStart.y + (rowH - defFrameH) * 0.5f)); // center vs. pills
|
||||
ImGui::Checkbox(TR("portfolio_funded"), &s_pfEdit.fundedOnly);
|
||||
ImGui::SetCursorScreenPos(ImVec2(rowStart.x, rowStart.y + rowH));
|
||||
ImGui::Dummy(ImVec2(rowW, 0));
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
std::string search = s_pfEdit.search;
|
||||
std::vector<const AddressInfo*> filtered;
|
||||
for (const auto& a : state.addresses) {
|
||||
if (s_pfEdit.typeFilter == 1 && a.type != "shielded") continue;
|
||||
if (s_pfEdit.typeFilter == 2 && a.type != "transparent") continue;
|
||||
if (s_pfEdit.fundedOnly && 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_pfEdit.addrs, x->address);
|
||||
bool sy = data::PortfolioEntryContains(s_pfEdit.addrs, y->address);
|
||||
if (sx != sy) return sx;
|
||||
return x->balance > y->balance;
|
||||
});
|
||||
if (selAllClicked)
|
||||
for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pfEdit.addrs, a->address);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImDrawList* ldl = pfBeginScrollChild("##pfAddrList", s_pfEdit.addrFade,
|
||||
ImVec2(Layout::spacingMd(), Layout::spacingSm()), dp);
|
||||
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_pfEdit.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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
}
|
||||
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);
|
||||
ImGui::PushID(a.address.c_str());
|
||||
ImGui::InvisibleButton("##pfrow", ImVec2(lw, rowH));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
if (inSet) data::PortfolioEntryRemove(s_pfEdit.addrs, a.address);
|
||||
else data::PortfolioEntryAdd(s_pfEdit.addrs, a.address);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
pfEndScrollChild(s_pfEdit.addrFade, ldl);
|
||||
}
|
||||
|
||||
// The "Manage portfolio" modal: list/add/edit/delete entries, each a label tied to a group
|
||||
// of wallet addresses. Persists to Settings on every mutation (like the address book).
|
||||
static void RenderPortfolioEditor(App* app)
|
||||
@@ -444,7 +794,6 @@ static void RenderPortfolioEditor(App* app)
|
||||
ImFont* capF = Type().caption();
|
||||
ImFont* sub1f = Type().subtitle1();
|
||||
ImFont* iconFont = Type().iconSmall();
|
||||
float iconFsz = iconFont->LegacySize;
|
||||
|
||||
// Persistence helpers are file-static (pfBuildWorking / pfWorkingMatches / pfCommitIfNeeded).
|
||||
|
||||
@@ -740,343 +1089,11 @@ static void RenderPortfolioEditor(App* app)
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingMd()));
|
||||
|
||||
// ---- APPEARANCE (color + outline opacity + icon) ----
|
||||
if (s_pfEdit.section == 0) {
|
||||
// Color picker: preset swatches in ONE row (overflow hidden) + a custom "+" cell that is
|
||||
// always visible at the right and opens a picker.
|
||||
{
|
||||
float sw = 22.0f * dp, gap = 5.0f * dp;
|
||||
ImGui::TextUnformatted(TR("portfolio_color"));
|
||||
ImDrawList* cdl = ImGui::GetWindowDrawList();
|
||||
int nColors = (int)(sizeof(kPfPalette) / sizeof(kPfPalette[0]));
|
||||
float availW = ImGui::GetContentRegionAvail().x;
|
||||
ImVec2 rowStart = ImGui::GetCursorScreenPos();
|
||||
// Reserved "+" slot at the right; inset by a few px so its selection outline
|
||||
// (drawn at radius sw*0.5) stays inside the modal instead of clipping at the edge.
|
||||
float plusX = rowStart.x + std::max(sw, availW - sw - 4.0f * dp);
|
||||
bool isCustom = (s_pfEdit.color != 0u);
|
||||
for (int c = 0; c < nColors; c++) if (s_pfEdit.color == kPfPalette[c]) isCustom = false;
|
||||
bool openCustom = false;
|
||||
// Preset swatches (0 = default/no color, 1..N = palette); stop before the "+" slot.
|
||||
float x = rowStart.x;
|
||||
for (int idx = 0; idx <= nColors; idx++) {
|
||||
if (x + sw > plusX - gap) break; // would reach the "+" -> hide the remaining colors
|
||||
ImVec2 mn(x, rowStart.y);
|
||||
ImVec2 cc(mn.x + sw * 0.5f, mn.y + sw * 0.5f);
|
||||
bool isDefault = (idx == 0);
|
||||
ImU32 col32 = isDefault ? 0u : kPfPalette[idx - 1];
|
||||
bool sel = isDefault ? (s_pfEdit.color == 0u) : (s_pfEdit.color == col32);
|
||||
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, col32);
|
||||
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 + idx);
|
||||
ImGui::SetCursorScreenPos(mn);
|
||||
ImGui::InvisibleButton("##pfcol", ImVec2(sw, sw));
|
||||
if (ImGui::IsItemClicked()) s_pfEdit.color = isDefault ? 0u : col32;
|
||||
ImGui::PopID();
|
||||
x += sw + gap;
|
||||
}
|
||||
// Custom "+" cell — always visible at the reserved right slot.
|
||||
{
|
||||
ImVec2 mn(plusX, rowStart.y);
|
||||
ImVec2 cc(mn.x + sw * 0.5f, mn.y + sw * 0.5f);
|
||||
bool hov = ImGui::IsMouseHoveringRect(mn, ImVec2(mn.x + sw, mn.y + sw));
|
||||
if (isCustom) {
|
||||
cdl->AddCircleFilled(cc, sw * 0.40f, (ImU32)s_pfEdit.color);
|
||||
} else {
|
||||
cdl->AddCircle(cc, sw * 0.40f, OnSurfaceMedium(), 0, 1.5f * dp);
|
||||
float pr = sw * 0.18f;
|
||||
cdl->AddLine(ImVec2(cc.x - pr, cc.y), ImVec2(cc.x + pr, cc.y), OnSurfaceMedium(), 1.5f * dp);
|
||||
cdl->AddLine(ImVec2(cc.x, cc.y - pr), ImVec2(cc.x, cc.y + pr), OnSurfaceMedium(), 1.5f * dp);
|
||||
}
|
||||
if (isCustom) 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(2999);
|
||||
ImGui::SetCursorScreenPos(mn);
|
||||
ImGui::InvisibleButton("##pfcustomcol", ImVec2(sw, sw));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
ImU32 base = s_pfEdit.color ? (ImU32)s_pfEdit.color : IM_COL32(0x4F, 0x9D, 0xFF, 0xFF);
|
||||
s_pfEdit.customRgb[0] = ((base >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
|
||||
s_pfEdit.customRgb[1] = ((base >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
|
||||
s_pfEdit.customRgb[2] = ((base >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
|
||||
openCustom = true;
|
||||
}
|
||||
if (hov) material::Tooltip("%s", TR("portfolio_custom_color"));
|
||||
ImGui::PopID();
|
||||
}
|
||||
ImGui::SetCursorScreenPos(ImVec2(rowStart.x, rowStart.y + sw));
|
||||
ImGui::Dummy(ImVec2(availW, 0));
|
||||
if (openCustom) ImGui::OpenPopup("##pfCustomColor");
|
||||
if (ImGui::BeginPopup("##pfCustomColor")) {
|
||||
ImGui::ColorPicker3("##pfpick", s_pfEdit.customRgb,
|
||||
ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoLabel);
|
||||
s_pfEdit.color = IM_COL32((int)(s_pfEdit.customRgb[0] * 255.0f + 0.5f),
|
||||
(int)(s_pfEdit.customRgb[1] * 255.0f + 0.5f),
|
||||
(int)(s_pfEdit.customRgb[2] * 255.0f + 0.5f), 255);
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
}
|
||||
// Accent-outline opacity (only meaningful when an accent color is set).
|
||||
ImGui::BeginDisabled(s_pfEdit.color == 0u);
|
||||
ImGui::TextUnformatted(TR("portfolio_outline_opacity"));
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::SliderInt("##pfOutlineOpacity", &s_pfEdit.outlineOpacity, 0, 100, "%d%%");
|
||||
if (s_pfEdit.outlineOpacity < 0) s_pfEdit.outlineOpacity = 0;
|
||||
if (s_pfEdit.outlineOpacity > 100) s_pfEdit.outlineOpacity = 100;
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
// Icon picker: search on the heading line + a 12-per-row grid that scales to width,
|
||||
// smooth-scrolled with the Settings-tab edge fade.
|
||||
{
|
||||
float cellGap = 6.0f * dp;
|
||||
ImFont* gIconFont = Type().iconXL();
|
||||
ImGui::TextUnformatted(TR("portfolio_icon"));
|
||||
ImGui::SameLine();
|
||||
{
|
||||
float searchW = std::min(ImGui::GetContentRegionAvail().x, 200.0f * dp);
|
||||
float off = ImGui::GetContentRegionAvail().x - searchW; // right-align the field
|
||||
if (off > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + off);
|
||||
ImGui::SetNextItemWidth(searchW);
|
||||
ImGui::InputTextWithHint("##pfIconSearch", TR("portfolio_search_icons"),
|
||||
s_pfEdit.iconSearch, sizeof(s_pfEdit.iconSearch));
|
||||
}
|
||||
// Filtered icon list (-1 = "None", shown only when not searching).
|
||||
std::string isearch = s_pfEdit.iconSearch;
|
||||
std::vector<int> iconIdx;
|
||||
if (isearch.empty()) iconIdx.push_back(-1);
|
||||
int nIcons = material::project_icons::walletIconCount();
|
||||
for (int i = 0; i < nIcons; i++) {
|
||||
if (!isearch.empty() &&
|
||||
!util::containsIgnoreCase(material::project_icons::walletIconName(i), isearch))
|
||||
continue;
|
||||
iconIdx.push_back(i);
|
||||
}
|
||||
ImDrawList* gdl = pfBeginScrollChild("##pfIconGrid", s_pfEdit.iconFade,
|
||||
ImVec2(Layout::spacingSm(), Layout::spacingSm()), dp);
|
||||
const int cols = 12;
|
||||
float availW = ImGui::GetContentRegionAvail().x;
|
||||
float cell = std::max(14.0f * dp, (availW - cellGap * (cols - 1)) / (float)cols);
|
||||
float gIconSz = cell * 0.5f;
|
||||
float gridW = cell * cols + cellGap * (cols - 1);
|
||||
float indent = std::max(0.0f, (availW - gridW) * 0.5f);
|
||||
int gcol = 0;
|
||||
for (size_t n = 0; n < iconIdx.size(); n++) {
|
||||
int idx = iconIdx[n];
|
||||
if (gcol == 0) { if (indent > 0.0f) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + indent); }
|
||||
else 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 = (idx < 0);
|
||||
const char* nm2 = isNone ? "" : material::project_icons::walletIconName(idx);
|
||||
bool sel = isNone ? s_pfEdit.icon.empty() : (s_pfEdit.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, gIconFont, gIconSz);
|
||||
}
|
||||
ImGui::PushID(idx + 2);
|
||||
ImGui::InvisibleButton("##pfic", ImVec2(cell, cell));
|
||||
if (ImGui::IsItemClicked()) s_pfEdit.icon = isNone ? std::string() : std::string(nm2);
|
||||
if (hov) material::Tooltip("%s", isNone ? TR("portfolio_no_icon") : nm2);
|
||||
ImGui::PopID();
|
||||
gcol = (gcol + 1) % cols;
|
||||
}
|
||||
pfEndScrollChild(s_pfEdit.iconFade, gdl);
|
||||
}
|
||||
}
|
||||
// ---- Detail sections (one shown at a time via the segmented control above) ----
|
||||
if (s_pfEdit.section == 0) pfDrawAppearanceSection();
|
||||
else if (s_pfEdit.section == 1) pfDrawPriceSection();
|
||||
else if (s_pfEdit.section == 2) pfDrawAddressSection(app);
|
||||
|
||||
// ---- PRICE (basis, manual price, shown fields, sparkline) ----
|
||||
if (s_pfEdit.section == 1) {
|
||||
// Price basis — radio group (all options visible; no dropdown).
|
||||
const char* basisItems[4] = { TR("portfolio_price_usd"), TR("portfolio_price_btc"),
|
||||
TR("portfolio_price_drgx"), TR("portfolio_price_manual") };
|
||||
for (int b = 0; b < 4; b++) { if (b) ImGui::SameLine(); ImGui::RadioButton(basisItems[b], &s_pfEdit.priceBasis, b); }
|
||||
if (s_pfEdit.priceBasis == 3) { // Manual: price-per-DRGX + currency label
|
||||
float half = (ImGui::GetContentRegionAvail().x - Layout::spacingSm()) * 0.62f;
|
||||
ImGui::SetNextItemWidth(half);
|
||||
ImGui::InputDouble("##pfManPrice", &s_pfEdit.manualPrice, 0.0, 0.0, "%.6f");
|
||||
if (s_pfEdit.manualPrice < 0.0) s_pfEdit.manualPrice = 0.0;
|
||||
ImGui::SameLine();
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##pfManCcy", TR("portfolio_currency"), s_pfEdit.manualCcy, sizeof(s_pfEdit.manualCcy));
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
// Shown fields.
|
||||
ImGui::TextUnformatted(TR("portfolio_show"));
|
||||
ImGui::Checkbox(DRAGONX_TICKER "##pfShowDrgx", &s_pfEdit.showDrgx);
|
||||
ImGui::SameLine();
|
||||
ImGui::BeginDisabled(s_pfEdit.priceBasis == 2); // DRGX-only -> no value field
|
||||
ImGui::Checkbox(TR("portfolio_show_value"), &s_pfEdit.showValue);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::SameLine();
|
||||
// 24h + sparkline toggles share one line (both live-market only).
|
||||
ImGui::BeginDisabled(!(s_pfEdit.priceBasis == 0 || s_pfEdit.priceBasis == 1));
|
||||
ImGui::Checkbox(TR("portfolio_show_24h"), &s_pfEdit.show24h);
|
||||
ImGui::SameLine();
|
||||
ImGui::Checkbox(TR("portfolio_show_sparkline"), &s_pfEdit.showSparkline);
|
||||
ImGui::EndDisabled();
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
|
||||
// Sparkline interval — radio group (enabled only when the sparkline is shown).
|
||||
ImGui::BeginDisabled(!s_pfEdit.showSparkline || !(s_pfEdit.priceBasis == 0 || s_pfEdit.priceBasis == 1));
|
||||
const char* spItems[5] = { TR("portfolio_spark_min"), TR("portfolio_spark_hour"),
|
||||
TR("portfolio_spark_day"), TR("portfolio_spark_week"),
|
||||
TR("portfolio_spark_month") };
|
||||
for (int s = 0; s < 5; s++) { if (s) ImGui::SameLine(); ImGui::RadioButton(spItems[s], &s_pfEdit.sparkInterval, s); }
|
||||
ImGui::EndDisabled();
|
||||
}
|
||||
|
||||
// ---- ADDRESSES (search, filter, select) ----
|
||||
if (s_pfEdit.section == 2) {
|
||||
{
|
||||
char selc[48];
|
||||
snprintf(selc, sizeof(selc), TR("portfolio_addresses_sel"), (int)s_pfEdit.addrs.size());
|
||||
Type().textColored(TypeStyle::Caption, OnSurfaceMedium(), selc);
|
||||
}
|
||||
ImGui::SetNextItemWidth(-1);
|
||||
ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pfEdit.search, sizeof(s_pfEdit.search));
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
// Filter + selection on one row: type-filter segmented (left) + Select all / Clear /
|
||||
// Funded (right). Select-all is deferred so it applies to the freshly-filtered set below.
|
||||
bool selAllClicked = false;
|
||||
{
|
||||
const char* flabels[3] = { TR("portfolio_select_all"), TR("shielded"), TR("transparent") };
|
||||
const char* selLbl = TR("portfolio_select_shown");
|
||||
const char* clrLbl = TR("portfolio_clear_sel");
|
||||
ImGuiStyle& st = ImGui::GetStyle();
|
||||
ImVec2 pillPad(Layout::spacingLg(), Layout::spacingSm()); // roomier text padding
|
||||
float defFrameH = ImGui::GetFrameHeight();
|
||||
float fundedW = defFrameH + st.ItemInnerSpacing.x + ImGui::CalcTextSize(TR("portfolio_funded")).x;
|
||||
float pillH = ImGui::GetTextLineHeight() + pillPad.y * 2.0f;
|
||||
float selW = ImGui::CalcTextSize(selLbl).x + pillPad.x * 2.0f;
|
||||
float clrW = ImGui::CalcTextSize(clrLbl).x + pillPad.x * 2.0f;
|
||||
float groupW = selW + clrW + fundedW + st.ItemSpacing.x * 2.0f;
|
||||
float segH = 26.0f * dp;
|
||||
float rowW = ImGui::GetContentRegionAvail().x;
|
||||
float segW = std::min(360.0f * dp, std::max(160.0f * dp, rowW - groupW - Layout::spacingMd()));
|
||||
float rowH = std::max(segH, pillH);
|
||||
ImVec2 rowStart = ImGui::GetCursorScreenPos();
|
||||
// Segmented type filter (left), vertically centered in the row.
|
||||
float segY = rowStart.y + (rowH - segH) * 0.5f;
|
||||
int tclk = pfSegmentedControl(ImGui::GetWindowDrawList(), ImVec2(rowStart.x, segY),
|
||||
segW, segH, flabels, 3, s_pfEdit.typeFilter, capF, "##pftf", dp);
|
||||
if (tclk >= 0) s_pfEdit.typeFilter = tclk;
|
||||
// Select all / Clear (rounded pills) + Funded toggle (right), vertically centered.
|
||||
float grpX = rowStart.x + std::max(0.0f, rowW - groupW);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 12.0f * dp);
|
||||
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, pillPad);
|
||||
ImGui::SetCursorScreenPos(ImVec2(grpX, rowStart.y + (rowH - pillH) * 0.5f));
|
||||
if (ImGui::Button(selLbl)) selAllClicked = true;
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button(clrLbl)) s_pfEdit.addrs.clear();
|
||||
ImGui::PopStyleVar(2);
|
||||
ImGui::SameLine();
|
||||
ImGui::SetCursorScreenPos(ImVec2(ImGui::GetCursorScreenPos().x,
|
||||
rowStart.y + (rowH - defFrameH) * 0.5f)); // center vs. pills
|
||||
ImGui::Checkbox(TR("portfolio_funded"), &s_pfEdit.fundedOnly);
|
||||
ImGui::SetCursorScreenPos(ImVec2(rowStart.x, rowStart.y + rowH));
|
||||
ImGui::Dummy(ImVec2(rowW, 0));
|
||||
}
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
std::string search = s_pfEdit.search;
|
||||
std::vector<const AddressInfo*> filtered;
|
||||
for (const auto& a : state.addresses) {
|
||||
if (s_pfEdit.typeFilter == 1 && a.type != "shielded") continue;
|
||||
if (s_pfEdit.typeFilter == 2 && a.type != "transparent") continue;
|
||||
if (s_pfEdit.fundedOnly && 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_pfEdit.addrs, x->address);
|
||||
bool sy = data::PortfolioEntryContains(s_pfEdit.addrs, y->address);
|
||||
if (sx != sy) return sx;
|
||||
return x->balance > y->balance;
|
||||
});
|
||||
if (selAllClicked)
|
||||
for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pfEdit.addrs, a->address);
|
||||
ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
|
||||
ImDrawList* ldl = pfBeginScrollChild("##pfAddrList", s_pfEdit.addrFade,
|
||||
ImVec2(Layout::spacingMd(), Layout::spacingSm()), dp);
|
||||
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_pfEdit.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();
|
||||
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();
|
||||
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();
|
||||
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();
|
||||
}
|
||||
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);
|
||||
ImGui::PushID(a.address.c_str());
|
||||
ImGui::InvisibleButton("##pfrow", ImVec2(lw, rowH));
|
||||
if (ImGui::IsItemClicked()) {
|
||||
if (inSet) data::PortfolioEntryRemove(s_pfEdit.addrs, a.address);
|
||||
else data::PortfolioEntryAdd(s_pfEdit.addrs, a.address);
|
||||
}
|
||||
ImGui::PopID();
|
||||
}
|
||||
pfEndScrollChild(s_pfEdit.addrFade, ldl);
|
||||
}
|
||||
ImGui::EndChild(); // ##pfDetailBody
|
||||
// Group-scoped actions (Add-entry height → lower hierarchy than the modal Close in the footer).
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user