// editor_paint_chrome.cpp — the CHROME band's painter: the toolbar row (product title + // live readout, then the control run — preview, preview-velocity knob, 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/note/musical_division.h" // divisionLabel (the Hold readout) #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]"; } // A bake outcome outranks the identity readout while it lasts: the click's only other // feedback is the sound itself, which is by design indistinguishable from before. if (bakeMessageTicks_ > 0 && !bakeMessage_.empty()) title = bakeMessage_; 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; // Resample bake. Disabled without the extension: the bank is the extension's surface, // so with it absent there is no writer and the control must read unavailable rather // than accept a click it cannot honour. { const KitButtonBox box{toKitBox(cr.bake)}; const InteractionState st = !bakeAvailable_ ? InteractionState::Disabled : (bakePending_ ? InteractionState::Active : (isHovered(HoverKind::kBake, -1) ? InteractionState::Hover : InteractionState::Rest)); drawButton(bmp, box, "Bake", st, /*warn=*/false); } // Bake Hold: the note length the bake holds the gate for. Drawn ONLY while the window // cannot be derived without it — every other dialed sound bakes with no user input, and a // control that did nothing there would read as one that did. if (bakeHoldNeeded_) { const bool dragging = (drag_ == DragKind::kDeckKnob && dragParamId_ == kBakeHoldKnobId); const bool hov = isHovered(HoverKind::kHoldKnob, -1); const InteractionState st = dragging ? InteractionState::Dragging : (hov ? InteractionState::Hover : InteractionState::Rest); drawKnobFace(bmp, cr.holdKnob, bakeHoldNorm(), st); // "Bake Hold", not "Hold": the AMP deck's AHDSR Hold knob is on screen in the same // frame, and two knobs labelled the same are two knobs the user has to guess between. const std::string label = dragging || hov ? instrument::note::divisionLabel(params_.bakeHold) : std::string("Bake Hold"); kitTextCentered(bmp, cr.holdLabel, label.c_str(), Font::Micro, Role::TextDim); } // Preview-trigger button (fires the loaded capture at root through the live voice engine). // A drawn play triangle rather than a label or an embedded image: it inherits the button's // own foreground role, so it stays legible in every interaction state at no build cost. { const KitButtonBox box{toKitBox(cr.preview)}; const bool active = (previewingNote_ >= 0); const InteractionState st = active ? InteractionState::Active : (isHovered(HoverKind::kPreview, -1) ? InteractionState::Hover : InteractionState::Rest); drawButton(bmp, box, nullptr, st, /*warn=*/false); const PreviewGlyph g = previewGlyph(cr.preview); if (!g.empty()) { // Same surface as the button label, so the glyph tracks drawButton's own rule // (buttonLabelRole) rather than a second copy of it that could desync from the kit. const LICE_pixel ink = toLice(roleColor(buttonLabelRole(st))); LICE_FillTriangle(bmp, g.leftX, g.topY, g.leftX, g.bottomY, g.apexX, g.apexY, ink, 1.0f, 0); // LICE_FillTriangle takes no aa flag, so its two sloped edges come out stepped. // Re-stroke them in the same ink: an AA line over the fill's own boundary softens // the step without changing the shape. The vertical edge needs none. const float lx = static_cast(g.leftX); const float ax = static_cast(g.apexX); const float ay = static_cast(g.apexY); LICE_FLine(bmp, lx, static_cast(g.topY), ax, ay, ink, 1.0f, 0, true); LICE_FLine(bmp, lx, static_cast(g.bottomY), ax, ay, ink, 1.0f, 0, true); } } // 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); } } // 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