// Standalone tests for reasampler::component_geometry — no REAPER, no LICE, no framework. // Same fast assert loop as the sibling pure tests (prune_button / mode_switch). // // Covers (brief §L1 point 2 + §test cases): button box inset + graceful suppression; slider // track/filled/handle geometry for representative values incl. endpoints, value->px inverse, // too-small/degenerate suppression; list-row rect for representative indices, partial last // row, hover hit-test returns the right row and "no hit" outside/past the last row; and the // shared half-open box hit-test agrees with layout (no double-claimed pixel). #include "../src/core/ui/component_geometry.h" #include using namespace reasampler; using namespace reasampler::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) // --- hitTestBox (the shared primitive) --------------------------------------- static void testHitTestBoxHalfOpen() { KitBox b{10, 20, 30, 40}; CHECK(hitTestBox(10, 20, b)); // top-left inclusive CHECK(hitTestBox(39, 59, b)); // bottom-right inclusive (x+w-1, y+h-1) CHECK(!hitTestBox(40, 20, b)); // right edge excluded CHECK(!hitTestBox(10, 60, b)); // bottom edge excluded CHECK(!hitTestBox(9, 20, b)); // just left CHECK(!hitTestBox(10, 19, b)); // just above KitBox empty{}; CHECK(!hitTestBox(0, 0, empty)); // empty claims nothing } // --- Button box -------------------------------------------------------------- static void testButtonBoxInset() { KitBox cell{0, 0, 100, 40}; const KitButtonBox b = computeButtonBox(cell, 4); CHECK(!b.box.empty()); CHECK((b.box == KitBox{4, 4, 92, 32})); } static void testButtonBoxZeroPadding() { KitBox cell{5, 6, 20, 10}; const KitButtonBox b = computeButtonBox(cell, 0); CHECK((b.box == cell)); // no inset -> the whole cell } static void testButtonBoxNegativePaddingTreatedAsZero() { KitBox cell{5, 6, 20, 10}; CHECK((computeButtonBox(cell, -8).box == cell)); } static void testButtonBoxSuppressedWhenPaddingCollapses() { KitBox cell{0, 0, 10, 10}; CHECK(computeButtonBox(cell, 5).box.empty()); // 10 - 2*5 = 0 width -> suppressed CHECK(computeButtonBox(cell, 6).box.empty()); // negative -> suppressed CHECK(computeButtonBox(KitBox{}, 2).box.empty()); // degenerate cell -> suppressed } // --- Slider ------------------------------------------------------------------ // control 200x20, handle 12, track thickness 4. half = 6. track.x = 6, track.width = // 200-12 = 188, track.y = (20-4)/2 = 8. At value 0 the handle centre is track.x=6 -> // handle.x = 0; filled.width = 0. At value 1 the centre is 6+188=194 -> handle.x=188. static void testSliderEndpoints() { KitBox ctrl{0, 0, 200, 20}; const SliderGeometry lo = computeSlider(ctrl, 0.0, 12, 4); CHECK(!lo.track.empty()); CHECK((lo.track == KitBox{6, 8, 188, 4})); CHECK(lo.handle.x == 0); // flush left CHECK(lo.filled.width == 0); // nothing filled at 0 const SliderGeometry hi = computeSlider(ctrl, 1.0, 12, 4); CHECK(hi.handle.x == 188); // flush right (200-12) CHECK(hi.filled.width == 188); // fully filled at 1 } static void testSliderMidpoint() { KitBox ctrl{0, 0, 200, 20}; const SliderGeometry m = computeSlider(ctrl, 0.5, 12, 4); // centre = 6 + round(0.5*188) = 6 + 94 = 100; handle.x = 100 - 6 = 94. CHECK(m.handle.x == 94); CHECK(m.filled.width == 94); } static void testSliderClampsValue() { KitBox ctrl{0, 0, 200, 20}; CHECK((computeSlider(ctrl, -1.0, 12, 4).handle.x == computeSlider(ctrl, 0.0, 12, 4).handle.x)); CHECK((computeSlider(ctrl, 5.0, 12, 4).handle.x == computeSlider(ctrl, 1.0, 12, 4).handle.x)); } static void testSliderSuppressedWhenTooSmallOrDegenerate() { CHECK(computeSlider(KitBox{0, 0, 8, 20}, 0.5, 12, 4).track.empty()); // width < handle CHECK(computeSlider(KitBox{0, 0, 200, 8}, 0.5, 12, 4).track.empty()); // height < handle CHECK(computeSlider(KitBox{}, 0.5, 12, 4).track.empty()); // degenerate CHECK(computeSlider(KitBox{0, 0, 200, 20}, 0.5, 0, 4).track.empty()); // no handle CHECK(computeSlider(KitBox{0, 0, 200, 20}, 0.5, 12, 0).track.empty()); // no track } // value->px is the inverse of the handle placement (a track jump). static void testSliderValueAtInverse() { KitBox ctrl{0, 0, 200, 20}; CHECK(sliderValueAt(6, ctrl, 12) == 0.0); // at/left of track start CHECK(sliderValueAt(0, ctrl, 12) == 0.0); // left of the control -> 0 CHECK(sliderValueAt(194, ctrl, 12) == 1.0); // at track end -> 1 CHECK(sliderValueAt(300, ctrl, 12) == 1.0); // past the end -> clamped const double mid = sliderValueAt(100, ctrl, 12); // (100-6)/188 CHECK(mid > 0.49 && mid < 0.51); CHECK(sliderValueAt(50, KitBox{0, 0, 8, 20}, 12) == 0.0); // too small -> 0 } // --- List row ---------------------------------------------------------------- static void testListRowStacking() { KitBox list{0, 100, 220, 90}; // 3 full rows of 30 fit CHECK((computeListRow(list, 0, 30).box == KitBox{0, 100, 220, 30})); CHECK((computeListRow(list, 1, 30).box == KitBox{0, 130, 220, 30})); CHECK((computeListRow(list, 2, 30).box == KitBox{0, 160, 220, 30})); CHECK(computeListRow(list, 0, 30).index == 0); CHECK(computeListRow(list, 2, 30).index == 2); } // A row that starts inside the list but overhangs the bottom keeps full height (caller // clips the draw); a row starting AT/BELOW the bottom is suppressed. static void testListRowPartialAndClipped() { KitBox list{0, 0, 100, 50}; // rows of 30: row 0 [0,30), row 1 [30,60) overhangs const ListRowBox partial = computeListRow(list, 1, 30); CHECK(!partial.box.empty()); CHECK(partial.box.y == 30); CHECK(partial.box.height == 30); // full height; caller clips CHECK(computeListRow(list, 2, 30).box.empty()); // starts at y=60 >= bottom -> none } static void testListRowDegenerate() { CHECK(computeListRow(KitBox{}, 0, 30).box.empty()); CHECK(computeListRow(KitBox{0, 0, 100, 90}, -1, 30).box.empty()); CHECK(computeListRow(KitBox{0, 0, 100, 90}, 0, 0).box.empty()); } static void testListRowHitTest() { KitBox list{0, 100, 220, 90}; // 3 rows of 30, rowCount = 3 CHECK(hitTestListRow(10, 100, list, 30, 3) == 0); // top of row 0 CHECK(hitTestListRow(10, 129, list, 30, 3) == 0); // bottom of row 0 (inclusive) CHECK(hitTestListRow(10, 130, list, 30, 3) == 1); // top of row 1 CHECK(hitTestListRow(10, 189, list, 30, 3) == 2); // last pixel of row 2 // Misses. CHECK(hitTestListRow(10, 99, list, 30, 3) == -1); // above the list CHECK(hitTestListRow(10, 190, list, 30, 3) == -1); // below the list band CHECK(hitTestListRow(-1, 100, list, 30, 3) == -1); // left of the list CHECK(hitTestListRow(220, 100, list, 30, 3) == -1); // right edge excluded } // rowCount bounds the hit: a taller list with fewer rows than fit reports the empty tail // as a miss (no phantom row past the data). static void testListRowHitTestBoundedByCount() { KitBox list{0, 0, 100, 200}; // room for 6 rows of 30, but only 2 exist CHECK(hitTestListRow(10, 10, list, 30, 2) == 0); CHECK(hitTestListRow(10, 40, list, 30, 2) == 1); CHECK(hitTestListRow(10, 70, list, 30, 2) == -1); // row 2 is empty tail -> miss CHECK(hitTestListRow(10, 10, list, 30, 0) == -1); // zero rows -> always miss } // Draw/hit-test agreement: every pixel inside a computed row hit-tests to that row. static void testListRowLayoutHitAgreement() { KitBox list{3, 7, 97, 120}; const int rh = 24, rowCount = 4; for (int idx = 0; idx < rowCount; ++idx) { const ListRowBox r = computeListRow(list, idx, rh); if (r.box.empty()) continue; for (int py = r.box.y; py < r.box.y + r.box.height && py < list.y + list.height; ++py) CHECK(hitTestListRow(list.x + 1, py, list, rh, rowCount) == idx); } } // --- waveformColumnCount ----------------------------------------------------- static void testWaveformColumnCount() { // Normal box: width minus the two 2px side insets. CHECK(waveformColumnCount(KitBox{0, 0, 100, 40}) == 96); CHECK(waveformColumnCount(KitBox{10, 5, 50, 20}) == 46); // Minimum: a 5-wide box yields exactly 1 drawable column. CHECK(waveformColumnCount(KitBox{0, 0, 5, 10}) == 1); // Too narrow to have any drawable columns: 4 or fewer columns returns 0. CHECK(waveformColumnCount(KitBox{0, 0, 4, 10}) == 0); CHECK(waveformColumnCount(KitBox{0, 0, 1, 10}) == 0); // Degenerate (empty) box returns 0 — graceful suppression. CHECK(waveformColumnCount(KitBox{}) == 0); CHECK(waveformColumnCount(KitBox{0, 0, 0, 40}) == 0); } int main() { testHitTestBoxHalfOpen(); testButtonBoxInset(); testButtonBoxZeroPadding(); testButtonBoxNegativePaddingTreatedAsZero(); testButtonBoxSuppressedWhenPaddingCollapses(); testSliderEndpoints(); testSliderMidpoint(); testSliderClampsValue(); testSliderSuppressedWhenTooSmallOrDegenerate(); testSliderValueAtInverse(); testListRowStacking(); testListRowPartialAndClipped(); testListRowDegenerate(); testListRowHitTest(); testListRowHitTestBoundedByCount(); testListRowLayoutHitAgreement(); testWaveformColumnCount(); if (g_fail == 0) std::printf("component_geometry: all tests passed\n"); else std::printf("component_geometry: %d CHECK(s) FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }