Files
reasampler/src/shell/instrument/editor_paint_deck.cpp
T

243 lines
13 KiB
C++

// editor_paint_deck.cpp — the DECKS band's painter: the fenced control groups, their
// captions, the compact caption and row toggles, and the radial knobs with the label<->value
// swap on hover/drag. Windows-only; the deck's cell geometry is the pure knob_deck layout and
// its group composition the pure deck_groups list.
#include "shell/instrument/reasampler_editor.h"
#ifdef _WIN32
#include <string>
#include <vector>
#include "core/instrument/engine/filter/filter_morph.h" // MorphLaw (the law toggle's state)
#include "core/instrument/ui/knob_deck.h" // deck layout + kDeckKnobSize
#include "shell/instrument/editor_internal.h" // kit adapters + knob face
#include "shell/instrument/reasampler_processor.h"
namespace reasampler::vst {
using namespace reasampler::ui; // kit vocabulary
using namespace reasampler::instrument::ui; // deck geometry
// The knob's own name/value band. One size up from the group captions and the toggles, which
// are chrome you read once — this is the readout you read while turning something.
constexpr Font kCellLabelFont = Font::Label;
void ReaSamplerEditor::paintDeck(LICE_IBitmap* bmp, const FaceLayout& fl) {
const Rect& deckArea = fl.bands.decks;
if (deckArea.width <= 0 || deckArea.height <= 0) return;
const DeckLayout dl = layoutDeck(fl.deckDescs, deckArea.x, deckArea.y, deckArea.width);
const PlaySeconds& play = params_.play;
const bool isMono = (voiceMode_ == VoiceMode::Mono);
const LICE_pixel hairline = toLice(roleColor(Role::LineHairline));
// One compact-toggle draw (the Mono/Stereo segment grammar at Micro scale). Disabled
// segments draw inert so the dependency (Retrig|Legato needs Mono) reads at a glance.
// `seg0Disabled` disables ONE segment: Gate is unselectable while an EG is drawn, but
// Trigger — the mode it is stuck in — must still read as the live choice.
const auto drawToggle = [&](const DeckToggleLayout& t, const char* s0, const char* s1,
bool seg1Active, bool disabled, bool seg0Disabled = false) {
const bool hov = !disabled && isHovered(HoverKind::kControl, t.id);
const bool d0 = disabled || seg0Disabled;
const InteractionState st0 =
d0 ? InteractionState::Disabled
: (!seg1Active ? InteractionState::Active
: (hov ? InteractionState::Hover : InteractionState::Rest));
const InteractionState st1 =
disabled ? InteractionState::Disabled
: (seg1Active ? InteractionState::Active
: (hov ? InteractionState::Hover : InteractionState::Rest));
fillSurface(bmp, toKitBox(t.seg0), Role::BgCell, st0);
fillSurface(bmp, toKitBox(t.seg1), Role::BgCell, st1);
kitTextCentered(bmp, t.seg0, s0, Font::Micro,
d0 ? Role::TextDim
: (!seg1Active ? Role::BgBase : Role::TextPrimary));
kitTextCentered(bmp, t.seg1, s1, Font::Micro,
disabled ? Role::TextDim
: (seg1Active ? Role::BgBase : Role::TextPrimary));
};
const bool anySpline = splineActive(play);
// The knob's short name label (swapped for the live value during hover/drag — no third
// line, no permanent value clutter).
const auto knobName = [](ParamControl c) -> const char* {
switch (c) {
case ParamControl::kAttack: return "Attack";
case ParamControl::kHold: return "Hold";
case ParamControl::kDecay: return "Decay";
case ParamControl::kSustain: return "Sustain";
case ParamControl::kRelease: return "Release";
case ParamControl::kTrigLength: return "Len %";
case ParamControl::kTrigAttack: return "Attack";
case ParamControl::kTrigHold: return "Hold";
case ParamControl::kTrigDecay: return "Decay";
case ParamControl::kKeyTrack: return "Key Trk";
case ParamControl::kRate: return "Rate";
case ParamControl::kPitch: return "Pitch";
case ParamControl::kPitchEnvAttack: return "P.Att";
case ParamControl::kPitchEnvHold: return "P.Hold";
case ParamControl::kPitchEnvDecay: return "P.Dec";
case ParamControl::kPitchEnvDepth: return "P.Depth";
case ParamControl::kVoiceCount: return "Voices";
case ParamControl::kMasterGain: return "Gain";
case ParamControl::kFilterMorph: return "Mode";
case ParamControl::kFilterCutoff: return "Cutoff";
case ParamControl::kFilterQ: return "Res";
case ParamControl::kFilterDrive: return "Drive";
case ParamControl::kFilterModAmt: return "Mod";
case ParamControl::kFilterVel: return "Vel";
case ParamControl::kFilterKeyTrack: return "Key Trk";
case ParamControl::kAmpVelCurve: return "Amp";
case ParamControl::kPitchVelCurve: return "Pitch";
case ParamControl::kFilterVelCurve: return "Filter";
case ParamControl::kFilterEnvAttack: return "F.Att";
case ParamControl::kFilterEnvHold: return "F.Hold";
case ParamControl::kFilterEnvDecay: return "F.Dec";
case ParamControl::kFilterEnvSustain: return "F.Sus";
case ParamControl::kFilterEnvRelease: return "F.Rel";
case ParamControl::kFilterTrigAttack: return "F.Att";
case ParamControl::kFilterTrigHold: return "F.Hold";
case ParamControl::kFilterTrigDecay: return "F.Dec";
default: return "";
}
};
for (const DeckGroupLayout& g : dl.groups) {
// The fence: a bg/panel box with a hairline border, caption micro-caps left.
fillSurface(bmp, toKitBox(g.box), Role::BgPanel, InteractionState::Rest);
LICE_DrawRect(bmp, g.box.x, g.box.y, g.box.width - 1, g.box.height - 1,
hairline, 1.0f, 0);
const char* caption = "";
switch (g.id) {
case kGroupAmpEnv: caption = "AMP ENVELOPE"; break;
case kGroupPitch: caption = "PITCH/RATE"; break;
case kGroupPitchEnv: caption = "PITCH ENV"; break;
case kGroupFilter: caption = "FILTER"; break;
case kGroupFilterEnv: caption = "FILTER ENV"; break;
case kGroupVelocity: caption = "VELOCITY"; break;
case kGroupVoice: caption = "VOICE"; break;
case kGroupMaster: caption = "MASTER"; break;
default: break;
}
kitText(bmp, g.caption, caption, Font::Micro, Role::TextDim);
// The overlay-select radio: filled in the tertiary accent (the colour the overlay
// traces in) when this group's envelope is the one on the waveform, hollow otherwise.
if (g.captionRadio.id >= 0) {
// overlayEnvForRadio returns kNone for BOTH "not a radio id" and "no selection" —
// a non-radio id must never read as lit just because nothing is selected, so the
// picked env has to be checked against kNone itself, not just matched by equality.
const OverlayEnv picked = overlayEnvForRadio(g.captionRadio.id);
const bool on = picked != OverlayEnv::kNone && overlayEnv_ == picked;
const bool hov = isHovered(HoverKind::kEnvRadio, g.captionRadio.id);
const Rect& rb = g.captionRadio.box;
LICE_DrawRect(bmp, rb.x, rb.y, rb.width - 1, rb.height - 1,
toLice(roleColor(on || hov ? Role::AccentTertiary
: Role::LineHairline)),
1.0f, 0);
if (on) {
LICE_FillRect(bmp, rb.x + 3, rb.y + 3, rb.width - 6, rb.height - 6,
toLice(roleColor(Role::AccentTertiary)), 1.0f, 0);
}
}
// The compact caption toggles (right-anchored in the caption row, never full-width).
for (const DeckToggleLayout* tp : {&g.captionToggle, &g.captionToggle2}) {
if (tp->id < 0) continue;
const DeckToggleLayout& t = *tp;
switch (static_cast<ParamControl>(t.id)) {
case ParamControl::kPlayMode:
drawToggle(t, "Gate", "Trigger", play.playMode == PlayMode::Trigger, false,
/*seg0Disabled=*/anySpline);
break;
case ParamControl::kPitchEngine:
drawToggle(t, "Varisp", "Presrv",
play.pitchEngine == PitchEngine::Preserve, false);
break;
case ParamControl::kPitchEnvEnable:
drawToggle(t, "Off", "On", play.pitchEnv.enabled, false);
break;
case ParamControl::kVoiceMode:
drawToggle(t, "Poly", "Mono", isMono, false);
break;
case ParamControl::kFilterEnable:
drawToggle(t, "Off", "On", play.filter.enabled, false);
break;
case ParamControl::kAmpEnvMode:
drawToggle(t, "Stg", "Spl", play.ampSpline.mode == EnvMode::Spline, false);
break;
case ParamControl::kPitchEnvMode:
drawToggle(t, "Stg", "Spl", play.pitchSpline.mode == EnvMode::Spline, false);
break;
case ParamControl::kFilterEnvMode:
drawToggle(t, "Stg", "Spl", play.filterSpline.mode == EnvMode::Spline, false);
break;
default: break;
}
}
// Row toggles: VOICE's Retrig|Legato (live only in Mono) and FILTER's morph law.
if (g.rowToggle.id >= 0) {
if (static_cast<ParamControl>(g.rowToggle.id) == ParamControl::kFilterLaw) {
drawToggle(g.rowToggle, "Band", "Notch",
play.filter.settings.morphLaw ==
instrument::engine::filter::MorphLaw::HighNotchLow,
!play.filter.enabled);
} else {
drawToggle(g.rowToggle, "Retrig", "Legato",
monoTrigger_ == MonoTrigger::Legato, !isMono);
}
}
// The knobs. A dependent group's knobs draw Disabled (not hidden) — stable geometry.
// The predicate is the input side's, so the drawn state and the inert grab agree.
for (const DeckCellLayout& c : g.cells) {
const bool disabled = deckKnobDisabled(c.id);
// A VELOCITY cell is a popup opener, not a dial: it shows its curve in miniature
// where a knob face would be, and its whole cell is the click target.
const CurveTarget curveCell = curveTargetFor(c.id);
if (curveCell != CurveTarget::kNone) {
paintCurveButton(bmp, c.knob, curveCell, disabled,
!disabled && isHovered(HoverKind::kControl, c.id));
kitTextCentered(bmp, c.label, knobName(static_cast<ParamControl>(c.id)),
kCellLabelFont, Role::TextDim);
continue;
}
const bool dragging = (drag_ == DragKind::kDeckKnob && dragParamId_ == c.id);
const bool hov = !disabled && isHovered(HoverKind::kControl, c.id);
const InteractionState st =
disabled ? InteractionState::Disabled
: (dragging ? InteractionState::Dragging
: (hov ? InteractionState::Hover : InteractionState::Rest));
drawKnobFace(bmp, c.knob, deckControlNorm(c.id), st);
// The inner dial rides only the knobs whose stage is sloped — deck_groups owns
// that rule, so a Hold or Sustain cell simply has no curve id and draws none.
const ParamControl curve = curveParamFor(static_cast<ParamControl>(c.id));
const bool innerDragging =
(drag_ == DragKind::kDeckKnob && dragInnerCellId_ == c.id);
const bool innerHov = !disabled && isHovered(HoverKind::kInnerDial, c.id);
if (curve != ParamControl::kCount) {
const InteractionState ist =
disabled ? InteractionState::Disabled
: (innerDragging ? InteractionState::Dragging
: (innerHov ? InteractionState::Hover
: InteractionState::Rest));
drawInnerDial(bmp, c.inner, deckControlNorm(static_cast<int>(curve)), ist);
}
// One label band, so the inner dial's readout takes it while the inner dial is the
// one being touched.
std::string label;
if (innerDragging || innerHov) label = deckValueLabel(static_cast<int>(curve));
else if (dragging || hov) label = deckValueLabel(c.id);
else label = std::string(knobName(static_cast<ParamControl>(c.id)));
kitTextCentered(bmp, c.label, label.c_str(), kCellLabelFont, Role::TextDim);
}
}
}
} // namespace reasampler::vst
#endif // _WIN32