From 4543cdc49519019c15ea8fb83ad296431958aaeb Mon Sep 17 00:00:00 2001 From: DanS Date: Sat, 4 Jul 2026 01:02:11 -0500 Subject: [PATCH] refactor(market): extract portfolio section into mktDrawPortfolio MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Move the balance summary (fiat/DRGX/BTC + shielded ratio bar) and the draggable/resizable custom-group card grid (drag/resize gestures, dot grid, click-to-edit) out of RenderMarketTab into mktDrawPortfolio(MktCtx); the local x-coord `cx` renamed `cx0` (ctx param is `cx`). Drop the now-unused gap/dl/glassSpec/buf locals. RenderMarketTab is now ~150 lines (from 962) — a thin orchestrator: precompute -> build MktCtx -> hero/chart/pair/portfolio. Verbatim bodies. No behavior change. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/ui/windows/market_tab.cpp | 530 +++++++++++++++++----------------- 1 file changed, 273 insertions(+), 257 deletions(-) diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index fa5754e..5103f85 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -1811,6 +1811,276 @@ static void mktDrawPriceChart(const MktCtx& cx) ImGui::Dummy(ImVec2(availWidth, 0)); } +// Portfolio: floating balance summary (fiat/DRGX/BTC + shielded ratio bar) and the draggable / +// resizable custom-group card grid (click a card to edit; drag to move; corner-drag to resize). +static void mktDrawPortfolio(const MktCtx& cx) +{ + App* app = cx.app; + const auto& state = app->getWalletState(); + const auto& market = state.market; + ImDrawList* dl = ImGui::GetWindowDrawList(); + float availWidth = ImGui::GetContentRegionAvail().x; + float gap = Layout::cardGap(); + float mktDp = Layout::dpiScale(); + ImFont* capFont = Type().caption(); + ImFont* sub1 = Type().subtitle1(); + ImFont* body2 = Type().body2(); + float ratioBarH = cx.ratioBarH, pfSummaryH = cx.pfSummaryH, portfolioH = cx.portfolioH; + const auto& pfEntriesGeo = *cx.pfEntriesGeo; + const auto& pfLayout = *cx.pfLayout; + int pfN = cx.pfN, pfCols = cx.pfCols, pfMinW = cx.pfMinW, pfMinH = cx.pfMinH, pfMaxRow = cx.pfMaxRow; + float pfCellW = cx.pfCellW, pfCellH = cx.pfCellH, pfInset = cx.pfInset, pfCardPad = cx.pfCardPad, pfCardGap = cx.pfCardGap; + char buf[128]; + + Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio")); + // "Manage…" button, right-aligned on the header row — opens the portfolio editor. + { + const char* ml = TR("portfolio_manage"); + float mBtnW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, ml).x + Layout::spacingMd() * 2; + ImGui::SameLine(); + pfRightAlignX(mBtnW); + if (ImGui::Button(ml, ImVec2(mBtnW, 0))) { + // Open the combined editor selecting the first group (or the empty state if none). + PortfolioBeginEdit(app, app->settings()->getPortfolioEntries().empty() ? -1 : 0); + s_pfEdit.open = true; + } + } + ImGui::Dummy(ImVec2(0, Layout::spacingXs())); + + double total_balance = state.totalBalance; + double private_balance = state.privateBalance; + double transparent_balance = state.transparentBalance; + + // No parent card background — the summary floats directly under the "MY DRGX" header. + ImVec2 cardMin = ImGui::GetCursorScreenPos(); + float rightEdge = cardMin.x + availWidth; + float cx0 = cardMin.x; + float cy = cardMin.y; + + // ---- SUMMARY: fiat value (hero) + 24h change, BTC right-aligned ---- + if (market.price_usd > 0) { + double portfolio_usd = total_balance * market.price_usd; + if (portfolio_usd >= 1.0) snprintf(buf, sizeof(buf), "$%.2f USD", portfolio_usd); + else snprintf(buf, sizeof(buf), "$%.6f USD", portfolio_usd); + DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx0, cy), Success(), buf); + float usdW = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf).x; + + if (market.change_24h != 0.0) { + char chg[48]; + snprintf(chg, sizeof(chg), "%+.2f%% %s", market.change_24h, TR("market_24h_change")); + ImU32 chgCol = market.change_24h >= 0 ? Success() : Error(); + dl->AddText(capFont, capFont->LegacySize, + ImVec2(cx0 + usdW + Layout::spacingMd(), + cy + (sub1->LegacySize - capFont->LegacySize)), + chgCol, chg); + } + + double portfolio_btc = total_balance * market.price_btc; + snprintf(buf, sizeof(buf), "\xE2\x89\x88 %.8f BTC", portfolio_btc); // "≈ BTC" + float btcW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x; + dl->AddText(capFont, capFont->LegacySize, + ImVec2(rightEdge - btcW, cy + (sub1->LegacySize - capFont->LegacySize)), + OnSurfaceMedium(), buf); + } else { + dl->AddText(sub1, sub1->LegacySize, ImVec2(cx0, cy), OnSurfaceDisabled(), TR("market_no_price")); + } + cy += sub1->LegacySize + Layout::spacingSm(); + + // DRGX balance + Z/T breakdown (right-aligned). + snprintf(buf, sizeof(buf), "%.8f %s", total_balance, DRAGONX_TICKER); + dl->AddText(body2, body2->LegacySize, ImVec2(cx0, cy), OnSurface(), buf); + snprintf(buf, sizeof(buf), "Z %.4f \xC2\xB7 T %.4f", private_balance, transparent_balance); + float brkW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x; + dl->AddText(capFont, capFont->LegacySize, + ImVec2(rightEdge - brkW, cy + 2), OnSurfaceDisabled(), buf); + cy += body2->LegacySize + Layout::spacingSm(); + + // Full-width shielded/transparent ratio bar + % label. + if (total_balance > 0) { + float barW = availWidth; + float shieldedRatio = (float)(private_balance / total_balance); + if (shieldedRatio > 1.0f) shieldedRatio = 1.0f; + if (shieldedRatio < 0.0f) shieldedRatio = 0.0f; + float shieldedW = barW * shieldedRatio; + float transpW = barW - shieldedW; + ImVec2 barStart(cx0, cy); + + dl->AddRectFilled(barStart, ImVec2(barStart.x + barW, barStart.y + ratioBarH), + IM_COL32(255, 255, 255, 10), 3.0f); + if (shieldedW > 0.5f) + dl->AddRectFilled(barStart, ImVec2(barStart.x + shieldedW, barStart.y + ratioBarH), + WithAlpha(Success(), 200), + transpW > 0.5f ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, 3.0f); + if (transpW > 0.5f) + dl->AddRectFilled(ImVec2(barStart.x + shieldedW, barStart.y), + ImVec2(barStart.x + barW, barStart.y + ratioBarH), + WithAlpha(Warning(), 200), + shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f); + + // market_pct_shielded is "%.0f%% Shielded" — pass a double (an int to %f is UB). + snprintf(buf, sizeof(buf), TR("market_pct_shielded"), static_cast(shieldedRatio) * 100.0); + dl->AddText(capFont, capFont->LegacySize, + ImVec2(cx0, cy + ratioBarH + Layout::spacingXs()), OnSurfaceDisabled(), buf); + } + + // ---- CUSTOM GROUPS — dashboard grid: drag a card to move it, drag its corner to resize. + // A subtle dot grid shows while rearranging. Click (no drag) opens the editor. ---- + if (pfN > 0) { + float gy = cardMin.y + pfSummaryH; + ImGuiIO& io = ImGui::GetIO(); + bool interacting = (s_grid.drag >= 0 || s_grid.resize >= 0); + float rh = 20.0f * mktDp; // corner resize-grip size (generous so a short resize is easy to grab) + + auto cellMin = [&](const PfCell& L) { + return ImVec2(cx0 + L.col * (pfCellW + pfCardGap), gy + L.row * (pfCellH + pfCardGap)); + }; + auto cellSize = [&](int w, int h) { + return ImVec2(w * pfCellW + (w - 1) * pfCardGap, h * pfCellH + (h - 1) * pfCardGap); + }; + + // 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. + MktCardCtx cardCx{ dl, &state, &market, mktDp, pfInset, pfCardPad, sub1, capFont }; + + // Subtle dot grid while rearranging (fine 32px cells). Fill the whole visible grid area + // — down to the bottom of the scroll viewport, not just the occupied rows — so a card can + // be dropped anywhere, including the empty space below the current cards. + if (interacting) { + ImU32 dotc = WithAlpha(OnSurface(), 38); + float visibleBottom = dl->GetClipRectMax().y; // bottom edge of the scroll child + int rowsToBottom = (int)((visibleBottom - gy) / pfCellH) + 1; + int lastRow = std::max(pfMaxRow + 2, rowsToBottom); + for (int r = 0; r <= lastRow; r++) + for (int c = 0; c <= pfCols; c++) + dl->AddCircleFilled(ImVec2(cx0 + c * pfCellW, gy + r * pfCellH), 1.2f * mktDp, dotc); + } + + // Draw + hit-test each card. Skip the one being dragged/resized (drawn specially below). + for (int i = 0; i < pfN; i++) { + ImVec2 gcMin = cellMin(pfLayout[i]); + ImVec2 sz = cellSize(pfLayout[i].w, pfLayout[i].h); + ImVec2 gcMax(gcMin.x + sz.x, gcMin.y + sz.y); + + if (i == s_grid.drag && s_grid.moved) { + dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 45), 10.0f, 0, 1.0f); // source ghost + continue; + } + if (i == s_grid.resize) continue; + + bool hov = !interacting && material::IsRectHovered(gcMin, gcMax); + mktDrawCard(cardCx, gcMin, gcMax, pfEntriesGeo[i], hov); + + // Resize grip (bottom-right). + bool gripHov = material::IsRectHovered(ImVec2(gcMax.x - rh, gcMax.y - rh), gcMax); + ImU32 gcc = WithAlpha(OnSurface(), gripHov ? 170 : 80); + for (int k = 1; k <= 2; k++) { + float o = k * 4.0f * mktDp; + dl->AddLine(ImVec2(gcMax.x - o, gcMax.y - 3.0f), ImVec2(gcMax.x - 3.0f, gcMax.y - o), gcc, 1.3f * mktDp); + } + + // Start a gesture only when idle. One body button; the press location (corner vs body) + // decides resize vs move — avoids overlapping-item hit-test issues. + if (s_grid.drag < 0 && s_grid.resize < 0) { + ImGui::PushID(3000 + i); + ImGui::SetCursorScreenPos(gcMin); + ImGui::InvisibleButton("##pfBody", sz); + bool bodyAct = ImGui::IsItemActivated(); + bool bodyHov = ImGui::IsItemHovered(); + ImGui::PopID(); + if (bodyHov) ImGui::SetMouseCursor(gripHov ? ImGuiMouseCursor_ResizeNWSE : ImGuiMouseCursor_Hand); + if (bodyAct) { + if (gripHov) { s_grid.resize = i; s_grid.old = pfLayout[i]; } + else { + s_grid.drag = i; s_grid.moved = false; s_grid.old = pfLayout[i]; + s_grid.press = io.MousePos; + s_grid.grab = ImVec2(io.MousePos.x - gcMin.x, io.MousePos.y - gcMin.y); + } + } + } + } + + auto pfPersist = [&](const std::vector& v) { + app->settings()->setPortfolioEntries(v); app->settings()->save(); + }; + + // ----- Resize in progress (corner drag) ----- + if (s_grid.resize >= 0 && s_grid.resize < pfN) { + int i = s_grid.resize; + ImVec2 mn = cellMin(pfLayout[i]); + int nw = std::max(pfMinW, std::min(pfCols - pfLayout[i].col, + (int)((io.MousePos.x - mn.x + pfCardGap) / (pfCellW + pfCardGap) + 0.5f))); + int nh = std::max(pfMinH, (int)((io.MousePos.y - mn.y + pfCardGap) / (pfCellH + pfCardGap) + 0.5f)); + ImVec2 sz = cellSize(nw, nh), mx(mn.x + sz.x, mn.y + sz.y); + mktDrawCard(cardCx, mn, mx, pfEntriesGeo[i], true); + dl->AddRect(mn, mx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp); + // Live size readout (e.g. "8\xC3\x972") so the reached grid span is visible — the + // minimum is pfMinW x pfMinH (8x2). + { + char szb[32]; snprintf(szb, sizeof(szb), "%d\xC3\x97%d", nw, nh); + ImVec2 tsz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, szb); + ImVec2 tp((mn.x + mx.x) * 0.5f - tsz.x * 0.5f, (mn.y + mx.y) * 0.5f - tsz.y * 0.5f); + dl->AddRectFilled(ImVec2(tp.x - 6, tp.y - 3), ImVec2(tp.x + tsz.x + 6, tp.y + tsz.y + 3), + IM_COL32(20, 20, 30, 205), 4.0f); + dl->AddText(sub1, sub1->LegacySize, tp, OnSurface(), szb); + } + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNWSE); + if (!io.MouseDown[0]) { + auto entries = app->settings()->getPortfolioEntries(); + if (i < (int)entries.size()) { + entries[i].gridCol = pfLayout[i].col; entries[i].gridRow = pfLayout[i].row; + entries[i].gridW = nw; entries[i].gridH = nh; + pfPersist(entries); + } + s_grid.resize = -1; + } + } + + // ----- Drag in progress ----- + if (s_grid.drag >= 0 && s_grid.drag < pfN) { + int i = s_grid.drag; + if (io.MouseDown[0]) { + if (std::abs(io.MousePos.x - s_grid.press.x) > 4.0f || std::abs(io.MousePos.y - s_grid.press.y) > 4.0f) + s_grid.moved = true; + if (s_grid.moved) { + int w = pfLayout[i].w, h = pfLayout[i].h; + float tx = io.MousePos.x - s_grid.grab.x, ty = io.MousePos.y - s_grid.grab.y; + int tc = std::max(0, std::min(pfCols - w, (int)((tx - cx0) / (pfCellW + pfCardGap) + 0.5f))); + int tr = std::max(0, (int)((ty - gy) / (pfCellH + pfCardGap) + 0.5f)); + s_grid.target = {tc, tr, w, h}; + ImVec2 tmn(cx0 + tc * (pfCellW + pfCardGap), gy + tr * (pfCellH + pfCardGap)); + ImVec2 tsz = cellSize(w, h), tmx(tmn.x + tsz.x, tmn.y + tsz.y); + dl->AddRectFilled(tmn, tmx, WithAlpha(Primary(), 25), 10.0f); + dl->AddRect(tmn, tmx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp); + ImVec2 fmn(tx, ty), fmx(fmn.x + tsz.x, fmn.y + tsz.y); + mktDrawCard(cardCx, fmn, fmx, pfEntriesGeo[i], true); // dragged card follows the mouse + ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); + } + } else { + if (!s_grid.moved) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; } + else { + auto entries = app->settings()->getPortfolioEntries(); + if (i < (int)entries.size()) { + int occ = -1; + for (int j = 0; j < pfN; j++) + if (j != i && pfLayout[j].col == s_grid.target.col && pfLayout[j].row == s_grid.target.row) { occ = j; break; } + entries[i].gridCol = s_grid.target.col; entries[i].gridRow = s_grid.target.row; + if (occ >= 0 && occ < (int)entries.size()) { + entries[occ].gridCol = s_grid.old.col; entries[occ].gridRow = s_grid.old.row; + } + pfPersist(entries); + } + } + s_grid.drag = -1; s_grid.moved = false; + } + } + } + + ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + portfolioH)); + ImGui::Dummy(ImVec2(availWidth, 0)); + ImGui::Dummy(ImVec2(0, gap)); +} + void RenderMarketTab(App* app) { auto& S = schema::UI(); @@ -1850,20 +2120,14 @@ void RenderMarketTab(App* app) ImGui::BeginChild("##MarketScroll", marketAvail, false, ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoScrollbar); - // Responsive: scale factors per frame + // Responsive: scale factors per frame (the section helpers recompute their own draw list / + // fonts / paddings; only the values the precompute below needs are kept here). 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. // ================================================================ @@ -1959,255 +2223,7 @@ void RenderMarketTab(App* app) // ================================================================ // PORTFOLIO — floating summary (no card background) + custom-group cards // ================================================================ - { - Type().textColored(TypeStyle::Overline, OnSurfaceMedium(), TR("market_portfolio")); - // "Manage…" button, right-aligned on the header row — opens the portfolio editor. - { - const char* ml = TR("portfolio_manage"); - float mBtnW = body2->CalcTextSizeA(body2->LegacySize, FLT_MAX, 0, ml).x + Layout::spacingMd() * 2; - ImGui::SameLine(); - pfRightAlignX(mBtnW); - if (ImGui::Button(ml, ImVec2(mBtnW, 0))) { - // Open the combined editor selecting the first group (or the empty state if none). - PortfolioBeginEdit(app, app->settings()->getPortfolioEntries().empty() ? -1 : 0); - s_pfEdit.open = true; - } - } - ImGui::Dummy(ImVec2(0, Layout::spacingXs())); - - double total_balance = state.totalBalance; - double private_balance = state.privateBalance; - double transparent_balance = state.transparentBalance; - - // No parent card background — the summary floats directly under the "MY DRGX" header. - ImVec2 cardMin = ImGui::GetCursorScreenPos(); - float rightEdge = cardMin.x + availWidth; - float cx = cardMin.x; - float cy = cardMin.y; - - // ---- SUMMARY: fiat value (hero) + 24h change, BTC right-aligned ---- - if (market.price_usd > 0) { - double portfolio_usd = total_balance * market.price_usd; - if (portfolio_usd >= 1.0) snprintf(buf, sizeof(buf), "$%.2f USD", portfolio_usd); - else snprintf(buf, sizeof(buf), "$%.6f USD", portfolio_usd); - DrawTextShadow(dl, sub1, sub1->LegacySize, ImVec2(cx, cy), Success(), buf); - float usdW = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, buf).x; - - if (market.change_24h != 0.0) { - char chg[48]; - snprintf(chg, sizeof(chg), "%+.2f%% %s", market.change_24h, TR("market_24h_change")); - ImU32 chgCol = market.change_24h >= 0 ? Success() : Error(); - dl->AddText(capFont, capFont->LegacySize, - ImVec2(cx + usdW + Layout::spacingMd(), - cy + (sub1->LegacySize - capFont->LegacySize)), - chgCol, chg); - } - - double portfolio_btc = total_balance * market.price_btc; - snprintf(buf, sizeof(buf), "\xE2\x89\x88 %.8f BTC", portfolio_btc); // "≈ BTC" - float btcW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x; - dl->AddText(capFont, capFont->LegacySize, - ImVec2(rightEdge - btcW, cy + (sub1->LegacySize - capFont->LegacySize)), - OnSurfaceMedium(), buf); - } else { - dl->AddText(sub1, sub1->LegacySize, ImVec2(cx, cy), OnSurfaceDisabled(), TR("market_no_price")); - } - cy += sub1->LegacySize + Layout::spacingSm(); - - // DRGX balance + Z/T breakdown (right-aligned). - snprintf(buf, sizeof(buf), "%.8f %s", total_balance, DRAGONX_TICKER); - dl->AddText(body2, body2->LegacySize, ImVec2(cx, cy), OnSurface(), buf); - snprintf(buf, sizeof(buf), "Z %.4f \xC2\xB7 T %.4f", private_balance, transparent_balance); - float brkW = capFont->CalcTextSizeA(capFont->LegacySize, FLT_MAX, 0, buf).x; - dl->AddText(capFont, capFont->LegacySize, - ImVec2(rightEdge - brkW, cy + 2), OnSurfaceDisabled(), buf); - cy += body2->LegacySize + Layout::spacingSm(); - - // Full-width shielded/transparent ratio bar + % label. - if (total_balance > 0) { - float barW = availWidth; - float shieldedRatio = (float)(private_balance / total_balance); - if (shieldedRatio > 1.0f) shieldedRatio = 1.0f; - if (shieldedRatio < 0.0f) shieldedRatio = 0.0f; - float shieldedW = barW * shieldedRatio; - float transpW = barW - shieldedW; - ImVec2 barStart(cx, cy); - - dl->AddRectFilled(barStart, ImVec2(barStart.x + barW, barStart.y + ratioBarH), - IM_COL32(255, 255, 255, 10), 3.0f); - if (shieldedW > 0.5f) - dl->AddRectFilled(barStart, ImVec2(barStart.x + shieldedW, barStart.y + ratioBarH), - WithAlpha(Success(), 200), - transpW > 0.5f ? ImDrawFlags_RoundCornersLeft : ImDrawFlags_RoundCornersAll, 3.0f); - if (transpW > 0.5f) - dl->AddRectFilled(ImVec2(barStart.x + shieldedW, barStart.y), - ImVec2(barStart.x + barW, barStart.y + ratioBarH), - WithAlpha(Warning(), 200), - shieldedW > 0.5f ? ImDrawFlags_RoundCornersRight : ImDrawFlags_RoundCornersAll, 3.0f); - - // market_pct_shielded is "%.0f%% Shielded" — pass a double (an int to %f is UB). - snprintf(buf, sizeof(buf), TR("market_pct_shielded"), static_cast(shieldedRatio) * 100.0); - dl->AddText(capFont, capFont->LegacySize, - ImVec2(cx, cy + ratioBarH + Layout::spacingXs()), OnSurfaceDisabled(), buf); - } - - // ---- CUSTOM GROUPS — dashboard grid: drag a card to move it, drag its corner to resize. - // A subtle dot grid shows while rearranging. Click (no drag) opens the editor. ---- - if (pfN > 0) { - float gy = cardMin.y + pfSummaryH; - ImGuiIO& io = ImGui::GetIO(); - bool interacting = (s_grid.drag >= 0 || s_grid.resize >= 0); - float rh = 20.0f * mktDp; // corner resize-grip size (generous so a short resize is easy to grab) - - auto cellMin = [&](const PfCell& L) { - return ImVec2(cx + L.col * (pfCellW + pfCardGap), gy + L.row * (pfCellH + pfCardGap)); - }; - auto cellSize = [&](int w, int h) { - return ImVec2(w * pfCellW + (w - 1) * pfCardGap, h * pfCellH + (h - 1) * pfCardGap); - }; - - // 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. - MktCardCtx cardCx{ dl, &state, &market, mktDp, pfInset, pfCardPad, sub1, capFont }; - - // Subtle dot grid while rearranging (fine 32px cells). Fill the whole visible grid area - // — down to the bottom of the scroll viewport, not just the occupied rows — so a card can - // be dropped anywhere, including the empty space below the current cards. - if (interacting) { - ImU32 dotc = WithAlpha(OnSurface(), 38); - float visibleBottom = dl->GetClipRectMax().y; // bottom edge of the scroll child - int rowsToBottom = (int)((visibleBottom - gy) / pfCellH) + 1; - int lastRow = std::max(pfMaxRow + 2, rowsToBottom); - for (int r = 0; r <= lastRow; r++) - for (int c = 0; c <= pfCols; c++) - dl->AddCircleFilled(ImVec2(cx + c * pfCellW, gy + r * pfCellH), 1.2f * mktDp, dotc); - } - - // Draw + hit-test each card. Skip the one being dragged/resized (drawn specially below). - for (int i = 0; i < pfN; i++) { - ImVec2 gcMin = cellMin(pfLayout[i]); - ImVec2 sz = cellSize(pfLayout[i].w, pfLayout[i].h); - ImVec2 gcMax(gcMin.x + sz.x, gcMin.y + sz.y); - - if (i == s_grid.drag && s_grid.moved) { - dl->AddRect(gcMin, gcMax, WithAlpha(OnSurface(), 45), 10.0f, 0, 1.0f); // source ghost - continue; - } - if (i == s_grid.resize) continue; - - bool hov = !interacting && material::IsRectHovered(gcMin, gcMax); - mktDrawCard(cardCx, gcMin, gcMax, pfEntriesGeo[i], hov); - - // Resize grip (bottom-right). - bool gripHov = material::IsRectHovered(ImVec2(gcMax.x - rh, gcMax.y - rh), gcMax); - ImU32 gcc = WithAlpha(OnSurface(), gripHov ? 170 : 80); - for (int k = 1; k <= 2; k++) { - float o = k * 4.0f * mktDp; - dl->AddLine(ImVec2(gcMax.x - o, gcMax.y - 3.0f), ImVec2(gcMax.x - 3.0f, gcMax.y - o), gcc, 1.3f * mktDp); - } - - // Start a gesture only when idle. One body button; the press location (corner vs body) - // decides resize vs move — avoids overlapping-item hit-test issues. - if (s_grid.drag < 0 && s_grid.resize < 0) { - ImGui::PushID(3000 + i); - ImGui::SetCursorScreenPos(gcMin); - ImGui::InvisibleButton("##pfBody", sz); - bool bodyAct = ImGui::IsItemActivated(); - bool bodyHov = ImGui::IsItemHovered(); - ImGui::PopID(); - if (bodyHov) ImGui::SetMouseCursor(gripHov ? ImGuiMouseCursor_ResizeNWSE : ImGuiMouseCursor_Hand); - if (bodyAct) { - if (gripHov) { s_grid.resize = i; s_grid.old = pfLayout[i]; } - else { - s_grid.drag = i; s_grid.moved = false; s_grid.old = pfLayout[i]; - s_grid.press = io.MousePos; - s_grid.grab = ImVec2(io.MousePos.x - gcMin.x, io.MousePos.y - gcMin.y); - } - } - } - } - - auto pfPersist = [&](const std::vector& v) { - app->settings()->setPortfolioEntries(v); app->settings()->save(); - }; - - // ----- Resize in progress (corner drag) ----- - if (s_grid.resize >= 0 && s_grid.resize < pfN) { - int i = s_grid.resize; - ImVec2 mn = cellMin(pfLayout[i]); - int nw = std::max(pfMinW, std::min(pfCols - pfLayout[i].col, - (int)((io.MousePos.x - mn.x + pfCardGap) / (pfCellW + pfCardGap) + 0.5f))); - int nh = std::max(pfMinH, (int)((io.MousePos.y - mn.y + pfCardGap) / (pfCellH + pfCardGap) + 0.5f)); - ImVec2 sz = cellSize(nw, nh), mx(mn.x + sz.x, mn.y + sz.y); - mktDrawCard(cardCx, mn, mx, pfEntriesGeo[i], true); - dl->AddRect(mn, mx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp); - // Live size readout (e.g. "8\xC3\x972") so the reached grid span is visible — the - // minimum is pfMinW x pfMinH (8x2). - { - char szb[32]; snprintf(szb, sizeof(szb), "%d\xC3\x97%d", nw, nh); - ImVec2 tsz = sub1->CalcTextSizeA(sub1->LegacySize, FLT_MAX, 0, szb); - ImVec2 tp((mn.x + mx.x) * 0.5f - tsz.x * 0.5f, (mn.y + mx.y) * 0.5f - tsz.y * 0.5f); - dl->AddRectFilled(ImVec2(tp.x - 6, tp.y - 3), ImVec2(tp.x + tsz.x + 6, tp.y + tsz.y + 3), - IM_COL32(20, 20, 30, 205), 4.0f); - dl->AddText(sub1, sub1->LegacySize, tp, OnSurface(), szb); - } - ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeNWSE); - if (!io.MouseDown[0]) { - auto entries = app->settings()->getPortfolioEntries(); - if (i < (int)entries.size()) { - entries[i].gridCol = pfLayout[i].col; entries[i].gridRow = pfLayout[i].row; - entries[i].gridW = nw; entries[i].gridH = nh; - pfPersist(entries); - } - s_grid.resize = -1; - } - } - - // ----- Drag in progress ----- - if (s_grid.drag >= 0 && s_grid.drag < pfN) { - int i = s_grid.drag; - if (io.MouseDown[0]) { - if (std::abs(io.MousePos.x - s_grid.press.x) > 4.0f || std::abs(io.MousePos.y - s_grid.press.y) > 4.0f) - s_grid.moved = true; - if (s_grid.moved) { - int w = pfLayout[i].w, h = pfLayout[i].h; - float tx = io.MousePos.x - s_grid.grab.x, ty = io.MousePos.y - s_grid.grab.y; - int tc = std::max(0, std::min(pfCols - w, (int)((tx - cx) / (pfCellW + pfCardGap) + 0.5f))); - int tr = std::max(0, (int)((ty - gy) / (pfCellH + pfCardGap) + 0.5f)); - s_grid.target = {tc, tr, w, h}; - ImVec2 tmn(cx + tc * (pfCellW + pfCardGap), gy + tr * (pfCellH + pfCardGap)); - ImVec2 tsz = cellSize(w, h), tmx(tmn.x + tsz.x, tmn.y + tsz.y); - dl->AddRectFilled(tmn, tmx, WithAlpha(Primary(), 25), 10.0f); - dl->AddRect(tmn, tmx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp); - ImVec2 fmn(tx, ty), fmx(fmn.x + tsz.x, fmn.y + tsz.y); - mktDrawCard(cardCx, fmn, fmx, pfEntriesGeo[i], true); // dragged card follows the mouse - ImGui::SetMouseCursor(ImGuiMouseCursor_ResizeAll); - } - } else { - if (!s_grid.moved) { PortfolioBeginEdit(app, i); s_pfEdit.open = true; } - else { - auto entries = app->settings()->getPortfolioEntries(); - if (i < (int)entries.size()) { - int occ = -1; - for (int j = 0; j < pfN; j++) - if (j != i && pfLayout[j].col == s_grid.target.col && pfLayout[j].row == s_grid.target.row) { occ = j; break; } - entries[i].gridCol = s_grid.target.col; entries[i].gridRow = s_grid.target.row; - if (occ >= 0 && occ < (int)entries.size()) { - entries[occ].gridCol = s_grid.old.col; entries[occ].gridRow = s_grid.old.row; - } - pfPersist(entries); - } - } - s_grid.drag = -1; s_grid.moved = false; - } - } - } - - ImGui::SetCursorScreenPos(ImVec2(cardMin.x, cardMin.y + portfolioH)); - ImGui::Dummy(ImVec2(availWidth, 0)); - ImGui::Dummy(ImVec2(0, gap)); - } + mktDrawPortfolio(mktc); ImGui::EndChild(); // ##MarketScroll