// editor_input.cpp — the ReaSamplerEditor's input dispatch and drag-state machine: the // mouse-down routing (curve popup first, then Browse or the three bands in order), the // onMouseMove drag router, the release commit, and the hover resolver. The per-band // branches live in the editor_input_ TUs; this TU only sequences them. // Windows-only. All hit-test math is pure; this family routes and mutates editor state. #include "shell/instrument/reasampler_editor.h" #ifdef _WIN32 #include "shell/instrument/editor_internal.h" // curveBoxFromRect + kCurveDragOffMargin #include "shell/instrument/reasampler_processor.h" namespace reasampler::vst { using namespace reasampler::ui; using namespace reasampler::instrument::ui; void ReaSamplerEditor::onMouseDown(int x, int y) { if (!processor_) return; RECT cr{}; GetClientRect(childHwnd_, &cr); const int w = cr.right - cr.left; const int h = cr.bottom - cr.top; if (view_ == View::kBrowse) { mouseDownBrowse(w, h, x, y); return; } // The curve popup is modal over the face — while open it owns every left-click. if (handlePopupMouseDown(w, h, x, y)) return; // Band order matters only where bands can overlap on a degenerate window; each branch // reports whether it consumed the click so the next band gets a clean shot. const FaceLayout fl = faceLayout(w, h); // The overlay focus follows the deck (mouseDownDeck sets it), and clicking a control // surface OUTSIDE the envelope decks clears it. The waveform band is deliberately not one // of those surfaces: it IS the overlay, so editing what you selected must not deselect it. // Chrome's own dead space (the title band, the gaps between its controls) is not a control // surface either — hoverChrome answers only for chrome's actual interactive elements. if (drag_ == DragKind::kNone && hoverChrome(fl, x, y).kind != HoverKind::kNone && overlayEnv_ != OverlayEnv::kNone) { overlayEnv_ = OverlayEnv::kNone; invalidate(); // the chrome branches below repaint only what THEY changed } if (mouseDownChrome(fl, x, y)) return; if (selectedId_.empty()) return; // empty state — chrome nav only if (mouseDownDeck(fl, x, y)) return; mouseDownWaveform(fl, x, y); } bool ReaSamplerEditor::onMouseDoubleClick(int x, int y) { // Only the Sample face's radial knobs claim the gesture. Everything else — Browse's // load accelerator, the spline surfaces' point grammar — is left to the caller's // fall-through to onMouseDown, which is the path those surfaces were built on. if (!processor_ || view_ != View::kSample || curvePopup_ != CurveTarget::kNone) return false; RECT cr{}; GetClientRect(childHwnd_, &cr); const FaceLayout fl = faceLayout(cr.right - cr.left, cr.bottom - cr.top); if (doubleClickChrome(fl, x, y)) return true; if (selectedId_.empty()) return false; return doubleClickDeck(fl, x, y); } void ReaSamplerEditor::onMouseMove(int x, int y) { if (drag_ == DragKind::kNone) return; dragCurX_ = x; // keep the live cursor position for drag-state draw cues (e.g. drag-off warn) dragCurY_ = y; // The three band-free drags resolve without a layout pass at all. switch (drag_) { case DragKind::kDeckKnob: dragDeck(x, y); return; case DragKind::kScrollThumb: dragBrowse(x, y); return; case DragKind::kCurveNode: dragCurve(x, y); return; default: break; } RECT rc{}; GetClientRect(childHwnd_, &rc); const FaceLayout fl = faceLayout(rc.right - rc.left, rc.bottom - rc.top); if (drag_ == DragKind::kRootMarker) { dragChrome(fl, x, y); } else { dragWaveform(fl, x, y); } } void ReaSamplerEditor::onMouseUp(int x, int y) { // Release a held preview note first (the preview button is a momentary key: note-off on up). // This runs regardless of drag state — the preview press does not start a drag. if (previewingNote_ >= 0) { if (processor_) processor_->previewNoteOff(previewingNote_); previewingNote_ = -1; invalidate(); } if (drag_ == DragKind::kNone) return; // Stack RAII rather than a call at each exit: this handler leaves through several early // returns, and the release commit's final performEdit must land INSIDE the bracket the grab // opened while the bracket itself may not outlive the handler on any path. struct GestureClose { ReaSamplerProcessor* p; ~GestureClose() { if (p) p->endParamGesture(); } } gestureClose{processor_}; const DragKind kind = drag_; const int paramId = dragParamId_; const int curveIdx = curvePointIndex_; const Rect curveRect = dragCurveRect_; drag_ = DragKind::kNone; dragParamId_ = -1; dragInnerCellId_ = -1; curvePointIndex_ = -1; // 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. 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 // that don't reload the instrument. Master gain is an atomic the audio thread reads // directly. Voice count: the label/needle tracks live during the drag but the engine // rebuild (setVoiceCount) fires ONCE here on release — not per integer step. const bool deckTransient = kind == DragKind::kDeckKnob && deckKnobIsProcessorSide(paramId); if (kind == DragKind::kScrollThumb || deckTransient) { // Commit the voice count now that the drag is complete (one rebuild per full drag). if (deckTransient && processor_ && paramId == static_cast(ParamControl::kVoiceCount)) processor_->setVoiceCount(voiceCount_); invalidate(); return; } // A bare click on the Hold cell — no drag, no value change — would otherwise buy a bridge // read, a WAV re-decode and an engine rebuild for a parameter the engine never reads. if (kind == DragKind::kDeckKnob && paramId == kBakeHoldKnobId && params_.bakeHold == dragStartParams_.bakeHold) { invalidate(); return; } // A live control already reached the voices during the drag; its release commits the // final value through the same tier. if (dragCommitsLive(kind, paramId)) { commitLive(); invalidate(); return; } // Drag-off delete: releasing a curve-node drag well outside its box removes the dragged // point on EITHER spline surface — the popup's kCurveNode and the overlay's kSplineNode // share this grammar, not just their click grammar (deletePoint refuses the two endpoints, // so an endpoint drag-off is a plain move — its amp keeps the last clamped drag value). if ((kind == DragKind::kCurveNode || kind == DragKind::kSplineNode) && curveIdx >= 0) { const bool off = x < curveRect.x - kCurveDragOffMargin || x > curveRect.right() + kCurveDragOffMargin || y < curveRect.y - kCurveDragOffMargin || y > curveRect.bottom() + kCurveDragOffMargin; if (off) { if (kind == DragKind::kCurveNode) { editedCurve().deletePoint(static_cast(curveIdx)); } else { splineFor(overlayEnv_).deletePoint(static_cast(curveIdx)); } hover_ = HoverTarget{}; // stale node index would light a shifted node } } commitAndReload(); } // Resolve the interactive element under (x, y) into hover_ and repaint only on change (an // idle move is free). Mirrors onMouseDown's routing order, but read-only. void ReaSamplerEditor::resolveHover(int x, int y) { RECT cr{}; GetClientRect(childHwnd_, &cr); const int w = cr.right - cr.left; const int hgt = cr.bottom - cr.top; HoverTarget h; // kNone by default if (view_ == View::kBrowse) { h = hoverBrowse(w, hgt, x, y); } else if (curvePopup_ != CurveTarget::kNone) { // modal over the face h = hoverCurvePopup(w, hgt, x, y); } else { const FaceLayout fl = faceLayout(w, hgt); h = hoverChrome(fl, x, y); if (h.kind == HoverKind::kNone && !selectedId_.empty()) h = hoverDeck(fl, x, y); if (h.kind == HoverKind::kNone && !selectedId_.empty()) h = hoverWaveform(fl, x, y); } if (h != hover_) { hover_ = h; invalidate(); } } } // namespace reasampler::vst #endif // _WIN32