// browser_scroll.cpp — see browser_scroll.h. Pure scroll + search geometry; no host types. #include "core/instrument/ui/browser_scroll.h" #include "core/instrument/ui/editor_geometry.h" // kPad / kTitleHeight / kNavButtonWidth #include #include 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(std::tolower(static_cast(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(static_cast(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( static_cast(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(static_cast(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(dyPixels) * maxOff + (dyPixels >= 0 ? trackSpan / 2 : -trackSpan / 2)) / trackSpan; const long long proposed = static_cast(startOffset) + deltaOffset; if (proposed < 0) return 0; if (proposed > maxOff) return maxOff; return static_cast(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 filterNameIndices(const std::vector& names, const std::string& query) { std::vector out; out.reserve(names.size()); for (int i = 0; i < static_cast(names.size()); ++i) { if (nameMatchesQuery(names[static_cast(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