341 lines
18 KiB
C++
341 lines
18 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 "core/instrument/ui/master_meter.h" // the bus meter's column interior + ballistics
|
|
#include "core/instrument/ui/waveform_view.h" // resolveLaneSplit (THE lane-split fold)
|
|
#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;
|
|
|
|
namespace {
|
|
|
|
// One tick's numeral, in whole dB ("0", "-12"). No unit suffix — the column is 22px wide and
|
|
// the scale's unit is stated once, by the caption.
|
|
std::string tickLabel(int db) {
|
|
char buf[8];
|
|
std::snprintf(buf, sizeof(buf), "%d", db);
|
|
return std::string(buf);
|
|
}
|
|
|
|
// The MASTER column: dB scale in the label gutter, one or two bars, the held peak tick, and
|
|
// the latched clip cap. `split` is the RESOLVED lane decision — see master_meter.h.
|
|
void paintMeterColumn(LICE_IBitmap* bmp, const Rect& column, const MasterMeterUi& state,
|
|
LaneSplit split) {
|
|
if (column.width <= 0 || column.height <= 0) return;
|
|
const MeterRects m = meterRects(column, split);
|
|
// A column narrower than the interior needs yields all-empty rects, which under rect.h's
|
|
// contract means suppressed — not a zero-height field to fill, tick twelve times and cap.
|
|
if (m.field.empty()) return;
|
|
fillSurface(bmp, toKitBox(m.field), Role::BgCell, InteractionState::Rest);
|
|
|
|
// Scale: a rule every 6 dB, numeralled every 12 with 0 dB heavier — the reference the
|
|
// limiter-off case is read against.
|
|
const LICE_pixel hairline = toLice(roleColor(Role::LineHairline));
|
|
for (int db = static_cast<int>(instrument::engine::kMeterTopDb);
|
|
db >= static_cast<int>(instrument::engine::kMeterFloorDb);
|
|
db -= static_cast<int>(kMeterTickStepDb)) {
|
|
const int y = meterDbToY(m.field, db);
|
|
const bool zero = (db == 0);
|
|
LICE_FillRect(bmp, m.field.x, y, m.field.width, zero ? 2 : 1,
|
|
zero ? toLice(roleColor(Role::TextDim)) : hairline, 1.0f, 0);
|
|
if (meterTickNumeralled(db)) {
|
|
kitText(bmp, meterNumeralRect(m.labels, y), tickLabel(db).c_str(), Font::Micro,
|
|
Role::TextDim, Align::Right);
|
|
}
|
|
}
|
|
|
|
// The bars. A single-lane surface shows ONE bar folding both channels per field
|
|
// (meterSingleLaneState) — the two are the same signal there (dual-mono), so two bars would
|
|
// be a duplicate rather than a reading.
|
|
const LICE_pixel barInk = toLice(roleColor(Role::AccentPrimary));
|
|
const LICE_pixel holdInk = toLice(roleColor(Role::TextPrimary));
|
|
const auto drawBar = [&](const Rect& bar, const instrument::engine::MeterState& ch) {
|
|
if (bar.empty()) return;
|
|
const int top = meterDbToY(bar, ch.levelDb);
|
|
if (top < bar.bottom()) {
|
|
LICE_FillRect(bmp, bar.x, top, bar.width, bar.bottom() - top, barInk, 1.0f, 0);
|
|
}
|
|
if (ch.holdDb > instrument::engine::kMeterFloorDb) {
|
|
// Clamped so the 2px tick cannot hang past the bar when the hold sits on the floor.
|
|
const int hold = (std::min)(meterDbToY(bar, ch.holdDb), bar.bottom() - 2);
|
|
LICE_FillRect(bmp, bar.x, hold, bar.width, 2, holdInk, 1.0f, 0);
|
|
}
|
|
};
|
|
if (split == LaneSplit::Single) {
|
|
drawBar(m.barA, meterSingleLaneState(state));
|
|
} else {
|
|
drawBar(m.barA, state.left);
|
|
drawBar(m.barB, state.right);
|
|
}
|
|
|
|
// The clip cap: latched over the whole field, click to clear.
|
|
if (meterClipped(state)) {
|
|
LICE_FillRect(bmp, m.field.x, m.field.y, m.field.width, 3,
|
|
toLice(roleColor(Role::Warn)), 1.0f, 0);
|
|
}
|
|
}
|
|
|
|
} // namespace
|
|
|
|
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));
|
|
// The meter's bar count is the SAME resolved decision the waveform's lane split is —
|
|
// resolveLaneSplit is the one home of it. Asked directly rather than read back off
|
|
// waveformSurface, whose laneCount additionally folds in the waveform BAND's pixel height,
|
|
// which decides nothing about how many channels the bus is carrying.
|
|
const LaneSplit meterSplit = resolveLaneSplit(channelMode_ == ChannelMode::Stereo,
|
|
channelPcmFor(selectedId_).channelCount);
|
|
|
|
// 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));
|
|
};
|
|
// The single-button forms. ENABLE names what it controls and reads Primary on / dim gray
|
|
// off; MODE reads the current mode and has no off state. A control the MODE refuses draws
|
|
// Disabled, which is a third state and not a synonym for off: off is live and clickable,
|
|
// and the washed Disabled surface is what separates them.
|
|
const auto drawButtonToggle = [&](const DeckToggleLayout& t, const char* label, bool active,
|
|
bool disabled) {
|
|
const bool hov = !disabled && isHovered(HoverKind::kControl, t.id);
|
|
const InteractionState st =
|
|
disabled ? InteractionState::Disabled
|
|
: (active ? InteractionState::Active
|
|
: (hov ? InteractionState::Hover : InteractionState::Rest));
|
|
fillSurface(bmp, toKitBox(t.seg0), Role::BgCell, st);
|
|
kitTextCentered(bmp, t.seg0, label, Font::Micro,
|
|
active && !disabled ? Role::BgBase
|
|
: (hov ? Role::TextPrimary : Role::TextDim));
|
|
};
|
|
const auto drawModeToggle = [&](const DeckToggleLayout& t, EnvMode mode) {
|
|
drawButtonToggle(t, mode == EnvMode::Spline ? "Spline" : "Stage", /*active=*/true,
|
|
/*disabled=*/false);
|
|
};
|
|
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. An
|
|
// envelope deck whose overlay is the one on the waveform takes the primary accent
|
|
// instead — the deck itself is the selection affordance, so the whole box says so.
|
|
const OverlayEnv groupEnv = overlayEnvForGroup(g.id);
|
|
const bool focused = groupEnv != OverlayEnv::kNone && groupEnv == overlayEnv_;
|
|
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,
|
|
focused ? toLice(roleColor(Role::AccentPrimary)) : 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 gain-reduction lamp. ROUND, where the overlay radios in this same slot are
|
|
// square, so it reads as a lamp rather than a control.
|
|
if (g.captionRadio.id >= 0 && g.captionRadio.passive) {
|
|
const Rect& rb = g.captionRadio.box;
|
|
const float r = rb.width / 2.0f - 0.5f;
|
|
LICE_FillCircle(bmp, rb.x + rb.width / 2.0f, rb.y + rb.height / 2.0f, r,
|
|
toLice(roleColor(grLampLit(masterMeter_) ? Role::Warn
|
|
: Role::LineHairline)),
|
|
1.0f, 0, true);
|
|
}
|
|
// 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:
|
|
drawButtonToggle(t, "Envelope", play.pitchEnv.enabled, false);
|
|
break;
|
|
case ParamControl::kVoiceMode:
|
|
drawToggle(t, "Poly", "Mono", isMono, false);
|
|
break;
|
|
case ParamControl::kFilterEnable:
|
|
drawButtonToggle(t, "Filter", play.filter.enabled, false);
|
|
break;
|
|
case ParamControl::kFilterLaw:
|
|
drawToggle(t, "Band", "Notch",
|
|
play.filter.settings.morphLaw ==
|
|
instrument::engine::filter::MorphLaw::HighNotchLow,
|
|
!play.filter.enabled);
|
|
break;
|
|
case ParamControl::kLimiterEnable:
|
|
drawButtonToggle(t, "Limiter", params_.limiterEnabled, false);
|
|
break;
|
|
// A mode SELECTOR: the label is the state, so it is drawn Active either way —
|
|
// there is nothing here for a dim-gray off to mean.
|
|
case ParamControl::kAmpEnvMode:
|
|
drawModeToggle(t, play.ampSpline.mode);
|
|
break;
|
|
case ParamControl::kPitchEnvMode:
|
|
drawModeToggle(t, play.pitchSpline.mode);
|
|
break;
|
|
case ParamControl::kFilterEnvMode:
|
|
drawModeToggle(t, play.filterSpline.mode);
|
|
break;
|
|
default: break;
|
|
}
|
|
}
|
|
// The one row toggle left: VOICE's Retrig|Legato, live only in Mono.
|
|
if (g.rowToggle.id >= 0) {
|
|
drawToggle(g.rowToggle, "Retrig", "Legato",
|
|
monoTrigger_ == MonoTrigger::Legato, !isMono);
|
|
}
|
|
|
|
if (g.column.id >= 0) paintMeterColumn(bmp, g.column.box, masterMeter_, meterSplit);
|
|
|
|
// 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
|