diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 05bb63d..fcf8caf 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -276,16 +276,52 @@ void RenderMarketTab(App* app) char buf[128]; // ================================================================ - // Proportional section budget — all content fits without scrolling + // Section geometry — this tab does NOT scroll, so everything must fit. Precompute the + // portfolio card height (refined summary + a grid of group cards) and the hero header, + // then let the price chart absorb the remaining vertical space (capped at its schema + // height, floored so it stays useful). Fixes the old layout where an oversized chart + // squeezed the portfolio into a cramped strip. // ================================================================ - float mkSHdr = ovFont->LegacySize + Layout::spacingXs() - + ImGui::GetStyle().ItemSpacing.y * 2.0f; - float mkGapOver = gap + ImGui::GetStyle().ItemSpacing.y; - float mkOverhead = 3.0f * (mkSHdr + mkGapOver) + 2.0f * mkGapOver; - float mkCardBudget = std::max(200.0f, marketAvail.y - mkOverhead); + float mktDp = Layout::dpiScale(); - Layout::SectionBudget mb(mkCardBudget); - float portfolioBudgetH = mb.allocate(0.18f, 50.0f); + // -- Portfolio card geometry -- + const auto& pfEntriesGeo = app->settings()->getPortfolioEntries(); + float pfInnerW = availWidth - Layout::spacingLg() * 2.0f; + float ratioBarH = std::max(S.drawElement("tabs.market", "ratio-bar-min-height").size, + S.drawElement("tabs.market", "ratio-bar-height").size * vs); + float pfSummaryH = Layout::spacingLg() + + sub1->LegacySize + Layout::spacingSm() // fiat hero row + + body2->LegacySize + Layout::spacingSm() // DRGX balance row + + ratioBarH + Layout::spacingXs() + capFont->LegacySize // ratio bar + % label + + Layout::spacingMd(); // bottom pad + int pfN = (int)pfEntriesGeo.size(); + float pfCardGap = Layout::spacingSm(); + float pfMinCardW = 200.0f * mktDp; + int pfCardsPerRow = std::max(1, (int)((pfInnerW + pfCardGap) / (pfMinCardW + pfCardGap))); + if (pfN > 0 && pfCardsPerRow > pfN) pfCardsPerRow = pfN; + float pfCardW = (pfN > 0) ? (pfInnerW - (pfCardsPerRow - 1) * pfCardGap) / (float)pfCardsPerRow : 0.0f; + float pfCardH = body2->LegacySize + Layout::spacingXs() + body2->LegacySize + + capFont->LegacySize + Layout::spacingSm() * 2.0f; + int pfRows = (pfN > 0) ? (pfN + pfCardsPerRow - 1) / pfCardsPerRow : 0; + float pfGroupsH = (pfN > 0) + ? (ovFont->LegacySize + Layout::spacingSm() + + pfRows * pfCardH + std::max(0, pfRows - 1) * pfCardGap + + Layout::spacingMd()) + : 0.0f; + float portfolioCardH = pfSummaryH + pfGroupsH; + + // -- Hero header (price + stats, excluding the chart) -- + float mktStatsRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + pad; + float heroHeaderH = std::max( + S.drawElement("tabs.market", "hero-card-min-height").size + mktStatsRowH + pad, + (S.drawElement("tabs.market", "hero-card-height").size + mktStatsRowH + pad) * vs); + + // -- Chart absorbs the slack: capped at its schema height, floored so it stays useful -- + float mktChipH = S.drawElement("tabs.market", "pair-chip-height").height; + float mktPairsReserve = mktChipH * 2.0f + Layout::spacingXl() + capFont->LegacySize; + float chartH = std::min(chartElem.height * vs, + marketAvail.y - heroHeaderH - portfolioCardH - mktPairsReserve - gap * 3.0f); + chartH = std::max(chartH, 130.0f * vs); // ================================================================ // PRICE SUMMARY — Combined hero card with price, stats, and exchange @@ -293,13 +329,11 @@ void RenderMarketTab(App* app) { float dp = Layout::dpiScale(); ImVec2 cardMin = ImGui::GetCursorScreenPos(); - float heroMinH = S.drawElement("tabs.market", "hero-card-min-height").size; - float statsRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + pad; - float cardH = std::max(heroMinH + statsRowH + pad, (S.drawElement("tabs.market", "hero-card-height").size + statsRowH + pad) * vs); + float cardH = heroHeaderH; ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH); // Combined hero + chart: draw ONE glass panel spanning the price/stats header AND the - // chart below it. The chart block no longer draws its own panel or an inter-card gap. - float mergedChartH = std::max(60.0f, chartElem.height * vs); + // chart below it (chartH precomputed so the chart shrinks to fit the portfolio). + float mergedChartH = chartH; ImVec2 mergedMax(cardMin.x + availWidth, cardMax.y + mergedChartH); DrawGlassPanel(dl, cardMin, mergedMax, glassSpec); @@ -448,9 +482,9 @@ void RenderMarketTab(App* app) s_price_history = market.price_history; s_history_initialized = true; - // Chart height from schema. No glass panel here — it shares the combined hero+chart - // card's panel drawn in PRICE SUMMARY above (mergedChartH uses this same height). - float chartH = std::max(60.0f, chartElem.height * vs); + // Chart height precomputed above (responsive — the chart absorbs the vertical slack + // left by the hero header + portfolio card). No glass panel here — it shares the + // combined hero+chart card's panel drawn in PRICE SUMMARY above. ImVec2 chartMin = ImGui::GetCursorScreenPos(); ImVec2 chartMax(chartMin.x + availWidth, chartMin.y + chartH); @@ -741,111 +775,126 @@ void RenderMarketTab(App* app) double private_balance = state.privateBalance; double transparent_balance = state.transparentBalance; - // Custom portfolio entries render below the "all funds" summary; grow the card to fit. - const auto& pfEntries = app->settings()->getPortfolioEntries(); - float entryRowH = body2->LegacySize + Layout::spacingSm(); - float entriesBlockH = pfEntries.empty() - ? 0.0f - : (Layout::spacingSm() * 2.0f + (float)pfEntries.size() * entryRowH); - ImVec2 cardMin = ImGui::GetCursorScreenPos(); - float baseH = std::max(60.0f, portfolioBudgetH); - float cardH = baseH + entriesBlockH; + float cardH = portfolioCardH; // precomputed: summary block + group-card grid ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH); DrawGlassPanel(dl, cardMin, cardMax, glassSpec); float cx = cardMin.x + Layout::spacingLg(); float cy = cardMin.y + Layout::spacingLg(); + // ---- SUMMARY: fiat value (hero) + 24h change, BTC right-aligned ---- if (market.price_usd > 0) { double portfolio_usd = total_balance * market.price_usd; - if (portfolio_usd >= 1.0) - snprintf(buf, sizeof(buf), "$%.2f USD", portfolio_usd); - else - snprintf(buf, sizeof(buf), "$%.6f USD", portfolio_usd); + if (portfolio_usd >= 1.0) snprintf(buf, sizeof(buf), "$%.2f USD", portfolio_usd); + else snprintf(buf, sizeof(buf), "$%.6f USD", portfolio_usd); DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy), Success(), buf); + float usdW = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf).x; - // 24h change on the holdings (the coin's 24h % applies to the value), shown - // beside the USD figure and colored by direction. if (market.change_24h != 0.0) { - float usdW = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf).x; char chg[48]; snprintf(chg, sizeof(chg), "%+.2f%% %s", market.change_24h, TR("market_24h_change")); ImU32 chgCol = market.change_24h >= 0 ? Success() : Error(); dl->AddText(capFont, capFont->LegacySize, - ImVec2(cx + usdW + Layout::spacingMd(), cy + 3), chgCol, chg); + ImVec2(cx + usdW + Layout::spacingMd(), + cy + (sub1->LegacySize - capFont->LegacySize)), + chgCol, chg); } double portfolio_btc = total_balance * market.price_btc; - snprintf(buf, sizeof(buf), "%.10f BTC", portfolio_btc); - float valW = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf).x; + 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(cardMax.x - valW - pad, cy + 2), OnSurfaceMedium(), buf); + ImVec2(cardMax.x - btcW - pad, cy + (sub1->LegacySize - capFont->LegacySize)), + OnSurfaceMedium(), buf); } else { dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), TR("market_no_price")); } + cy += sub1->LegacySize + Layout::spacingSm(); - cy += sub1->LegacySize + 8; - + // DRGX balance + Z/T breakdown (right-aligned). snprintf(buf, sizeof(buf), "%.8f %s", total_balance, DRAGONX_TICKER); dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), OnSurface(), buf); - - snprintf(buf, sizeof(buf), "Z: %.4f | T: %.4f", private_balance, transparent_balance); + snprintf(buf, sizeof(buf), "Z %.4f \xC2\xB7 T %.4f", private_balance, transparent_balance); float brkW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x; dl->AddText(capFont, capFont->LegacySize, ImVec2(cardMax.x - brkW - pad, cy + 2), OnSurfaceDisabled(), buf); + cy += body2->LegacySize + Layout::spacingSm(); - cy += body2->LegacySize + 8; - + // Full-width shielded/transparent ratio bar + % label. if (total_balance > 0) { - float barW = availWidth - Layout::spacingXxl() * 1.5f; - float barH = std::max(S.drawElement("tabs.market", "ratio-bar-min-height").size, S.drawElement("tabs.market", "ratio-bar-height").size * vs); + float barW = pfInnerW; float shieldedRatio = (float)(private_balance / total_balance); if (shieldedRatio > 1.0f) shieldedRatio = 1.0f; if (shieldedRatio < 0.0f) shieldedRatio = 0.0f; float shieldedW = barW * shieldedRatio; float transpW = barW - shieldedW; - ImVec2 barStart(cx, cy); - dl->AddRectFilled(barStart, ImVec2(barStart.x + barW, barStart.y + barH), + dl->AddRectFilled(barStart, ImVec2(barStart.x + barW, barStart.y + ratioBarH), IM_COL32(255, 255, 255, 10), 3.0f); if (shieldedW > 0.5f) - dl->AddRectFilled(barStart, ImVec2(barStart.x + shieldedW, barStart.y + barH), + dl->AddRectFilled(barStart, ImVec2(barStart.x + shieldedW, barStart.y + ratioBarH), WithAlpha(Success(), 200), transpW > 0.5f ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, 3.0f); if (transpW > 0.5f) dl->AddRectFilled(ImVec2(barStart.x + shieldedW, barStart.y), - ImVec2(barStart.x + barW, barStart.y + barH), + ImVec2(barStart.x + barW, barStart.y + ratioBarH), WithAlpha(Warning(), 200), shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f); - // market_pct_shielded is "%.0f%% Shielded" — pass a double, not an int (passing - // an int to a %f conversion is UB and renders garbage on x86-64). - snprintf(buf, sizeof(buf), TR("market_pct_shielded"), - static_cast(shieldedRatio) * 100.0); + // 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); dl->AddText(capFont, capFont->LegacySize, - ImVec2(cx, cy + barH + 2), OnSurfaceDisabled(), buf); + ImVec2(cx, cy + ratioBarH + Layout::spacingXs()), OnSurfaceDisabled(), buf); } - // ---- CUSTOM PORTFOLIO ENTRIES (below the all-funds summary) ---- - if (!pfEntries.empty()) { - float ey = cardMin.y + baseH - Layout::spacingSm(); - dl->AddLine(ImVec2(cardMin.x + glassSpec.rounding * 0.5f, ey), - ImVec2(cardMax.x - glassSpec.rounding * 0.5f, ey), WithAlpha(OnSurface(), 20), 1.0f); - ey += Layout::spacingSm(); - for (const auto& e : pfEntries) { + // ---- CUSTOM GROUPS — one glass card per entry, wrapping into a grid ---- + if (pfN > 0) { + float gy = cardMin.y + pfSummaryH; + dl->AddText(ovFont, ovFont->LegacySize, ImVec2(cx, gy), OnSurfaceMedium(), TR("portfolio_groups")); + gy += ovFont->LegacySize + Layout::spacingSm(); + + for (int i = 0; i < pfN; i++) { + const auto& e = pfEntriesGeo[i]; + int col = i % pfCardsPerRow; + int row = i / pfCardsPerRow; + ImVec2 gcMin(cx + col * (pfCardW + pfCardGap), gy + row * (pfCardH + pfCardGap)); + ImVec2 gcMax(gcMin.x + pfCardW, gcMin.y + pfCardH); + + GlassPanelSpec gcGlass; + gcGlass.rounding = 8.0f; + gcGlass.fillAlpha = 18; + gcGlass.borderAlpha = 30; + DrawGlassPanel(dl, gcMin, gcMax, gcGlass); + double bal = data::SumPortfolioBalance(e.addresses, state.addresses); - dl->AddText(body2, body2->LegacySize, ImVec2(cx, ey), OnSurface(), e.label.c_str()); - char amt[96]; - if (market.price_usd > 0) - snprintf(amt, sizeof(amt), "%.4f %s ($%.2f)", bal, DRAGONX_TICKER, bal * market.price_usd); - else - snprintf(amt, sizeof(amt), "%.4f %s", bal, DRAGONX_TICKER); - float amtW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, amt).x; - dl->AddText(capFont, capFont->LegacySize, - ImVec2(cardMax.x - amtW - pad, ey + 1), OnSurfaceMedium(), amt); - ey += entryRowH; + float tcx = gcMin.x + Layout::spacingSm(); + float tcy = gcMin.y + Layout::spacingSm(); + float maxTextW = pfCardW - Layout::spacingSm() * 2.0f; + + // Label (truncated with an ellipsis to the card width). + std::string label = e.label; + bool truncated = false; + while (label.size() > 1 && + body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, label.c_str()).x > maxTextW) { + label.pop_back(); + truncated = true; + } + if (truncated) label += "\xE2\x80\xA6"; + dl->AddText(body2, body2->LegacySize, ImVec2(tcx, tcy), OnSurfaceMedium(), label.c_str()); + tcy += body2->LegacySize + Layout::spacingXs(); + + // DRGX amount. + snprintf(buf, sizeof(buf), "%.4f %s", bal, DRAGONX_TICKER); + dl->AddText(body2, body2->LegacySize, ImVec2(tcx, tcy), OnSurface(), buf); + tcy += body2->LegacySize; + + // Fiat value. + if (market.price_usd > 0) { + snprintf(buf, sizeof(buf), "$%.2f", bal * market.price_usd); + dl->AddText(capFont, capFont->LegacySize, ImVec2(tcx, tcy), OnSurfaceDisabled(), buf); + } } } diff --git a/src/util/i18n.cpp b/src/util/i18n.cpp index 97484d4..25dc5b9 100644 --- a/src/util/i18n.cpp +++ b/src/util/i18n.cpp @@ -1119,6 +1119,7 @@ void I18n::loadBuiltinEnglish() strings_["market_pct_shielded"] = "%.0f%% Shielded"; strings_["market_portfolio"] = "MY DRGX"; strings_["portfolio_all_funds"] = "All funds"; + strings_["portfolio_groups"] = "GROUPS"; strings_["portfolio_manage"] = "Manage\xE2\x80\xA6"; strings_["portfolio_manage_title"] = "Manage portfolio"; strings_["portfolio_add_entry"] = "Add entry";