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:
@@ -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, ¤tExchange,
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user