// browser_scroll.h — PURE scroll + type-to-filter geometry LAYERED over the S10 // capture_browser. NO VST3, NO REAPER, NO SWELL/LICE types at the boundary. The mirror of // capture_browser / editor_geometry: the fiddly scroll-window + scrollbar-thumb + search-box // arithmetic lives here, unit-tested outside the DAW, while the editor shell draws the // clipped card window + the scrollbar + the search field and routes wheel/drag/keystrokes // into these functions. // // WHY IT EXISTS (S12). capture_browser (S10) lays out EVERY card top-down and the shell // clips at the browser bottom — a bank longer than the panel runs off with no way to reach // it (the S12 gap). This module adds the two things S12 layers over that stable geometry: // * SCROLL — a vertical pixel offset into the card grid, with the max-offset clamp, the // visible-row window, a scrollbar thumb rect, and the thumb-drag<->offset mapping so a // wheel tick or a thumb drag reaches every card; and // * SEARCH — a name-substring filter (case-insensitive) that narrows the drawn cards, // COMPOSING with capture_browser's bank filter (the shell applies the bank filter first, // then this search narrows within it) + the search-box rect the shell draws the field in. // // It holds NO card data and draws nothing — it knows only the browser layout (from // capture_browser), COUNTS, and the scroll OFFSET the shell owns as transient UI state. It // reuses capture_browser's BrowserLayout + the shared Rect (one geometry idiom). #pragma once #include #include #include "capture_browser.h" // BrowserLayout, cardCellRect, kBrowserCardHeight, Rect namespace reasampler::vst { // The width (px) of the vertical scrollbar gutter at the right edge of the grid. The shell // draws the track + thumb here and hit-tests thumb grabs against scrollThumbRect. Exposed so // the shell and tests agree. When the content fits (no scroll needed) the scrollbar is // suppressed (scrollThumbRect returns empty) and the shell may reclaim the gutter. inline constexpr int kScrollbarWidth = 10; // The height (px) of the type-to-filter search box the shell draws ABOVE the tab strip (a // thin band spanning the browser width). Exposed so the shell reserves the band and tests // agree. capture_browser's tab strip + grid sit BELOW this band (the shell offsets the // BrowserLayout it feeds to capture_browser by kSearchBoxHeight). inline constexpr int kSearchBoxHeight = 22; // The total pixel HEIGHT the card grid needs to draw all `cardCount` cards at `layout`'s // column count: the number of ROWS (ceil(cardCount / columns)) times the fixed cell height. // Zero cards -> 0. Pure — the content extent the scroll offset ranges over. int scrollContentHeight(const BrowserLayout& layout, int cardCount); // The maximum scroll offset (px): content height minus the visible grid height, floored at 0. // When the content fits within the grid this is 0 (nothing to scroll). Pure — the clamp // ceiling for every offset the shell tracks. int scrollMaxOffset(const BrowserLayout& layout, int cardCount); // Clamp a proposed scroll offset into [0, scrollMaxOffset]. The shell clamps after every wheel // tick / thumb drag so an over-scroll pins to an edge rather than showing past the last card // or above the first. Pure. int clampScrollOffset(const BrowserLayout& layout, int cardCount, int proposedOffset); // The half-open range of card INDICES [first, last) at least partially visible in the grid at // scroll `offset`. The shell draws only these cards (the S12 clip window) rather than every // card. `offset` is assumed pre-clamped (the shell clamps on input); a first past the last row // yields an empty range (first==last==cardCount). Pure. struct VisibleRange { int first = 0; // first card index drawn (inclusive) int last = 0; // one past the last card index drawn (exclusive) }; VisibleRange visibleCardRange(const BrowserLayout& layout, int cardCount, int offset); // The cell rect of card `index` SHIFTED UP by the scroll offset, ready to draw (the shell // still adds the browser sub-area origin). Equivalent to capture_browser::cardCellRect with // the offset subtracted from top/bottom. Pure — the one place the offset applies to a card. Rect scrolledCardCellRect(const BrowserLayout& layout, int index, int offset); // The vertical scrollbar THUMB rect within the grid's right-edge gutter, sized proportional to // the visible fraction (grid height / content height) and positioned proportional to the // scroll offset. Returns an EMPTY rect when the content fits (no scroll needed) — the shell // suppresses the scrollbar then. A minimum thumb height keeps a tiny thumb grabbable on a very // long bank. Pure — the geometry the shell draws + hit-tests the thumb grab against. Rect scrollThumbRect(const BrowserLayout& layout, int cardCount, int offset); // Map a thumb-drag to a scroll offset. Given the offset the thumb held at grab time // (`startOffset`) and the vertical pixel delta since grab (`dyPixels`), returns the new // (clamped) scroll offset: startOffset shifted by the delta scaled from thumb-track pixels to // content pixels (a 1px thumb move covers content/track px of content). A degenerate track / // fitting content pins to startOffset. Pure — the inverse of scrollThumbRect's position map. int thumbDragToOffset(const BrowserLayout& layout, int cardCount, int startOffset, int dyPixels); // The search-box rect: a full-width band of height kSearchBoxHeight at the TOP of the browser // area (above where capture_browser's tab strip draws). `w` is the browser sub-area width; // the shell adds its origin. A zero/negative width yields an empty rect. Pure. Rect searchBoxRect(int w); // True iff `name` contains `query` as a case-insensitive ASCII substring. An EMPTY query // matches everything (the no-filter identity). Matching is ASCII case-folded (the display // names are ASCII until the Phase L type kit lands, mirroring the editor's other ASCII-only // text). Pure — the single match predicate the shell's search narrow is built from. bool nameMatchesQuery(const std::string& name, const std::string& query); // Narrow a list of display `names` to the INDICES whose name matches `query`, preserving // order. An EMPTY query returns every index [0, names.size()) (the composition base so "bank // filter, no search" == today's browser). Kept name-only (indices, not card structs) so this // module stays free of the sample_map/bank_book chain — the shell owns the SampleChoice list // and applies the bank filter FIRST, then feeds the surviving display names here (search // narrows within the bank). Pure. std::vector filterNameIndices(const std::vector& names, const std::string& query); } // namespace reasampler::vst