refactor(market): extract MktCtx + price hero into mktDrawPriceHero

Introduce a MktCtx struct holding RenderMarketTab's precomputed geometry/series,
built once after the precompute, and extract the combined hero card (glass panel
spanning header + chart, price/ticker/period-badge/trade button) into
mktDrawPriceHero(MktCtx). Cheap locals (dl/fonts/dp/market) recomputed in the
helper; the hero's local x-coord `cx` renamed `cx0` (the ctx param is `cx`).
Verbatim body. No behavior change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-04 00:53:27 -05:00
parent 50873d1112
commit a092097141

View File

@@ -1393,6 +1393,155 @@ static void mktDrawPairSelector(App* app, const std::vector<data::ExchangeInfo>&
}
}
// Shared per-frame context for the three coupled RenderMarketTab sections (hero/chart/portfolio):
// the precomputed geometry + chart series. Cheap locals (dl, fonts, dp, state, market, schema) are
// recomputed inside each helper.
struct MktCtx {
App* app;
const data::ExchangeInfo* currentExchange;
float chartH, heroHeaderH, pfSummaryH, portfolioH, ratioBarH;
const std::vector<std::time_t>* chartTimes;
std::time_t nowSec;
bool chartUp;
double periodChangePct;
std::string periodSuffix;
const std::vector<config::Settings::PortfolioEntry>* pfEntriesGeo;
const std::vector<PfCell>* pfLayout;
int pfN, pfCols, pfMinW, pfMinH, pfMaxRow;
float pfCellW, pfCellH, pfInset, pfCardPad, pfCardGap;
};
// Combined hero card: draws the ONE glass panel spanning the price header AND the chart below it,
// then the price / ticker / period-change badge / trade button (or a loading/error state).
static void mktDrawPriceHero(const MktCtx& cx)
{
App* app = cx.app;
const auto& market = app->getWalletState().market;
const auto& currentExchange = *cx.currentExchange;
ImDrawList* dl = ImGui::GetWindowDrawList();
float availWidth = ImGui::GetContentRegionAvail().x;
float pad = Layout::cardInnerPadding();
ImFont* capFont = Type().caption();
ImFont* sub1 = Type().subtitle1();
ImFont* body2 = Type().body2();
GlassPanelSpec glassSpec; glassSpec.rounding = Layout::glassRounding();
float chartH = cx.chartH, heroHeaderH = cx.heroHeaderH;
bool chartUp = cx.chartUp; double periodChangePct = cx.periodChangePct;
const std::string& periodSuffix = cx.periodSuffix;
char buf[128];
float dp = Layout::dpiScale();
ImVec2 cardMin = ImGui::GetCursorScreenPos();
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 (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);
float cx0 = cardMin.x + Layout::spacingLg();
float cy = cardMin.y + Layout::spacingLg();
if (market.price_usd > 0) {
// ---- HERO PRICE (large, prominent) ----
ImFont* h3 = Type().h3();
std::string priceStr = FormatPrice(market.price_usd);
ImU32 priceCol = Success();
DrawTextShadow(dl, h3, h3->LegacySize, ImVec2(cx0, cy), priceCol, priceStr.c_str());
// Ticker label after price
float priceW = h3->CalcTextSizeA(h3->LegacySize, FLT_MAX, 0, priceStr.c_str()).x;
dl->AddText(body2, body2->LegacySize,
ImVec2(cx0 + priceW + Layout::spacingSm(), cy + (h3->LegacySize - body2->LegacySize)),
OnSurfaceMedium(), DRAGONX_TICKER);
// Change badge — over the displayed chart period (or 24h for Live) — right of the ticker.
float tickerW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, DRAGONX_TICKER).x;
float badgeX = cx0 + priceW + Layout::spacingSm() + tickerW + Layout::spacingMd();
ImU32 chgCol = chartUp ? Success() : Error();
snprintf(buf, sizeof(buf), "%s%.2f%% %s", chartUp ? "+" : "", periodChangePct, periodSuffix.c_str());
ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
float badgePadH = Layout::spacingSm();
float badgePadV = Layout::spacingXs();
ImVec2 bMin(badgeX, cy + (h3->LegacySize - chgSz.y - badgePadV * 2) * 0.5f);
ImVec2 bMax(badgeX + chgSz.x + badgePadH * 2, bMin.y + chgSz.y + badgePadV * 2);
ImU32 badgeBg = chartUp ? WithAlpha(Success(), 30) : WithAlpha(Error(), 30);
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(bMin.x + badgePadH, bMin.y + badgePadV), chgCol, buf);
// (Divider is drawn in the chart block, below the interval buttons.)
// ---- TRADE BUTTON (top-right of card) ----
if (!currentExchange.pairs.empty()) {
const char* pairName = currentExchange.pairs[s_mkt.pairIdx].displayName.c_str();
ImFont* iconFont = Type().iconSmall();
ImVec2 textSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, pairName);
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_OPEN_IN_NEW);
float iconGap = Layout::spacingSm();
float tradePadH = Layout::spacingMd();
float tradePadV = Layout::spacingSm();
float tradeBtnW = textSz.x + iconGap + iconSz.x + tradePadH * 2;
float tradeBtnH = std::max(textSz.y, iconSz.y) + tradePadV * 2;
float tradeBtnX = cardMax.x - pad - tradeBtnW;
float tradeBtnY = cardMin.y + Layout::spacingSm();
ImVec2 tMin(tradeBtnX, tradeBtnY), tMax(tradeBtnX + tradeBtnW, tradeBtnY + tradeBtnH);
bool tradeHov = material::IsRectHovered(tMin, tMax);
// Glass pill background
GlassPanelSpec tradeBtnGlass;
tradeBtnGlass.rounding = tradeBtnH * 0.5f;
tradeBtnGlass.fillAlpha = tradeHov ? 35 : 20;
DrawGlassPanel(dl, tMin, tMax, tradeBtnGlass);
if (tradeHov)
dl->AddRectFilled(tMin, tMax, WithAlpha(Primary(), 20), tradeBtnH * 0.5f);
// Text (pair name with body2, icon with icon font)
ImU32 tradeCol = tradeHov ? OnSurface() : OnSurfaceMedium();
float contentY = tradeBtnY + tradePadV;
float curX = tradeBtnX + tradePadH;
dl->AddText(body2, body2->LegacySize,
ImVec2(curX, contentY), tradeCol, pairName);
curX += textSz.x + iconGap;
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(curX, contentY + (textSz.y - iconSz.y) * 0.5f), tradeCol, ICON_MD_OPEN_IN_NEW);
// Click
ImVec2 savedCur = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(tMin);
ImGui::InvisibleButton("##TradeOnExchange", ImVec2(tradeBtnW, tradeBtnH));
if (ImGui::IsItemClicked()) {
util::Platform::openUrl(currentExchange.pairs[s_mkt.pairIdx].tradeUrl);
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip(TR("market_trade_on"), currentExchange.name.c_str());
}
ImGui::SetCursorScreenPos(savedCur);
}
} else {
const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable");
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy + 10), OnSurfaceDisabled(), status);
if (!market.price_loading && !market.price_error.empty()) {
std::string errorText = market.price_error;
float maxErrorW = cardMax.x - cx0 - Layout::spacingLg();
while (errorText.size() > 4 &&
capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, errorText.c_str()).x > maxErrorW) {
errorText.pop_back();
}
if (errorText.size() < market.price_error.size()) errorText += "...";
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx0, cy + 10 + sub1->LegacySize + Layout::spacingXs()),
Warning(), errorText.c_str());
}
}
// No inter-card gap — the chart is drawn immediately below, inside the same panel.
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
void RenderMarketTab(App* app)
{
auto& S = schema::UI();
@@ -1520,118 +1669,14 @@ void RenderMarketTab(App* app)
// ================================================================
// PRICE SUMMARY — Combined hero card with price, stats, and exchange
// ================================================================
{
float dp = Layout::dpiScale();
ImVec2 cardMin = ImGui::GetCursorScreenPos();
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 (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);
float cx = cardMin.x + Layout::spacingLg();
float cy = cardMin.y + Layout::spacingLg();
if (market.price_usd > 0) {
// ---- HERO PRICE (large, prominent) ----
ImFont* h3 = Type().h3();
std::string priceStr = FormatPrice(market.price_usd);
ImU32 priceCol = Success();
DrawTextShadow(dl, h3, h3->LegacySize, ImVec2(cx, cy), priceCol, priceStr.c_str());
// Ticker label after price
float priceW = h3->CalcTextSizeA(h3->LegacySize, FLT_MAX, 0, priceStr.c_str()).x;
dl->AddText(body2, body2->LegacySize,
ImVec2(cx + priceW + Layout::spacingSm(), cy + (h3->LegacySize - body2->LegacySize)),
OnSurfaceMedium(), DRAGONX_TICKER);
// Change badge — over the displayed chart period (or 24h for Live) — right of the ticker.
float tickerW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, DRAGONX_TICKER).x;
float badgeX = cx + priceW + Layout::spacingSm() + tickerW + Layout::spacingMd();
ImU32 chgCol = chartUp ? Success() : Error();
snprintf(buf, sizeof(buf), "%s%.2f%% %s", chartUp ? "+" : "", periodChangePct, periodSuffix.c_str());
ImVec2 chgSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf);
float badgePadH = Layout::spacingSm();
float badgePadV = Layout::spacingXs();
ImVec2 bMin(badgeX, cy + (h3->LegacySize - chgSz.y - badgePadV * 2) * 0.5f);
ImVec2 bMax(badgeX + chgSz.x + badgePadH * 2, bMin.y + chgSz.y + badgePadV * 2);
ImU32 badgeBg = chartUp ? WithAlpha(Success(), 30) : WithAlpha(Error(), 30);
dl->AddRectFilled(bMin, bMax, badgeBg, 4.0f * dp);
dl->AddText(capFont, capFont->LegacySize, ImVec2(bMin.x + badgePadH, bMin.y + badgePadV), chgCol, buf);
// (Divider is drawn in the chart block, below the interval buttons.)
// ---- TRADE BUTTON (top-right of card) ----
if (!currentExchange.pairs.empty()) {
const char* pairName = currentExchange.pairs[s_mkt.pairIdx].displayName.c_str();
ImFont* iconFont = Type().iconSmall();
ImVec2 textSz = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, pairName);
ImVec2 iconSz = iconFont->CalcTextSizeA(iconFont->LegacySize, FLT_MAX, 0, ICON_MD_OPEN_IN_NEW);
float iconGap = Layout::spacingSm();
float tradePadH = Layout::spacingMd();
float tradePadV = Layout::spacingSm();
float tradeBtnW = textSz.x + iconGap + iconSz.x + tradePadH * 2;
float tradeBtnH = std::max(textSz.y, iconSz.y) + tradePadV * 2;
float tradeBtnX = cardMax.x - pad - tradeBtnW;
float tradeBtnY = cardMin.y + Layout::spacingSm();
ImVec2 tMin(tradeBtnX, tradeBtnY), tMax(tradeBtnX + tradeBtnW, tradeBtnY + tradeBtnH);
bool tradeHov = material::IsRectHovered(tMin, tMax);
// Glass pill background
GlassPanelSpec tradeBtnGlass;
tradeBtnGlass.rounding = tradeBtnH * 0.5f;
tradeBtnGlass.fillAlpha = tradeHov ? 35 : 20;
DrawGlassPanel(dl, tMin, tMax, tradeBtnGlass);
if (tradeHov)
dl->AddRectFilled(tMin, tMax, WithAlpha(Primary(), 20), tradeBtnH * 0.5f);
// Text (pair name with body2, icon with icon font)
ImU32 tradeCol = tradeHov ? OnSurface() : OnSurfaceMedium();
float contentY = tradeBtnY + tradePadV;
float curX = tradeBtnX + tradePadH;
dl->AddText(body2, body2->LegacySize,
ImVec2(curX, contentY), tradeCol, pairName);
curX += textSz.x + iconGap;
dl->AddText(iconFont, iconFont->LegacySize,
ImVec2(curX, contentY + (textSz.y - iconSz.y) * 0.5f), tradeCol, ICON_MD_OPEN_IN_NEW);
// Click
ImVec2 savedCur = ImGui::GetCursorScreenPos();
ImGui::SetCursorScreenPos(tMin);
ImGui::InvisibleButton("##TradeOnExchange", ImVec2(tradeBtnW, tradeBtnH));
if (ImGui::IsItemClicked()) {
util::Platform::openUrl(currentExchange.pairs[s_mkt.pairIdx].tradeUrl);
}
if (ImGui::IsItemHovered()) {
ImGui::SetMouseCursor(ImGuiMouseCursor_Hand);
material::Tooltip(TR("market_trade_on"), currentExchange.name.c_str());
}
ImGui::SetCursorScreenPos(savedCur);
}
} else {
const char* status = market.price_loading ? TR("market_price_loading") : TR("market_price_unavailable");
DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy + 10), OnSurfaceDisabled(), status);
if (!market.price_loading && !market.price_error.empty()) {
std::string errorText = market.price_error;
float maxErrorW = cardMax.x - cx - Layout::spacingLg();
while (errorText.size() > 4 &&
capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, errorText.c_str()).x > maxErrorW) {
errorText.pop_back();
}
if (errorText.size() < market.price_error.size()) errorText += "...";
dl->AddText(capFont, capFont->LegacySize,
ImVec2(cx, cy + 10 + sub1->LegacySize + Layout::spacingXs()),
Warning(), errorText.c_str());
}
}
// No inter-card gap — the chart is drawn immediately below, inside the same panel.
ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMax.y));
ImGui::Dummy(ImVec2(availWidth, 0));
}
MktCtx mktc{
app, &currentExchange,
chartH, heroHeaderH, pfSummaryH, portfolioH, ratioBarH,
&chartTimes, nowSec, chartUp, periodChangePct, periodSuffix,
&pfEntriesGeo, &pfLayout, pfN, pfCols, pfMinW, pfMinH, pfMaxRow,
pfCellW, pfCellH, pfInset, pfCardPad, pfCardGap
};
mktDrawPriceHero(mktc);
// ================================================================
// PRICE CHART — drawn inside the combined hero card's glass panel (above)