180 lines
7.1 KiB
C++
180 lines
7.1 KiB
C++
// browser_scroll.cpp — see browser_scroll.h. Pure scroll + search geometry; no host types.
|
|
|
|
#include "core/instrument/ui/browser_scroll.h"
|
|
|
|
// The Browse modal is a full-window sheet drawn over the Sample face, so it reuses that
|
|
// face's chrome metrics rather than minting its own — a divergent title height or pad would
|
|
// make the sheet visibly not line up with what it covers.
|
|
#include "core/instrument/ui/sample_bands.h" // kPad / kTitleHeight
|
|
#include "core/instrument/ui/sample_chrome.h" // kNavButtonWidth (the Back button's slot)
|
|
|
|
#include <algorithm>
|
|
#include <cctype>
|
|
|
|
namespace reasampler::instrument::ui {
|
|
|
|
namespace {
|
|
// The minimum thumb height so a very long bank still yields a grabbable thumb.
|
|
constexpr int kMinThumbHeight = 20;
|
|
|
|
char asciiLower(char c) {
|
|
return static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
}
|
|
} // namespace
|
|
|
|
int scrollContentHeight(const BrowserLayout& layout, int cardCount) {
|
|
if (cardCount <= 0) return 0;
|
|
const int columns = (std::max)(1, layout.columns);
|
|
const int rows = (cardCount + columns - 1) / columns; // ceil
|
|
return rows * kBrowserCardHeight;
|
|
}
|
|
|
|
int scrollMaxOffset(const BrowserLayout& layout, int cardCount) {
|
|
const int content = scrollContentHeight(layout, cardCount);
|
|
const int gridH = (std::max)(0, layout.grid.height);
|
|
return (std::max)(0, content - gridH);
|
|
}
|
|
|
|
int clampScrollOffset(const BrowserLayout& layout, int cardCount, int proposedOffset) {
|
|
const int maxOff = scrollMaxOffset(layout, cardCount);
|
|
if (proposedOffset < 0) return 0;
|
|
if (proposedOffset > maxOff) return maxOff;
|
|
return proposedOffset;
|
|
}
|
|
|
|
VisibleRange visibleCardRange(const BrowserLayout& layout, int cardCount, int offset) {
|
|
VisibleRange vr;
|
|
if (cardCount <= 0) return vr;
|
|
const int columns = (std::max)(1, layout.columns);
|
|
const int gridH = (std::max)(0, layout.grid.height);
|
|
if (gridH <= 0 || kBrowserCardHeight <= 0) {
|
|
vr.first = 0;
|
|
vr.last = 0;
|
|
return vr;
|
|
}
|
|
if (offset < 0) offset = 0;
|
|
// First row: floored so a row partially scrolled off the top still draws. Last row:
|
|
// the row containing pixel (offset + gridH - 1), +1 for the exclusive end, so a row
|
|
// straddling the bottom edge still draws.
|
|
const int firstRow = offset / kBrowserCardHeight;
|
|
const int lastRow = (offset + gridH - 1) / kBrowserCardHeight + 1;
|
|
int first = firstRow * columns;
|
|
int last = lastRow * columns;
|
|
if (first > cardCount) first = cardCount;
|
|
if (last > cardCount) last = cardCount;
|
|
if (last < first) last = first;
|
|
vr.first = first;
|
|
vr.last = last;
|
|
return vr;
|
|
}
|
|
|
|
Rect scrolledCardCellRect(const BrowserLayout& layout, int index, int offset) {
|
|
Rect r = cardCellRect(layout, index);
|
|
if (r.right() <= r.x && r.bottom() <= r.y) return r; // empty (negative index) stays empty
|
|
return Rect::ltrb(r.x, r.y - offset, r.right(), r.bottom() - offset);
|
|
}
|
|
|
|
Rect scrollThumbRect(const BrowserLayout& layout, int cardCount, int offset) {
|
|
const int content = scrollContentHeight(layout, cardCount);
|
|
const int gridH = (std::max)(0, layout.grid.height);
|
|
if (content <= gridH || gridH <= 0) return Rect{}; // fits -> no scrollbar
|
|
const int maxOff = content - gridH;
|
|
if (offset < 0) offset = 0;
|
|
if (offset > maxOff) offset = maxOff;
|
|
|
|
const int trackRight = layout.grid.right();
|
|
const int trackLeft = trackRight - kScrollbarWidth;
|
|
const int trackTop = layout.grid.y;
|
|
|
|
// Thumb height proportional to the visible fraction, floored/capped to the track.
|
|
int thumbH = static_cast<int>(static_cast<long long>(gridH) * gridH / content);
|
|
thumbH = (std::max)(kMinThumbHeight, thumbH);
|
|
thumbH = (std::min)(thumbH, gridH);
|
|
|
|
const int trackSpan = gridH - thumbH; // >= 0
|
|
int thumbTop = trackTop;
|
|
if (maxOff > 0 && trackSpan > 0) {
|
|
thumbTop = trackTop + static_cast<int>(
|
|
static_cast<long long>(offset) * trackSpan / maxOff);
|
|
}
|
|
return Rect::ltrb(trackLeft, thumbTop, trackRight, thumbTop + thumbH);
|
|
}
|
|
|
|
int thumbDragToOffset(const BrowserLayout& layout, int cardCount, int startOffset,
|
|
int dyPixels) {
|
|
const int content = scrollContentHeight(layout, cardCount);
|
|
const int gridH = (std::max)(0, layout.grid.height);
|
|
if (content <= gridH || gridH <= 0) return clampScrollOffset(layout, cardCount, startOffset);
|
|
|
|
// Same thumb-height formula as scrollThumbRect.
|
|
int thumbH = static_cast<int>(static_cast<long long>(gridH) * gridH / content);
|
|
thumbH = (std::max)(kMinThumbHeight, thumbH);
|
|
thumbH = (std::min)(thumbH, gridH);
|
|
const int trackSpan = gridH - thumbH;
|
|
if (trackSpan <= 0) return clampScrollOffset(layout, cardCount, startOffset);
|
|
|
|
const int maxOff = content - gridH;
|
|
// A 1px thumb move covers maxOff/trackSpan content px. Round to nearest for symmetry.
|
|
const long long deltaOffset =
|
|
(static_cast<long long>(dyPixels) * maxOff + (dyPixels >= 0 ? trackSpan / 2 : -trackSpan / 2)) /
|
|
trackSpan;
|
|
const long long proposed = static_cast<long long>(startOffset) + deltaOffset;
|
|
if (proposed < 0) return 0;
|
|
if (proposed > maxOff) return maxOff;
|
|
return static_cast<int>(proposed);
|
|
}
|
|
|
|
Rect searchBoxRect(int w) {
|
|
if (w <= 0) return Rect{};
|
|
return Rect::ltrb(0, 0, w, kSearchBoxHeight);
|
|
}
|
|
|
|
bool nameMatchesQuery(const std::string& name, const std::string& query) {
|
|
if (query.empty()) return true;
|
|
if (query.size() > name.size()) return false;
|
|
// Case-insensitive substring scan (ASCII fold). Small strings; a naive scan is fine.
|
|
for (std::size_t i = 0; i + query.size() <= name.size(); ++i) {
|
|
bool match = true;
|
|
for (std::size_t j = 0; j < query.size(); ++j) {
|
|
if (asciiLower(name[i + j]) != asciiLower(query[j])) {
|
|
match = false;
|
|
break;
|
|
}
|
|
}
|
|
if (match) return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
std::vector<int> filterNameIndices(const std::vector<std::string>& names,
|
|
const std::string& query) {
|
|
std::vector<int> out;
|
|
out.reserve(names.size());
|
|
for (int i = 0; i < static_cast<int>(names.size()); ++i) {
|
|
if (nameMatchesQuery(names[static_cast<std::size_t>(i)], query))
|
|
out.push_back(i);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
BrowseModal computeBrowseModal(int w, int h) {
|
|
constexpr int kBrowseFooterH = 30;
|
|
BrowseModal m;
|
|
const int titleH = (std::min)(kTitleHeight, h);
|
|
m.title = Rect::ltrb(0, 0, w, titleH);
|
|
m.back = Rect::ltrb(w - kPad - kNavButtonWidth, 2, w - kPad, (std::max)(2, titleH - 2));
|
|
// Search box below the title, spanning the width (searchBoxRect lays it out from 0).
|
|
const Rect sb = searchBoxRect(w);
|
|
m.search = Rect::ltrb(kPad, titleH, w - kPad, titleH + sb.height);
|
|
const int footerTop = (std::max)(m.search.bottom(), h - kBrowseFooterH);
|
|
m.content = Rect::ltrb(0, m.search.bottom(), w, footerTop);
|
|
// Footer: Cancel (left) + Load (right).
|
|
const int fTop = footerTop + 3;
|
|
const int fBot = (std::max)(fTop, h - 3);
|
|
m.cancel = Rect::ltrb(kPad, fTop, kPad + 90, fBot);
|
|
m.confirm = Rect::ltrb(w - kPad - 90, fTop, w - kPad, fBot);
|
|
return m;
|
|
}
|
|
|
|
} // namespace reasampler::instrument::ui
|