199 lines
6.0 KiB
C++
199 lines
6.0 KiB
C++
// bank_grid — pure implementation. See bank_grid.h.
|
|
|
|
#include "core/ui/bank_grid.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace reasampler::ui {
|
|
|
|
namespace {
|
|
|
|
// Sorted, unique ascending index vector for the inclusive range [a, b] (order-agnostic in a/b).
|
|
std::vector<int> rangeIndices(int a, int b) {
|
|
if (a > b) std::swap(a, b);
|
|
std::vector<int> out;
|
|
out.reserve(static_cast<std::size_t>(b - a + 1));
|
|
for (int i = a; i <= b; ++i) out.push_back(i);
|
|
return out;
|
|
}
|
|
|
|
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).
|
|
const int cell = spec.cellWidth + spec.gap;
|
|
if (cell <= 0) return 1;
|
|
const int usable = panelWidth - spec.gap;
|
|
if (usable < spec.cellWidth) return 1;
|
|
const int cols = usable / cell;
|
|
return cols < 1 ? 1 : cols;
|
|
}
|
|
|
|
std::vector<CellRect> computeCellRects(int itemCount,
|
|
int panelWidth,
|
|
const GridSpec& spec) {
|
|
std::vector<CellRect> rects;
|
|
if (itemCount <= 0) return rects;
|
|
|
|
const int cols = columnsForWidth(panelWidth, spec);
|
|
rects.reserve(static_cast<std::size_t>(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);
|
|
const int rows = (itemCount + cols - 1) / cols; // ceil-divide
|
|
return spec.gap + rows * (spec.cellHeight + spec.gap);
|
|
}
|
|
|
|
std::string thumbnailKeyString(const ThumbnailKey& key) {
|
|
// Length-prefix sampleId so a delimiter byte inside it can't forge a collision.
|
|
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<CellRect>& rects) {
|
|
for (std::size_t i = 0; i < rects.size(); ++i) {
|
|
const CellRect& r = rects[i];
|
|
if (px >= r.x && px < r.x + r.width &&
|
|
py >= r.y && py < r.y + r.height)
|
|
return static_cast<int>(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;
|
|
|
|
if (shift) {
|
|
const int anchor = current.anchor >= 0 && current.anchor < itemCount
|
|
? current.anchor
|
|
: index;
|
|
Selection s;
|
|
s.indices = rangeIndices(anchor, index);
|
|
s.focus = index;
|
|
s.anchor = anchor;
|
|
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);
|
|
else
|
|
s.indices.insert(it, index);
|
|
s.focus = index;
|
|
s.anchor = index;
|
|
return s;
|
|
}
|
|
|
|
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;
|
|
|
|
// Fresh panel: first key press focuses cell 0 without moving.
|
|
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:
|
|
if (from > 0) to = from - 1;
|
|
break;
|
|
case NavKey::Right:
|
|
if (from < itemCount - 1) to = from + 1;
|
|
break;
|
|
case NavKey::Up:
|
|
if (from - cols >= 0) to = from - cols;
|
|
break;
|
|
case NavKey::Down: {
|
|
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);
|
|
|
|
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;
|
|
}
|
|
|
|
float compressAmplitudeForDisplay(float linear) {
|
|
const float mag = linear < 0.0f ? -linear : linear;
|
|
|
|
// std::pow isn't constexpr pre-C++20; derive at runtime, cheap since it's once per bin.
|
|
const float floorMag = std::pow(10.0f, kDisplayFloorDb / 20.0f);
|
|
|
|
if (mag <= floorMag) return 0.0f; // below floor (and guards log10(0))
|
|
|
|
const float db = 20.0f * std::log10(mag);
|
|
const float fraction = (db - kDisplayFloorDb) / (0.0f - kDisplayFloorDb);
|
|
|
|
const float clamped = fraction < 0.0f ? 0.0f : (fraction > 1.0f ? 1.0f : fraction);
|
|
return linear < 0.0f ? -clamped : clamped;
|
|
}
|
|
|
|
} // namespace reasampler::ui
|