feat(market): compact chart w/ time axis; floating portfolio + redesigned group cards
Follow-up to the Market tab rework, per feedback:
- Chart is more compact: a modest responsive height (~22% of the tab, floored
at 110px) instead of stretching to fill.
- Chart now has a time axis. The price series is one sample per ~60s market
refresh (RefreshScheduler::kPrice), so the x-axis is labelled in approximate
minutes-ago ("~45m … ~15m … now") instead of only "Now" — you can now read
the interval/span of each plot point.
- Portfolio: removed the top-level card background — the summary (fiat hero +
24h change + BTC, DRGX balance + Z/T, ratio bar) now floats directly under the
MY DRGX header, and the "GROUPS" heading is gone.
- Group cards redesigned: a max width (280px, shrinks on narrow tabs) so they
float rather than stretch; the group name is larger (subtitle font) top-left;
the DRGX + fiat amounts moved to the card's top-right corner; and more inner
padding around the content.
Removed the now-unused portfolio_groups i18n key. Full-node + Lite build clean;
ctest 1/1; hygiene clean. Rendering change — needs a screenshot check.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -276,39 +276,12 @@ void RenderMarketTab(App* app)
|
||||
char buf[128];
|
||||
|
||||
// ================================================================
|
||||
// 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.
|
||||
// Section geometry.
|
||||
// ================================================================
|
||||
float mktDp = Layout::dpiScale();
|
||||
|
||||
// -- 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;
|
||||
// -- Compact price chart: a modest responsive height, no longer stretched to fill the tab. --
|
||||
float chartH = std::max(110.0f * vs, std::min(chartElem.height * vs, marketAvail.y * 0.22f));
|
||||
|
||||
// -- Hero header (price + stats, excluding the chart) --
|
||||
float mktStatsRowH = ovFont->LegacySize + Layout::spacingXs() + sub1->LegacySize + pad;
|
||||
@@ -316,12 +289,28 @@ void RenderMarketTab(App* app)
|
||||
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);
|
||||
// -- Portfolio geometry. No parent card background: the summary floats under the header
|
||||
// and the custom-group cards float below it (each with its own glass background). --
|
||||
const auto& pfEntriesGeo = app->settings()->getPortfolioEntries();
|
||||
float ratioBarH = std::max(S.drawElement("tabs.market", "ratio-bar-min-height").size,
|
||||
S.drawElement("tabs.market", "ratio-bar-height").size * vs);
|
||||
float pfSummaryH = sub1->LegacySize + Layout::spacingSm() // fiat hero row
|
||||
+ body2->LegacySize + Layout::spacingSm() // DRGX balance row
|
||||
+ ratioBarH + Layout::spacingXs() + capFont->LegacySize // ratio bar + % label
|
||||
+ Layout::spacingMd(); // gap before the group cards
|
||||
int pfN = (int)pfEntriesGeo.size();
|
||||
float pfCardGap = Layout::spacingSm();
|
||||
float pfCardPad = Layout::spacingMd(); // inner padding of each group card
|
||||
float pfCardW = std::min(280.0f * mktDp, availWidth); // max card width (shrinks on a narrow tab)
|
||||
int pfCardsPerRow = std::max(1, (int)((availWidth + pfCardGap) / (pfCardW + pfCardGap)));
|
||||
if (pfN > 0 && pfCardsPerRow > pfN) pfCardsPerRow = pfN;
|
||||
// Card: a name row (sub1) + a fiat row (caption); the DRGX/fiat amounts sit top-right.
|
||||
float pfCardH = pfCardPad * 2.0f + sub1->LegacySize + Layout::spacingXs() + capFont->LegacySize;
|
||||
int pfRows = (pfN > 0) ? (pfN + pfCardsPerRow - 1) / pfCardsPerRow : 0;
|
||||
float pfGroupsH = (pfN > 0)
|
||||
? (pfRows * pfCardH + std::max(0, pfRows - 1) * pfCardGap)
|
||||
: 0.0f;
|
||||
float portfolioH = pfSummaryH + pfGroupsH;
|
||||
|
||||
// ================================================================
|
||||
// PRICE SUMMARY — Combined hero card with price, stats, and exchange
|
||||
@@ -558,14 +547,29 @@ void RenderMarketTab(App* app)
|
||||
dl->AddCircleFilled(points[i], dotR, dotCol);
|
||||
}
|
||||
|
||||
// X-axis: samples are per-refresh, not hourly-timestamped, so only the most
|
||||
// recent point can be labelled truthfully ("Now"). The series simply runs
|
||||
// older→newer, left→right. (Avoids the old fake "24h…6h" hourly gradations.)
|
||||
// X-axis time labels. The price series is one sample per market refresh (~60s
|
||||
// cadence, see RefreshScheduler::kPrice), so label the axis in approximate
|
||||
// minutes-ago (rightmost = "now"). Approximate — a refresh can be delayed — hence "~".
|
||||
{
|
||||
const char* nowText = TR("market_now");
|
||||
ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, nowText);
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(plotRight - lblSz.x, plotBottom + 2), OnSurfaceDisabled(), nowText);
|
||||
const float kSecPerPoint = 60.0f; // RefreshScheduler::kPrice
|
||||
int nPts = (int)n;
|
||||
int ticks = std::min(4, nPts);
|
||||
for (int tk = 0; tk < ticks; tk++) {
|
||||
float t = (ticks > 1) ? (float)tk / (float)(ticks - 1) : 1.0f;
|
||||
float xpos = plotLeft + t * plotW;
|
||||
int idx = (int)(t * (nPts - 1) + 0.5f);
|
||||
int minsAgo = (int)(((float)(nPts - 1 - idx) * kSecPerPoint) / 60.0f + 0.5f);
|
||||
char tlbl[32];
|
||||
if (minsAgo <= 0) snprintf(tlbl, sizeof(tlbl), "%s", TR("market_now"));
|
||||
else if (minsAgo < 90) snprintf(tlbl, sizeof(tlbl), "~%dm", minsAgo);
|
||||
else snprintf(tlbl, sizeof(tlbl), "~%.1fh", minsAgo / 60.0f);
|
||||
ImVec2 lblSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, tlbl);
|
||||
float lx = (tk == 0) ? plotLeft
|
||||
: (tk == ticks - 1) ? plotRight - lblSz.x
|
||||
: xpos - lblSz.x * 0.5f;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(lx, plotBottom + 2), OnSurfaceDisabled(), tlbl);
|
||||
}
|
||||
}
|
||||
|
||||
// Hover crosshair + tooltip
|
||||
@@ -756,7 +760,7 @@ void RenderMarketTab(App* app)
|
||||
}
|
||||
|
||||
// ================================================================
|
||||
// PORTFOLIO — Glass card with balance breakdown
|
||||
// PORTFOLIO — floating summary (no card background) + custom-group cards
|
||||
// ================================================================
|
||||
{
|
||||
Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio"));
|
||||
@@ -775,13 +779,11 @@ void RenderMarketTab(App* app)
|
||||
double private_balance = state.privateBalance;
|
||||
double transparent_balance = state.transparentBalance;
|
||||
|
||||
// No parent card background — the summary floats directly under the "MY DRGX" header.
|
||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
||||
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();
|
||||
float rightEdge = cardMin.x + availWidth;
|
||||
float cx = cardMin.x;
|
||||
float cy = cardMin.y;
|
||||
|
||||
// ---- SUMMARY: fiat value (hero) + 24h change, BTC right-aligned ----
|
||||
if (market.price_usd > 0) {
|
||||
@@ -805,7 +807,7 @@ void RenderMarketTab(App* app)
|
||||
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 - btcW - pad, cy + (sub1->LegacySize - capFont->LegacySize)),
|
||||
ImVec2(rightEdge - btcW, cy + (sub1->LegacySize - capFont->LegacySize)),
|
||||
OnSurfaceMedium(), buf);
|
||||
} else {
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), TR("market_no_price"));
|
||||
@@ -818,12 +820,12 @@ void RenderMarketTab(App* app)
|
||||
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);
|
||||
ImVec2(rightEdge - brkW, cy + 2), OnSurfaceDisabled(), buf);
|
||||
cy += body2->LegacySize + Layout::spacingSm();
|
||||
|
||||
// Full-width shielded/transparent ratio bar + % label.
|
||||
if (total_balance > 0) {
|
||||
float barW = pfInnerW;
|
||||
float barW = availWidth;
|
||||
float shieldedRatio = (float)(private_balance / total_balance);
|
||||
if (shieldedRatio > 1.0f) shieldedRatio = 1.0f;
|
||||
if (shieldedRatio < 0.0f) shieldedRatio = 0.0f;
|
||||
@@ -849,12 +851,9 @@ void RenderMarketTab(App* app)
|
||||
ImVec2(cx, cy + ratioBarH + Layout::spacingXs()), OnSurfaceDisabled(), buf);
|
||||
}
|
||||
|
||||
// ---- CUSTOM GROUPS — one glass card per entry, wrapping into a grid ----
|
||||
// ---- CUSTOM GROUPS — floating glass cards below the summary (no heading) ----
|
||||
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;
|
||||
@@ -863,42 +862,45 @@ void RenderMarketTab(App* app)
|
||||
ImVec2 gcMax(gcMin.x + pfCardW, gcMin.y + pfCardH);
|
||||
|
||||
GlassPanelSpec gcGlass;
|
||||
gcGlass.rounding = 8.0f;
|
||||
gcGlass.fillAlpha = 18;
|
||||
gcGlass.borderAlpha = 30;
|
||||
gcGlass.rounding = 10.0f;
|
||||
gcGlass.fillAlpha = 22;
|
||||
gcGlass.borderAlpha = 40;
|
||||
DrawGlassPanel(dl, gcMin, gcMax, gcGlass);
|
||||
|
||||
double bal = data::SumPortfolioBalance(e.addresses, state.addresses);
|
||||
float tcx = gcMin.x + Layout::spacingSm();
|
||||
float tcy = gcMin.y + Layout::spacingSm();
|
||||
float maxTextW = pfCardW - Layout::spacingSm() * 2.0f;
|
||||
float rowY = gcMin.y + pfCardPad;
|
||||
float gcRight = gcMax.x - pfCardPad;
|
||||
|
||||
// Label (truncated with an ellipsis to the card width).
|
||||
// Amounts, top-right corner: DRGX on the name row, fiat directly below it.
|
||||
snprintf(buf, sizeof(buf), "%.4f %s", bal, DRAGONX_TICKER);
|
||||
float drgxW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, buf).x;
|
||||
dl->AddText(body2, body2->LegacySize,
|
||||
ImVec2(gcRight - drgxW, rowY + (sub1->LegacySize - body2->LegacySize) * 0.5f),
|
||||
OnSurface(), buf);
|
||||
if (market.price_usd > 0) {
|
||||
char usd[48];
|
||||
snprintf(usd, sizeof(usd), "$%.2f", bal * market.price_usd);
|
||||
float usdW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, usd).x;
|
||||
dl->AddText(capFont, capFont->LegacySize,
|
||||
ImVec2(gcRight - usdW, rowY + sub1->LegacySize + Layout::spacingXs()),
|
||||
OnSurfaceDisabled(), usd);
|
||||
}
|
||||
|
||||
// Group name, top-left, larger — truncated so it never overlaps the amount.
|
||||
float nameMaxW = (gcRight - drgxW - Layout::spacingMd()) - (gcMin.x + pfCardPad);
|
||||
std::string label = e.label;
|
||||
bool truncated = false;
|
||||
while (label.size() > 1 &&
|
||||
body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, label.c_str()).x > maxTextW) {
|
||||
sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, label.c_str()).x > nameMaxW) {
|
||||
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);
|
||||
}
|
||||
dl->AddText(sub1, sub1->LegacySize, ImVec2(gcMin.x + pfCardPad, rowY), OnSurface(), label.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + cardH));
|
||||
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + portfolioH));
|
||||
ImGui::Dummy(ImVec2(availWidth, 0));
|
||||
ImGui::Dummy(ImVec2(0, gap));
|
||||
}
|
||||
|
||||
@@ -1119,7 +1119,6 @@ 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