Files
reasampler/src/shell/instrument/editor_input.cpp
T
daniel 65f6070348 Bake window: derived note lengths carry exact durations, not ladder rungs — a long take is no longer cut at 384 beats
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.
2026-08-01 21:10:38 -04:00

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