// editor_input_curve.cpp — the velocity-curve popup's input: the modal click routing, // node grab/add/toggle/delete inside the curve box, the live node drag, the right-click // delete on EITHER spline surface, and the popup's hover. Band-independent (the sheet floats // over the whole face). // Windows-only. #include "shell/instrument/reasampler_editor.h" #ifdef _WIN32 #include "core/instrument/ui/curve_popup.h" // computeCurvePopup / popupOutsideSheet #include "core/instrument/ui/spline_edit.h" // the shared point-editing grammar #include "core/instrument/ui/waveform_view.h" // waveformOverlayArea (the overlay right-click) #include "shell/instrument/editor_internal.h" // curveBoxFromRect #include "shell/instrument/reasampler_processor.h" namespace reasampler::vst { using namespace reasampler::ui; using namespace reasampler::instrument::ui; bool ReaSamplerEditor::handlePopupMouseDown(int w, int h, int x, int y) { // While open the sheet is modal over the face — it owns every left-click. Close click / // outside-wash click dismiss (outside only when no drag is in flight); in-box clicks // route to the curve machinery; anything else on the sheet is swallowed. if (curvePopup_ == CurveTarget::kNone) return false; const CurvePopupLayout pl = computeCurvePopup(w, h); if (contains(pl.close, x, y)) { closeCurvePopup(); invalidate(); return true; } if (contains(pl.curveBox, x, y)) { handleCurveMouseDown(pl.curveBox, x, y); return true; } if (popupOutsideSheet(pl, x, y) && drag_ == DragKind::kNone) { closeCurvePopup(); invalidate(); } return true; } void ReaSamplerEditor::handleCurveMouseDown(const Rect& r, int x, int y) { const VelocityCurve::Box box = curveBoxFromRect(r); if (box.width <= 0 || box.height <= 1) return; // Alt-click delete is retired (the spec's right-click supersedes it — one grammar, no // migration on either side): every gesture here routes through the shared resolver. const SplineGesture gesture = dragModifiers().ctrl ? SplineGesture::kControlLeft : SplineGesture::kLeft; const SplineEdit edit = resolveSplineEdit(editedCurve(), box, gesture, x, y); if (edit.kind == SplineEditKind::kToggleHard) { if (editedCurve().toggleHard(static_cast(edit.index))) commitAndReload(); return; } if (edit.kind == SplineEditKind::kNone) return; // ring click with no node hit // Snapshot BEFORE any mutation so a capture-loss rollback also cancels an in-flight ADD // (mirror of the other parameter-editing drags' dragStartParams_ contract). dragStartParams_ = params_; int idx = edit.index; if (edit.kind == SplineEditKind::kAdd) { const VelocityPoint p = editedCurve().pointFromPixel(box, x, y); idx = editedCurve().addPoint(p.velocity, p.value); if (idx < 0) return; // at the point ceiling: refused, curve untouched } drag_ = DragKind::kCurveNode; curvePointIndex_ = idx; dragStartCurve_ = editedCurve(); // AFTER the add — resolvePointDrag's delta base dragCurveRect_ = r; dragStartX_ = x; dragStartY_ = y; invalidate(); // live feedback; the commit lands on WM_LBUTTONUP } void ReaSamplerEditor::dragCurve(int x, int y) { // Resolve the grabbed control point from the pixel delta through the pure inverse map // (box + neighbour-X + endpoint-pin clamps), against the grab-time curve + box (absolute // delta — the mirror of the envelope-node drag). Live feedback only; commit on release. if (curvePointIndex_ < 0) return; editedCurve() = VelocityCurve::resolvePointDrag( dragStartCurve_, static_cast(curvePointIndex_), curveBoxFromRect(dragCurveRect_), x - dragStartX_, y - dragStartY_); invalidate(); } void ReaSamplerEditor::onMouseRDown(int x, int y) { // Right-click deletes a node on EITHER spline surface — the primary delete affordance; // Alt-click and drag-off remain as landed alternates in the popup. deletePoint's endpoint // guard makes an endpoint right-click a safe no-op. Never acts during an in-flight left // drag; the modal popup wins when it is open. if (!processor_ || view_ == View::kBrowse) return; if (drag_ != DragKind::kNone) return; RECT rc{}; GetClientRect(childHwnd_, &rc); if (curvePopup_ != CurveTarget::kNone) { const CurvePopupLayout pl = computeCurvePopup(rc.right - rc.left, rc.bottom - rc.top); if (!contains(pl.curveBox, x, y)) return; const SplineEdit edit = resolveSplineEdit(editedCurve(), curveBoxFromRect(pl.curveBox), SplineGesture::kRight, x, y); if (edit.kind != SplineEditKind::kDelete) return; if (editedCurve().deletePoint(static_cast(edit.index))) { hover_ = HoverTarget{}; // a stale kCurveNode index would light a shifted node commitAndReload(); } return; } if (!overlayIsSpline() || !overlayEnvEnabled(overlayEnv_, deckEnableState())) return; const FaceLayout fl = faceLayout(rc.right - rc.left, rc.bottom - rc.top); splineOverlayClick(waveformOverlayArea(fl.bands.waveform), x, y, SplineGesture::kRight, /*addOnEmptySpace=*/false); } ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverCurvePopup(int w, int h, int x, int y) const { const CurvePopupLayout pl = computeCurvePopup(w, h); if (contains(pl.close, x, y)) return {HoverKind::kPopupClose, -1}; if (!contains(pl.curveBox, x, y)) return {}; // A curve node under the pointer lights accent-hot. const int idx = editedCurve().pointAtPixel(curveBoxFromRect(pl.curveBox), x, y); if (idx < 0) return {}; return {HoverKind::kCurveNode, idx}; } } // namespace reasampler::vst #endif // _WIN32