239 lines
12 KiB
C++
239 lines
12 KiB
C++
// editor_input_deck.cpp — the DECKS band's input: toggles (committed at once, a discrete
|
|
// final edit), knob grabs (grab-anchored vertical drag, committed on release), the live
|
|
// knob-drag resolution, and the band's hover. Windows-only.
|
|
|
|
#include "shell/instrument/reasampler_editor.h"
|
|
|
|
#ifdef _WIN32
|
|
|
|
#include "core/instrument/ui/deck_values.h" // snapDeckParamNorm (Shift's whole-unit table)
|
|
#include "core/instrument/ui/knob_deck.h" // hitTestDeck / layoutDeck
|
|
#include "core/instrument/ui/param_slider.h" // knobDragValue (grab-anchored drag)
|
|
#include "shell/instrument/editor_internal.h"
|
|
#include "shell/instrument/reasampler_processor.h"
|
|
|
|
namespace reasampler::vst {
|
|
|
|
using namespace reasampler::ui;
|
|
using namespace reasampler::instrument::ui;
|
|
|
|
bool ReaSamplerEditor::deckKnobDisabled(int id) const {
|
|
// Thin int-id wrapper over the pure, CTest-covered predicate — see deckKnobInert
|
|
// (deck_groups.h) for which cells go inert and why.
|
|
return deckKnobInert(static_cast<DeckParam>(id), deckEnableState());
|
|
}
|
|
|
|
bool ReaSamplerEditor::mouseDownDeck(const FaceLayout& fl, int x, int y) {
|
|
const Rect& band = fl.bands.decks;
|
|
if (!contains(band, x, y)) return false;
|
|
|
|
const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
|
|
const DeckHit hit = hitTestDeck(dl, x, y);
|
|
// The deck IS the overlay's click target: anywhere inside an envelope group — panel
|
|
// background, knob or button — focuses that envelope, and any other group clears it. Set
|
|
// BEFORE the kind switch and it consumes nothing, so every grab and commit below still
|
|
// runs. A drag in flight owns the surface, so it refuses the change — always true at this
|
|
// call site today (mouse capture makes a real WM_LBUTTONDOWN-while-dragging unreachable;
|
|
// WM_CAPTURECHANGED and WM_LBUTTONUP both reset drag_ before another down can land), but
|
|
// left explicit rather than assumed so a future capture-handling change fails loud, not
|
|
// by silently letting a drag's own surface steal its own focus mid-gesture.
|
|
if (drag_ == DragKind::kNone) {
|
|
const OverlayEnv focus = overlayEnvForGroup(hit.group);
|
|
if (focus != overlayEnv_) {
|
|
overlayEnv_ = focus;
|
|
invalidate(); // view state only: no parameter write, no reload
|
|
}
|
|
}
|
|
if (hit.kind == DeckHitKind::CaptionToggle || hit.kind == DeckHitKind::RowToggle) {
|
|
switch (static_cast<ParamControl>(hit.id)) {
|
|
case ParamControl::kVoiceMode: {
|
|
// Processor-side per-instance param: live setter (engine rebuild via the
|
|
// drain-slot swap — tails survive), local snapshot in step.
|
|
const VoiceMode m = (hit.segment == 1) ? VoiceMode::Mono : VoiceMode::Poly;
|
|
if (m != voiceMode_) {
|
|
voiceMode_ = m;
|
|
processor_->setVoiceMode(m);
|
|
}
|
|
invalidate();
|
|
break;
|
|
}
|
|
case ParamControl::kMonoTrigger: {
|
|
if (voiceMode_ != VoiceMode::Mono) break; // Disabled (inert) in Poly
|
|
const MonoTrigger t =
|
|
(hit.segment == 1) ? MonoTrigger::Legato : MonoTrigger::Retrigger;
|
|
if (t != monoTrigger_) {
|
|
monoTrigger_ = t;
|
|
processor_->setMonoTrigger(t);
|
|
}
|
|
invalidate();
|
|
break;
|
|
}
|
|
case ParamControl::kFilterLaw:
|
|
// Inert while the filter is off, matching its Disabled paint.
|
|
if (!params_.play.filter.enabled) break;
|
|
applyParamControl(hit.id, 0.0, hit.segment);
|
|
commitAndReload();
|
|
break;
|
|
case ParamControl::kLimiterEnable: {
|
|
// One button, so its next state is the opposite of the current one — which is
|
|
// also why the old "did it actually change" guard is gone: it always does.
|
|
const bool on = !params_.limiterEnabled;
|
|
params_.limiterEnabled = on;
|
|
// Commits the audible state and the persisted state together, here, because
|
|
// this is a control the user A/Bs. The funnel only ARMS the host's latency
|
|
// restart — the sync tick delivers it — so nothing on this path calls into
|
|
// the host from inside a mouse handler.
|
|
processor_->setLimiterEnabled(on);
|
|
invalidate();
|
|
break;
|
|
}
|
|
default: {
|
|
// Parameter-set toggles (play mode / pitch engine / pitch-env + filter enable,
|
|
// and the three env-mode selectors). A single button carries no segment, so
|
|
// its next state is derived from the parameter set rather than read off the
|
|
// click — deck_values owns that derivation.
|
|
const int segment =
|
|
hit.segment >= 0
|
|
? hit.segment
|
|
: nextToggleSegment(static_cast<ParamControl>(hit.id), params_.play);
|
|
applyParamControl(hit.id, 0.0, segment);
|
|
commitAndReload();
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
if (hit.kind == DeckHitKind::Column) {
|
|
// The meter's ONLY gesture: clear the latched clip cap. Both latches go — the audio
|
|
// thread's is what the next tick would otherwise re-latch the UI's from.
|
|
masterMeter_ = clearMasterMeterClip(masterMeter_);
|
|
processor_->clearMasterBusClip();
|
|
invalidate();
|
|
return true;
|
|
}
|
|
if (hit.kind == DeckHitKind::Knob) {
|
|
// Knobs of a disabled group are drawn but inert.
|
|
if (deckKnobDisabled(hit.id)) return true;
|
|
// A VELOCITY cell summons the popup editor instead of starting a knob drag.
|
|
const CurveTarget curveCell = curveTargetFor(hit.id);
|
|
if (curveCell != CurveTarget::kNone) {
|
|
curvePopup_ = curveCell;
|
|
invalidate();
|
|
return true;
|
|
}
|
|
// A grab on the inner disc drags the CURVE control instead, but only where the stage
|
|
// is sloped; on a Hold or Sustain cell the inner region is just more of the knob.
|
|
const ParamControl curve = curveParamFor(static_cast<ParamControl>(hit.id));
|
|
const bool inner = hit.inner && curve != ParamControl::kCount;
|
|
drag_ = DragKind::kDeckKnob;
|
|
dragParamId_ = inner ? static_cast<int>(curve) : hit.id;
|
|
dragInnerCellId_ = inner ? hit.id : -1;
|
|
dragKnobStartValue_ = deckControlNorm(dragParamId_);
|
|
dragMods_ = dragModifiers();
|
|
// Processor-side knobs (voice count / master gain) are transient live writes with no
|
|
// parameter-set mutation, so they need no rollback snapshot.
|
|
dragStartParams_ = params_;
|
|
dragStartX_ = x;
|
|
dragStartY_ = y;
|
|
// Opens the host's edit bracket for the whole gesture, so a host in touch or latch mode
|
|
// records one continuous edit rather than a burst of one-point ones. Closed on release
|
|
// and on capture-lost; a control with no parameter row is a no-op inside the processor.
|
|
if (processor_) {
|
|
processor_->beginParamGesture(static_cast<instrument::ui::DeckParam>(dragParamId_));
|
|
}
|
|
invalidate();
|
|
}
|
|
// The deck band swallows its own clicks either way — no fall-through to the waveform.
|
|
return true;
|
|
}
|
|
|
|
bool ReaSamplerEditor::doubleClickDeck(const FaceLayout& fl, int x, int y) {
|
|
const Rect& band = fl.bands.decks;
|
|
if (!contains(band, x, y)) return false;
|
|
const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
|
|
// Resolved against the drawn CIRCLES (hitTestKnobFace), not the cell a drag grabs anywhere
|
|
// in: the two rings are concentric, so only a radial resolve can tell "reset the exponent"
|
|
// from "reset the value".
|
|
const DeckFaceHit hit = hitTestKnobFace(dl, x, y);
|
|
if (hit.id < 0) return false;
|
|
if (deckKnobDisabled(hit.id)) return true; // drawn-but-dead: swallow, change nothing
|
|
// A VELOCITY cell is a popup opener with no scalar value to reset.
|
|
if (curveTargetFor(hit.id) != CurveTarget::kNone) return true;
|
|
|
|
const ParamControl curve = curveParamFor(static_cast<ParamControl>(hit.id));
|
|
const bool inner = hit.inner && curve != ParamControl::kCount;
|
|
const int target = inner ? static_cast<int>(curve) : hit.id;
|
|
// The processor-side knobs own their own defaults — they never reach the parameter set.
|
|
if (target == static_cast<int>(ParamControl::kVoiceCount)) {
|
|
voiceCount_ = kDefaultVoiceCount;
|
|
processor_->setVoiceCount(voiceCount_);
|
|
invalidate();
|
|
return true;
|
|
}
|
|
if (target == static_cast<int>(ParamControl::kMasterGain)) {
|
|
processor_->setMasterGainLinear(1.0);
|
|
invalidate();
|
|
return true;
|
|
}
|
|
resetParamControl(target);
|
|
// Same commit tiering a drag of this control takes, so a reset reaches the sounding note
|
|
// exactly as a drag to the same value would.
|
|
if (dragCommitsLive(DragKind::kDeckKnob, target)) commitLive();
|
|
else commitAndReload();
|
|
invalidate();
|
|
return true;
|
|
}
|
|
|
|
void ReaSamplerEditor::dragDeck(int x, int y) {
|
|
// Radial knob: grab-anchored vertical drag — knobDragValue maps the y delta from the
|
|
// value at grab (up = increase), so the value tracks relative motion and never jumps on
|
|
// grab. Live feedback; parameter-set commits land on WM_LBUTTONUP.
|
|
(void)x;
|
|
const DragModifiers mods = dragModifiers();
|
|
if (mods != dragMods_) {
|
|
// Re-anchor (see dragMods_). Reading the anchor back off the control also means a Shift
|
|
// RELEASE re-anchors from the SNAPPED value, so the knob does not spring back.
|
|
dragKnobStartValue_ = deckControlNorm(dragParamId_);
|
|
dragStartY_ = y;
|
|
dragMods_ = mods;
|
|
}
|
|
double norm = knobDragValue(dragKnobStartValue_, y - dragStartY_, mods);
|
|
// The preview-velocity sentinel (-2) and the discrete controls have no whole unit to snap to.
|
|
if (mods.shift && dragParamId_ >= 0) {
|
|
norm = snapDeckParamNorm(static_cast<DeckParam>(dragParamId_), norm);
|
|
}
|
|
applyDeckKnob(dragParamId_, norm);
|
|
// A live control is delivered on every move, not only on release — that is the whole
|
|
// point: the note already sounding tracks the hand on the knob. The processor-side knobs
|
|
// are skipped because applyDeckKnob already wrote them straight through; commitLive would
|
|
// only re-push an unchanged parameter set. The release and capture-lost paths ask this same
|
|
// question; the reset path never reaches the commit for them at all.
|
|
if (!deckKnobIsProcessorSide(dragParamId_) &&
|
|
dragCommitsLive(DragKind::kDeckKnob, dragParamId_)) {
|
|
commitLive();
|
|
}
|
|
invalidate();
|
|
}
|
|
|
|
HoverTarget ReaSamplerEditor::hoverDeck(const FaceLayout& fl, int x,
|
|
int y) const {
|
|
const Rect& band = fl.bands.decks;
|
|
if (!contains(band, x, y)) return {};
|
|
const DeckLayout dl = layoutDeck(fl.deckDescs, band.x, band.y, band.width);
|
|
const DeckHit dh = hitTestDeck(dl, x, y);
|
|
if (dh.kind == DeckHitKind::None) return {};
|
|
// The meter reports its own state continuously; a hover on it would only mean "the clip
|
|
// cap is clearable", which the cap's presence already says.
|
|
if (dh.kind == DeckHitKind::Column) return {};
|
|
if (dh.kind == DeckHitKind::Knob && dh.inner &&
|
|
curveParamFor(static_cast<ParamControl>(dh.id)) != ParamControl::kCount) {
|
|
// Indexed by the OUTER cell id so the paint side can find the cell it belongs to.
|
|
return {HoverKind::kInnerDial, dh.id};
|
|
}
|
|
return {HoverKind::kControl, dh.id};
|
|
}
|
|
|
|
} // namespace reasampler::vst
|
|
|
|
#endif // _WIN32
|