225 lines
12 KiB
C++
225 lines
12 KiB
C++
// editor_paint_chrome.cpp — the CHROME band's painter: the toolbar row (product title +
|
|
// live readout, then the control run — preview, preview-velocity knob, curve button,
|
|
// Mono|Stereo, Browse) over the strip row, which the piano strip has to itself. Windows-only;
|
|
// all rects come from the pure sample_chrome interior and the pure keyboard_strip geometry.
|
|
|
|
#include "shell/instrument/reasampler_editor.h"
|
|
|
|
#ifdef _WIN32
|
|
|
|
#include <cstdio>
|
|
#include <string>
|
|
|
|
#include "core/instrument/ui/keyboard_strip.h" // StripLayout / keyRect / noteName
|
|
#include "core/instrument/ui/knob_deck.h" // kDeckKnobSize (the shared knob square)
|
|
#include "core/ui/tooltip.h" // computeTooltip (shared placement math)
|
|
#include "core/version/app_version.h" // vstPluginName (channel-derived title band)
|
|
#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; // chrome geometry + keyboard strip
|
|
using namespace reasampler::instrument::map; // SampleRefs / findRef (title readout fallback)
|
|
|
|
namespace {
|
|
|
|
// Every text element on the toolbar row draws at this one size/weight — including the
|
|
// product title, which used to be the row's odd one out.
|
|
constexpr Font kToolbarFont = Font::Label;
|
|
|
|
// Kit font is proportional, so the char width is a generous estimate (pads, never clips).
|
|
constexpr int kTooltipCharPx = 7;
|
|
constexpr int kTooltipTextH = 14;
|
|
|
|
constexpr int kRootBadgeW = 38;
|
|
constexpr int kRootBadgeH = 13;
|
|
|
|
// The piano strip: white keys tiled at one width, black keys overlaid at one width, each
|
|
// tinted with its spectral hue so pitch position reads at a glance. `hoverNote` is outlined
|
|
// (-1 for none). All rects are strip-local; `area` supplies the origin.
|
|
void drawKeyboard(LICE_IBitmap* bmp, const Rect& area, const StripLayout& sl, int hoverNote) {
|
|
fillSurface(bmp, toKitBox(area), Role::BgBase, InteractionState::Rest);
|
|
if (sl.keys.empty()) return;
|
|
|
|
const LICE_pixel hairline = toLice(roleColor(Role::LineHairline));
|
|
const LICE_pixel shadow = toLice(roleColor(Role::BgBase));
|
|
const auto hueOf = [](int n) {
|
|
return toLice(spectralColor(static_cast<double>(n) / (kStripKeyCount - 1)));
|
|
};
|
|
|
|
for (int n = 0; n < kStripKeyCount; ++n) {
|
|
if (!isNaturalKey(n)) continue;
|
|
const Rect k = keyRect(sl, n);
|
|
LICE_FillRect(bmp, area.x + k.x, area.y + k.y, k.width, k.height, hueOf(n), 0.55f, 0);
|
|
LICE_Line(bmp, area.x + k.right() - 1, area.y + k.y, area.x + k.right() - 1,
|
|
area.y + k.bottom() - 1, hairline, 0.6f, 0, false);
|
|
}
|
|
// Blacks last: they overlap the whites they straddle.
|
|
for (int n = 0; n < kStripKeyCount; ++n) {
|
|
if (isNaturalKey(n)) continue;
|
|
const Rect k = keyRect(sl, n);
|
|
LICE_FillRect(bmp, area.x + k.x, area.y + k.y, k.width, k.height, shadow, 1.0f, 0);
|
|
LICE_FillRect(bmp, area.x + k.x, area.y + k.y, k.width, k.height, hueOf(n), 0.35f, 0);
|
|
}
|
|
if (hoverNote >= 0) {
|
|
const Rect k = keyRect(sl, hoverNote);
|
|
LICE_DrawRect(bmp, area.x + k.x, area.y + k.y, k.width - 1, k.height - 1,
|
|
toLice(roleColor(Role::TextPrimary)), 0.8f, 0);
|
|
}
|
|
}
|
|
|
|
// The root affordance: the root key lit accent-primary with a static glow, plus a name badge
|
|
// (a key is far too narrow to carry text itself). The badge is clamped inside the strip.
|
|
void drawRootKey(LICE_IBitmap* bmp, const Rect& area, const StripLayout& sl, int root) {
|
|
if (sl.keys.empty()) return;
|
|
const Rect k = rootMarkerRect(sl, root);
|
|
const int kx = area.x + k.x;
|
|
const LICE_pixel accent = toLice(roleColor(Role::AccentPrimary));
|
|
const LICE_pixel glow = toLice(roleColor(Role::AccentHot));
|
|
// Static glow: a wider low-alpha halo behind the lit key (a drawn state, not a pulse).
|
|
LICE_FillRect(bmp, kx - 3, area.y + k.y, k.width + 6, k.height, glow, 0.30f, 0);
|
|
LICE_FillRect(bmp, kx, area.y + k.y, k.width, k.height, accent, 1.0f, 0);
|
|
|
|
const int badgeH = (std::min)(kRootBadgeH, area.height);
|
|
const int badgeW = (std::min)(kRootBadgeW, area.width);
|
|
int bx = kx + (k.width - badgeW) / 2;
|
|
bx = (std::max)(area.x, (std::min)(bx, area.right() - badgeW));
|
|
const Rect badge = Rect::ltrb(bx, area.bottom() - badgeH, bx + badgeW, area.bottom());
|
|
LICE_FillRect(bmp, badge.x, badge.y, badge.width, badge.height, accent, 0.92f, 0);
|
|
kitTextCentered(bmp, badge, noteName(root).c_str(), Font::Micro, Role::BgBase);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool empty) {
|
|
const ChromeRects& cr = fl.chrome;
|
|
|
|
fillSurface(bmp, toKitBox(cr.toolbar), Role::BgPanel, InteractionState::Rest);
|
|
|
|
// Toolbar: product name + live readout. The beta channel gets no distinct accent; the
|
|
// channel-derived vstPluginName is the only beta-vs-stable signal.
|
|
std::string title = version::vstPluginName();
|
|
if (processor_ && processor_->bridge().isConnected()) {
|
|
// The instance's own loaded state outranks bank availability (the bank is a browser
|
|
// source, not the instrument's identity) — a self-contained instance names its sound
|
|
// (refs displayName fallback) even when the bank snapshot is empty.
|
|
if (!selectedId_.empty())
|
|
title += " [" + sampleLabel(samples_, processor_->sampleRefs(), selectedId_) + "]";
|
|
else if (samples_.empty()) title += " [bank empty]";
|
|
else title += " [pick a capture]";
|
|
} else {
|
|
title += " [host: no bridge]";
|
|
}
|
|
kitText(bmp, cr.title, title.c_str(), kToolbarFont, Role::TextPrimary);
|
|
|
|
// Browse: the picker. When nothing is loaded it is the empty state's dominant
|
|
// call-to-action — draw it Active (accent-primary) so it reads as "start here".
|
|
{
|
|
const KitButtonBox box{toKitBox(cr.navBrowse)};
|
|
const InteractionState st = empty ? InteractionState::Active
|
|
: (isHovered(HoverKind::kNavBrowse, -1) ? InteractionState::Hover
|
|
: InteractionState::Rest);
|
|
drawButton(bmp, box, "Browse", st, /*warn=*/false);
|
|
}
|
|
|
|
// The rest of the run and the strip row draw only once a capture is loaded — with
|
|
// nothing picked there is no root, no preview and no channel decision to make.
|
|
if (empty) return;
|
|
|
|
// Preview-trigger button (fires the loaded capture at root through the live voice engine).
|
|
{
|
|
const KitButtonBox box{toKitBox(cr.preview)};
|
|
const InteractionState st = (previewingNote_ >= 0) ? InteractionState::Active
|
|
: (isHovered(HoverKind::kPreview, -1) ? InteractionState::Hover
|
|
: InteractionState::Rest);
|
|
drawButton(bmp, box, "Preview", st, /*warn=*/false);
|
|
}
|
|
|
|
// Preview velocity: a radial knob cell (the deck cell grammar), bound to the same
|
|
// persisted previewVelocity seam. Label swaps to the live value during hover/drag.
|
|
{
|
|
const bool dragging = (drag_ == DragKind::kDeckKnob && dragParamId_ == -2);
|
|
const bool hov = isHovered(HoverKind::kVelKnob, -1);
|
|
const InteractionState st = dragging ? InteractionState::Dragging
|
|
: (hov ? InteractionState::Hover
|
|
: InteractionState::Rest);
|
|
drawKnobFace(bmp, cr.velKnob, previewVelocity01(), st);
|
|
if (dragging || hov) {
|
|
char buf[8];
|
|
snprintf(buf, sizeof(buf), "%d",
|
|
static_cast<int>(previewVelocity01() * 127.0 + 0.5));
|
|
kitTextCentered(bmp, cr.velLabel, buf, Font::Micro, Role::TextDim);
|
|
} else {
|
|
kitTextCentered(bmp, cr.velLabel, "Vel", Font::Micro, Role::TextDim);
|
|
}
|
|
}
|
|
|
|
// The mini curve-preview button: opens the popup editor.
|
|
paintCurveButton(bmp, cr.curveBtn);
|
|
|
|
// Mono | Stereo output-mode toggle.
|
|
{
|
|
const bool isStereo = (channelMode_ == ChannelMode::Stereo);
|
|
const InteractionState monoState = !isStereo ? InteractionState::Active
|
|
: (isHovered(HoverKind::kChanMono, -1) ? InteractionState::Hover
|
|
: InteractionState::Rest);
|
|
const InteractionState stereoState = isStereo ? InteractionState::Active
|
|
: (isHovered(HoverKind::kChanStereo, -1) ? InteractionState::Hover
|
|
: InteractionState::Rest);
|
|
fillSurface(bmp, toKitBox(cr.chanMono), Role::BgCell, monoState);
|
|
fillSurface(bmp, toKitBox(cr.chanStereo), Role::BgCell, stereoState);
|
|
kitTextCentered(bmp, cr.chanMono, "Mono", kToolbarFont,
|
|
!isStereo ? Role::BgBase : Role::TextPrimary);
|
|
kitTextCentered(bmp, cr.chanStereo, "Stereo", kToolbarFont,
|
|
isStereo ? Role::BgBase : Role::TextPrimary);
|
|
}
|
|
|
|
// The strip row: the full 128-key piano with the root lit. The loaded capture responds
|
|
// across the whole strip, repitched from that root.
|
|
if (cr.controls.empty() || cr.rootStrip.empty()) return;
|
|
fillSurface(bmp, toKitBox(cr.controls), Role::BgPanel, InteractionState::Rest);
|
|
const StripLayout sl = layoutStrip(cr.rootStrip.width, cr.rootStrip.height);
|
|
// Same staleness guard as the tooltip: a latched hover note outlives a drag it started.
|
|
const int hoverNote = (drag_ == DragKind::kNone && hover_.kind == HoverKind::kStripKey)
|
|
? hover_.index : -1;
|
|
drawKeyboard(bmp, cr.rootStrip, sl, hoverNote);
|
|
drawRootKey(bmp, cr.rootStrip, sl, effectiveRoot());
|
|
}
|
|
|
|
// Drawn after every band so the chip is never painted over. No hover delay: the strip is a
|
|
// continuous readout you sweep, and a delay there reads as a dead surface — unlike the bank
|
|
// panel's buttons, where the delay stops tooltips firing on every traverse.
|
|
void ReaSamplerEditor::paintChromeTooltip(LICE_IBitmap* bmp, const FaceLayout& fl, int w,
|
|
int h) {
|
|
if (hover_.kind != HoverKind::kStripKey || hover_.index < 0) return;
|
|
// Hover is deliberately not re-resolved mid-drag, so the latched note would go stale
|
|
// under a root drag — the root badge is the live readout there.
|
|
if (drag_ != DragKind::kNone) return;
|
|
const Rect& area = fl.chrome.rootStrip;
|
|
if (area.empty()) return;
|
|
const StripLayout sl = layoutStrip(area.width, area.height);
|
|
const Rect key = keyRect(sl, hover_.index);
|
|
if (key.empty()) return;
|
|
|
|
const std::string label = noteName(hover_.index);
|
|
const int textW = static_cast<int>(label.size()) * kTooltipCharPx;
|
|
// Anchor y/h to the whole strip row, not the hovered key: a black key (18px) is shorter
|
|
// than a white key (30px), and anchoring to the key rect placed the chip 12px higher for
|
|
// black keys — landing on the keyboard's bottom edge and, near the root, on the badge.
|
|
const TooltipBox tb = computeTooltip(area.x + key.x, area.y, key.width, area.height,
|
|
textW, kTooltipTextH, w, h, TooltipSpec{});
|
|
if (tb.empty()) return;
|
|
|
|
const Rect box = Rect::ltrb(tb.x, tb.y, tb.x + tb.width, tb.y + tb.height);
|
|
fillSurface(bmp, toKitBox(box), Role::BgCell, InteractionState::Hover);
|
|
LICE_DrawRect(bmp, box.x, box.y, box.width - 1, box.height - 1,
|
|
toLice(roleColor(Role::LineHairline)), 1.0f, 0);
|
|
kitTextCentered(bmp, box, label.c_str(), Font::Label, Role::TextPrimary);
|
|
}
|
|
|
|
} // namespace reasampler::vst
|
|
|
|
#endif // _WIN32
|