// Standalone tests for reasampler::instrument::ui::browser_scroll — no VST3, no REAPER, no framework. // Same fast assert loop as the sibling pure editor tests: assert the S12 scroll-window + // scrollbar-thumb + type-to-filter-search geometry LAYERED over the S10 capture_browser. // // Covers: scrollContentHeight (ceil rows * card height, 0 for no cards); scrollMaxOffset (0 // when content fits, else content-visible); clampScrollOffset pinning to [0,max]; visibleCardRange // windowing (top rows only, scrolled window, empty when scrolled past the end); scrolledCardCellRect // shifting a cell up by the offset; scrollThumbRect (empty when it fits, proportional height + // position, minimum height, at-max pins to the track bottom); thumbDragToOffset as the position // inverse (a full-track drag reaches max, round-trips); searchBoxRect; nameMatchesQuery // (case-insensitive substring, empty-query identity, no-match); filterNameIndices preserving order // and returning every index for an empty query. #include "../src/core/instrument/ui/browser_scroll.h" #include #include #include using namespace reasampler; using namespace reasampler::instrument::ui; static int g_fail = 0; #define CHECK(cond) do { if(!(cond)) { \ std::printf("FAIL line %d: %s\n", __LINE__, #cond); ++g_fail; } } while(0) // A layout wide enough for a few columns and tall enough to show a few rows. static BrowserLayout wideLayout() { return layoutBrowser(560, 300); } // --- content / max / clamp ---------------------------------------------------- static void testContentHeight() { const BrowserLayout L = wideLayout(); CHECK(scrollContentHeight(L, 0) == 0); // One card -> one row -> one card height. CHECK(scrollContentHeight(L, 1) == kBrowserCardHeight); // columns+1 cards -> two rows. CHECK(scrollContentHeight(L, L.columns + 1) == 2 * kBrowserCardHeight); // Exactly `columns` cards -> one row. CHECK(scrollContentHeight(L, L.columns) == kBrowserCardHeight); } static void testMaxOffsetFitsAndOverflows() { const BrowserLayout L = wideLayout(); // A single row fits within the 300px area -> no scroll. CHECK(scrollMaxOffset(L, L.columns) == 0); // Many rows overflow -> max = content - gridHeight. const int many = L.columns * 20; const int expect = scrollContentHeight(L, many) - L.grid.height; CHECK(scrollMaxOffset(L, many) == expect); CHECK(expect > 0); } static void testClamp() { const BrowserLayout L = wideLayout(); const int many = L.columns * 20; const int maxOff = scrollMaxOffset(L, many); CHECK(clampScrollOffset(L, many, -50) == 0); CHECK(clampScrollOffset(L, many, maxOff + 500) == maxOff); CHECK(clampScrollOffset(L, many, maxOff / 2) == maxOff / 2); } // --- visible window ----------------------------------------------------------- static void testVisibleRangeTop() { const BrowserLayout L = wideLayout(); const int many = L.columns * 20; const VisibleRange vr = visibleCardRange(L, many, 0); CHECK(vr.first == 0); // At offset 0, the last visible row is the one containing (gridH-1). const int expectedLastRow = (L.grid.height - 1) / kBrowserCardHeight + 1; CHECK(vr.last == expectedLastRow * L.columns); } static void testVisibleRangeScrolled() { const BrowserLayout L = wideLayout(); const int many = L.columns * 20; // Scroll one full card row down. const VisibleRange vr = visibleCardRange(L, many, kBrowserCardHeight); CHECK(vr.first == L.columns); // the first row scrolled off the top } static void testVisibleRangeEmptyWhenNoCards() { const BrowserLayout L = wideLayout(); const VisibleRange vr = visibleCardRange(L, 0, 0); CHECK(vr.first == 0 && vr.last == 0); } static void testScrolledCellShiftsUp() { const BrowserLayout L = wideLayout(); const Rect base = cardCellRect(L, 3); const Rect shifted = scrolledCardCellRect(L, 3, 40); CHECK(shifted.y == base.y - 40); CHECK(shifted.bottom() == base.bottom() - 40); CHECK(shifted.x == base.x); } // --- scrollbar thumb ---------------------------------------------------------- static void testThumbEmptyWhenFits() { const BrowserLayout L = wideLayout(); CHECK(scrollThumbRect(L, L.columns, 0).height == 0); // one row fits -> no thumb } static void testThumbProportionalAndClamped() { const BrowserLayout L = wideLayout(); const int many = L.columns * 20; const Rect atTop = scrollThumbRect(L, many, 0); CHECK(atTop.height > 0); CHECK(atTop.y == L.grid.y); // at offset 0 the thumb starts at the track top CHECK(atTop.width == kScrollbarWidth); CHECK(atTop.right() == L.grid.right()); // At max offset, the thumb bottom reaches the grid bottom (pinned to the end). const int maxOff = scrollMaxOffset(L, many); const Rect atMax = scrollThumbRect(L, many, maxOff); CHECK(atMax.bottom() == L.grid.y + L.grid.height); } static void testThumbDragIsInverse() { const BrowserLayout L = wideLayout(); const int many = L.columns * 20; const int maxOff = scrollMaxOffset(L, many); // A zero drag holds the start offset. CHECK(thumbDragToOffset(L, many, 0, 0) == 0); // A large positive drag pins to max; a large negative drag pins to 0. CHECK(thumbDragToOffset(L, many, 0, 100000) == maxOff); CHECK(thumbDragToOffset(L, many, maxOff, -100000) == 0); // Dragging the thumb by the whole track span from top reaches (near) max. const Rect thumb = scrollThumbRect(L, many, 0); const int trackSpan = L.grid.height - thumb.height; const int off = thumbDragToOffset(L, many, 0, trackSpan); CHECK(off >= maxOff - 2 && off <= maxOff); } // --- search ------------------------------------------------------------------- static void testSearchBoxRect() { const Rect r = searchBoxRect(200); CHECK(r.x == 0 && r.y == 0 && r.right() == 200 && r.height == kSearchBoxHeight); CHECK(searchBoxRect(0).width == 0); } static void testNameMatch() { CHECK(nameMatchesQuery("Kick Drum 01", "")); // empty query matches all CHECK(nameMatchesQuery("Kick Drum 01", "drum")); // case-insensitive substring CHECK(nameMatchesQuery("Kick Drum 01", "KICK")); CHECK(!nameMatchesQuery("Kick Drum 01", "snare")); CHECK(!nameMatchesQuery("ab", "abc")); // query longer than name } static void testFilterIndices() { std::vector names{"Kick", "Snare", "Kick Sub", "Hat"}; // Empty query -> every index, in order. const std::vector all = filterNameIndices(names, ""); CHECK(all.size() == 4 && all[0] == 0 && all[3] == 3); // "kick" -> indices 0 and 2, order preserved. const std::vector kicks = filterNameIndices(names, "kick"); CHECK(kicks.size() == 2 && kicks[0] == 0 && kicks[1] == 2); // No match -> empty. CHECK(filterNameIndices(names, "zzz").empty()); } // --- The Browse-modal regions (Q-W2v hoist, T2-06) --------------------------- // Title / search / content / footer partition the window top-to-bottom without gaps; // Back right-anchors in the title band; Cancel/Load flank the footer. static void testBrowseModalPartition() { const BrowseModal m = computeBrowseModal(840, 620); CHECK(m.title.y == 0 && m.title.width == 840 && m.title.height == kTitleHeight); CHECK(m.back.right() == 840 - kPad && m.back.bottom() <= m.title.bottom()); CHECK(m.search.y == m.title.bottom()); CHECK(m.search.height == searchBoxRect(840).height); CHECK(m.content.y == m.search.bottom()); CHECK(m.content.bottom() == 620 - 30); // the footer band (kBrowseFooterH) CHECK(m.cancel.x == kPad); CHECK(m.confirm.right() == 840 - kPad); CHECK(m.cancel.y == m.content.bottom() + 3 && m.cancel.y == m.confirm.y); // Degenerate short window: the footer clamps below the search box (no inversion). const BrowseModal s = computeBrowseModal(840, 40); CHECK(s.content.bottom() >= s.content.y); } int main() { testContentHeight(); testMaxOffsetFitsAndOverflows(); testClamp(); testVisibleRangeTop(); testVisibleRangeScrolled(); testVisibleRangeEmptyWhenNoCards(); testScrolledCellShiftsUp(); testThumbEmptyWhenFits(); testThumbProportionalAndClamped(); testThumbDragIsInverse(); testSearchBoxRect(); testNameMatch(); testFilterIndices(); testBrowseModalPartition(); if (g_fail == 0) std::printf("browser_scroll: all tests passed\n"); return g_fail != 0; }