refactor(market): extract price chart into mktDrawPriceChart

Move the ~250-line chart block (interval strip + 24h stats + refresh, plotted
curve with grid/area-fill/hi-lo+time labels/hover crosshair+tooltip, empty
state) out of RenderMarketTab into mktDrawPriceChart(MktCtx), recomputing cheap
locals and reading the precomputed series via the ctx. Drop the now-unused
hs/pad locals. 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:57:59 -05:00
parent a092097141
commit 71fb81670f

View File

@@ -1542,146 +1542,27 @@ static void mktDrawPriceHero(const MktCtx& cx)
ImGui::Dummy(ImVec2(availWidth, 0)); ImGui::Dummy(ImVec2(availWidth, 0));
} }
void RenderMarketTab(App* app) // Price chart, drawn inside the hero's shared glass panel: interval strip + 24h stats + refresh,
// then the plotted curve (grid, area fill, hi/lo + time labels, hover crosshair/tooltip) or an
// empty state. Reads the precomputed series (s_mkt.history / cx.chartTimes).
static void mktDrawPriceChart(const MktCtx& cx)
{ {
App* app = cx.app;
const auto& market = app->getWalletState().market;
auto& S = schema::UI(); auto& S = schema::UI();
auto summaryPanel = S.table("tabs.market", "summary-panel");
auto btcPriceLbl = S.label("tabs.market", "btc-price-label");
auto change24hLbl = S.label("tabs.market", "change-24h-label");
auto volumeLbl = S.label("tabs.market", "volume-label");
auto volumeValLbl = S.label("tabs.market", "volume-value-label");
auto mktCapLbl = S.label("tabs.market", "market-cap-label");
auto mktCapValLbl = S.label("tabs.market", "market-cap-value-label");
auto chartElem = S.drawElement("tabs.market", "chart");
auto portfolioValLbl = S.label("tabs.market", "portfolio-value-label");
auto portfolioBtcLbl = S.label("tabs.market", "portfolio-btc-label");
const auto& state = app->getWalletState();
const auto& market = state.market;
// Kick a once-per-session live exchange-list fetch (self-guarded), then source the
// registry from it (falling back to the compiled-in list until it arrives).
app->refreshExchanges();
// Also kick the historical price-chart fetch (self-throttled) so the chart's hour/day/week/
// month intervals populate promptly when the user opens the Market tab.
app->refreshMarketChart();
const auto& registry = EffectiveRegistry(market);
// Load persisted exchange/pair on first frame
LoadMarketState(app->settings(), registry);
if (s_mkt.exchangeIdx >= (int)registry.size()) s_mkt.exchangeIdx = 0;
const auto& currentExchange = registry[s_mkt.exchangeIdx];
if (s_mkt.pairIdx >= (int)currentExchange.pairs.size()) s_mkt.pairIdx = 0;
// (Panel theme-effects are suppressed frame-wide in App::render() while the modal is open, so
// their foreground borders don't bleed over the overlay — see PortfolioEditorActive().)
ImVec2 marketAvail = ImGui::GetContentRegionAvail();
// Scrollable (the portfolio grid can extend past the window) but with no visible scrollbar —
// scrolling still works via the mouse wheel.
ImGui::BeginChild("##MarketScroll", marketAvail, false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
// Responsive: scale factors per frame
float availWidth = ImGui::GetContentRegionAvail().x;
float hs = Layout::hScale(availWidth);
float vs = Layout::vScale(marketAvail.y);
float pad = Layout::cardInnerPadding();
float gap = Layout::cardGap();
ImDrawList* dl = ImGui::GetWindowDrawList(); ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec; float availWidth = ImGui::GetContentRegionAvail().x;
glassSpec.rounding = Layout::glassRounding(); float pad = Layout::cardInnerPadding();
float hs = Layout::hScale(availWidth);
float mktDp = Layout::dpiScale();
ImFont* capFont = Type().caption(); ImFont* capFont = Type().caption();
ImFont* sub1 = Type().subtitle1(); ImFont* sub1 = Type().subtitle1();
ImFont* body2 = Type().body2(); float chartH = cx.chartH;
const auto& chartTimes = *cx.chartTimes;
std::time_t nowSec = cx.nowSec;
bool chartUp = cx.chartUp;
char buf[128]; char buf[128];
// ================================================================
// Section geometry.
// ================================================================
float mktDp = Layout::dpiScale();
// -- 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: size to the ACTUAL content (price row + separator gap + stats row) so the
// chart starts right after "24H VOLUME"/"Market Cap" instead of below a reserved empty band. --
ImFont* mktH3 = Type().h3();
// Header holds only the price row; 24H volume + market cap and the divider moved onto the
// chart's top strip (with the interval buttons) so they no longer create an empty band.
float heroHeaderH = Layout::spacingLg() // top pad
+ mktH3->LegacySize // price row
+ Layout::spacingXs(); // small gap before the chart strip
// -- 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 pfCardPad = Layout::spacingMd(); // inner padding of each group card
// Fine SQUARE grid: cells are ~32px minimum and scale up to evenly fill the width. Cards snap to
// this grid and span many cells; they're drawn with an inner inset so adjacent cards don't touch.
float pfCardGap = 0.0f; // gapless cell grid
float pfInset = 3.0f * mktDp; // per-card inset for visual separation
const int pfMinW = 8, pfMinH = 2; // minimum card size in cells (~256x64)
int pfCols = std::max(pfMinW, (int)(availWidth / (32.0f * mktDp)));
float pfCellW = availWidth / (float)pfCols; // square cell edge (>= ~32px)
float pfCellH = pfCellW;
// Resolve each group's grid cell: honor a stored placement when it fits, else auto-place
// row-major into the first free cells. Deterministic each frame.
std::vector<PfCell> pfLayout = pfComputeGridLayout(pfEntriesGeo, pfCols, pfMinW, pfMinH);
int pfMaxRow = 0; for (const auto& L : pfLayout) pfMaxRow = std::max(pfMaxRow, L.row + L.h);
// Content height = the grid plus a couple of empty rows so cards can be dragged below the
// current bottom (the tab scrolls when the total exceeds the window).
float pfGroupsH = (pfN > 0) ? ((pfMaxRow + 2) * pfCellH) : 0.0f;
float portfolioH = pfSummaryH + pfGroupsH;
// Chart series for the selected interval (timestamped), shared by the hero change badge and
// the chart block below. Computed once here so the badge can report the displayed period's change.
std::time_t nowSec = std::time(nullptr);
std::vector<std::time_t> chartTimes;
s_mkt.history.clear();
for (const auto& pr : data::chartSeries(market, s_mkt.chartInterval, nowSec)) {
s_mkt.history.push_back(pr.second);
chartTimes.push_back(pr.first);
}
// Change over the displayed period (Live falls back to the market's 24h figure).
double periodChangePct = market.change_24h;
if (s_mkt.chartInterval != 0 && s_mkt.history.size() >= 2 && s_mkt.history.front() > 0.0)
periodChangePct = (s_mkt.history.back() - s_mkt.history.front()) / s_mkt.history.front() * 100.0;
bool chartUp = periodChangePct >= 0.0;
// Suffix = the SELECTED range (not the raw data span), so 1D reads "24h", 1W reads "7d", etc.
std::string periodSuffix = "24h";
switch (s_mkt.chartInterval) {
case 1: periodSuffix = "1h"; break; // 1H
case 2: periodSuffix = "24h"; break; // 1D
case 3: periodSuffix = "7d"; break; // 1W
case 4: periodSuffix = "30d"; break; // 1M
default: periodSuffix = "24h"; break; // Live (uses the market's 24h change)
}
// ================================================================
// PRICE SUMMARY — Combined hero card with price, stats, and exchange
// ================================================================
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)
// ================================================================
{
// The chart series (s_mkt.history) + timestamps (chartTimes) were computed in the // The chart series (s_mkt.history) + timestamps (chartTimes) were computed in the
// precompute above. Historical intervals come from CoinGecko market_chart; "Live" is the // precompute above. Historical intervals come from CoinGecko market_chart; "Live" is the
// in-session buffer. Until two points exist, the empty-state below is shown. // in-session buffer. Until two points exist, the empty-state below is shown.
@@ -1930,6 +1811,145 @@ void RenderMarketTab(App* app)
ImGui::Dummy(ImVec2(availWidth, 0)); ImGui::Dummy(ImVec2(availWidth, 0));
} }
void RenderMarketTab(App* app)
{
auto& S = schema::UI();
auto summaryPanel = S.table("tabs.market", "summary-panel");
auto btcPriceLbl = S.label("tabs.market", "btc-price-label");
auto change24hLbl = S.label("tabs.market", "change-24h-label");
auto volumeLbl = S.label("tabs.market", "volume-label");
auto volumeValLbl = S.label("tabs.market", "volume-value-label");
auto mktCapLbl = S.label("tabs.market", "market-cap-label");
auto mktCapValLbl = S.label("tabs.market", "market-cap-value-label");
auto chartElem = S.drawElement("tabs.market", "chart");
auto portfolioValLbl = S.label("tabs.market", "portfolio-value-label");
auto portfolioBtcLbl = S.label("tabs.market", "portfolio-btc-label");
const auto& state = app->getWalletState();
const auto& market = state.market;
// Kick a once-per-session live exchange-list fetch (self-guarded), then source the
// registry from it (falling back to the compiled-in list until it arrives).
app->refreshExchanges();
// Also kick the historical price-chart fetch (self-throttled) so the chart's hour/day/week/
// month intervals populate promptly when the user opens the Market tab.
app->refreshMarketChart();
const auto& registry = EffectiveRegistry(market);
// Load persisted exchange/pair on first frame
LoadMarketState(app->settings(), registry);
if (s_mkt.exchangeIdx >= (int)registry.size()) s_mkt.exchangeIdx = 0;
const auto& currentExchange = registry[s_mkt.exchangeIdx];
if (s_mkt.pairIdx >= (int)currentExchange.pairs.size()) s_mkt.pairIdx = 0;
// (Panel theme-effects are suppressed frame-wide in App::render() while the modal is open, so
// their foreground borders don't bleed over the overlay — see PortfolioEditorActive().)
ImVec2 marketAvail = ImGui::GetContentRegionAvail();
// Scrollable (the portfolio grid can extend past the window) but with no visible scrollbar —
// scrolling still works via the mouse wheel.
ImGui::BeginChild("##MarketScroll", marketAvail, false,
ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar);
// Responsive: scale factors per frame
float availWidth = ImGui::GetContentRegionAvail().x;
float vs = Layout::vScale(marketAvail.y);
float gap = Layout::cardGap();
ImDrawList* dl = ImGui::GetWindowDrawList();
GlassPanelSpec glassSpec;
glassSpec.rounding = Layout::glassRounding();
ImFont* capFont = Type().caption();
ImFont* sub1 = Type().subtitle1();
ImFont* body2 = Type().body2();
char buf[128];
// ================================================================
// Section geometry.
// ================================================================
float mktDp = Layout::dpiScale();
// -- 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: size to the ACTUAL content (price row + separator gap + stats row) so the
// chart starts right after "24H VOLUME"/"Market Cap" instead of below a reserved empty band. --
ImFont* mktH3 = Type().h3();
// Header holds only the price row; 24H volume + market cap and the divider moved onto the
// chart's top strip (with the interval buttons) so they no longer create an empty band.
float heroHeaderH = Layout::spacingLg() // top pad
+ mktH3->LegacySize // price row
+ Layout::spacingXs(); // small gap before the chart strip
// -- 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 pfCardPad = Layout::spacingMd(); // inner padding of each group card
// Fine SQUARE grid: cells are ~32px minimum and scale up to evenly fill the width. Cards snap to
// this grid and span many cells; they're drawn with an inner inset so adjacent cards don't touch.
float pfCardGap = 0.0f; // gapless cell grid
float pfInset = 3.0f * mktDp; // per-card inset for visual separation
const int pfMinW = 8, pfMinH = 2; // minimum card size in cells (~256x64)
int pfCols = std::max(pfMinW, (int)(availWidth / (32.0f * mktDp)));
float pfCellW = availWidth / (float)pfCols; // square cell edge (>= ~32px)
float pfCellH = pfCellW;
// Resolve each group's grid cell: honor a stored placement when it fits, else auto-place
// row-major into the first free cells. Deterministic each frame.
std::vector<PfCell> pfLayout = pfComputeGridLayout(pfEntriesGeo, pfCols, pfMinW, pfMinH);
int pfMaxRow = 0; for (const auto& L : pfLayout) pfMaxRow = std::max(pfMaxRow, L.row + L.h);
// Content height = the grid plus a couple of empty rows so cards can be dragged below the
// current bottom (the tab scrolls when the total exceeds the window).
float pfGroupsH = (pfN > 0) ? ((pfMaxRow + 2) * pfCellH) : 0.0f;
float portfolioH = pfSummaryH + pfGroupsH;
// Chart series for the selected interval (timestamped), shared by the hero change badge and
// the chart block below. Computed once here so the badge can report the displayed period's change.
std::time_t nowSec = std::time(nullptr);
std::vector<std::time_t> chartTimes;
s_mkt.history.clear();
for (const auto& pr : data::chartSeries(market, s_mkt.chartInterval, nowSec)) {
s_mkt.history.push_back(pr.second);
chartTimes.push_back(pr.first);
}
// Change over the displayed period (Live falls back to the market's 24h figure).
double periodChangePct = market.change_24h;
if (s_mkt.chartInterval != 0 && s_mkt.history.size() >= 2 && s_mkt.history.front() > 0.0)
periodChangePct = (s_mkt.history.back() - s_mkt.history.front()) / s_mkt.history.front() * 100.0;
bool chartUp = periodChangePct >= 0.0;
// Suffix = the SELECTED range (not the raw data span), so 1D reads "24h", 1W reads "7d", etc.
std::string periodSuffix = "24h";
switch (s_mkt.chartInterval) {
case 1: periodSuffix = "1h"; break; // 1H
case 2: periodSuffix = "24h"; break; // 1D
case 3: periodSuffix = "7d"; break; // 1W
case 4: periodSuffix = "30d"; break; // 1M
default: periodSuffix = "24h"; break; // Live (uses the market's 24h change)
}
// ================================================================
// PRICE SUMMARY — Combined hero card with price, stats, and exchange
// ================================================================
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)
// ================================================================
mktDrawPriceChart(mktc);
// ================================================================ // ================================================================
// PAIR SELECTOR — every trading pair across all exchanges shown as // PAIR SELECTOR — every trading pair across all exchanges shown as
// round buttons (flat; no exchange dropdown). Wraps to multiple rows. // round buttons (flat; no exchange dropdown). Wraps to multiple rows.