// editor_internal.h — shared helpers for the ReaSamplerEditor TU family. Included ONLY by // the editor's own shell TUs (editor_session / editor_controls / editor_paint_* / // editor_input_* / editor_platform) — never a public seam. Holds the Rect<->kit adapters, // small draw primitives (knob face / title band), label helpers, and the velocity-curve box // derivation. All inline. #pragma once #include #include #include #include #include "core/instrument/engine/velocity_curve.h" // VelocityCurve::Box (curveBoxFromRect) #include "core/instrument/map/sample_map.h" // SampleChoice / SampleRefs (sampleLabel) #include "core/instrument/ui/editor_geometry.h" // Rect (the shared sub-rect type) #include "core/instrument/ui/keyboard_strip.h" // noteName (the one note-naming source) #ifdef _WIN32 #include "wdltypes.h" #include "lice/lice.h" #include "core/audio/peaks.h" // Envelope (drawEnvelope) #include "core/instrument/ui/capture_browser.h" // BrowserLayout / cardThumbnailRect (thumbBins) #include "core/instrument/ui/param_slider.h" // KnobGeometry / KnobArc (drawKnobFace) #include "core/ui/component_geometry.h" // KitBox / waveformColumnCount #include "core/ui/theme.h" // Role / InteractionState / KitColor / spectralColor #include "shell/panel/draw_kit.h" // the L1 draw kit: fillSurface/text/drawWaveform/toLice #endif namespace reasampler::vst { // Velocity-curve editor box metrics. The inset keeps node handles + the pick radius // inside the border so an endpoint at amp 0/1 stays grabbable; drag-off beyond // box+margin deletes the dragged node. inline constexpr int kVelCurveInset = 14; inline constexpr int kCurveDragOffMargin = 24; // The pure-module mapping Box for a drawn curve rect. Every consumer (paint, hit-test, // add, drag) derives it through this ONE formula, so drawn nodes and grabs never drift. inline instrument::engine::VelocityCurve::Box curveBoxFromRect( const instrument::ui::Rect& r) { return instrument::engine::VelocityCurve::Box{ r.x + kVelCurveInset, r.y + kVelCurveInset, (std::max)(0, r.width - 2 * kVelCurveInset), (std::max)(0, r.height - 2 * kVelCurveInset)}; } // A short MIDI-note label ("C4", "F#3"). The naming itself is the pure strip module's, so // a browser badge and a strip tooltip can never disagree about what a note is called. inline std::string noteLabel(int note) { return instrument::ui::noteName(note); } // A display name for a bank sample id: the snapshotted bank list first, then the // instance-owned ref's displayName (survives with the extension absent). "?" if neither // source knows the id. inline std::string sampleLabel(const std::vector& samples, const instrument::map::SampleRefs& refs, const std::string& id) { for (const instrument::map::SampleChoice& c : samples) { if (c.id == id) return c.displayName.empty() ? c.id : c.displayName; } for (const instrument::map::SampleRefEntry& e : refs) { if (e.sampleId == id && !e.displayName.empty()) return e.displayName; } return "?"; } #ifdef _WIN32 // The editor's own sub-rect type is `Rect` (editor_geometry); the kit draws against // `KitBox` (component_geometry). This is the single boundary that bridges them so every // draw routes through the shared kit (theme roles + draw_kit). inline ui::KitBox toKitBox(const instrument::ui::Rect& r) { return ui::KitBox{r.x, r.y, r.width, r.height}; } // Kit text in a palette ROLE (the common case). Left/Right/Center via Align. inline void kitText(LICE_IBitmap* bmp, const instrument::ui::Rect& r, const char* s, Font font, ui::Role role, Align align = Align::Left) { text(bmp, toKitBox(r), s, font, role, align); } inline void kitTextCentered(LICE_IBitmap* bmp, const instrument::ui::Rect& r, const char* s, Font font, ui::Role role) { text(bmp, toKitBox(r), s, font, role, Align::Center); } // Draw a peak envelope in `r` through the kit's shared waveform primitive. inline void drawEnvelope(LICE_IBitmap* bmp, const instrument::ui::Rect& r, const audio::Envelope& env) { drawWaveform(bmp, toKitBox(r), env); } // The bin count a card's thumbnail is computed at: one bin per drawn pixel column (the // gap-free render comes from peaks::columnMinMax's exact partition). inline int thumbBins(const instrument::ui::BrowserLayout& layout) { return (std::max)(1, kWaveformOversample * ui::waveformColumnCount(toKitBox( instrument::ui::cardThumbnailRect(layout, 0)))); } // Draws the title band with the live readout. The Browse modal draws its own back button in // place of the nav. inline void drawTitleBand(LICE_IBitmap* bmp, const instrument::ui::Rect& title, const std::string& readout) { fillSurface(bmp, toKitBox(title), ui::Role::BgPanel, ui::InteractionState::Rest); instrument::ui::Rect titleText = instrument::ui::Rect::ltrb(title.x + 8, title.y, title.right() - 8, title.bottom()); kitText(bmp, titleText, readout.c_str(), Font::Title, ui::Role::TextPrimary); } // Stroke widths for the radial faces. The value arc is drawn as adjacent 1px AA arcs rather // than one thick primitive — LICE has no thick-arc call, and stacking radii is what keeps every // ring antialiased. inline constexpr int kKnobValueArcPx = 3; inline constexpr int kInnerDialArcPx = 2; inline constexpr int kKnobNeedlePx = 2; // Draws one radial knob face: param_slider owns the value<->angle map; this turns it into // LICE calls. LICE takes radians, and drawing the 7->5 o'clock sweep through the top needs // a continuous angle span, so degrees convert as (deg - 360) * pi/180, mapping 210..510 // onto -150..+150 degrees. inline void drawKnobFace(LICE_IBitmap* bmp, const instrument::ui::Rect& knobRect, double value01, ui::InteractionState st) { using instrument::ui::KnobArc; using instrument::ui::KnobGeometry; using instrument::ui::KnobPoint; const KnobGeometry kg = instrument::ui::computeKnob(knobRect); if (kg.radius <= 1.0) return; constexpr double kDegToRad = 3.14159265358979323846 / 180.0; const KnobArc arc{}; // the default 7->5 o'clock sweep const float cx = static_cast(kg.centerX); const float cy = static_cast(kg.centerY); const float rOuter = static_cast(kg.radius) - 0.5f; const bool disabled = (st == ui::InteractionState::Disabled); const bool hot = (st == ui::InteractionState::Dragging || st == ui::InteractionState::Hover); LICE_FillCircle(bmp, cx, cy, rOuter - 1.f, toLice(ui::roleColorState(ui::Role::BgCell, st)), 1.0f, 0, true); // Track: the full sweep as a hairline arc (the dead 60-degree arc at the bottom stays bare). const float a0 = static_cast((arc.startDeg - 360.0) * kDegToRad); const float a1 = static_cast( (arc.startDeg + instrument::ui::knobSweepDeg(arc) - 360.0) * kDegToRad); LICE_Arc(bmp, cx, cy, rOuter, a0, a1, toLice(ui::roleColor(ui::Role::LineHairline)), 1.0f, 0, true); const double v = value01 < 0.0 ? 0.0 : (value01 > 1.0 ? 1.0 : value01); if (v > 0.0) { const float av = static_cast( (arc.startDeg + v * instrument::ui::knobSweepDeg(arc) - 360.0) * kDegToRad); const ui::Role valueRole = disabled ? ui::Role::TextDim : (hot ? ui::Role::AccentHot : ui::Role::AccentPrimary); const LICE_pixel valueCol = toLice(ui::roleColor(valueRole)); for (int i = 0; i < kKnobValueArcPx; ++i) { LICE_Arc(bmp, cx, cy, rOuter - static_cast(i), a0, av, valueCol, 1.0f, 0, true); } } // Needle: from ~35% radius out to the rim at the value's angle. ThickFLine keeps the float // endpoints AND is always antialiased, so the needle is smooth at every angle. const KnobPoint tip = instrument::ui::knobNeedlePoint(kg, arc, v); const double ix = kg.centerX + (tip.x - kg.centerX) * 0.35; const double iy = kg.centerY + (tip.y - kg.centerY) * 0.35; const ui::Role needleRole = disabled ? ui::Role::TextDim : ui::Role::TextPrimary; LICE_ThickFLine(bmp, ix, iy, tip.x, tip.y, toLice(ui::roleColor(needleRole)), 1.0f, 0, kKnobNeedlePx); } // The concentric INNER dial: a second value on the same cell, drawn in the categorical // tertiary accent so it reads as a different KIND of control rather than a louder one — the // same purple the overlay traces the envelope in (curve_law.h owns the knot/dial pairing this // ties into). Shares the outer knob's value<->angle map (param_slider's), so both needles // point the same way for the same normalized value. inline void drawInnerDial(LICE_IBitmap* bmp, const instrument::ui::Rect& innerRect, double value01, ui::InteractionState st) { using instrument::ui::KnobArc; using instrument::ui::KnobGeometry; using instrument::ui::KnobPoint; const KnobGeometry kg = instrument::ui::computeKnob(innerRect); if (kg.radius <= 1.0) return; constexpr double kDegToRad = 3.14159265358979323846 / 180.0; const KnobArc arc{}; const float cx = static_cast(kg.centerX); const float cy = static_cast(kg.centerY); const float r = static_cast(kg.radius) - 0.5f; const bool disabled = (st == ui::InteractionState::Disabled); const bool hot = (st == ui::InteractionState::Dragging || st == ui::InteractionState::Hover); LICE_FillCircle(bmp, cx, cy, r - 1.f, toLice(ui::roleColorState(ui::Role::BgPanel, st)), 1.0f, 0, true); const double v = value01 < 0.0 ? 0.0 : (value01 > 1.0 ? 1.0 : value01); const float a0 = static_cast((arc.startDeg - 360.0) * kDegToRad); const float av = static_cast( (arc.startDeg + v * instrument::ui::knobSweepDeg(arc) - 360.0) * kDegToRad); const ui::Role arcRole = disabled ? ui::Role::TextDim : (hot ? ui::Role::AccentHot : ui::Role::AccentTertiary); const LICE_pixel arcCol = toLice(ui::roleColor(arcRole)); for (int i = 0; i < kInnerDialArcPx; ++i) { LICE_Arc(bmp, cx, cy, r - static_cast(i), a0, av, arcCol, 1.0f, 0, true); } const KnobPoint tip = instrument::ui::knobNeedlePoint(kg, arc, v); LICE_FLine(bmp, static_cast(kg.centerX), static_cast(kg.centerY), static_cast(tip.x), static_cast(tip.y), toLice(ui::roleColor(disabled ? ui::Role::TextDim : ui::Role::AccentTertiary)), 1.0f, 0, true); } #endif // _WIN32 } // namespace reasampler::vst