diff --git a/CMakeLists.txt b/CMakeLists.txt index e0adb22..9d3dd3f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1034,7 +1034,7 @@ target_link_libraries(capture_browser_tests PRIVATE capture_browser) add_test(NAME capture_browser_tests COMMAND capture_browser_tests) add_executable(keyboard_strip_tests tests/test_keyboard_strip.cpp) -target_link_libraries(keyboard_strip_tests PRIVATE keyboard_strip sample_bands) +target_link_libraries(keyboard_strip_tests PRIVATE keyboard_strip sample_bands sample_chrome) add_test(NAME keyboard_strip_tests COMMAND keyboard_strip_tests) # waveform_view (S11): the pure marker geometry + zero-crossing snap. Links waveform_view (+ diff --git a/src/shell/instrument/editor_input.cpp b/src/shell/instrument/editor_input.cpp index 2bb0bc1..38ad2f6 100644 --- a/src/shell/instrument/editor_input.cpp +++ b/src/shell/instrument/editor_input.cpp @@ -82,7 +82,10 @@ void ReaSamplerEditor::onMouseUp(int x, int y) { // hover_ is deliberately not re-resolved during a drag (see resolveHover's caller), so it // still names wherever the drag started. Re-resolve now against the release position, for // every drag kind — otherwise the next paint latches a stale hover (wrong note name/tooltip, - // wrong control outline) until the next WM_MOUSEMOVE. + // wrong control outline) until the next WM_MOUSEMOVE. This resolve runs before the release + // branches below can delete or relocate the hovered element; a future branch that does so + // must clear hover_ itself afterwards (as the curve drag-off delete does below) rather than + // rely on this resolve, which reflects pre-mutation state. resolveHover(x, y); // A scrollbar drag is transient UI (no parameter change), and the processor-side knobs // (the preview-velocity -2 sentinel, voice count, master gain) are per-instance settings diff --git a/src/shell/instrument/editor_input_curve.cpp b/src/shell/instrument/editor_input_curve.cpp index c6d4d7e..dbcd6b5 100644 --- a/src/shell/instrument/editor_input_curve.cpp +++ b/src/shell/instrument/editor_input_curve.cpp @@ -47,7 +47,10 @@ void ReaSamplerEditor::handleCurveMouseDown(const Rect& r, int x, int y) { // Modifier-click (Alt) deletes an interior node — a discrete, final edit committed at once // (deletePoint refuses the two endpoints, so an Alt-click on them is a safe no-op). if (idx >= 0 && (GetKeyState(VK_MENU) & 0x8000) != 0) { - if (params_.velocityCurve.deletePoint(static_cast(idx))) commitAndReload(); + if (params_.velocityCurve.deletePoint(static_cast(idx))) { + hover_ = HoverTarget{}; // a stale kCurveNode index would light a shifted node + commitAndReload(); + } return; } diff --git a/src/shell/instrument/editor_platform.cpp b/src/shell/instrument/editor_platform.cpp index 8693969..00df4af 100644 --- a/src/shell/instrument/editor_platform.cpp +++ b/src/shell/instrument/editor_platform.cpp @@ -231,6 +231,10 @@ LRESULT CALLBACK ReaSamplerEditor::wndProc(HWND hwnd, UINT msg, WPARAM wParam, self->drag_ = DragKind::kNone; self->dragParamId_ = -1; self->curvePointIndex_ = -1; // curve-node drag state (peer reset) + // No cursor position is available here to re-resolve hover (unlike + // onMouseUp's release coordinates), so clear rather than leave it naming + // wherever the drag started. + self->hover_ = HoverTarget{}; self->invalidate(); } } diff --git a/tests/test_keyboard_strip.cpp b/tests/test_keyboard_strip.cpp index a9aec23..d0f4afe 100644 --- a/tests/test_keyboard_strip.cpp +++ b/tests/test_keyboard_strip.cpp @@ -16,9 +16,11 @@ // unverified by this suite (see src/core/instrument/CLAUDE.md). #include "../src/core/instrument/ui/keyboard_strip.h" -#include "../src/core/instrument/ui/sample_bands.h" // kPad, to derive the strip width the - // shipped default window (840x620, +#include "../src/core/instrument/ui/sample_bands.h" // computeSampleBands, to derive the CHROME + // band the shipped default window (840x620, // editor_session.cpp) hands the strip +#include "../src/core/instrument/ui/sample_chrome.h" // chromeRects, to derive rootStrip's width + // the same way the shell does #include #include @@ -288,8 +290,15 @@ static void testNoteNamesFollowTheC4Convention() { // one pixel of resize (840->841) collapses both gutters to zero and grows every white key // from 10px to 11px. static void testGutterAtTheShippedDefaultWindowSize() { - constexpr int kShippedDefaultWindowW = 840; // editor_session.cpp's ViewRect default - const int stripW = kShippedDefaultWindowW - 2 * kPad; // chromeRects insets rootStrip by kPad + constexpr int kShippedDefaultWindowW = 840; // editor_session.cpp's ViewRect default + constexpr int kShippedDefaultWindowH = 620; // editor_session.cpp's ViewRect default + // Derive rootStrip's width the same way the shell does, through the real allocator + + // chrome layout, rather than re-deriving the inset formula — so a change to either one + // fails this test instead of silently moving the shipped gutter out from under it. + const SampleBands bands = + computeSampleBands(kShippedDefaultWindowW, kShippedDefaultWindowH, 0); + const ChromeRects chrome = chromeRects(bands.chrome, /*knobSize=*/24); + const int stripW = chrome.rootStrip.width; CHECK(stripW == 824); const StripLayout L = layoutStrip(stripW, 30);