221 lines
12 KiB
C++
221 lines
12 KiB
C++
// 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 <algorithm>
|
|
#include <cstdio>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#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)
|
|
#include "core/instrument/ui/param_taper.h" // DragModifiers (the shared interaction law)
|
|
|
|
#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/instrument/editor_stroke.h" // strokeArcAA / strokePolylineAA
|
|
#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<instrument::map::SampleChoice>& 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 modifier read, for every drag surface and every gesture resolver. One helper so the editor
|
|
// cannot grow a second modifier grammar. GetKeyState rather than WM_MOUSEMOVE's wParam because a
|
|
// modifier can be pressed or released with the mouse standing still, and the re-anchor has to see
|
|
// that on the next move it does get.
|
|
inline instrument::ui::DragModifiers dragModifiers() {
|
|
return instrument::ui::DragModifiers{(GetKeyState(VK_SHIFT) & 0x8000) != 0,
|
|
(GetKeyState(VK_CONTROL) & 0x8000) != 0};
|
|
}
|
|
|
|
// 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. Every arc is ONE analytic stroke (editor_stroke.h) whose
|
|
// outer edge sits on the knob's radius, so the centerline is inset by half the width. Every
|
|
// width here is >= 2 px, the stroker's guaranteed-opaque-core floor (core/ui/CLAUDE.md) —
|
|
// anything narrower modulates peak alpha with pixel-grid alignment instead of pinning solid.
|
|
inline constexpr float kKnobTrackArcPx = 2.0f;
|
|
inline constexpr float kKnobValueArcPx = 3.0f;
|
|
inline constexpr float kInnerDialArcPx = 2.0f;
|
|
inline constexpr float kKnobNeedlePx = 2.0f;
|
|
inline constexpr float kInnerDialNeedlePx = 2.0f;
|
|
|
|
// 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<float>(kg.centerX);
|
|
const float cy = static_cast<float>(kg.centerY);
|
|
const float rOuter = static_cast<float>(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<float>((arc.startDeg - 360.0) * kDegToRad);
|
|
const float a1 = static_cast<float>(
|
|
(arc.startDeg + instrument::ui::knobSweepDeg(arc) - 360.0) * kDegToRad);
|
|
strokeArcAA(bmp, cx, cy, rOuter - kKnobTrackArcPx * 0.5f, a0, a1, kKnobTrackArcPx,
|
|
toLice(ui::roleColor(ui::Role::LineHairline)));
|
|
const double v = value01 < 0.0 ? 0.0 : (value01 > 1.0 ? 1.0 : value01);
|
|
if (v > 0.0) {
|
|
const float av = static_cast<float>(
|
|
(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);
|
|
strokeArcAA(bmp, cx, cy, rOuter - kKnobValueArcPx * 0.5f, a0, av, kKnobValueArcPx,
|
|
toLice(ui::roleColor(valueRole)));
|
|
}
|
|
// Needle: from ~35% radius out to the rim at the value's angle, stroked through the same
|
|
// analytic path as the arcs so it holds its weight at every knob position.
|
|
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;
|
|
strokeLineAA(bmp, static_cast<float>(ix), static_cast<float>(iy),
|
|
static_cast<float>(tip.x), static_cast<float>(tip.y), kKnobNeedlePx,
|
|
toLice(ui::roleColor(needleRole)));
|
|
}
|
|
|
|
// 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<float>(kg.centerX);
|
|
const float cy = static_cast<float>(kg.centerY);
|
|
const float r = static_cast<float>(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<float>((arc.startDeg - 360.0) * kDegToRad);
|
|
const float av = static_cast<float>(
|
|
(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);
|
|
strokeArcAA(bmp, cx, cy, r - kInnerDialArcPx * 0.5f, a0, av, kInnerDialArcPx,
|
|
toLice(ui::roleColor(arcRole)));
|
|
const KnobPoint tip = instrument::ui::knobNeedlePoint(kg, arc, v);
|
|
strokeLineAA(bmp, static_cast<float>(kg.centerX), static_cast<float>(kg.centerY),
|
|
static_cast<float>(tip.x), static_cast<float>(tip.y), kInnerDialNeedlePx,
|
|
toLice(ui::roleColor(disabled ? ui::Role::TextDim : ui::Role::AccentTertiary)));
|
|
}
|
|
|
|
#endif // _WIN32
|
|
|
|
} // namespace reasampler::vst
|