L7: capture-order slot model, card metadata overlay, tertiary-border selection
Add gap-preserving per-bank SlotMap (reorder + Alt-replace mutators, JSON round-trip, insertion-order migration) to bank_book; stamp captureTimeSig on Sample; pure card_meta formatters + card_drag gesture/slot module; card metadata overlay + purple selection border in the panel. Shell drop/cursor wiring deferred. Fixes: removeSample syncs SlotMap; card_drag gap-probe coordinate.
This commit is contained in:
@@ -0,0 +1,201 @@
|
||||
// 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 <cstdio>
|
||||
|
||||
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<SlotCellRect> 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<SlotCellRect> s = computeSlotRects(2, 340, kSpec);
|
||||
const std::vector<CellRect> 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<SlotCellRect> 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<SlotCellRect> 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;
|
||||
}
|
||||
Reference in New Issue
Block a user