// Standalone tests for reasampler::card_drag — no REAPER, no test framework. Asserts the // L7 in-grid reorder drag decision logic + sparse-aware slot layout/hit-test. // // Covers: gesture precedence (no-drag/empty -> None; leave-client -> OsDragOut wins first; // other-bank -> Move/Copy on Ctrl; same-bank grid empty vs occupied+no-mod -> Reorder; // same-bank occupied+Alt -> Replace; Alt over EMPTY slot -> Reorder not Replace; dead space // -> None); cursor-cue mapping (incl. Replace only for Replace); slot rects include empties // (gap layout), dense layout matches a plain grid, slot hit-test returns slot index + miss. #include "../src/card_drag.h" #include using namespace reasampler; 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 200x200 client at origin. A pointer at (10,10) is inside; (-5,10) / (250,10) are outside. static const PanelClientRect kClient{0, 0, 200, 200}; static const DragState kLiveDrag{/*dragging=*/true, /*hasArmedSamples=*/true}; static DragModifiers mods(DropRegion region, int slot, bool occupied, bool ctrl, bool alt) { DragModifiers m; m.region = region; m.targetSlot = slot; m.slotOccupied = occupied; m.ctrl = ctrl; m.alt = alt; return m; } // --- Gesture: guard cases ---------------------------------------------------- static void testNoDragIsNone() { const DragState idle{/*dragging=*/false, /*hasArmedSamples=*/true}; CHECK(decideCardGesture(10, 10, kClient, idle, mods(DropRegion::SameBankGrid, 0, true, false, false)) == CardGesture::None); } static void testEmptyPayloadIsNone() { const DragState noSamples{/*dragging=*/true, /*hasArmedSamples=*/false}; CHECK(decideCardGesture(10, 10, kClient, noSamples, mods(DropRegion::SameBankGrid, 0, true, false, false)) == CardGesture::None); } // --- Gesture: precedence 1 — leave client wins first ------------------------- static void testLeaveClientIsOsDragOut() { // Pointer outside the client -> OsDragOut EVEN when the region verdict says same-bank // and Alt is held (leave-client wins first — invariant #4 boundary). CHECK(decideCardGesture(-5, 10, kClient, kLiveDrag, mods(DropRegion::SameBankGrid, 0, true, /*ctrl=*/true, /*alt=*/true)) == CardGesture::OsDragOut); CHECK(decideCardGesture(250, 10, kClient, kLiveDrag, mods(DropRegion::OtherBankOrTab, -1, false, false, false)) == CardGesture::OsDragOut); } // --- Gesture: precedence 2 — other bank/tab = move/copy ---------------------- static void testOtherBankIsMove() { CHECK(decideCardGesture(10, 10, kClient, kLiveDrag, mods(DropRegion::OtherBankOrTab, -1, false, /*ctrl=*/false, false)) == CardGesture::Move); } static void testOtherBankCtrlIsCopy() { CHECK(decideCardGesture(10, 10, kClient, kLiveDrag, mods(DropRegion::OtherBankOrTab, -1, false, /*ctrl=*/true, false)) == CardGesture::Copy); } // --- Gesture: precedence 3 — same-bank grid reorder / replace ---------------- static void testSameBankEmptySlotIsReorder() { CHECK(decideCardGesture(10, 10, kClient, kLiveDrag, mods(DropRegion::SameBankGrid, 3, /*occupied=*/false, false, false)) == CardGesture::Reorder); } static void testSameBankOccupiedNoModIsReorder() { // Occupied + no modifier = insert-before-and-shift, which is still Reorder. CHECK(decideCardGesture(10, 10, kClient, kLiveDrag, mods(DropRegion::SameBankGrid, 2, /*occupied=*/true, false, false)) == CardGesture::Reorder); } static void testSameBankOccupiedAltIsReplace() { CHECK(decideCardGesture(10, 10, kClient, kLiveDrag, mods(DropRegion::SameBankGrid, 2, /*occupied=*/true, false, /*alt=*/true)) == CardGesture::Replace); } static void testAltOverEmptySlotIsReorderNotReplace() { // Alt over an EMPTY slot must NOT be Replace (replace needs an occupant). CHECK(decideCardGesture(10, 10, kClient, kLiveDrag, mods(DropRegion::SameBankGrid, 5, /*occupied=*/false, false, /*alt=*/true)) == CardGesture::Reorder); } static void testDeadSpaceIsNone() { CHECK(decideCardGesture(10, 10, kClient, kLiveDrag, mods(DropRegion::DeadSpace, -1, false, false, true)) == CardGesture::None); } // --- Cursor cue mapping ------------------------------------------------------ static void testCursorCueMapping() { CHECK(cursorForGesture(CardGesture::None) == CursorCue::Default); CHECK(cursorForGesture(CardGesture::OsDragOut) == CursorCue::OsDragOut); CHECK(cursorForGesture(CardGesture::Move) == CursorCue::Move); CHECK(cursorForGesture(CardGesture::Copy) == CursorCue::Copy); CHECK(cursorForGesture(CardGesture::Reorder) == CursorCue::Reorder); CHECK(cursorForGesture(CardGesture::Replace) == CursorCue::Replace); } static void testReplaceCueOnlyFromReplace() { // The Replace cue is produced by NO gesture other than Replace (which itself requires // Alt-over-occupied) — the "replace cursor only while Alt over occupied" guarantee. CHECK(cursorForGesture(CardGesture::Reorder) != CursorCue::Replace); CHECK(cursorForGesture(CardGesture::Move) != CursorCue::Replace); CHECK(cursorForGesture(CardGesture::Copy) != CursorCue::Replace); CHECK(cursorForGesture(CardGesture::None) != CursorCue::Replace); } // --- Sparse slot layout + hit-test ------------------------------------------- // Spec: cell 100x40, gap 10. In a 340-wide panel: usable = 340-10 = 330; cell+gap = 110; // cols = 330/110 = 3. static const GridSpec kSpec{/*cellWidth=*/100, /*cellHeight=*/40, /*gap=*/10}; static void testSlotRectsIncludeEmpties() { // maxSlot = 4 -> 5 rects, slots 0..4, three per row. const std::vector r = computeSlotRects(4, 340, kSpec); CHECK(r.size() == 5); CHECK(r[0].slot == 0); CHECK(r[4].slot == 4); // Slot 0: x = gap = 10, y = gap = 10. CHECK((r[0] == SlotCellRect{0, 10, 10, 100, 40})); // Slot 3 wraps to row 1, col 0: y = gap + 1*(40+10) = 60. CHECK((r[3] == SlotCellRect{3, 10, 60, 100, 40})); } static void testSlotRectsEmptyWhenNoOccupied() { CHECK(computeSlotRects(-1, 340, kSpec).empty()); } static void testSlotRectsDenseMatchesGrid() { // A dense slot layout (slots 0..N-1) lays out identically to bank_grid's item tiling. const std::vector s = computeSlotRects(2, 340, kSpec); const std::vector g = computeCellRects(3, 340, kSpec); CHECK(s.size() == g.size()); for (std::size_t i = 0; i < s.size(); ++i) { CHECK(s[i].x == g[i].x && s[i].y == g[i].y); CHECK(s[i].width == g[i].width && s[i].height == g[i].height); } } static void testHitTestSlotReturnsSlotIndex() { const std::vector r = computeSlotRects(4, 340, kSpec); // A point inside slot 3's rect (x=10,y=60) returns slot 3, not vector index 3 (they // coincide here, but the point maps by geometry). CHECK(hitTestSlot(15, 65, r) == 3); // Inside slot 1 (x = gap + 1*(100+10) = 120). CHECK(hitTestSlot(125, 15, r) == 1); } static void testHitTestSlotMiss() { const std::vector r = computeSlotRects(4, 340, kSpec); CHECK(hitTestSlot(0, 0, r) == -1); // top-left margin (gap) is a miss CHECK(hitTestSlot(115, 15, r) == -1); // inter-cell gap between slot 0 (ends x=110) and slot 1 (starts x=120) } // --- computeSlotRectsForDrop — beyond-extent trailing row --------------------- // Spec: kSpec, 340px wide, 3 cols. maxSlot=2 -> render rects for 0..2. // Drop rects extend one trailing row: slots 0..2 (existing) + 3..5 (trailing). static void testDropRectsExtendOneTrailingRow() { // computeSlotRects(2, ...) gives 3 rects (0..2). Drop rects should give 6 (0..5). const std::vector render = computeSlotRects(2, 340, kSpec); const std::vector drop = computeSlotRectsForDrop(2, 340, kSpec); CHECK(render.size() == 3); CHECK(drop.size() == 6); // First three match (existing slots preserved). for (std::size_t i = 0; i < 3; ++i) { CHECK(drop[i].slot == render[i].slot); CHECK(drop[i].x == render[i].x && drop[i].y == render[i].y); } // Slot 3 starts the trailing row: row 1, col 0. y = gap + 1*(40+10) = 60. CHECK(drop[3].slot == 3); CHECK(drop[3].x == 10 && drop[3].y == 60); } static void testDropRectsEmptyBankHasTrailingRow() { // maxSlot=-1 (no occupied slots): drop rects start at slot 0 (one trailing row). const std::vector drop = computeSlotRectsForDrop(-1, 340, kSpec); CHECK(drop.size() == 3); // 3 cols = one trailing row CHECK(drop[0].slot == 0); CHECK(drop[1].slot == 1); CHECK(drop[2].slot == 2); } static void testBeyondExtentResolvesToTrailingSlot() { // A pointer placed in the trailing row (below the last occupied card row) resolves // to a valid slot via the drop rects, but returns -1 via the render rects — this is // the exact gap the fix closes. const std::vector render = computeSlotRects(2, 340, kSpec); const std::vector drop = computeSlotRectsForDrop(2, 340, kSpec); // Slot 3 rect: row 1, col 0. A point at (15, 65) is inside it (y=60..99, x=10..109). CHECK(hitTestSlot(15, 65, render) == -1); // render rects: miss — bug confirmed CHECK(hitTestSlot(15, 65, drop) == 3); // drop rects: hits trailing slot 3 — fix } static void testGapPixelStillMissesInDropRects() { // Inter-cell pixel gaps should still be misses even in the drop rects. const std::vector drop = computeSlotRectsForDrop(4, 340, kSpec); // x=115 is in the gap between slot 0 (x=10..109) and slot 1 (x=120..219). CHECK(hitTestSlot(115, 15, drop) == -1); // Top-left margin (before any cell) is still a miss. CHECK(hitTestSlot(0, 0, drop) == -1); } static void testEmptySlotWithinExtentHitsViaRenderRects() { // Slot 1 is empty (gap), slot 0 and slot 2 are "occupied" (just position — the // render rects cover ALL slots 0..maxSlot including empties). A pointer at slot 1's // cell center returns slot 1 from both render AND drop rects. const std::vector render = computeSlotRects(2, 340, kSpec); const std::vector drop = computeSlotRectsForDrop(2, 340, kSpec); // Slot 1: x = gap + 1*(100+10) = 120, y = gap = 10. Center: (170, 30). CHECK(hitTestSlot(170, 30, render) == 1); // works via render rects (existing) CHECK(hitTestSlot(170, 30, drop) == 1); // also works via drop rects } int main() { testNoDragIsNone(); testEmptyPayloadIsNone(); testLeaveClientIsOsDragOut(); testOtherBankIsMove(); testOtherBankCtrlIsCopy(); testSameBankEmptySlotIsReorder(); testSameBankOccupiedNoModIsReorder(); testSameBankOccupiedAltIsReplace(); testAltOverEmptySlotIsReorderNotReplace(); testDeadSpaceIsNone(); testCursorCueMapping(); testReplaceCueOnlyFromReplace(); testSlotRectsIncludeEmpties(); testSlotRectsEmptyWhenNoOccupied(); testSlotRectsDenseMatchesGrid(); testHitTestSlotReturnsSlotIndex(); testHitTestSlotMiss(); testDropRectsExtendOneTrailingRow(); testDropRectsEmptyBankHasTrailingRow(); testBeyondExtentResolvesToTrailingSlot(); testGapPixelStillMissesInDropRects(); testEmptySlotWithinExtentHitsViaRenderRects(); if (g_fail == 0) std::printf("card_drag: all tests passed\n"); else std::printf("card_drag: %d CHECK(s) FAILED\n", g_fail); return g_fail == 0 ? 0 : 1; }