fix(card_drag): extend drop rects one trailing row so beyond-extent drops land

computeSlotRectsForDrop adds cols slots past maxSlot; classifyCardDrag and
drawCardDropTarget use it instead of the render rects. doReorderDrop already
accepts any non-negative slot. New tests cover beyond-extent resolves,
empty-bank trailing row, gap pixels still miss, empty-cell within-extent works.
This commit is contained in:
2026-07-27 03:31:29 -04:00
parent 9a1bcbcf7a
commit d2cc33e9de
5 changed files with 124 additions and 2 deletions
+65
View File
@@ -176,6 +176,66 @@ static void testHitTestSlotMiss() {
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<SlotCellRect> render = computeSlotRects(2, 340, kSpec);
const std::vector<SlotCellRect> 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<SlotCellRect> 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<SlotCellRect> render = computeSlotRects(2, 340, kSpec);
const std::vector<SlotCellRect> 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<SlotCellRect> 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<SlotCellRect> render = computeSlotRects(2, 340, kSpec);
const std::vector<SlotCellRect> 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();
@@ -194,6 +254,11 @@ int main() {
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);