// 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); 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); } 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; const DragKind kind = drag_; const int paramId = dragParamId_; const int curveIdx = curvePointIndex_; const Rect curveRect = dragCurveRect_; drag_ = DragKind::kNone; dragParamId_ = -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. 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 && (paramId == -2 || paramId == static_cast(ParamControl::kVoiceCount) || paramId == static_cast(ParamControl::kMasterGain)); 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; } // Drag-off delete: releasing a curve-node drag well outside the box removes the dragged // point (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 && curveIdx >= 0) { const bool off = x < curveRect.x - kCurveDragOffMargin || x > curveRect.right() + kCurveDragOffMargin || y < curveRect.y - kCurveDragOffMargin || y > curveRect.bottom() + kCurveDragOffMargin; if (off) { params_.velocityCurve.deletePoint(static_cast(curveIdx)); hover_ = HoverTarget{}; // stale kCurveNode 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 (curvePopupOpen_) { // 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 != hover_) { hover_ = h; invalidate(); } } } // namespace reasampler::vst #endif // _WIN32