From 1585f0bbe664304c6d5c6ed8169e0757a2a5fe05 Mon Sep 17 00:00:00 2001 From: daniel-c-harvey Date: Mon, 27 Jul 2026 02:47:29 -0400 Subject: [PATCH] fix: same-slot reorder is a no-op; WM_DESTROY clears card drag state; fix card_meta.h ms-rounding comment --- src/bank_book.cpp | 1 + src/bank_panel.cpp | 4 ++++ src/card_meta.h | 2 +- tests/test_bank_book.cpp | 23 +++++++++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) diff --git a/src/bank_book.cpp b/src/bank_book.cpp index c03df30..786e265 100644 --- a/src/bank_book.cpp +++ b/src/bank_book.cpp @@ -76,6 +76,7 @@ bool SlotMap::remove(const std::string& id) { bool SlotMap::reorder(const std::string& id, int targetSlot) { if (slotOf(id) < 0) return false; // not mapped -> no mutation if (targetSlot < 0) targetSlot = 0; + if (slotOf(id) == targetSlot) return false; // already there — true no-op // Detach the moving id first so the occupancy test below sees the post-move world. remove(id); diff --git a/src/bank_panel.cpp b/src/bank_panel.cpp index 7db0a9f..99bc4f2 100644 --- a/src/bank_panel.cpp +++ b/src/bank_panel.cpp @@ -2922,6 +2922,7 @@ void onMouseMove(int x, int y) { g_panel.dropBankId.clear(); g_panel.cardGesture = CardGesture::None; g_panel.dragTargetSlot = -1; + g_panel.dragPrimaryId.clear(); invalidatePanel(); // Empty path list -> nothing draggable (all stale/missing); do not start a drag. @@ -3117,6 +3118,9 @@ WDL_DLGRET dlgProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) { stopAudition(); g_panel.selection = Selection{}; g_panel.dragArmed = g_panel.dragging = false; + g_panel.cardGesture = CardGesture::None; + g_panel.dragTargetSlot = -1; + g_panel.dragPrimaryId.clear(); g_panel.hovered = Hover{}; g_panel.tooltipShown = false; g_panel.hwnd = nullptr; diff --git a/src/card_meta.h b/src/card_meta.h index 4b7bae0..1c97fad 100644 --- a/src/card_meta.h +++ b/src/card_meta.h @@ -47,7 +47,7 @@ struct MusicalLength { std::string formatBarsBeats(const MusicalLength& m); // seconds.milliseconds from a wall-clock length (always derivable, meter-independent). -// * "S.mmm" — integer seconds, a dot, zero-padded 3-digit milliseconds (floored). +// * "S.mmm" — integer seconds, a dot, zero-padded 3-digit milliseconds (rounded to nearest ms). // e.g. 0.0 -> "0.000", 1.5 -> "1.500", 62.037 -> "62.037". // * negative length is clamped to "0.000" (a length is never negative; defensive). std::string formatSecondsMs(double lengthSeconds); diff --git a/tests/test_bank_book.cpp b/tests/test_bank_book.cpp index d41c34e..d1c68eb 100644 --- a/tests/test_bank_book.cpp +++ b/tests/test_bank_book.cpp @@ -973,6 +973,28 @@ static void testReorderSampleRejectsUnknown() { CHECK(book.pool().slots.slotOf("id-r1") == 0); // unchanged } +// Same-slot reorder is a true no-op: returns false, no undo point triggered, +// JSON byte-identical before/after (the invariant that blocks spurious dirty-state). +static void testReorderSampleSameSlotIsNoOp() { + BankBook book; + CHECK(book.pool().index.add(sampleWith("a")) == AddResult::Added); + CHECK(book.pool().index.add(sampleWith("b")) == AddResult::Added); + CHECK(book.pool().index.add(sampleWith("c")) == AddResult::Added); + book.reconcileSlots(); // a@0, b@1, c@2 + const std::string jsonBefore = book.serialize(); + + // Drop each card onto its own current slot — must return false every time. + CHECK(!book.reorderSample("id-a", kPoolBankId, 0)); + CHECK(!book.reorderSample("id-b", kPoolBankId, 1)); + CHECK(!book.reorderSample("id-c", kPoolBankId, 2)); + + // Slots and JSON are byte-identical — no mutation occurred. + CHECK(book.pool().slots.slotOf("id-a") == 0); + CHECK(book.pool().slots.slotOf("id-b") == 1); + CHECK(book.pool().slots.slotOf("id-c") == 2); + CHECK(book.serialize() == jsonBefore); +} + // --- BankBook L7: Alt-replace mutator ---------------------------------------- static void testReplaceSampleTakesSlotAndRemovesOccupant() { @@ -1090,6 +1112,7 @@ int main() { testMigrationDefaultsToInsertionOrderDense(); testOrderedSampleIdsReconcilesLazily(); testReorderSampleRejectsUnknown(); + testReorderSampleSameSlotIsNoOp(); testReplaceSampleTakesSlotAndRemovesOccupant(); testReplaceSampleNonDestructiveFileStays(); testReplaceSampleRejectionsNoMutation();