diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 9d00a46..3d5ae27 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -148,6 +148,28 @@ static void pfDrawSparkline(ImDrawList* dl, ImVec2 mn, ImVec2 mx, dl->AddPolyline(pts.data(), n, col, ImDrawFlags_None, 1.2f); } +// Sparkline with a soft fill under the line — for the card's dedicated bottom strip. +static void pfDrawSparklineFilled(ImDrawList* dl, ImVec2 mn, ImVec2 mx, + const std::vector& hist, ImU32 lineCol) +{ + int n = (int)hist.size(); + if (n < 2) return; + double lo = *std::min_element(hist.begin(), hist.end()); + double hi = *std::max_element(hist.begin(), hist.end()); + if (hi <= lo) hi = lo + 1e-9; + float w = mx.x - mn.x, h = mx.y - mn.y; + std::vector pts(n); + for (int i = 0; i < n; i++) { + float t = (n > 1) ? (float)i / (float)(n - 1) : 0.0f; + pts[i] = ImVec2(mn.x + t * w, mx.y - (float)((hist[i] - lo) / (hi - lo)) * h); + } + for (int i = 0; i < n; i++) dl->PathLineTo(pts[i]); + dl->PathLineTo(ImVec2(pts[n - 1].x, mx.y)); + dl->PathLineTo(ImVec2(pts[0].x, mx.y)); + dl->PathFillConcave(WithAlpha(lineCol, 28)); + dl->AddPolyline(pts.data(), n, WithAlpha(lineCol, 220), ImDrawFlags_None, 1.4f); +} + // Dashboard-grid state for the portfolio cards (drag to move, corner-drag to resize). struct PfCell { int col, row, w, h; }; static int s_pf_drag = -1; // card index being dragged (-1 = none) @@ -751,6 +773,7 @@ void RenderMarketTab(App* app) ImVec2 marketAvail = ImGui::GetContentRegionAvail(); ImGui::BeginChild("##MarketScroll", marketAvail, false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar | ImGuiWindowFlags_NoScrollWithMouse); + ImVec2 mktOrigin = ImGui::GetCursorScreenPos(); // scroll-child content top (for filling the group grid) // Responsive: scale factors per frame float availWidth = ImGui::GetContentRegionAvail().x; @@ -1384,6 +1407,13 @@ void RenderMarketTab(App* app) // A subtle dot grid shows while rearranging. Click (no drag) opens the editor. ---- if (pfN > 0) { float gy = cardMin.y + pfSummaryH; + // Grid fills the available width (cols span it) AND height: size the rows to reach the + // bottom of the tab so there's no dead space and the dot grid extends to the edges. + float groupBottom = mktOrigin.y + marketAvail.y; + float availGroupH = std::max(pfMaxRow * (76.0f * mktDp), groupBottom - gy); + if (pfMaxRow > 0) + pfCellH = (availGroupH - (pfMaxRow - 1) * pfCardGap) / (float)pfMaxRow; + portfolioH = pfSummaryH + availGroupH; ImGuiIO& io = ImGui::GetIO(); bool interacting = (s_pf_drag >= 0 || s_pf_resize >= 0); float rh = 14.0f * mktDp; // corner resize-grip size @@ -1396,68 +1426,95 @@ void RenderMarketTab(App* app) }; // Draw one card's content into [gcMin, gcMax]. + // Stat tile: header (icon + name, 24h badge) / hero value (large, centered band) / + // footer (DRGX amount) + a dedicated sparkline strip along the bottom. auto drawCard = [&](ImVec2 gcMin, ImVec2 gcMax, const config::Settings::PortfolioEntry& e, bool hov) { double bal = data::SumPortfolioBalance(e.addresses, state.addresses); ImU32 accent = e.color ? (ImU32)e.color : 0; - float ch = gcMax.y - gcMin.y; + float pad = pfCardPad; GlassPanelSpec gcGlass; gcGlass.rounding = 10.0f; gcGlass.fillAlpha = hov ? 34 : 22; gcGlass.borderAlpha = 40; DrawGlassPanel(dl, gcMin, gcMax, gcGlass); if (hov) dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 70), 10.0f, 0, 1.0f); - if (accent) { - float bx = gcMin.x + 2.0f; - dl->AddRectFilled(ImVec2(bx, gcMin.y + pfCardPad * 0.6f), - ImVec2(bx + 3.0f, gcMax.y - pfCardPad * 0.6f), accent, 2.0f); + if (accent) + dl->AddRectFilled(ImVec2(gcMin.x + 2.0f, gcMin.y + pad * 0.6f), + ImVec2(gcMin.x + 5.0f, gcMax.y - pad * 0.6f), accent, 2.0f); + + float left = gcMin.x + pad + (accent ? 6.0f : 0.0f); + float right = gcMax.x - pad; + float availW = right - left; + auto fit = [&](std::string s, ImFont* f, float maxW) { + bool t = false; + while (s.size() > 1 && f->CalcTextSizeA(f->LegacySize, FLT_MAX, 0, s.c_str()).x > maxW) { s.pop_back(); t = true; } + if (t) s += "\xE2\x80\xA6"; + return s; + }; + + // Value + DRGX strings per basis. + char vb[80]; std::string valueStr; bool hasValue = false; + if (e.priceBasis == 0 && market.price_usd > 0) { snprintf(vb, sizeof(vb), "$%.2f", bal * market.price_usd); valueStr = vb; hasValue = true; } + else if (e.priceBasis == 1 && market.price_btc > 0) { snprintf(vb, sizeof(vb), "%.6f BTC", bal * market.price_btc); valueStr = vb; hasValue = true; } + else if (e.priceBasis == 3 && e.manualPrice > 0.0) { snprintf(vb, sizeof(vb), "%.2f %s", bal * e.manualPrice, e.manualCurrency.c_str()); valueStr = vb; hasValue = true; } + char db[48]; snprintf(db, sizeof(db), "%.4f %s", bal, DRAGONX_TICKER); + bool heroIsValue = e.showValue && hasValue; + std::string heroStr = heroIsValue ? valueStr : (e.showDrgx ? std::string(db) : (hasValue ? valueStr : std::string(db))); + std::string footStr = (heroIsValue && e.showDrgx) ? std::string(db) : std::string(); + + // Header: icon + name (left), 24h badge (right). + float hy = gcMin.y + pad, nameX = left, nameMaxW = availW; + if (!e.icon.empty()) { + material::project_icons::drawByName(dl, e.icon.c_str(), + ImVec2(nameX + body2->LegacySize * 0.5f, hy + body2->LegacySize * 0.5f), + accent ? accent : OnSurfaceMedium(), Type().iconSmall(), body2->LegacySize); + nameX += body2->LegacySize + Layout::spacingSm(); + nameMaxW = right - nameX; } + if (e.show24h && (e.priceBasis == 0 || e.priceBasis == 1) && market.change_24h != 0.0) { + char cb[32]; snprintf(cb, sizeof(cb), "%+.2f%%", market.change_24h); + float cw = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, cb).x; + dl->AddText(capFont, capFont->LegacySize, + ImVec2(right - cw, hy + (body2->LegacySize - capFont->LegacySize) * 0.5f), + market.change_24h >= 0 ? Success() : Error(), cb); + nameMaxW -= cw + Layout::spacingSm(); + } + dl->AddText(body2, body2->LegacySize, ImVec2(nameX, hy), OnSurfaceMedium(), + fit(e.label, body2, nameMaxW).c_str()); + + // Sparkline strip along the bottom. + float sparkH = 0.0f; if (e.showSparkline && (e.priceBasis == 0 || e.priceBasis == 1)) { std::vector hh = pfResampleHistory(market.price_history, e.sparklineInterval); if (hh.size() >= 2) { - ImU32 spCol = WithAlpha(hh.back() >= hh.front() ? Success() : Error(), 80); - pfDrawSparkline(dl, ImVec2(gcMin.x + pfCardPad + (accent ? 6.0f : 0.0f), gcMin.y + ch * 0.46f), - ImVec2(gcMax.x - pfCardPad, gcMax.y - pfCardPad * 0.6f), hh, spCol); + sparkH = std::max(18.0f * mktDp, (gcMax.y - gcMin.y) * 0.24f); + pfDrawSparklineFilled(dl, ImVec2(left, gcMax.y - pad - sparkH), ImVec2(right, gcMax.y - pad), + hh, hh.back() >= hh.front() ? Success() : Error()); } } - float rowY = gcMin.y + pfCardPad, gcRight = gcMax.x - pfCardPad; - PfDisplay disp = pfBuildDisplay(e, bal, market); - float primaryW = disp.primary.empty() ? 0.0f - : body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, disp.primary.c_str()).x; - if (!disp.primary.empty()) - dl->AddText(body2, body2->LegacySize, - ImVec2(gcRight - primaryW, rowY + (sub1->LegacySize - body2->LegacySize) * 0.5f), - OnSurface(), disp.primary.c_str()); - { - float belowY = rowY + sub1->LegacySize + Layout::spacingXs(); - float cursorR = gcRight; - if (!disp.change.empty()) { - float cw = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, disp.change.c_str()).x; - dl->AddText(capFont, capFont->LegacySize, ImVec2(gcRight - cw, belowY), disp.changeCol, disp.change.c_str()); - cursorR = gcRight - cw - Layout::spacingSm(); - } - if (!disp.secondary.empty()) { - float sw2 = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, disp.secondary.c_str()).x; - dl->AddText(capFont, capFont->LegacySize, ImVec2(cursorR - sw2, belowY), OnSurfaceDisabled(), disp.secondary.c_str()); - } + + // Footer: DRGX amount just above the sparkline. + float footBottom = gcMax.y - pad - (sparkH > 0 ? sparkH + Layout::spacingXs() : 0.0f); + float footTop = footBottom; + if (!footStr.empty()) { + footTop = footBottom - capFont->LegacySize; + dl->AddText(capFont, capFont->LegacySize, ImVec2(left, footTop), + OnSurfaceDisabled(), fit(footStr, capFont, availW).c_str()); } - float nameX = gcMin.x + pfCardPad + (accent ? 6.0f : 0.0f); - if (!e.icon.empty()) { - material::project_icons::drawByName(dl, e.icon.c_str(), - ImVec2(nameX + sub1->LegacySize * 0.5f, rowY + sub1->LegacySize * 0.5f), - accent ? accent : OnSurface(), Type().iconSmall(), sub1->LegacySize); - nameX += sub1->LegacySize + Layout::spacingSm(); + + // Hero value: large, centered in the band between the header and footer. + if (!heroStr.empty()) { + ImFont* heroFont = ((gcMax.y - gcMin.y) > 120.0f * mktDp) ? h4 : sub1; + float bandTop = hy + body2->LegacySize + Layout::spacingSm(); + float bandBot = (footStr.empty() ? footBottom : footTop) - Layout::spacingXs(); + float heroY = std::max(bandTop, (bandTop + bandBot) * 0.5f - heroFont->LegacySize * 0.5f); + dl->AddText(heroFont, heroFont->LegacySize, ImVec2(left, heroY), + OnSurface(), fit(heroStr, heroFont, availW).c_str()); } - float nameMaxW = (gcRight - primaryW - Layout::spacingMd()) - nameX; - std::string label = e.label; bool tr2 = false; - while (label.size() > 1 && sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, label.c_str()).x > nameMaxW) { - label.pop_back(); tr2 = true; - } - if (tr2) label += "\xE2\x80\xA6"; - dl->AddText(sub1, sub1->LegacySize, ImVec2(nameX, rowY), OnSurface(), label.c_str()); }; // Subtle dot grid while rearranging. if (interacting) { ImU32 dotc = WithAlpha(OnSurface(), 55); - for (int r = 0; r <= pfMaxRow + 1; r++) + for (int r = 0; r <= pfMaxRow; r++) for (int c = 0; c <= pfCols; c++) dl->AddCircleFilled(ImVec2(cx + c * (pfCellW + pfCardGap) - pfCardGap * 0.5f, gy + r * (pfCellH + pfCardGap) - pfCardGap * 0.5f),