diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 00e77ce..6569c28 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -1393,6 +1393,155 @@ static void mktDrawPairSelector(App* app, const std::vector& } } +// 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* chartTimes; + std::time_t nowSec; + bool chartUp; + double periodChangePct; + std::string periodSuffix; + const std::vector* pfEntriesGeo; + const std::vector* 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)