diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index f3c3365..2e82025 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -298,7 +298,8 @@ static config::Settings::PortfolioEntry pfBuildWorking(const config::Settings::P config::Settings::PortfolioEntry e = base; e.label = s_pfEdit.label; e.addresses = s_pfEdit.addrs; e.icon = s_pfEdit.icon; e.color = s_pfEdit.color; e.outlineOpacity = s_pfEdit.outlineOpacity; - e.priceBasis = s_pfEdit.priceBasis; e.manualPrice = s_pfEdit.manualPrice; e.manualCurrency = s_pfEdit.manualCcy; + e.priceBasis = s_pfEdit.priceBasis; e.manualPrice = s_pfEdit.manualPrice; + e.manualCurrency = (s_pfEdit.manualCcy[0] != '\0') ? std::string(s_pfEdit.manualCcy) : std::string("USD"); e.showDrgx = s_pfEdit.showDrgx; e.showValue = s_pfEdit.showValue; e.show24h = s_pfEdit.show24h; e.showSparkline = s_pfEdit.showSparkline; e.sparklineInterval = s_pfEdit.sparkInterval; return e; @@ -315,6 +316,16 @@ static bool pfWorkingMatches(const config::Settings::PortfolioEntry& e) && e.showSparkline == s_pfEdit.showSparkline && e.sparklineInterval == s_pfEdit.sparkInterval; } +// A group is only worth persisting when it has a name, at least one address (else it's always $0), and +// — on the Manual price basis — a positive price (0 would render as "unavailable"). Gates both the Save +// button and the commit path (Close/switch commit too, so validating only the button wouldn't be enough). +static bool pfWorkingValid() +{ + return s_pfEdit.label[0] != '\0' + && !s_pfEdit.addrs.empty() + && !(s_pfEdit.priceBasis == 3 && s_pfEdit.manualPrice <= 0.0); +} + static void pfPersist(config::Settings* settings, const std::vector& entries) { settings->setPortfolioEntries(entries); @@ -327,7 +338,7 @@ static void pfPersist(config::Settings* settings, const std::vectorsettings(); - if (s_pfEdit.label[0] == '\0') return; + if (!pfWorkingValid()) return; // drop invalid drafts (unnamed / no addresses / manual price <= 0) auto entries = settings->getPortfolioEntries(); bool existing = (s_pfEdit.sel >= 0 && s_pfEdit.sel < (int)entries.size()); config::Settings::PortfolioEntry base; @@ -635,8 +646,8 @@ static void pfDrawAddressSection(App* app) 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); + if (inSet) ldl->AddRectFilled(rmn, rmx, WithAlpha(Primary(), 22), 4.0f * dp); + else if (rhov) ldl->AddRectFilled(rmn, rmx, IM_COL32(255, 255, 255, 14), 4.0f * dp); float rcy = rmn.y + rowH * 0.5f; float rx = rmn.x + Layout::spacingSm(); float cb = 15.0f * dp; @@ -654,7 +665,7 @@ static void pfDrawAddressSection(App* app) bool isZ = (a.type == "shielded"); const char* chip = isZ ? "Z" : "T"; ImU32 chipCol = isZ ? Success() : Warning(); - ImVec2 chipPos(rx, rcy - capF->LegacySize * 0.5f - 2.0f); + ImVec2 chipPos(rx, rcy - capF->LegacySize * 0.5f - 2.0f * dp); float chipW = material::DrawPill(ldl, chipPos, chip, capF, chipCol, WithAlpha(chipCol, 40), 0, ImVec2(4.0f * dp, 2.0f), 4.0f * dp).x; rx += chipW + Layout::spacingSm(); @@ -719,9 +730,14 @@ static void RenderPortfolioEditor(App* app) if (!material::BeginOverlayDialog(ov)) return; if (ImGui::IsKeyPressed(ImGuiKey_Escape)) s_pfEdit.open = false; - { // clamp/validate the selection against the current entry list + { // Clamp/validate the selection. The in-editor delete already reloads the working state, but if the + // list shrank via any other path, reload after clamping so a later commit can't overlay stale + // fields onto the clamped entry. const auto& entriesRO = settings->getPortfolioEntries(); - if (s_pfEdit.sel >= (int)entriesRO.size()) s_pfEdit.sel = entriesRO.empty() ? -1 : 0; + if (s_pfEdit.sel >= (int)entriesRO.size()) { + s_pfEdit.sel = entriesRO.empty() ? -1 : 0; + PortfolioBeginEdit(app, s_pfEdit.sel); + } } float footerH = 56.0f * dp; // room for the 40px buttons + separator so they aren't clipped @@ -869,7 +885,7 @@ static void RenderPortfolioEditor(App* app) DrawGlassPanel(pdl, pMin, pMax, g); if (accent) { int oa = (int)(std::max(0, std::min(100, s_pfEdit.outlineOpacity)) * 2.55f + 0.5f); - pdl->AddRect(pMin, pMax, WithAlpha(accent, oa), 10.0f, 0, 2.0f); + pdl->AddRect(pMin, pMax, WithAlpha(accent, oa), 10.0f * dp, 0, 2.0f * dp); } if (s_pfEdit.showSparkline && (s_pfEdit.priceBasis == 0 || s_pfEdit.priceBasis == 1)) { std::vector h = data::sparklineSeries(state.market, s_pfEdit.sparkInterval); @@ -956,7 +972,7 @@ static void RenderPortfolioEditor(App* app) if (material::TactileButton(TR("portfolio_revert"), ImVec2(bw, addH))) PortfolioBeginEdit(app, s_pfEdit.sel); // reload the working set from the stored entry ImGui::SameLine(0, sp); - ImGui::BeginDisabled(s_pfEdit.label[0] == '\0'); + ImGui::BeginDisabled(!pfWorkingValid()); if (material::TactileButton(TR("portfolio_save"), ImVec2(bw, addH))) pfCommitIfNeeded(app); ImGui::EndDisabled(); ImGui::EndDisabled(); @@ -1406,7 +1422,7 @@ static void mktDrawPriceChart(const MktCtx& cx) bool bhov = material::IsRectHovered(bmn, bmx); ImU32 bg = sel ? WithAlpha(Primary(), 200) : (bhov ? WithAlpha(OnSurface(), 35) : WithAlpha(OnSurface(), 18)); - dl->AddRectFilled(bmn, bmx, bg, 4.0f); + dl->AddRectFilled(bmn, bmx, bg, 4.0f * mktDp); dl->AddText(capFont, capFont->LegacySize, ImVec2(bx + Layout::spacingSm(), textY), sel ? IM_COL32(255, 255, 255, 255) : OnSurface(), kIvs[b].lbl); ImGui::SetCursorScreenPos(bmn); @@ -1429,7 +1445,7 @@ static void mktDrawPriceChart(const MktCtx& cx) const char* styleIcon = isCandle ? ICON_MD_CANDLESTICK_CHART : ICON_MD_SHOW_CHART; ImVec2 tmn(bx, rowTop), tmx(bx + pillH, rowTop + pillH); bool thov = material::IsRectHovered(tmn, tmx); - if (thov) { dl->AddRectFilled(tmn, tmx, IM_COL32(255, 255, 255, 20), 4.0f); + if (thov) { dl->AddRectFilled(tmn, tmx, IM_COL32(255, 255, 255, 20), 4.0f * mktDp); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } ImVec2 tiSz = icoF->CalcTextSizeA(icoF->LegacySize, FLT_MAX, 0, styleIcon); dl->AddText(icoF, icoF->LegacySize, @@ -1450,7 +1466,7 @@ static void mktDrawPriceChart(const MktCtx& cx) ImFont* iconSmall = material::Typography::instance().iconSmall(); ImVec2 rbMin(rEdge - pillH, rowTop), rbMax(rEdge, rowTop + pillH); bool refreshHov = material::IsRectHovered(rbMin, rbMax); - if (refreshHov) { dl->AddRectFilled(rbMin, rbMax, IM_COL32(255, 255, 255, 20), 4.0f); + if (refreshHov) { dl->AddRectFilled(rbMin, rbMax, IM_COL32(255, 255, 255, 20), 4.0f * mktDp); ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } ImVec2 icSz = iconSmall->CalcTextSizeA(iconSmall->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH); dl->AddText(iconSmall, iconSmall->LegacySize, @@ -1797,12 +1813,16 @@ static void mktDrawPortfolio(const MktCtx& cx) chgCol, chg); } - double portfolio_btc = total_balance * market.price_btc; - snprintf(buf, sizeof(buf), "\xE2\x89\x88 %.8f BTC", portfolio_btc); // "≈ BTC" - float btcW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x; - dl->AddText(capFont, capFont->LegacySize, - ImVec2(rightEdge - btcW, cy + (sub1->LegacySize - capFont->LegacySize)), - OnSurfaceMedium(), buf); + // Only show the BTC equivalent when a BTC price is actually available — otherwise it would read a + // misleading "≈ 0.00000000 BTC" (price_usd can be present while price_btc is still 0). + if (market.price_btc > 0) { + double portfolio_btc = total_balance * market.price_btc; + snprintf(buf, sizeof(buf), "\xE2\x89\x88 %.8f BTC", portfolio_btc); // "≈ BTC" + float btcW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x; + dl->AddText(capFont, capFont->LegacySize, + ImVec2(rightEdge - btcW, cy + (sub1->LegacySize - capFont->LegacySize)), + OnSurfaceMedium(), buf); + } } else { dl->AddText(sub1, sub1->LegacySize, ImVec2(cx0, cy), OnSurfaceDisabled(), TR("market_no_price")); } @@ -1832,16 +1852,16 @@ static void mktDrawPortfolio(const MktCtx& cx) const bool barLight = IsLightTheme(); const int fillA = barLight ? 205 : 165; dl->AddRectFilled(barStart, ImVec2(barStart.x + barW, barStart.y + ratioBarH), - barLight ? IM_COL32(0, 0, 0, 20) : IM_COL32(255, 255, 255, 10), 3.0f); + barLight ? IM_COL32(0, 0, 0, 20) : IM_COL32(255, 255, 255, 10), 3.0f * mktDp); if (shieldedW > 0.5f) dl->AddRectFilled(barStart, ImVec2(barStart.x + shieldedW, barStart.y + ratioBarH), WithAlpha(Success(), fillA), - transpW > 0.5f ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, 3.0f); + transpW > 0.5f ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, 3.0f * mktDp); if (transpW > 0.5f) dl->AddRectFilled(ImVec2(barStart.x + shieldedW, barStart.y), ImVec2(barStart.x + barW, barStart.y + ratioBarH), WithAlpha(Warning(), fillA), - shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f); + shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f * mktDp); // market_pct_shielded is "%.0f%% Shielded" — pass a double (an int to %f is UB). snprintf(buf, sizeof(buf), TR("market_pct_shielded"), static_cast(shieldedRatio) * 100.0);