// bank_grid — pure implementation. See bank_grid.h. NO REAPER / SWELL / vendor. #include "bank_grid.h" #include namespace reasampler { namespace { // Builds a sorted, unique ascending index vector for the inclusive range [a, b] // (order-agnostic in a/b). Both ends assumed already in-range by the caller. std::vector rangeIndices(int a, int b) { if (a > b) std::swap(a, b); std::vector out; out.reserve(static_cast(b - a + 1)); for (int i = a; i <= b; ++i) out.push_back(i); return out; } // Clamps `index` to a valid cell (single-selection) result: sole member, focus and // anchor both at index. Used by plain click and plain arrow. Selection singleSelection(int index) { Selection s; s.indices = {index}; s.focus = index; s.anchor = index; return s; } } // namespace int columnsForWidth(int panelWidth, const GridSpec& spec) { // Layout: [gap][cell][gap][cell]...[cell][gap]. n cells occupy // gap + n*(cellWidth + gap). Solve for the largest n that fits panelWidth, // clamped to at least 1 so a too-narrow panel still shows a (clipped) column. const int cell = spec.cellWidth + spec.gap; if (cell <= 0) return 1; // degenerate spec — one column, avoid divide-by-zero const int usable = panelWidth - spec.gap; if (usable < spec.cellWidth) return 1; const int cols = usable / cell; return cols < 1 ? 1 : cols; } std::vector computeCellRects(int itemCount, int panelWidth, const GridSpec& spec) { std::vector rects; if (itemCount <= 0) return rects; const int cols = columnsForWidth(panelWidth, spec); rects.reserve(static_cast(itemCount)); for (int i = 0; i < itemCount; ++i) { const int col = i % cols; const int row = i / cols; CellRect r; r.x = spec.gap + col * (spec.cellWidth + spec.gap); r.y = spec.gap + row * (spec.cellHeight + spec.gap); r.width = spec.cellWidth; r.height = spec.cellHeight; rects.push_back(r); } return rects; } int contentHeight(int itemCount, int panelWidth, const GridSpec& spec) { if (itemCount <= 0) return 0; const int cols = columnsForWidth(panelWidth, spec); // Ceil-divide item count by columns to get the row count (partial last row // still occupies a full row of height). const int rows = (itemCount + cols - 1) / cols; return spec.gap + rows * (spec.cellHeight + spec.gap); } std::string thumbnailKeyString(const ThumbnailKey& key) { // Length-prefix the sampleId so a delimiter byte inside an id cannot forge a // collision with a different (id, width, generation) triple. std::string s; s.reserve(key.sampleId.size() + 32); s += std::to_string(key.sampleId.size()); s += ':'; s += key.sampleId; s += '|'; s += std::to_string(key.width); s += '|'; s += std::to_string(key.generation); return s; } // --- Interaction -------------------------------------------------------------- int hitTestCell(int px, int py, const std::vector& rects) { for (std::size_t i = 0; i < rects.size(); ++i) { const CellRect& r = rects[i]; // Half-open bounds so adjacent (gapless) rects never both claim a pixel. if (px >= r.x && px < r.x + r.width && py >= r.y && py < r.y + r.height) return static_cast(i); } return -1; } bool Selection::contains(int index) const { return std::binary_search(indices.begin(), indices.end(), index); } Selection applyClick(const Selection& current, int index, bool ctrl, bool shift, int itemCount) { if (itemCount <= 0 || index < 0 || index >= itemCount) return current; // Shift takes precedence over ctrl (documented): range-select from the anchor. if (shift) { const int anchor = current.anchor >= 0 && current.anchor < itemCount ? current.anchor : index; // no valid anchor -> seed at the click Selection s; s.indices = rangeIndices(anchor, index); s.focus = index; s.anchor = anchor; // anchor unchanged across a shift-range return s; } if (ctrl) { Selection s = current; auto it = std::lower_bound(s.indices.begin(), s.indices.end(), index); if (it != s.indices.end() && *it == index) s.indices.erase(it); // toggle OUT else s.indices.insert(it, index); // toggle IN (keeps sorted order) s.focus = index; s.anchor = index; // ctrl-click reseeds the range origin return s; } // Plain click: sole selection. return singleSelection(index); } Selection navigate(const Selection& current, NavKey key, int cols, int itemCount, bool shift) { if (itemCount <= 0) return current; if (cols < 1) cols = 1; // A fresh panel (no focus): the first key press focuses cell 0 without moving, // so the user sees the caret appear before it steps. if (current.focus < 0 || current.focus >= itemCount) { if (shift) { Selection s; s.indices = {0}; s.focus = 0; s.anchor = 0; return s; } return singleSelection(0); } const int from = current.focus; int to = from; switch (key) { case NavKey::Left: // Move one; clamp at cell 0 (stay put on the first cell). if (from > 0) to = from - 1; break; case NavKey::Right: // Move one; clamp at the last cell (stay put on the last cell). if (from < itemCount - 1) to = from + 1; break; case NavKey::Up: // Move up a row; if that leaves the grid (top row) stay put. if (from - cols >= 0) to = from - cols; break; case NavKey::Down: { // Move down a row. If the cell directly below exists, go there. If it // does not (we're above a MISSING partial-last-row cell) but there ARE // more cells, clamp to the last cell so the partial row is reachable. // If we're already in the last populated row, stay put. const int below = from + cols; if (below < itemCount) to = below; else if (from + 1 < itemCount) // partial last row below us to = itemCount - 1; break; } case NavKey::Home: to = 0; break; case NavKey::End: to = itemCount - 1; break; } if (!shift) return singleSelection(to); // Shift-extend: keep the anchor (seed it at the origin cell on first extend). const int anchor = current.anchor >= 0 && current.anchor < itemCount ? current.anchor : from; Selection s; s.indices = rangeIndices(anchor, to); s.focus = to; s.anchor = anchor; return s; } } // namespace reasampler