diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 86375c5..4c1f710 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -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 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 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 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 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). {