// 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 #include #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(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(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(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