b4b64ce68f
Tag LoadedInstrument with installedAt; process() publishes that field (not a re-read of reloadGeneration_) so the pruner's displacedAt <= seen bound is airtight. Also fixes sampleRowHitTest canvas.bottom over-read and adds interleave-stride + canvas-clip tests.
219 lines
9.2 KiB
C++
219 lines
9.2 KiB
C++
// Standalone tests for reasampler::vst::editor_geometry — no VST3, no REAPER, no test
|
|
// framework. Same fast assert loop as the sibling pure tests (mode_switch et al.):
|
|
// assert the IPlugView LICE editor's layout math + hit-testing directly.
|
|
//
|
|
// Covers: contains() half-open convention + degenerate rects; layoutEditor regions on a
|
|
// normal view (title band + button + canvas), a tiny view (button clamped to canvas,
|
|
// never overhanging), and a zero view (all rects empty, no inversion); hitTest hitting
|
|
// the button, missing on the title/canvas, missing outside the surface, and boundary
|
|
// pixels; layout<->hit-test agreement (a click on the drawn button rect hits it).
|
|
|
|
#include "../src/vst/editor_geometry.h"
|
|
|
|
#include <cstdio>
|
|
|
|
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)
|
|
|
|
// --- contains() ---------------------------------------------------------------
|
|
|
|
static void testContainsHalfOpen() {
|
|
Rect r{10, 20, 50, 40}; // [10,50) x [20,40)
|
|
CHECK(contains(r, 10, 20)); // top-left inclusive
|
|
CHECK(contains(r, 49, 39)); // bottom-right exclusive edge, inside
|
|
CHECK(!contains(r, 50, 30)); // right edge excluded
|
|
CHECK(!contains(r, 30, 40)); // bottom edge excluded
|
|
CHECK(!contains(r, 9, 30)); // left of rect
|
|
CHECK(!contains(r, 30, 19)); // above rect
|
|
}
|
|
|
|
static void testContainsDegenerate() {
|
|
CHECK(!contains(Rect{10, 10, 10, 20}, 10, 15)); // zero width
|
|
CHECK(!contains(Rect{10, 10, 20, 10}, 15, 10)); // zero height
|
|
CHECK(!contains(Rect{20, 10, 10, 20}, 15, 15)); // inverted (right < left)
|
|
}
|
|
|
|
// --- layoutEditor: normal view ------------------------------------------------
|
|
|
|
static void testLayoutNormalView() {
|
|
// A comfortable 400x260 view: title band spans the top full width; canvas is the
|
|
// rest; button sits inside the canvas, inset by the margin.
|
|
const EditorLayout L = layoutEditor(400, 260);
|
|
|
|
CHECK(L.titleBar.left == 0 && L.titleBar.top == 0);
|
|
CHECK(L.titleBar.right == 400);
|
|
CHECK(L.titleBar.height() > 0 && L.titleBar.height() <= 260);
|
|
|
|
// Canvas begins right below the title bar and reaches the bottom-right.
|
|
CHECK(L.canvas.top == L.titleBar.bottom);
|
|
CHECK(L.canvas.right == 400 && L.canvas.bottom == 260);
|
|
|
|
// Button is inside the canvas (does not overhang any edge).
|
|
CHECK(L.button.left >= L.canvas.left);
|
|
CHECK(L.button.top >= L.canvas.top);
|
|
CHECK(L.button.right <= L.canvas.right);
|
|
CHECK(L.button.bottom <= L.canvas.bottom);
|
|
CHECK(L.button.width() > 0 && L.button.height() > 0);
|
|
}
|
|
|
|
// --- layoutEditor: tiny view (clamping) ---------------------------------------
|
|
|
|
static void testLayoutTinyViewClampsButton() {
|
|
// A view narrower/shorter than the button's natural size: the button must clamp to
|
|
// the canvas and never produce an inverted or overhanging rect.
|
|
const EditorLayout L = layoutEditor(40, 40);
|
|
CHECK(L.button.right <= L.canvas.right);
|
|
CHECK(L.button.bottom <= L.canvas.bottom);
|
|
CHECK(L.button.right >= L.button.left); // never inverted
|
|
CHECK(L.button.bottom >= L.button.top);
|
|
// Title bar clamps to the client height when the view is shorter than its height.
|
|
CHECK(L.titleBar.bottom <= 40);
|
|
}
|
|
|
|
// --- layoutEditor: zero view (all empty, no inversion) ------------------------
|
|
|
|
static void testLayoutZeroView() {
|
|
const EditorLayout L = layoutEditor(0, 0);
|
|
CHECK(L.titleBar.width() <= 0 || L.titleBar.height() <= 0);
|
|
CHECK(L.canvas.width() <= 0 || L.canvas.height() <= 0);
|
|
// No rect is inverted.
|
|
CHECK(L.button.right >= L.button.left);
|
|
CHECK(L.button.bottom >= L.button.top);
|
|
CHECK(L.canvas.right >= L.canvas.left);
|
|
CHECK(L.canvas.bottom >= L.canvas.top);
|
|
// A click anywhere on an empty layout hits nothing.
|
|
CHECK(hitTest(L, 0, 0) == HitTarget::kNone);
|
|
CHECK(hitTest(L, 5, 5) == HitTarget::kNone);
|
|
}
|
|
|
|
// --- hitTest ------------------------------------------------------------------
|
|
|
|
static void testHitTestButton() {
|
|
const EditorLayout L = layoutEditor(400, 260);
|
|
// Center of the button hits it.
|
|
const int cx = (L.button.left + L.button.right) / 2;
|
|
const int cy = (L.button.top + L.button.bottom) / 2;
|
|
CHECK(hitTest(L, cx, cy) == HitTarget::kButton);
|
|
}
|
|
|
|
static void testHitTestMissesNonButton() {
|
|
const EditorLayout L = layoutEditor(400, 260);
|
|
// Title bar is inert in the spike.
|
|
CHECK(hitTest(L, 200, L.titleBar.top + 1) == HitTarget::kNone);
|
|
// Empty canvas away from the button.
|
|
CHECK(hitTest(L, 380, 240) == HitTarget::kNone);
|
|
// Outside the surface entirely.
|
|
CHECK(hitTest(L, -5, -5) == HitTarget::kNone);
|
|
CHECK(hitTest(L, 500, 500) == HitTarget::kNone);
|
|
}
|
|
|
|
static void testHitTestButtonBoundary() {
|
|
const EditorLayout L = layoutEditor(400, 260);
|
|
// Top-left corner of the button is inclusive; the right/bottom edges are excluded.
|
|
CHECK(hitTest(L, L.button.left, L.button.top) == HitTarget::kButton);
|
|
CHECK(hitTest(L, L.button.right, L.button.top) == HitTarget::kNone);
|
|
CHECK(hitTest(L, L.button.left, L.button.bottom) == HitTarget::kNone);
|
|
}
|
|
|
|
// --- layout<->hit-test agreement ----------------------------------------------
|
|
|
|
// Every pixel inside the drawn button rect must hit the button; this is the
|
|
// load-bearing consistency invariant between what the shell draws and what it routes.
|
|
static void testHitTestMatchesDrawnButton() {
|
|
const EditorLayout L = layoutEditor(320, 200);
|
|
for (int y = L.button.top; y < L.button.bottom; ++y) {
|
|
for (int x = L.button.left; x < L.button.right; ++x) {
|
|
CHECK(hitTest(L, x, y) == HitTarget::kButton);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --- sample list (S4) ---------------------------------------------------------
|
|
|
|
static void testSampleRowRectStacks() {
|
|
const EditorLayout L = layoutEditor(400, 260);
|
|
const Rect r0 = sampleRowRect(L, 0);
|
|
const Rect r1 = sampleRowRect(L, 1);
|
|
// Row 0 starts at the canvas top and spans its full width.
|
|
CHECK(r0.top == L.canvas.top);
|
|
CHECK(r0.left == L.canvas.left && r0.right == L.canvas.right);
|
|
CHECK(r0.height() == kSampleRowHeight);
|
|
// Row 1 sits directly below row 0 (no gap, no overlap).
|
|
CHECK(r1.top == r0.bottom);
|
|
CHECK(r1.height() == kSampleRowHeight);
|
|
// A negative index is an empty rect.
|
|
CHECK(sampleRowRect(L, -1).width() == 0 && sampleRowRect(L, -1).height() == 0);
|
|
}
|
|
|
|
static void testSampleRowHitTestMapsClickToRow() {
|
|
const EditorLayout L = layoutEditor(400, 260);
|
|
const int rows = 5;
|
|
// A click in the vertical middle of row 2 resolves to index 2.
|
|
const Rect r2 = sampleRowRect(L, 2);
|
|
const int midY = (r2.top + r2.bottom) / 2;
|
|
CHECK(sampleRowHitTest(L, rows, 200, midY) == 2);
|
|
// Row 0's top-left corner hits row 0.
|
|
const Rect r0 = sampleRowRect(L, 0);
|
|
CHECK(sampleRowHitTest(L, rows, r0.left, r0.top) == 0);
|
|
}
|
|
|
|
static void testSampleRowHitTestMisses() {
|
|
const EditorLayout L = layoutEditor(400, 260);
|
|
const int rows = 3;
|
|
// Above the first row (in the title bar) -> no row.
|
|
CHECK(sampleRowHitTest(L, rows, 200, L.titleBar.top) == -1);
|
|
// Below the last row -> no row.
|
|
const Rect last = sampleRowRect(L, rows - 1);
|
|
CHECK(sampleRowHitTest(L, rows, 200, last.bottom + 1) == -1);
|
|
// Left of the canvas -> no row.
|
|
CHECK(sampleRowHitTest(L, rows, L.canvas.left - 1, last.top) == -1);
|
|
// Zero rows -> always -1.
|
|
CHECK(sampleRowHitTest(L, 0, 200, L.canvas.top + 1) == -1);
|
|
// At or below canvas.bottom -> always -1, even if rowCount would cover that y.
|
|
// This guards paint<->hit-test agreement: sampleRowRect does not clamp to canvas,
|
|
// so without this clip a row that extends past canvas.bottom would hit-test but
|
|
// never be drawn (or vice versa).
|
|
CHECK(sampleRowHitTest(L, rows, 200, L.canvas.bottom) == -1);
|
|
// Use a large rowCount so index arithmetic would return a valid row without the
|
|
// canvas.bottom guard — proving the guard fires independently of rowCount.
|
|
const int bigRows = 1000;
|
|
CHECK(sampleRowHitTest(L, bigRows, 200, L.canvas.bottom) == -1);
|
|
CHECK(sampleRowHitTest(L, bigRows, 200, L.canvas.bottom + 5) == -1);
|
|
}
|
|
|
|
// The drawn-row <-> hit-test agreement: every pixel inside a row rect must resolve to
|
|
// that row's index (the same load-bearing invariant as the button).
|
|
static void testSampleRowHitTestMatchesDrawnRows() {
|
|
const EditorLayout L = layoutEditor(320, 200);
|
|
const int rows = 4;
|
|
for (int i = 0; i < rows; ++i) {
|
|
const Rect r = sampleRowRect(L, i);
|
|
if (r.top >= L.canvas.bottom) break; // clipped rows aren't clickable targets
|
|
const int y = (r.top + r.bottom) / 2;
|
|
if (y >= L.canvas.bottom) continue;
|
|
CHECK(sampleRowHitTest(L, rows, r.left + 1, y) == i);
|
|
}
|
|
}
|
|
|
|
int main() {
|
|
testContainsHalfOpen();
|
|
testContainsDegenerate();
|
|
testLayoutNormalView();
|
|
testLayoutTinyViewClampsButton();
|
|
testLayoutZeroView();
|
|
testHitTestButton();
|
|
testHitTestMissesNonButton();
|
|
testHitTestButtonBoundary();
|
|
testHitTestMatchesDrawnButton();
|
|
testSampleRowRectStacks();
|
|
testSampleRowHitTestMapsClickToRow();
|
|
testSampleRowHitTestMisses();
|
|
testSampleRowHitTestMatchesDrawnRows();
|
|
|
|
if (g_fail == 0) std::printf("editor_geometry: all tests passed\n");
|
|
return g_fail != 0;
|
|
}
|