From db212a81c6a9aec53f09187ff71632a54d2d644b Mon Sep 17 00:00:00 2001 From: DanS Date: Fri, 3 Jul 2026 14:30:04 -0500 Subject: [PATCH] feat(market): fine 32px square portfolio grid that scales to width and scrolls Replace the fill-to-height portfolio dashboard with a fine gapless grid of ~32px square cells whose column count scales to fill the tab width. The grid extends past the window and the Market tab scrolls when cards are placed lower. - Cells are a fixed square edge (>= ~32px), gapless; cards inset by 3dp so neighbours don't touch. Column count = availWidth / 32dp (min pfMinW cols). - Card minimum span is pfMinW x pfMinH (~128x96); layout/resize clamps to it. New groups default to 6x4 cells. - Content height reserves two extra rows below the lowest card so a card can be dragged past the current bottom; the scroll child grows to match. - Dot grid (shown while rearranging) softened and extended into the buffer rows. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/config/settings.h | 8 +++--- src/ui/windows/market_tab.cpp | 50 ++++++++++++++++------------------- 2 files changed, 27 insertions(+), 31 deletions(-) diff --git a/src/config/settings.h b/src/config/settings.h index 529105b..c13c778 100644 --- a/src/config/settings.h +++ b/src/config/settings.h @@ -91,12 +91,12 @@ public: bool show24h = false; // show the 24h % change (live-market bases only) bool showSparkline = false; // show a price-trend sparkline (live-market bases only) int sparklineInterval = 0; // 0=minute 1=hour 2=day 3=week 4=month (resample of price history) - // Dashboard grid placement on the Market portfolio. col/row in square grid cells - // (-1 = auto-place), w/h in cells (span; minimum 2x1). Set when the user drags/resizes. + // Dashboard grid placement on the Market portfolio. col/row in fine ~32px square cells + // (-1 = auto-place), w/h in cells (span). Set when the user drags/resizes a card. int gridCol = -1; int gridRow = -1; - int gridW = 2; - int gridH = 1; + int gridW = 6; + int gridH = 4; }; // Theme diff --git a/src/ui/windows/market_tab.cpp b/src/ui/windows/market_tab.cpp index 3d5ae27..c87297f 100644 --- a/src/ui/windows/market_tab.cpp +++ b/src/ui/windows/market_tab.cpp @@ -771,9 +771,8 @@ void RenderMarketTab(App* app) // Non-scrolling container — content resizes to fit available height 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) + // Scrollable: the portfolio grid can extend past the window and scroll into view. + ImGui::BeginChild("##MarketScroll", marketAvail, false, ImGuiWindowFlags_NoBackground); // Responsive: scale factors per frame float availWidth = ImGui::GetContentRegionAvail().x; @@ -820,13 +819,15 @@ void RenderMarketTab(App* app) + ratioBarH + Layout::spacingXs() + capFont->LegacySize // ratio bar + % label + Layout::spacingMd(); // gap before the group cards int pfN = (int)pfEntriesGeo.size(); - float pfCardGap = Layout::spacingSm(); float pfCardPad = Layout::spacingMd(); // inner padding of each group card - // Dashboard grid of SQUARE cells. Column count is responsive; min 2 columns so a card can meet - // its 2x1 minimum size. Cards drag to a new cell and corner-resize to span multiple cells. - int pfCols = std::max(2, std::min(8, (int)(availWidth / (110.0f * mktDp) + 0.5f))); - float pfCellW = (availWidth - (pfCols - 1) * pfCardGap) / (float)pfCols; - float pfCellH = pfCellW; // square cells; the smallest card is 2x1 + // 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 = 4, pfMinH = 3; // minimum card size in cells (~128x96) + 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 pfLayout((size_t)std::max(0, pfN)); @@ -844,7 +845,7 @@ void RenderMarketTab(App* app) std::vector placed((size_t)pfN, 0); for (int i = 0; i < pfN; i++) { const auto& e = pfEntriesGeo[i]; - int w = std::max(2, std::min(e.gridW, pfCols)), h = std::max(1, e.gridH); + int w = std::max(pfMinW, std::min(e.gridW, pfCols)), h = std::max(pfMinH, e.gridH); if (e.gridCol >= 0 && e.gridRow >= 0 && fits(e.gridCol, e.gridRow, w, h)) { pfLayout[i] = {e.gridCol, e.gridRow, w, h}; mark(e.gridCol, e.gridRow, w, h); placed[i] = 1; } @@ -852,7 +853,7 @@ void RenderMarketTab(App* app) for (int i = 0; i < pfN; i++) { if (placed[i]) continue; const auto& e = pfEntriesGeo[i]; - int w = std::max(2, std::min(e.gridW, pfCols)), h = std::max(1, e.gridH); + int w = std::max(pfMinW, std::min(e.gridW, pfCols)), h = std::max(pfMinH, e.gridH); int fc = 0, fr = 0; bool found = false; for (int r = 0; !found && r < 4096; r++) for (int c = 0; c + w <= pfCols; c++) @@ -861,7 +862,9 @@ void RenderMarketTab(App* app) } } int pfMaxRow = 0; for (const auto& L : pfLayout) pfMaxRow = std::max(pfMaxRow, L.row + L.h); - float pfGroupsH = (pfN > 0) ? (pfMaxRow * (pfCellH + pfCardGap) - pfCardGap) : 0.0f; + // 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; // ================================================================ @@ -1407,13 +1410,6 @@ 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 @@ -1429,6 +1425,8 @@ void RenderMarketTab(App* app) // 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) { + // The grid is gapless, so inset the card within its cell block for separation. + gcMin.x += pfInset; gcMin.y += pfInset; gcMax.x -= pfInset; gcMax.y -= pfInset; double bal = data::SumPortfolioBalance(e.addresses, state.addresses); ImU32 accent = e.color ? (ImU32)e.color : 0; float pad = pfCardPad; @@ -1511,14 +1509,12 @@ void RenderMarketTab(App* app) } }; - // Subtle dot grid while rearranging. + // Subtle dot grid while rearranging (fine 32px cells; extends into the buffer rows). if (interacting) { - ImU32 dotc = WithAlpha(OnSurface(), 55); - for (int r = 0; r <= pfMaxRow; r++) + ImU32 dotc = WithAlpha(OnSurface(), 38); + for (int r = 0; r <= pfMaxRow + 2; 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), - 1.5f * mktDp, dotc); + 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). @@ -1573,9 +1569,9 @@ void RenderMarketTab(App* app) if (s_pf_resize >= 0 && s_pf_resize < pfN) { int i = s_pf_resize; ImVec2 mn = cellMin(pfLayout[i]); - int nw = std::max(2, std::min(pfCols - pfLayout[i].col, + 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(1, (int)((io.MousePos.y - mn.y + pfCardGap) / (pfCellH + 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); drawCard(mn, mx, pfEntriesGeo[i], true); dl->AddRect(mn, mx, WithAlpha(Primary(), 200), 10.0f, 0, 2.0f * mktDp);