diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 6569c28..fa5754e 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -1542,6 +1542,275 @@ static void mktDrawPriceHero(const MktCtx& cx) ImGui::Dummy(ImVec2(availWidth, 0)); } +// 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(); + ImDrawList* dl = ImGui::GetWindowDrawList(); + float availWidth = ImGui::GetContentRegionAvail().x; + float pad = Layout::cardInnerPadding(); + float hs = Layout::hScale(availWidth); + float mktDp = Layout::dpiScale(); + ImFont* capFont = Type().caption(); + ImFont* sub1 = Type().subtitle1(); + float chartH = cx.chartH; + const auto& chartTimes = *cx.chartTimes; + std::time_t nowSec = cx.nowSec; + bool chartUp = cx.chartUp; + char buf[128]; + + // The chart series (s_mkt.history) + timestamps (chartTimes) were computed in 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. + // The chart shares the combined hero+chart glass panel drawn in PRICE SUMMARY above. + ImVec2 chartMin = ImGui::GetCursorScreenPos(); + ImVec2 chartMax(chartMin.x + availWidth, chartMin.y + chartH); + + float chartPad = pad; + float pillH = capFont->LegacySize + Layout::spacingXs() * 2.0f; + // Buttons sit near the top of the chart area, with a gap below them before the plot. + float stripTop = chartMin.y + Layout::spacingXs(); + + // ---- Top strip: interval buttons (left) + 24H volume / market cap + refresh (right) ---- + { + float rowTop = stripTop; + float textY = rowTop + (pillH - capFont->LegacySize) * 0.5f; + + // Interval buttons (left). Live = in-session buffer; the rest are historical. + const struct { const char* lbl; int iv; } kIvs[] = { + { TR("market_iv_live"), 0 }, { "1H", 1 }, { "1D", 2 }, { "1W", 3 }, { "1M", 4 } + }; + float bx = chartMin.x + chartPad; + for (int b = 0; b < 5; b++) { + float tw = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, kIvs[b].lbl).x; + float bw = tw + Layout::spacingSm() * 2.0f; + ImVec2 bmn(bx, rowTop), bmx(bx + bw, rowTop + pillH); + bool sel = (s_mkt.chartInterval == kIvs[b].iv); + bool bhov = material::IsRectHovered(bmn, bmx); + ImU32 bg = sel ? WithAlpha(Primary(), 200) + : (bhov ? WithAlpha(OnSurface(), 35) : WithAlpha(OnSurface(), 18)); + dl->AddRectFilled(bmn, bmx, bg, 4.0f); + dl->AddText(capFont, capFont->LegacySize, ImVec2(bx + Layout::spacingSm(), textY), + sel ? IM_COL32(255, 255, 255, 255) : OnSurface(), kIvs[b].lbl); + ImGui::SetCursorScreenPos(bmn); + ImGui::PushID(9100 + b); + if (ImGui::InvisibleButton("##civ", ImVec2(bw, pillH))) { + s_mkt.chartInterval = kIvs[b].iv; + } + if (ImGui::IsItemHovered()) ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); + ImGui::PopID(); + bx += bw + Layout::spacingXs(); + } + + // Refresh button (far right). + float rEdge = chartMax.x - chartPad; + ImFont* iconSmall = material::Typography::instance().iconSmall(); + ImVec2 rbMin(rEdge - pillH, rowTop), rbMax(rEdge, rowTop + pillH); + bool refreshHov = material::IsRectHovered(rbMin, rbMax); + if (refreshHov) { dl->AddRectFilled(rbMin, rbMax, IM_COL32(255, 255, 255, 20), 4.0f); + ImGui::SetMouseCursor(ImGuiMouseCursor_Hand); } + ImVec2 icSz = iconSmall->CalcTextSizeA(iconSmall->LegacySize, FLT_MAX, 0, ICON_MD_REFRESH); + dl->AddText(iconSmall, iconSmall->LegacySize, + ImVec2(rbMin.x + (pillH - icSz.x) * 0.5f, rbMin.y + (pillH - icSz.y) * 0.5f), + refreshHov ? OnSurface() : OnSurfaceMedium(), ICON_MD_REFRESH); + ImGui::SetCursorScreenPos(rbMin); + if (ImGui::InvisibleButton("##RefreshMarket", ImVec2(pillH, pillH))) { + app->refreshMarketData(); + } + if (ImGui::IsItemHovered()) material::Tooltip("%s", TR("market_refresh_price")); + + // 24H volume + market cap, right-aligned to the left of the refresh button. Skipped + // when there's no room (narrow window) so they never overlap the interval buttons. + float sxr = rbMin.x - Layout::spacingMd(); + auto drawStat = [&](const char* label, const std::string& val) { + char sb[64]; snprintf(sb, sizeof(sb), "%s %s", label, val.c_str()); + float w = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, sb).x; + if (sxr - w < bx + Layout::spacingMd()) return; + sxr -= w; + dl->AddText(capFont, capFont->LegacySize, ImVec2(sxr, textY), OnSurfaceMedium(), sb); + sxr -= Layout::spacingMd(); + }; + if (market.price_usd > 0) { + drawStat(TR("market_cap_short"), FormatCompactUSD(market.market_cap)); + drawStat(TR("market_vol_short"), FormatCompactUSD(market.volume_24h)); + } + } + + float labelPadLeft = std::max(S.drawElement("tabs.market", "chart-y-axis-min-padding").size, + S.drawElement("tabs.market", "chart-y-axis-padding").size * hs); + // Extra bottom room so the time labels aren't crowded against the card's bottom edge. + float labelPadBottom = Layout::spacingXl() + Layout::spacingSm(); + float plotLeft = chartMin.x + labelPadLeft; + float plotRight = chartMax.x - chartPad; + float plotTop = stripTop + pillH + Layout::spacingMd(); // gap below the buttons before the plot + float plotBottom = chartMax.y - labelPadBottom; + float plotW = plotRight - plotLeft; + float plotH = plotBottom - plotTop; + + if (s_mkt.history.size() >= 2) { + // Compute Y range with padding + double yMin = *std::min_element(s_mkt.history.begin(), s_mkt.history.end()); + double yMax = *std::max_element(s_mkt.history.begin(), s_mkt.history.end()); + if (yMax <= yMin) { yMax = yMin + 1e-8; } + double yRange = yMax - yMin; + double yPadding = yRange * 0.12; + yMin -= yPadding; + yMax += yPadding; + + // Horizontal grid lines (4 lines) + for (int g = 0; g <= 4; g++) { + float gy = plotTop + plotH * (float)g / 4.0f; + dl->AddLine(ImVec2(plotLeft, gy), ImVec2(plotRight, gy), + IM_COL32(255, 255, 255, 12), 1.0f); + double labelVal = yMax - (yMax - yMin) * (double)g / 4.0; + // Adaptive precision (via the shared price formatter) — cleaner than a fixed 6 decimals. + snprintf(buf, sizeof(buf), "%s", FormatPrice(labelVal).c_str()); + ImVec2 labelSz = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf); + // Keep the axis label inside the card even on narrow windows (min-padding may be + // smaller than the label width) so it never spills onto the tab background. + float lblX = std::max(chartMin.x + 3.0f, plotLeft - labelSz.x - 6); + dl->AddText(capFont, capFont->LegacySize, + ImVec2(lblX, gy - labelSz.y * 0.5f), + OnSurfaceDisabled(), buf); + } + + // Build points + size_t n = s_mkt.history.size(); + std::vector points(n); + + // Colored by the DISPLAYED period's direction (chartUp), not just 24h. + ImU32 dirCol = chartUp ? Success() : Error(); + ImU32 lineCol = WithAlpha(dirCol, 220); + ImU32 dotCol = dirCol; + + for (size_t i = 0; i < n; i++) { + float t = (n > 1) ? (float)i / (float)(n - 1) : 0.0f; + float x = plotLeft + t * plotW; + float y = plotBottom - (float)((s_mkt.history[i] - yMin) / (yMax - yMin)) * plotH; + points[i] = ImVec2(x, y); + } + + // Flat translucent area fill under the curve (matches the portfolio group sparklines). + for (size_t i = 0; i < n; i++) dl->PathLineTo(points[i]); + dl->PathLineTo(ImVec2(points[n - 1].x, plotBottom)); + dl->PathLineTo(ImVec2(points[0].x, plotBottom)); + dl->PathFillConcave(WithAlpha(dirCol, 28)); + + // Line (no per-point dots — a clean curve). + dl->AddPolyline(points.data(), (int)points.size(), lineCol, ImDrawFlags_None, S.drawElement("tabs.market", "chart-line-thickness").size); + + // High/low price labels at the displayed range's extremes (no dot markers). + if (n >= 3) { + size_t hiIdx = (size_t)(std::max_element(s_mkt.history.begin(), s_mkt.history.end()) - s_mkt.history.begin()); + size_t loIdx = (size_t)(std::min_element(s_mkt.history.begin(), s_mkt.history.end()) - s_mkt.history.begin()); + auto markExtreme = [&](size_t idx, bool high) { + ImVec2 p = points[idx]; + std::string lbl = FormatPrice(s_mkt.history[idx]); + ImVec2 ls = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, lbl.c_str()); + float ly = high ? p.y - ls.y - 5.0f * mktDp : p.y + 5.0f * mktDp; + float lx = std::min(std::max(plotLeft, p.x - ls.x * 0.5f), plotRight - ls.x); + dl->AddText(capFont, capFont->LegacySize, ImVec2(lx, ly), OnSurfaceMedium(), lbl.c_str()); + }; + markExtreme(hiIdx, true); + markExtreme(loIdx, false); + } + + // X-axis time labels. The series is timestamped, so label each tick with the real + // "