feat(market): rework tab layout — responsive chart + refined portfolio + group cards
The Market tab is non-scrolling, but an oversized fixed-height chart ate ~65%
of the height and squeezed the portfolio into a cramped strip, with custom
groups rendered as tiny afterthought rows at the very bottom.
- Precompute section geometry (hero header + portfolio card height) up front and
let the price chart ABSORB the remaining vertical space, capped at its schema
height and floored at 130px*vs so it stays useful. The chart now shrinks to
make room instead of dominating. Replaces the barely-used SectionBudget.
- Portfolio summary refined: fiat value stays the hero with the 24h change beside
it and the BTC value right-aligned; DRGX balance + Z/T breakdown on the next
row; a full-width shielded/transparent ratio bar + % label.
- Custom groups now render as a wrapping grid of mini glass cards (label + DRGX +
fiat each), sized to the card width, instead of a cramped one-line list. A
"GROUPS" subheader separates them from the all-funds summary. The card grows to
fit the grid.
New i18n key portfolio_groups ("GROUPS"). Full-node + Lite build clean; ctest
1/1; hygiene clean. Rendering change — needs a screenshot check of the Market tab.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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); // "≈ <n> 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<double>(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<double>(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user