// 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 #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 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(static_cast(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( 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); // Thumb height (same formula as scrollThumbRect) -> movable track span in thumb pixels. 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; } } // namespace reasampler::instrument::ui