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) <noreply@anthropic.com>
This commit is contained in:
@@ -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<PfCell> pfLayout((size_t)std::max(0, pfN));
|
||||
@@ -844,7 +845,7 @@ void RenderMarketTab(App* app)
|
||||
std::vector<char> 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);
|
||||
|
||||
Reference in New Issue
Block a user