From 3568b12dbfbb0ad0d330d74953080a40c94cd664 Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 3 Jul 2026 21:42:08 -0500 Subject: [PATCH] =?UTF-8?q?feat(market):=20portfolio=20detail=20=E2=80=94?= =?UTF-8?q?=20radio=20price=20options,=20segmented=20address=20filter,=20+?= =?UTF-8?q?17=20colors?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Price: the basis and sparkline-interval dropdowns become radio groups (all options visible), now that the segmented layout gives the section full height. - Addresses: the All/Shielded/Transparent chips become a rounded segmented control matching the top switcher. "Select shown" renamed to "Select all"; Select all + Clear moved onto the Funded row, right-aligned. Address list gets inner WindowPadding. - Appearance: 17 more preset colors (25 total), and the swatches now wrap into a grid (with the custom-color cell) instead of a single overflowing row. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/windows/market_tab.cpp | 164 +++++++++++++++++++++++----------- src/util/i18n.cpp | 2 +- 2 files changed, 111 insertions(+), 55 deletions(-) diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 04396e9..d777ac2 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -303,6 +303,23 @@ static const ImU32 kPfPalette[] = { IM_COL32(0x4F, 0xD6, 0xD6, 0xFF), // teal IM_COL32(0xFF, 0x8F, 0xC7, 0xFF), // pink IM_COL32(0xC7, 0xCE, 0xD6, 0xFF), // slate + IM_COL32(0x6C, 0x7B, 0xFF, 0xFF), // indigo + IM_COL32(0x74, 0xC0, 0xFC, 0xFF), // sky + IM_COL32(0x3D, 0xD6, 0xF5, 0xFF), // cyan + IM_COL32(0x63, 0xE6, 0xBE, 0xFF), // mint + IM_COL32(0x2F, 0xB3, 0x80, 0xFF), // emerald + IM_COL32(0xA8, 0xE0, 0x5F, 0xFF), // lime + IM_COL32(0xB5, 0xB8, 0x4D, 0xFF), // olive + IM_COL32(0xF5, 0xC5, 0x42, 0xFF), // gold + IM_COL32(0xFF, 0x9F, 0x45, 0xFF), // orange + IM_COL32(0xFF, 0x8A, 0x65, 0xFF), // coral + IM_COL32(0xE6, 0x49, 0x80, 0xFF), // crimson + IM_COL32(0xFF, 0x8B, 0x94, 0xFF), // rose + IM_COL32(0xE5, 0x99, 0xF7, 0xFF), // magenta + IM_COL32(0x97, 0x75, 0xFA, 0xFF), // violet + IM_COL32(0xB1, 0x97, 0xFC, 0xFF), // lavender + IM_COL32(0xB0, 0x89, 0x68, 0xFF), // brown + IM_COL32(0x8F, 0xA3, 0xB8, 0xFF), // steel }; // Populate the working buffers for a new (index < 0) or existing entry. @@ -680,54 +697,58 @@ static void RenderPortfolioEditor(App* app) // ---- APPEARANCE (color + outline opacity + icon) ---- if (s_pf_section == 0) { - // Color picker: preset swatches + a custom-color circle that opens a picker popup. + // Color picker: preset swatches (wrapping grid) + a custom-color cell that opens a picker. { - float sw = 22.0f * dp; + 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])); - for (int c = 0; c <= nColors; c++) { // slot 0 = default / no color - if (c != 0) ImGui::SameLine(0, 5.0f * dp); + float availW = ImGui::GetContentRegionAvail().x; + int cols = std::max(1, (int)((availW + gap) / (sw + gap))); + bool isCustom = (s_pf_color != 0u); + 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; + int col = 0; + for (int idx = 0; idx < totalCells; idx++) { + if (col != 0) ImGui::SameLine(0, gap); 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 isDefault = (idx == 0); + bool isCustomCell = (idx == totalCells - 1); + ImU32 col32 = (!isDefault && !isCustomCell) ? kPfPalette[idx - 1] : 0u; + 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)); - 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); + if (isDefault || (isCustomCell && !isCustom)) { + cdl->AddCircle(cc, sw * 0.40f, OnSurfaceMedium(), 0, 1.5f * dp); + 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); + else if (hov) cdl->AddCircle(cc, sw * 0.50f, WithAlpha(OnSurface(), 120), 0, 1.5f * dp); + ImGui::PushID(2000 + idx); ImGui::InvisibleButton("##pfcol", ImVec2(sw, sw)); - if (ImGui::IsItemClicked()) s_pf_color = isDefault ? 0u : kPfPalette[c - 1]; + if (ImGui::IsItemClicked()) { + 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(); + col = (col + 1) % cols; } - bool isCustom = (s_pf_color != 0u); - for (int c = 0; c < nColors; c++) - if (s_pf_color == kPfPalette[c]) isCustom = false; - ImGui::SameLine(0, 5.0f * dp); - ImVec2 mn = ImGui::GetCursorScreenPos(); - 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::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; - ImGui::OpenPopup("##pfCustomColor"); - } - if (hov) material::Tooltip("%s", TR("portfolio_custom_color")); + if (openCustom) ImGui::OpenPopup("##pfCustomColor"); if (ImGui::BeginPopup("##pfCustomColor")) { ImGui::ColorPicker3("##pfpick", s_pf_custom_rgb, ImGuiColorEditFlags_NoSidePreview | ImGuiColorEditFlags_NoAlpha | ImGuiColorEditFlags_NoLabel); @@ -793,10 +814,10 @@ static void RenderPortfolioEditor(App* app) // ---- PRICE (basis, manual price, shown fields, sparkline) ---- if (s_pf_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") }; - ImGui::SetNextItemWidth(-1); - ImGui::Combo("##pfBasis", &s_pf_price_basis, basisItems, 4); + for (int b = 0; b < 4; b++) ImGui::RadioButton(basisItems[b], &s_pf_price_basis, b); if (s_pf_price_basis == 3) { // Manual: price-per-DRGX + currency label float half = (ImGui::GetContentRegionAvail().x - Layout::spacingSm()) * 0.62f; ImGui::SetNextItemWidth(half); @@ -806,6 +827,9 @@ static void RenderPortfolioEditor(App* app) ImGui::SetNextItemWidth(-1); ImGui::InputTextWithHint("##pfManCcy", TR("portfolio_currency"), s_pf_manual_ccy, sizeof(s_pf_manual_ccy)); } + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + // Shown fields. + ImGui::TextUnformatted(TR("portfolio_show")); ImGui::Checkbox(DRAGONX_TICKER "##pfShowDrgx", &s_pf_show_drgx); ImGui::SameLine(); ImGui::BeginDisabled(s_pf_price_basis == 2); // DRGX-only -> no value field @@ -815,15 +839,15 @@ static void RenderPortfolioEditor(App* app) ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1)); // 24h: live-market only ImGui::Checkbox(TR("portfolio_show_24h"), &s_pf_show_24h); ImGui::EndDisabled(); + ImGui::Dummy(ImVec2(0, Layout::spacingSm())); + // Sparkline + its interval — radio group. ImGui::BeginDisabled(!(s_pf_price_basis == 0 || s_pf_price_basis == 1)); ImGui::Checkbox(TR("portfolio_show_sparkline"), &s_pf_show_sparkline); - ImGui::SameLine(); ImGui::BeginDisabled(!s_pf_show_sparkline); const char* spItems[5] = { TR("portfolio_spark_min"), TR("portfolio_spark_hour"), TR("portfolio_spark_day"), TR("portfolio_spark_week"), TR("portfolio_spark_month") }; - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - ImGui::Combo("##pfSparkInterval", &s_pf_spark_interval, spItems, 5); + for (int s = 0; s < 5; s++) { if (s) ImGui::SameLine(); ImGui::RadioButton(spItems[s], &s_pf_spark_interval, s); } ImGui::EndDisabled(); ImGui::EndDisabled(); } @@ -838,18 +862,36 @@ static void RenderPortfolioEditor(App* app) ImGui::SetNextItemWidth(-1); ImGui::InputTextWithHint("##pfSearch", TR("portfolio_search"), s_pf_search, sizeof(s_pf_search)); ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + // Type filter — segmented control (All | Shielded | Transparent), matching the top switcher. { const char* flabels[3] = { TR("portfolio_select_all"), TR("shielded"), TR("transparent") }; + float segH = 26.0f * dp; + float segW = ImGui::GetContentRegionAvail().x; + float cellW = segW / 3.0f; + ImVec2 sMin = ImGui::GetCursorScreenPos(); + ImDrawList* fdl = ImGui::GetWindowDrawList(); + fdl->AddRectFilled(sMin, ImVec2(sMin.x + segW, sMin.y + segH), WithAlpha(OnSurface(), 20), segH * 0.5f); for (int f = 0; f < 3; f++) { - if (f) ImGui::SameLine(0, 4.0f * dp); + ImVec2 cMin(sMin.x + f * cellW, sMin.y), cMax(cMin.x + cellW, sMin.y + segH); 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(); + bool hov = ImGui::IsMouseHoveringRect(cMin, cMax); + if (active) + fdl->AddRectFilled(ImVec2(cMin.x + 2.0f * dp, cMin.y + 2.0f * dp), + ImVec2(cMax.x - 2.0f * dp, cMax.y - 2.0f * dp), + WithAlpha(Primary(), 210), (segH - 4.0f * dp) * 0.5f); + ImVec2 ts = capF->CalcTextSizeA(capF->LegacySize, FLT_MAX, 0, flabels[f]); + fdl->AddText(capF, capF->LegacySize, + ImVec2(cMin.x + (cellW - ts.x) * 0.5f, cMin.y + (segH - ts.y) * 0.5f), + active ? IM_COL32(255, 255, 255, 255) : (hov ? OnSurface() : OnSurfaceMedium()), + flabels[f]); + ImGui::PushID(3100 + f); + ImGui::SetCursorScreenPos(cMin); + if (ImGui::InvisibleButton("##pftf", ImVec2(cellW, segH))) s_pf_type_filter = f; + if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::PopID(); } - ImGui::SameLine(0, Layout::spacingMd()); - ImGui::Checkbox(TR("portfolio_funded"), &s_pf_funded_only); + ImGui::SetCursorScreenPos(ImVec2(sMin.x, sMin.y + segH)); + ImGui::Dummy(ImVec2(segW, 0)); } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); std::string search = s_pf_search; @@ -870,11 +912,24 @@ static void RenderPortfolioEditor(App* app) if (sx != sy) return sx; return x->balance > y->balance; }); - 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(); + // Funded toggle (left) + Select all / Clear (right-aligned) on one row. + ImGui::Checkbox(TR("portfolio_funded"), &s_pf_funded_only); + { + const char* selLbl = TR("portfolio_select_shown"); + const char* clrLbl = TR("portfolio_clear_sel"); + ImGuiStyle& st = ImGui::GetStyle(); + float grpW = ImGui::CalcTextSize(selLbl).x + ImGui::CalcTextSize(clrLbl).x + + st.FramePadding.x * 4.0f + st.ItemSpacing.x; + ImGui::SameLine(); + float avail = ImGui::GetContentRegionAvail().x; + if (avail > grpW) ImGui::SetCursorPosX(ImGui::GetCursorPosX() + avail - grpW); + if (ImGui::SmallButton(selLbl)) + for (const AddressInfo* a : filtered) data::PortfolioEntryAdd(s_pf_addrs, a->address); + ImGui::SameLine(); + if (ImGui::SmallButton(clrLbl)) s_pf_addrs.clear(); + } ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(Layout::spacingMd(), Layout::spacingSm())); ImGui::BeginChild("##pfAddrList", ImVec2(0, std::max(120.0f * dp, ImGui::GetContentRegionAvail().y)), true); ImDrawList* ldl = ImGui::GetWindowDrawList(); float rowH = std::max(26.0f * dp, body2f->LegacySize + Layout::spacingSm()); @@ -939,6 +994,7 @@ static void RenderPortfolioEditor(App* app) ImGui::PopID(); } ImGui::EndChild(); + ImGui::PopStyleVar(); // ##pfAddrList WindowPadding } } ImGui::EndChild(); // ##pfDetail diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 5dde156..b2386d5 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1149,7 +1149,7 @@ void I18n::loadBuiltinEnglish() 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_select_shown"] = "Select all"; strings_["portfolio_group_name"] = "Group name"; strings_["portfolio_no_addr_match"] = "No addresses match"; strings_["portfolio_manage_title"] = "Manage portfolio";