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)
|
void RenderMarketTab(App* app)
|
||||||
{
|
{
|
||||||
auto& S = schema::UI();
|
auto& S = schema::UI();
|
||||||
@@ -1520,118 +1669,14 @@ void RenderMarketTab(App* app)
|
|||||||
// ================================================================
|
// ================================================================
|
||||||
// PRICE SUMMARY — Combined hero card with price, stats, and exchange
|
// PRICE SUMMARY — Combined hero card with price, stats, and exchange
|
||||||
// ================================================================
|
// ================================================================
|
||||||
{
|
MktCtx mktc{
|
||||||
float dp = Layout::dpiScale();
|
app, ¤tExchange,
|
||||||
ImVec2 cardMin = ImGui::GetCursorScreenPos();
|
chartH, heroHeaderH, pfSummaryH, portfolioH, ratioBarH,
|
||||||
float cardH = heroHeaderH;
|
&chartTimes, nowSec, chartUp, periodChangePct, periodSuffix,
|
||||||
ImVec2 cardMax(cardMin.x + availWidth, cardMin.y + cardH);
|
&pfEntriesGeo, &pfLayout, pfN, pfCols, pfMinW, pfMinH, pfMaxRow,
|
||||||
// Combined hero + chart: draw ONE glass panel spanning the price/stats header AND the
|
pfCellW, pfCellH, pfInset, pfCardPad, pfCardGap
|
||||||
// chart below it (chartH precomputed so the chart shrinks to fit the portfolio).
|
};
|
||||||
float mergedChartH = chartH;
|
mktDrawPriceHero(mktc);
|
||||||
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));
|
|
||||||
}
|
|
||||||
|
|
||||||
// ================================================================
|
// ================================================================
|
||||||
// PRICE CHART — drawn inside the combined hero card's glass panel (above)
|
// PRICE CHART — drawn inside the combined hero card's glass panel (above)
|
||||||
|
|||||||
Reference in New Issue
Block a user