Q-W1 pt2: core/shell/app relocation + sub-namespaces; one concrete ui::Rect (LTRB fork retired); slot_map split from bank_book; BankIndex→BankModel; 59/59 green
This commit is contained in:
@@ -0,0 +1,158 @@
|
||||
// browser_scroll.cpp — see browser_scroll.h. PURE scroll + search geometry over the S10
|
||||
// capture_browser. No host types; only the shared Rect + BrowserLayout.
|
||||
|
||||
#include "core/instrument/ui/browser_scroll.h"
|
||||
|
||||
#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 visible ROW: the topmost row whose bottom edge is below the offset. Floor so a row
|
||||
// partially scrolled off the top still draws (its lower part is visible).
|
||||
const int firstRow = offset / kBrowserCardHeight;
|
||||
// Last visible ROW: the row containing the pixel (offset + gridH - 1), inclusive; +1 for
|
||||
// the exclusive end. A row straddling the bottom edge still draws.
|
||||
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 at a grabbable minimum but
|
||||
// never taller than the track.
|
||||
int thumbH = static_cast<int>(static_cast<long long>(gridH) * gridH / content);
|
||||
thumbH = (std::max)(kMinThumbHeight, thumbH);
|
||||
thumbH = (std::min)(thumbH, gridH);
|
||||
|
||||
// Thumb top proportional to the offset over the movable track span.
|
||||
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);
|
||||
|
||||
// Thumb height (same formula as scrollThumbRect) -> movable track span in thumb pixels.
|
||||
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;
|
||||
}
|
||||
|
||||
} // namespace reasampler::instrument::ui
|
||||
Reference in New Issue
Block a user