feat(market): portfolio detail polish — bigger icons, inline filter+funded, pill buttons, single-row colors

- Appearance: icon-picker cells doubled (56px, iconXL glyphs); color swatches back to a
  single row that hides overflow but always keeps the custom "+" cell visible at the right.
- Price: basis radios are now horizontal, matching the sparkline-interval radios.
- Addresses: All/Shielded/Transparent segmented control shrinks to sit on one line with
  the Funded toggle to its right; Select all / Clear are rounded (pill) buttons with
  padding, right-aligned on their own row.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 22:15:50 -05:00
parent 3568b12dbf
commit 32d8d5663a

View File

@@ -697,57 +697,70 @@ static void RenderPortfolioEditor(App* app)
// ---- APPEARANCE (color + outline opacity + icon) ---- // ---- APPEARANCE (color + outline opacity + icon) ----
if (s_pf_section == 0) { if (s_pf_section == 0) {
// Color picker: preset swatches (wrapping grid) + a custom-color cell that opens a picker. // 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; float sw = 22.0f * dp, gap = 5.0f * dp;
ImGui::TextUnformatted(TR("portfolio_color")); ImGui::TextUnformatted(TR("portfolio_color"));
ImDrawList* cdl = ImGui::GetWindowDrawList(); ImDrawList* cdl = ImGui::GetWindowDrawList();
int nColors = (int)(sizeof(kPfPalette) / sizeof(kPfPalette[0])); int nColors = (int)(sizeof(kPfPalette) / sizeof(kPfPalette[0]));
float availW = ImGui::GetContentRegionAvail().x; float availW = ImGui::GetContentRegionAvail().x;
int cols = std::max(1, (int)((availW + gap) / (sw + gap))); ImVec2 rowStart = ImGui::GetCursorScreenPos();
float plusX = rowStart.x + std::max(sw, availW - sw); // reserved "+" slot at the right
bool isCustom = (s_pf_color != 0u); bool isCustom = (s_pf_color != 0u);
for (int c = 0; c < nColors; c++) if (s_pf_color == kPfPalette[c]) isCustom = false; for (int c = 0; c < nColors; c++) if (s_pf_color == kPfPalette[c]) isCustom = false;
int totalCells = nColors + 2; // 0 = default (no color), 1..N = palette, last = custom
bool openCustom = false; bool openCustom = false;
int col = 0; // Preset swatches (0 = default/no color, 1..N = palette); stop before the "+" slot.
for (int idx = 0; idx < totalCells; idx++) { float x = rowStart.x;
if (col != 0) ImGui::SameLine(0, gap); for (int idx = 0; idx <= nColors; idx++) {
ImVec2 mn = ImGui::GetCursorScreenPos(); 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); ImVec2 cc(mn.x + sw * 0.5f, mn.y + sw * 0.5f);
bool isDefault = (idx == 0); bool isDefault = (idx == 0);
bool isCustomCell = (idx == totalCells - 1); ImU32 col32 = isDefault ? 0u : kPfPalette[idx - 1];
ImU32 col32 = (!isDefault && !isCustomCell) ? kPfPalette[idx - 1] : 0u; bool sel = isDefault ? (s_pf_color == 0u) : (s_pf_color == col32);
bool sel = isDefault ? (s_pf_color == 0u)
: isCustomCell ? isCustom : (s_pf_color == col32);
bool hov = ImGui::IsMouseHoveringRect(mn, ImVec2(mn.x + sw, mn.y + sw)); bool hov = ImGui::IsMouseHoveringRect(mn, ImVec2(mn.x + sw, mn.y + sw));
if (isDefault || (isCustomCell && !isCustom)) { if (isDefault) cdl->AddCircle(cc, sw * 0.40f, OnSurfaceMedium(), 0, 1.5f * dp);
cdl->AddCircle(cc, sw * 0.40f, OnSurfaceMedium(), 0, 1.5f * dp); else cdl->AddCircleFilled(cc, sw * 0.40f, col32);
if (isCustomCell) { // "+" glyph for the custom-color cell
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);
}
} else {
cdl->AddCircleFilled(cc, sw * 0.40f, isCustomCell ? (ImU32)s_pf_color : col32);
}
if (sel) cdl->AddCircle(cc, sw * 0.50f, OnSurface(), 0, 2.0f * dp); 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); else if (hov) cdl->AddCircle(cc, sw * 0.50f, WithAlpha(OnSurface(), 120), 0, 1.5f * dp);
ImGui::PushID(2000 + idx); ImGui::PushID(2000 + idx);
ImGui::SetCursorScreenPos(mn);
ImGui::InvisibleButton("##pfcol", ImVec2(sw, sw)); ImGui::InvisibleButton("##pfcol", ImVec2(sw, sw));
if (ImGui::IsItemClicked()) { if (ImGui::IsItemClicked()) s_pf_color = isDefault ? 0u : col32;
if (isDefault) s_pf_color = 0u;
else if (isCustomCell) {
ImU32 base = s_pf_color ? (ImU32)s_pf_color : IM_COL32(0x4F, 0x9D, 0xFF, 0xFF);
s_pf_custom_rgb[0] = ((base >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
s_pf_custom_rgb[1] = ((base >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
s_pf_custom_rgb[2] = ((base >> IM_COL32_B_SHIFT) & 0xFF) / 255.0f;
openCustom = true;
} else s_pf_color = col32;
}
if (isCustomCell && hov) material::Tooltip("%s", TR("portfolio_custom_color"));
ImGui::PopID(); ImGui::PopID();
col = (col + 1) % cols; 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_pf_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_pf_color ? (ImU32)s_pf_color : IM_COL32(0x4F, 0x9D, 0xFF, 0xFF);
s_pf_custom_rgb[0] = ((base >> IM_COL32_R_SHIFT) & 0xFF) / 255.0f;
s_pf_custom_rgb[1] = ((base >> IM_COL32_G_SHIFT) & 0xFF) / 255.0f;
s_pf_custom_rgb[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 (openCustom) ImGui::OpenPopup("##pfCustomColor");
if (ImGui::BeginPopup("##pfCustomColor")) { if (ImGui::BeginPopup("##pfCustomColor")) {
ImGui::ColorPicker3("##pfpick", s_pf_custom_rgb, ImGui::ColorPicker3("##pfpick", s_pf_custom_rgb,
@@ -767,9 +780,11 @@ static void RenderPortfolioEditor(App* app)
if (s_pf_outline_opacity > 100) s_pf_outline_opacity = 100; if (s_pf_outline_opacity > 100) s_pf_outline_opacity = 100;
ImGui::EndDisabled(); ImGui::EndDisabled();
ImGui::Dummy(ImVec2(0, Layout::spacingSm())); ImGui::Dummy(ImVec2(0, Layout::spacingSm()));
// Icon picker grid (fixed height, scrolls internally). // Icon picker grid (large cells, fixed height, scrolls internally).
{ {
float cell = 28.0f * dp, cellGap = 4.0f * dp; float cell = 56.0f * dp, cellGap = 6.0f * dp;
ImFont* gIconFont = Type().iconXL();
float gIconSz = cell * 0.5f;
ImGui::TextUnformatted(TR("portfolio_icon")); ImGui::TextUnformatted(TR("portfolio_icon"));
ImGui::BeginChild("##pfIconGrid", ImVec2(0, std::max(120.0f * dp, ImGui::GetContentRegionAvail().y)), true); ImGui::BeginChild("##pfIconGrid", ImVec2(0, std::max(120.0f * dp, ImGui::GetContentRegionAvail().y)), true);
ImDrawList* gdl = ImGui::GetWindowDrawList(); ImDrawList* gdl = ImGui::GetWindowDrawList();
@@ -799,7 +814,7 @@ static void RenderPortfolioEditor(App* app)
gdl->AddLine(ImVec2(cc.x - r * 0.7f, cc.y + r * 0.7f), 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); ImVec2(cc.x + r * 0.7f, cc.y - r * 0.7f), icol, 1.5f * dp);
} else { } else {
material::project_icons::drawByName(gdl, nm2, cc, icol, iconFont, iconFsz); material::project_icons::drawByName(gdl, nm2, cc, icol, gIconFont, gIconSz);
} }
ImGui::PushID(k); ImGui::PushID(k);
ImGui::InvisibleButton("##pfic", ImVec2(cell, cell)); ImGui::InvisibleButton("##pfic", ImVec2(cell, cell));
@@ -817,7 +832,7 @@ static void RenderPortfolioEditor(App* app)
// Price basis — radio group (all options visible; no dropdown). // Price basis — radio group (all options visible; no dropdown).
const char* basisItems[4] = { TR("portfolio_price_usd"), TR("portfolio_price_btc"), const char* basisItems[4] = { TR("portfolio_price_usd"), TR("portfolio_price_btc"),
TR("portfolio_price_drgx"), TR("portfolio_price_manual") }; TR("portfolio_price_drgx"), TR("portfolio_price_manual") };
for (int b = 0; b < 4; b++) ImGui::RadioButton(basisItems[b], &s_pf_price_basis, b); for (int b = 0; b < 4; b++) { if (b) ImGui::SameLine(); ImGui::RadioButton(basisItems[b], &s_pf_price_basis, b); }
if (s_pf_price_basis == 3) { // Manual: price-per-DRGX + currency label if (s_pf_price_basis == 3) { // Manual: price-per-DRGX + currency label
float half = (ImGui::GetContentRegionAvail().x - Layout::spacingSm()) * 0.62f; float half = (ImGui::GetContentRegionAvail().x - Layout::spacingSm()) * 0.62f;
ImGui::SetNextItemWidth(half); ImGui::SetNextItemWidth(half);
@@ -862,11 +877,15 @@ static void RenderPortfolioEditor(App* app)
ImGui::SetNextItemWidth(-1); ImGui::SetNextItemWidth(-1);
ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pf_search, sizeof(s_pf_search)); ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pf_search, sizeof(s_pf_search));
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
// Type filter segmented control (All | Shielded | Transparent), matching the top switcher. // Type filter (segmented) + Funded toggle on one row: the segmented control is sized to
// leave room for the Funded checkbox to its right.
{ {
const char* flabels[3] = { TR("portfolio_select_all"), TR("shielded"), TR("transparent") }; const char* flabels[3] = { TR("portfolio_select_all"), TR("shielded"), TR("transparent") };
float segH = 26.0f * dp; float segH = 26.0f * dp;
float segW = ImGui::GetContentRegionAvail().x; float fundedW = ImGui::GetFrameHeight() + ImGui::GetStyle().ItemInnerSpacing.x
+ ImGui::CalcTextSize(TR("portfolio_funded")).x;
float rowW = ImGui::GetContentRegionAvail().x;
float segW = std::max(180.0f * dp, rowW - fundedW - Layout::spacingMd());
float cellW = segW / 3.0f; float cellW = segW / 3.0f;
ImVec2 sMin = ImGui::GetCursorScreenPos(); ImVec2 sMin = ImGui::GetCursorScreenPos();
ImDrawList* fdl = ImGui::GetWindowDrawList(); ImDrawList* fdl = ImGui::GetWindowDrawList();
@@ -890,8 +909,12 @@ static void RenderPortfolioEditor(App* app)
if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
ImGui::PopID(); ImGui::PopID();
} }
// Funded checkbox — vertically centered, to the right of the segmented control.
ImGui::SetCursorScreenPos(ImVec2(sMin.x + segW + Layout::spacingMd(),
sMin.y + (segH - ImGui::GetFrameHeight()) * 0.5f));
ImGui::Checkbox(TR("portfolio_funded"), &s_pf_funded_only);
ImGui::SetCursorScreenPos(ImVec2(sMin.x, sMin.y + segH)); ImGui::SetCursorScreenPos(ImVec2(sMin.x, sMin.y + segH));
ImGui::Dummy(ImVec2(segW, 0)); ImGui::Dummy(ImVec2(rowW, 0));
} }
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
std::string search = s_pf_search; std::string search = s_pf_search;
@@ -912,21 +935,22 @@ static void RenderPortfolioEditor(App* app)
if (sx != sy) return sx; if (sx != sy) return sx;
return x->balance > y->balance; return x->balance > y->balance;
}); });
// Funded toggle (left) + Select all / Clear (right-aligned) on one row. // Select all / Clear — rounded (pill) buttons with padding, right-aligned.
ImGui::Checkbox(TR("portfolio_funded"), &s_pf_funded_only);
{ {
const char* selLbl = TR("portfolio_select_shown"); const char* selLbl = TR("portfolio_select_shown");
const char* clrLbl = TR("portfolio_clear_sel"); const char* clrLbl = TR("portfolio_clear_sel");
ImGui::PushStyleVar(ImGuiStyleVar_FrameRounding, 12.0f * dp);
ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(Layout::spacingMd(), Layout::spacingXs()));
ImGuiStyle& st = ImGui::GetStyle(); ImGuiStyle& st = ImGui::GetStyle();
float grpW = ImGui::CalcTextSize(selLbl).x + ImGui::CalcTextSize(clrLbl).x float grpW = ImGui::CalcTextSize(selLbl).x + ImGui::CalcTextSize(clrLbl).x
+ st.FramePadding.x * 4.0f + st.ItemSpacing.x; + st.FramePadding.x * 4.0f + st.ItemSpacing.x;
ImGui::SameLine();
float avail = ImGui::GetContentRegionAvail().x; float avail = ImGui::GetContentRegionAvail().x;
if (avail > grpW) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + avail - grpW); if (avail > grpW) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + avail - grpW);
if (ImGui::SmallButton(selLbl)) if (ImGui::Button(selLbl))
for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pf_addrs, a->address); for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pf_addrs, a->address);
ImGui::SameLine(); ImGui::SameLine();
if (ImGui::SmallButton(clrLbl)) s_pf_addrs.clear(); if (ImGui::Button(clrLbl)) s_pf_addrs.clear();
ImGui::PopStyleVar(2);
} }
ImGui::Dummy(ImVec2(0, Layout::spacingXs())); ImGui::Dummy(ImVec2(0, Layout::spacingXs()));
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingSm())); ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingSm()));