// 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) } int main() { testNoDragIsNone(); testEmptyPayloadIsNone(); testLeaveClientIsOsDragOut(); testOtherBankIsMove(); testOtherBankCtrlIsCopy(); testSameBankEmptySlotIsReorder(); testSameBankOccupiedNoModIsReorder(); testSameBankOccupiedAltIsReplace(); testAltOverEmptySlotIsReorderNotReplace(); testDeadSpaceIsNone(); testCursorCueMapping(); testReplaceCueOnlyFromReplace(); testSlotRectsIncludeEmpties(); testSlotRectsEmptyWhenNoOccupied(); testSlotRectsDenseMatchesGrid(); testHitTestSlotReturnsSlotIndex(); testHitTestSlotMiss(); 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; }