// editor_paint_sample.cpp — the ReaSamplerEditor's SAMPLE-FACE painting (Q-W2v split of // reasampler_editor.cpp, T4-11): the WM_PAINT dispatch, the r11 Sample home face (title // band + elastic hero waveform + root/preview cluster + bottom-anchored knob deck), the // S-VIEW-3 envelope overlay, the velocity-curve editor + mini preview button + popup // sheet (shared painters the Zone surface reuses, FB2), and the empty state. Windows-only // (D5); draws through the L1 kit by palette role. All layout math is pure // (editor_geometry / knob_deck / curve_popup) — this TU only draws. #include "shell/instrument/reasampler_editor.h" #ifdef _WIN32 #include #include #include #include #include #include "core/audio/peaks.h" // computeEnvelope (hero waveform binning) #include "core/instrument/ui/curve_popup.h" // r11 centered curve-popup sheet geometry (FB1) #include "core/instrument/ui/knob_deck.h" // deck layout + kDeckKnobSize #include "core/instrument/ui/waveform_view.h" // frameToX (S11 markers) #include "core/version/app_version.h" // vstPluginName (channel-derived title band, S18) #include "shell/instrument/editor_internal.h" // kit adapters + knob face/spectral strip/root marker #include "shell/instrument/reasampler_processor.h" namespace reasampler::vst { using namespace reasampler::ui; // kit vocabulary (Role / InteractionState / KitBox / …) using namespace reasampler::instrument::ui; // pure geometry (bands / cluster / deck / popup / strip) using namespace reasampler::instrument::map; // SampleRefs / findRef (title readout fallback) using audio::computeEnvelope; namespace { // Marker roles (Phase L, L3) — semantic, drawn through the kit's palette: start = teal // (secondary), loop start/end = purple (tertiary). The loop-span fill is a faint purple. constexpr Role kRoleStartMarker = Role::AccentSecondary; constexpr Role kRoleLoopMarker = Role::AccentTertiary; } // namespace void ReaSamplerEditor::paint(HDC hdc) { RECT cr{}; GetClientRect(childHwnd_, &cr); const int w = cr.right - cr.left; const int h = cr.bottom - cr.top; if (w <= 0 || h <= 0) return; LICE_SysBitmap bmp(w, h); LICE_Clear(&bmp, toLice(roleColor(Role::BgBase))); // S-VIEW-1 three-view dispatch. Sample is home; Browse is a full-window modal overlay drawn // OVER Sample; Zone is the dedicated surface. In the Browse view we draw Sample first so the // modal reads as a sheet layered over the home face (the "picker over the document" grammar). if (view_ == View::kZone) { paintZone(&bmp, w, h); } else { paintSample(&bmp, w, h); if (view_ == View::kBrowse) paintBrowse(&bmp, w, h); } // S13 (relay degraded): a transient banner flashed after a file was dropped ON THIS window. // It reiterates the shipped ingest gesture rather than swallowing the drop silently. Drawn // LAST so it overlays whatever view is up; decays via onSyncTimer (dropHintTicks_). if (dropHintTicks_ > 0) { const int bannerTop = (std::min)(kTitleHeight, h); const int bannerH = (std::min)(kTitleHeight + 8, (std::max)(0, h - bannerTop)); Rect banner = Rect::ltrb(0, bannerTop, w, bannerTop + bannerH); // A transient notice, not the live layer — draw it on the accent-tertiary categorical // hue with a dark label so it reads as "attention, not action". fillSurface(&bmp, toKitBox(banner), Role::AccentTertiary, InteractionState::Rest); kitTextCentered(&bmp, banner, "Dropped here isn't loaded yet - drop files onto the ReaSampler bank panel to add them.", Font::Label, Role::BgBase); } BitBlt(hdc, 0, 0, w, h, bmp.getDC(), 0, 0, SRCCOPY); } void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) { // r11: the deck height comes from the pure knob_deck wrap (mode-independent — the AMP // ENVELOPE group reserves its 5-cell Gate width, so Gate<->Trigger never changes it). const PerformanceZone deckZone = effectiveSampleZone(); const std::vector deckDescs = deckGroupDescs(deckZone.play); const SampleBands bands = computeSampleBands(w, h, deckHeight(deckDescs, w - 2 * kPad)); // Title: product name + live readout. Standard B palette — the beta channel gets NO distinct // accent (settled 2026-07-27); the channel-derived vstPluginName is the only beta-vs-stable // signal. std::string title = version::vstPluginName(); // channel-derived (S18) if (processor_ && processor_->bridge().isConnected()) { // The instance's OWN loaded state outranks bank availability (pS: 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 (!map_.zones.empty()) title += " [" + std::to_string(map_.zones.size()) + " zone(s)]"; else 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]"; } drawTitleBand(bmp, bands.title, title); // Browse + Zone nav buttons (right of the title). Browse is the picker; Zone opens the keymap // surface. When nothing is loaded, Browse is the empty state's dominant call-to-action — draw // it Active (accent-primary) so it reads as "start here". const bool empty = selectedId_.empty() && map_.zones.empty(); { const KitButtonBox box{toKitBox(bands.navBrowse)}; const InteractionState st = empty ? InteractionState::Active : (isHovered(HoverKind::kNavBrowse, -1) ? InteractionState::Hover : InteractionState::Rest); drawButton(bmp, box, "Browse", st, /*warn=*/false); } { const KitButtonBox box{toKitBox(bands.navZone)}; const InteractionState st = isHovered(HoverKind::kNavZone, -1) ? InteractionState::Hover : InteractionState::Rest; drawButton(bmp, box, "Zone", st, /*warn=*/false); } // Nothing loaded yet: the Sample face is the empty state — a "pick a capture" prompt pointing // at Browse (which is lit above). No hero waveform / controls to draw. if (empty) { Rect body = Rect::ltrb(bands.hero.x, bands.hero.y, bands.hero.right(), bands.deck.bottom()); paintEmptyState(bmp, body); return; } // Resolve the effective single-capture zone: the picked id's one-zone override when present, // else the product-default play params (S15-F2 — the single capture is a one-zone map). This // is the ONE storage site both Sample and Zone edit. const PerformanceZone& zone = deckZone; // --- Hero waveform band: envelope + S11 markers + S-VIEW-3 envelope overlay ----------- const std::vector& pcm = monoPcmFor(selectedId_); const std::int64_t frames = static_cast(pcm.size()); const Rect waveArea = bands.hero; fillSurface(bmp, toKitBox(waveArea), Role::BgBase, InteractionState::Rest); if (frames > 0 && waveArea.width > 0) { // FA3 gap-free: one bin per drawn pixel column (kWaveformOversample == 1, so this // multiplies by 1). The gap-free draw comes from peaks::columnMinMax's exact // partition — extra bins produce no visible change. Clamped to frame count below. const std::int64_t wantBins = static_cast((std::max)(1, waveformColumnCount(toKitBox(waveArea)))) * kWaveformOversample; const std::size_t bins = static_cast(wantBins < frames ? wantBins : frames); const Envelope env = computeEnvelope(pcm, 1, pcm.size(), bins); drawEnvelope(bmp, waveArea, env); const SetupMarkers m = pickedMarkers(frames); if (m.hasLoop && m.loopEnd > m.loopStart) { const int lx = frameToX(waveArea, frames, m.loopStart); const int rx = frameToX(waveArea, frames, m.loopEnd); if (rx > lx) { LICE_FillRect(bmp, lx, waveArea.y, rx - lx, waveArea.height, toLice(roleColor(kRoleLoopMarker)), 0.20f, 0); } } const std::int64_t markerFrames[3] = {m.start, m.loopStart, m.loopEnd}; const Role markerRoles[3] = {kRoleStartMarker, kRoleLoopMarker, kRoleLoopMarker}; for (int i = 0; i < 3; ++i) { const int mx = frameToX(waveArea, frames, markerFrames[i]); const bool loopMarker = (i != 0); const float alpha = (loopMarker && !m.hasLoop) ? 0.4f : 1.0f; LICE_FillRect(bmp, mx - 1, waveArea.y, 2, waveArea.height, toLice(roleColor(markerRoles[i])), alpha, 0); } // S-VIEW-3: trace the amp-envelope overlay + its draggable node handles over the hero. paintEnvelopeOverlay(bmp, waveArea, zone, frames); } else { kitTextCentered(bmp, waveArea, "(decoding...)", Font::Label, Role::TextDim); } // --- Root + preview cluster (r11: remainder-width root strip, preview button, radial // velocity knob, mini curve-preview button, channel toggle) ----------------------------- fillSurface(bmp, toKitBox(bands.cluster), Role::BgPanel, InteractionState::Rest); const ChannelToggleRects chan = channelToggleRects(bands.cluster); const ClusterRects cr = clusterRects(bands.cluster, chan.mono, kDeckKnobSize); int root = effectiveRoot(); if (cr.rootStrip.width > 0) { drawSpectralStrip(bmp, cr.rootStrip); const StripLayout sl = layoutStrip(cr.rootStrip.width, cr.rootStrip.height); drawRootMarker(bmp, cr.rootStrip, sl, root); } // 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 (r11 — 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 (r11): opens the popup editor. Shared painter with the // Zone panel's button (FB2 — one grammar on both surfaces). paintCurveButton(bmp, cr.curveBtn, zone); // 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(chan.mono), Role::BgCell, monoState); fillSurface(bmp, toKitBox(chan.stereo), Role::BgCell, stereoState); kitTextCentered(bmp, chan.mono, "Mono", Font::Label, !isStereo ? Role::BgBase : Role::TextPrimary); kitTextCentered(bmp, chan.stereo, "Stereo", Font::Label, isStereo ? Role::BgBase : Role::TextPrimary); } // --- The knob deck (r11: the fenced control groups, bottom-anchored) ------------------- paintKnobDeck(bmp, bands.deck, zone, deckDescs); // --- The curve popup (r11): a centered sheet over the whole Sample face, drawn LAST ---- if (curvePopupOpen_) paintCurvePopup(bmp, w, h); } void ReaSamplerEditor::paintEnvelopeOverlay(LICE_IBitmap* bmp, const Rect& waveArea, const PerformanceZone& zone, std::int64_t frames) { if (frames <= 0 || waveArea.width <= 0 || waveArea.height <= 0) return; const double rate = liveSampleRate(); if (rate <= 0.0) return; const double totalSeconds = static_cast(frames) / rate; const std::int64_t startFrame = zone.startPoint.value_or(0); const AmpEnvelope env = packEnvelope(zone.play, frames, startFrame); const std::vector poly = buildEnvelopePolyline(env, waveArea, totalSeconds); // Trace the polyline in the categorical secondary accent (teal) so it reads as a distinct // curve over the waveform. Clip x to the wave rect (a Gate release tail maps past the right). const LICE_pixel line = toLice(roleColor(Role::AccentSecondary)); for (std::size_t i = 1; i < poly.size(); ++i) { const int x0 = (std::max)(waveArea.x, (std::min)(waveArea.right() - 1, poly[i - 1].x)); const int x1 = (std::max)(waveArea.x, (std::min)(waveArea.right() - 1, poly[i].x)); LICE_Line(bmp, x0, poly[i - 1].y, x1, poly[i].y, line, 1.0f, 0, true); } // Draggable node handles: a small square per DRAGGABLE node (Origin + ReleaseStart are draw- // only). Lit accent-hot when this node is the grabbed one. FA2 guarantees every vertex is // in-bounds (the pre-FA2 right-edge clip is dead and removed — edge nodes like ReleaseEnd // at area.right()-1 MUST get handles); the handle SQUARE is additionally clamped inside the // hero rect so a 6px box on an edge node never overhangs into the neighbouring bands. const LICE_pixel handle = toLice(roleColor(Role::AccentPrimary)); const LICE_pixel handleHot = toLice(roleColor(Role::AccentHot)); for (const EnvVertex& v : poly) { if (v.node == EnvNode::Origin || v.node == EnvNode::ReleaseStart) continue; const bool grabbed = (drag_ == DragKind::kEnvNode && envNode_ == v.node); const int r = 3; const int hx = (std::max)(waveArea.x + r, (std::min)(waveArea.right() - 1 - r, v.x)); const int hy = (std::max)(waveArea.y + r, (std::min)(waveArea.bottom() - 1 - r, v.y)); LICE_FillRect(bmp, hx - r, hy - r, 2 * r, 2 * r, grabbed ? handleHot : handle, 1.0f, 0); } } void ReaSamplerEditor::paintVelocityCurve(LICE_IBitmap* bmp, const Rect& r, const PerformanceZone& zone) { if (r.width <= 0 || r.height <= 0) return; // defensive (degenerate rect) // The bordered box: a panel surface + hairline border, drawn by palette role. No corner // caption — the popup sheet's own "VELOCITY -> AMP" title labels this context (FB2: the // popup is the only host). fillSurface(bmp, toKitBox(r), Role::BgPanel, InteractionState::Rest); LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0); const VelocityCurve::Box box = curveBoxFromRect(r); if (box.width <= 0 || box.height <= 1) return; const VelocityCurve& curve = zone.velocityCurve; // Trace the monotone spline — ONE eval per x column over the mapping box, in the categorical // secondary accent (the same grammar as the envelope trace over the hero). The x -> velocity // and amp -> y mappings both go through the pure module so the trace, the node handles, and // the hit-test all share one coordinate system. const LICE_pixel line = toLice(roleColor(Role::AccentSecondary)); int prevX = 0, prevY = 0; for (int px = 0; px <= box.width; ++px) { const int cx = box.left + px; const double vel = VelocityCurve::pointFromPixel(box, cx, box.top).velocity; const int cy = VelocityCurve::pixelFromPoint(box, {vel, curve.eval(vel)}).y; if (px > 0) LICE_Line(bmp, prevX, prevY, cx, cy, line, 1.0f, 0, true); prevX = cx; prevY = cy; } // Draggable node handles (mirror of the envelope overlay's): accent-primary squares lifted // to accent-hot when grabbed or hovered, or warn when a drag-off delete is armed (cursor // has passed kCurveDragOffMargin outside the box — release will delete the node). const LICE_pixel handle = toLice(roleColor(Role::AccentPrimary)); const LICE_pixel handleHot = toLice(roleColor(Role::AccentHot)); const LICE_pixel handleWarn = toLice(roleColor(Role::Warn)); // Drag-off check: during a kCurveNode drag on THIS box, is the live cursor beyond the margin? const bool dragOffArmed = (drag_ == DragKind::kCurveNode && dragCurveRect_.x == r.x && dragCurveRect_.y == r.y) && (dragCurX_ < r.x - kCurveDragOffMargin || dragCurX_ > r.right() + kCurveDragOffMargin || dragCurY_ < r.y - kCurveDragOffMargin || dragCurY_ > r.bottom() + kCurveDragOffMargin); for (std::size_t i = 0; i < curve.points().size(); ++i) { const auto np = VelocityCurve::pixelFromPoint(box, curve.points()[i]); const bool grabbed = (drag_ == DragKind::kCurveNode && curvePointIndex_ == static_cast(i)); const bool hot = grabbed || isHovered(HoverKind::kCurveNode, static_cast(i)); // A grabbed node in drag-off territory draws warn to signal "release will delete." const LICE_pixel col = (grabbed && dragOffArmed) ? handleWarn : (hot ? handleHot : handle); const int nr = 3; LICE_FillRect(bmp, np.x - nr, np.y - nr, 2 * nr, 2 * nr, col, 1.0f, 0); } } void ReaSamplerEditor::paintKnobDeck(LICE_IBitmap* bmp, const Rect& deckArea, const PerformanceZone& zone, const std::vector& descs) { if (deckArea.width <= 0 || deckArea.height <= 0) return; const DeckLayout dl = layoutDeck(descs, deckArea.x, deckArea.y, deckArea.width); const ZonePlaySeconds& play = zone.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. const auto drawToggle = [&](const DeckToggleLayout& t, const char* s0, const char* s1, bool seg1Active, bool disabled) { const bool hov = !disabled && isHovered(HoverKind::kControl, t.id); const InteractionState st0 = disabled ? 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, disabled ? Role::TextDim : (!seg1Active ? Role::BgBase : Role::TextPrimary)); kitTextCentered(bmp, t.seg1, s1, Font::Micro, disabled ? Role::TextDim : (seg1Active ? Role::BgBase : Role::TextPrimary)); }; // The knob's short name label (swapped for the live value during hover/drag — r11: 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::kTrigFadeIn: return "Fade In"; case ParamControl::kTrigLength: return "Len %"; case ParamControl::kTrigFadeOut: return "Fade Out"; case ParamControl::kKeyTrack: return "Key Trk"; case ParamControl::kPitchEnvAttack: return "P.Att"; case ParamControl::kPitchEnvDecay: return "P.Dec"; case ParamControl::kPitchEnvDepth: return "P.Depth"; case ParamControl::kVoiceCount: return "Voices"; case ParamControl::kMasterGain: return "Gain"; 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"; break; case kGroupPitchEnv: caption = "PITCH ENV"; break; case kGroupVoice: caption = "VOICE"; break; case kGroupMaster: caption = "MASTER"; break; default: break; } kitText(bmp, g.caption, caption, Font::Micro, Role::TextDim); // The compact caption toggle (r11: right-anchored IN the caption row, never full-width). if (g.captionToggle.id >= 0) { switch (static_cast(g.captionToggle.id)) { case ParamControl::kPlayMode: drawToggle(g.captionToggle, "Gate", "Trigger", play.playMode == PlayMode::Trigger, false); break; case ParamControl::kPitchEngine: drawToggle(g.captionToggle, "Varisp", "Presrv", play.pitchEngine == PitchEngine::Preserve, false); break; case ParamControl::kPitchEnvEnable: drawToggle(g.captionToggle, "Off", "On", play.pitchEnv.enabled, false); break; case ParamControl::kVoiceMode: drawToggle(g.captionToggle, "Poly", "Mono", isMono, false); break; default: break; } } // The row toggle (VOICE group's Retrig|Legato) — live only in Mono. if (g.rowToggle.id >= 0) { drawToggle(g.rowToggle, "Retrig", "Legato", monoTrigger_ == MonoTrigger::Legato, !isMono); } // The knobs. PITCH ENV knobs draw Disabled (not hidden) while the envelope is off — // stable geometry (r11). for (const DeckCellLayout& c : g.cells) { if (c.id < 0) continue; // reserved blank cell (the Trigger face's two spares) const bool disabled = (g.id == kGroupPitchEnv && !play.pitchEnv.enabled); 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, zone), st); const std::string label = (dragging || hov) ? deckValueLabel(c.id, zone) : std::string(knobName(static_cast(c.id))); kitTextCentered(bmp, c.label, label.c_str(), Font::Micro, Role::TextDim); } } } void ReaSamplerEditor::paintCurveButton(LICE_IBitmap* bmp, const Rect& r, const PerformanceZone& zone) { if (r.width <= 0 || r.height <= 0) return; // The mini curve-preview button (r11/FB2 — shared by the Sample cluster and the Zone // panel): a hairline-bordered bg/cell square with the zone's live velocity curve traced // in miniature (no node markers at this scale). Hover lifts it; it draws ACTIVE // (accent-primary border) while its popup is open, and re-renders live as the popup // edits the curve (same zone, re-read each paint). const bool hov = isHovered(HoverKind::kCurveButton, -1); fillSurface(bmp, toKitBox(r), Role::BgCell, hov ? InteractionState::Hover : InteractionState::Rest); const KitColor border = curvePopupOpen_ ? roleColor(Role::AccentPrimary) : roleColor(Role::LineHairline); LICE_DrawRect(bmp, r.x, r.y, r.width - 1, r.height - 1, toLice(border), 1.0f, 0); const VelocityCurve& curve = zone.velocityCurve; const int inset = 3; const VelocityCurve::Box mini{r.x + inset, r.y + inset, r.width - 2 * inset, r.height - 2 * inset}; if (mini.width > 1 && mini.height > 1) { const LICE_pixel trace = toLice(roleColor(Role::AccentSecondary)); int prevX = 0, prevY = 0; for (int px = 0; px <= mini.width; ++px) { const int mx = mini.left + px; const double vel = VelocityCurve::pointFromPixel(mini, mx, mini.top).velocity; const int my = VelocityCurve::pixelFromPoint(mini, {vel, curve.eval(vel)}).y; if (px > 0) LICE_Line(bmp, prevX, prevY, mx, my, trace, 1.0f, 0, true); prevX = mx; prevY = my; } } } void ReaSamplerEditor::paintCurvePopup(LICE_IBitmap* bmp, int w, int h) { // The 0.50-alpha bg/base wash (lighter than Browse's 0.82 — a focused sub-editor; the // Sample face stays legible behind it), then the centered sheet. LICE_FillRect(bmp, 0, 0, w, h, toLice(roleColor(Role::BgBase)), 0.50f, 0); const CurvePopupLayout pl = computeCurvePopup(w, h); fillSurface(bmp, toKitBox(pl.sheet), Role::BgPanel, InteractionState::Rest); LICE_DrawRect(bmp, pl.sheet.x, pl.sheet.y, pl.sheet.width - 1, pl.sheet.height - 1, toLice(roleColor(Role::LineHairline)), 1.0f, 0); kitText(bmp, pl.title, "VELOCITY -> AMP", Font::Micro, Role::TextDim); { const KitButtonBox box{toKitBox(pl.close)}; const InteractionState st = isHovered(HoverKind::kPopupClose, -1) ? InteractionState::Hover : InteractionState::Rest; drawButton(bmp, box, "x", st, /*warn=*/false); } // The full-size editor: ONE draw path + the one curveBoxFromRect mapping formula, so // trace/handles/drag-off cues cannot drift between hosts. The popup edits popupZone() — // the picked capture's one-zone site on the Sample face, the selected zone on the Zone // surface (FB2). paintVelocityCurve(bmp, pl.curveBox, popupZone()); } void ReaSamplerEditor::paintEmptyState(LICE_IBitmap* bmp, const Rect& area) { // Shown when no card is drawn (nothing to pick): distinguish a genuinely empty bank from // a bank filter that hides everything. Either way it is the "pick a capture" empty state. const char* msg = samples_.empty() ? "No captures in this project yet - capture audio into the bank to play it here." : "No captures in this bank filter. Choose another bank tab above."; // Split the area so the primary line sits centered and the S13 ingest affordance sits just // below it. The affordance is the SHIPPED ingest gesture (drop onto the docked panel) — kept // discoverable here regardless of whether a drop ever lands on THIS window. Rect primary = Rect::ltrb(area.x, area.y, area.right(), area.y + area.height / 2); Rect hint = Rect::ltrb(area.x, primary.bottom(), area.right(), area.bottom()); kitTextCentered(bmp, primary, msg, Font::Label, Role::TextDim); kitTextCentered(bmp, hint, "To add a sample: drop a file onto the ReaSampler bank panel (the docked window).", Font::Micro, Role::TextDim); } } // namespace reasampler::vst #endif // _WIN32