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