Files
reasampler/src/core/instrument/ui/browser_scroll.cpp
T

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