65f6070348
Hold keeps its picker. Also: one home for the %-fold, duration-ordered Hold travel, and a corrupt tail degrades to absent rather than fabricating one.
184 lines
7.9 KiB
C++
184 lines
7.9 KiB
C++
// 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_<band> 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);
|
|
}
|
|
|
|
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;
|
|
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 &&
|
|
(paramId == -2 || paramId == static_cast<int>(ParamControl::kVoiceCount) ||
|
|
paramId == static_cast<int>(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<int>(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<std::size_t>(curveIdx));
|
|
} else {
|
|
splineFor(overlayEnv_).deletePoint(static_cast<std::size_t>(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 != hover_) {
|
|
hover_ = h;
|
|
invalidate();
|
|
}
|
|
}
|
|
|
|
} // namespace reasampler::vst
|
|
|
|
#endif // _WIN32
|