S12: editor scale + S15/S16 control surfaces

Pure browser_scroll/note_entry/param_slider modules (+ CTest) for scroll,
type-to-filter search, numeric note entry, and the AHDSR/Trigger/pitch-engine/
pitch-env control panel. Editor shell draws + routes through them; params edit
the selected zone's ZonePlayParams via commitAndReload.
This commit is contained in:
2026-07-27 01:09:19 -04:00
parent ddc2ec1e75
commit 4f30124ef7
13 changed files with 1667 additions and 31 deletions
+179
View File
@@ -0,0 +1,179 @@
// Standalone tests for reasampler::vst::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/vst/browser_scroll.h"
#include <cstdio>
#include <string>
#include <vector>
using namespace reasampler::vst;
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.top == base.top - 40);
CHECK(shifted.bottom == base.bottom - 40);
CHECK(shifted.left == base.left);
}
// --- 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.top == L.grid.top); // 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.top + 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.left == 0 && r.top == 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<std::string> names{"Kick", "Snare", "Kick Sub", "Hat"};
// Empty query -> every index, in order.
const std::vector<int> all = filterNameIndices(names, "");
CHECK(all.size() == 4 && all[0] == 0 && all[3] == 3);
// "kick" -> indices 0 and 2, order preserved.
const std::vector<int> kicks = filterNameIndices(names, "kick");
CHECK(kicks.size() == 2 && kicks[0] == 0 && kicks[1] == 2);
// No match -> empty.
CHECK(filterNameIndices(names, "zzz").empty());
}
int main() {
testContentHeight();
testMaxOffsetFitsAndOverflows();
testClamp();
testVisibleRangeTop();
testVisibleRangeScrolled();
testVisibleRangeEmptyWhenNoCards();
testScrolledCellShiftsUp();
testThumbEmptyWhenFits();
testThumbProportionalAndClamped();
testThumbDragIsInverse();
testSearchBoxRect();
testNameMatch();
testFilterIndices();
if (g_fail == 0) std::printf("browser_scroll: all tests passed\n");
return g_fail != 0;
}