Rebuild the chrome band: full-width piano strip with uniform key widths, note tooltips, one toolbar font

This commit is contained in:
2026-07-30 09:14:07 -04:00
parent ea52b14f2a
commit ae23ee0882
14 changed files with 656 additions and 386 deletions
+2 -2
View File
@@ -218,8 +218,8 @@ slider couldn't. Two pure modules split the forward (draw) and inverse (edit) ma
- `editor_geometry` (`core/instrument/ui`) — the shared geometry VOCABULARY every instrument UI module speaks: the `core::ui::Rect` alias, `contains()`, and `OverlayArea` (a one-field `Rect` wrapper, no implicit conversion from `Rect`). Header-only (an INTERFACE CMake target), so it carries no layout of its own.
- `sample_bands` — **THE band-stack allocator**, and the only module that owns the Sample face's vertical inventory: three bands top-to-bottom (CHROME toolbar+control row / WAVEFORM elastic, floored at two stacked lanes / DECKS bottom-anchored at the knob deck's own wrapped height), plus the waveform band's lane split (`waveformLanes` takes a resolved `LaneSplit`, not a raw bool — only `waveformSurface` folds the source-channel-count decision in). A shared READ-ONLY surface for every band owner — a band's interior module lays out inside the rect it is handed and never re-allocates the stack.
- `sample_chrome` — the CHROME band's interior: the toolbar row (title + Browse) over the control row (root strip, preview, velocity knob cell, curve button, channel toggle). The fixed run is right-anchored; the root strip takes the remainder.
- `keyboard_strip` — piano-keyboard strip: MIDI-note→key rect mapping, black/white key layout, hit-test, root-marker rect, and the drag-delta note resolver.
- `sample_chrome` — the CHROME band's interior: the toolbar row (title + the whole right-anchored control run — preview, velocity knob cell, curve button, channel toggle, Browse) over the strip row, which the piano strip owns outright. The title takes what the run leaves; the strip takes its whole row, inset only by the shared band pad so it lines up with the waveform band beneath.
- `keyboard_strip` — piano-keyboard strip: true white/black key geometry (whites tiled at one width, blacks overlaid at one width and height, straddling their boundary), hit-test resolving black-over-white by zone, root-marker rect, the absolute-position drag resolver, and MIDI note naming under the C4 convention. **Same-class keys are one integer width by construction; the residue of an indivisible band width (`w % 75`, up to 74 px) lands in symmetric end margins, never in a key** — uniform widths and gap-free edge-to-edge tiling cannot both hold, and uniformity wins.
- `waveform_view` — the WAVEFORM band's interior: `waveformSurface` resolves the drawn lane(s) (two stacked lanes, L over R, only when the mode is stereo AND the source has a second channel — a mono source under stereo mode is dual-mono and draws one lane) plus **the** overlay area, and `laneEnvelope` splits one multi-channel envelope pass per lane. Also maps frame span linearly across a rect; generic named draggable markers with drag-delta resolver, clamp, and zero-crossing snap.
- **Overlay contract (consumed by later waveform work).** `WaveformSurface::overlay` — equivalently the standalone `waveformOverlayArea(band)` — is the FULL band in both modes. Everything riding the waveform (the amp-envelope trace and its node handles, the start/loop markers, the loop region) draws ONCE into it, spanning both stacked lanes; hit-testing resolves against the same area so a grab in the lower lane reaches them. Anything drawn or hit-tested per lane is a duplicate and a defect — structurally enforced: `overlay` is the distinct `OverlayArea` type (`editor_geometry`), not `Rect`, so every overlay-consuming API (`frameToX`/`markerAtPoint`/`resolveDragFrame`, `envelope_edit`'s `nodeAtPoint`/`resolveNodeDrag`, `envelope_overlay`'s `buildEnvelopePolyline`) rejects a lane rect at compile time rather than silently accepting one.
- `capture_browser` — capture browser: card-grid layout + bank-filter tab strip geometry and hit-test; knows only counts and rects, draws nothing.
+78 -51
View File
@@ -8,18 +8,29 @@ namespace reasampler::instrument::ui {
namespace {
// Pitch class of each natural, and the count of naturals strictly below each pitch class.
constexpr int kNaturalPitchClass[7] = {0, 2, 4, 5, 7, 9, 11};
constexpr int kNaturalsBelowPc[12] = {0, 1, 1, 2, 2, 3, 4, 4, 5, 5, 6, 6};
int clampNote(int n) {
if (n < 0) return 0;
if (n > kStripKeyCount - 1) return kStripKeyCount - 1;
return n;
}
// Maps a key boundary (0..128) to an x pixel. Key N's left is keyEdgeToX(N), right is
// keyEdgeToX(N+1) — tiles adjacent keys without a seam. Mirrors embed_strip::keyEdgeToX.
int keyEdgeToX(int bandLeft, int bandWidth, int keyEdge) {
if (keyEdge <= 0) return bandLeft;
if (keyEdge >= kStripKeyCount) return bandLeft + bandWidth;
return bandLeft + (keyEdge * bandWidth) / kStripKeyCount;
// The MIDI note of white key `index` (0..74).
int whiteNoteAt(int index) {
const int i = index < 0 ? 0 : (index > kStripWhiteKeyCount - 1 ? kStripWhiteKeyCount - 1
: index);
return clampNote((i / 7) * 12 + kNaturalPitchClass[i % 7]);
}
// The black key straddling white-key boundary `b`, or -1 where the scale has none (E-F and
// B-C are adjacent naturals).
int blackNoteAtBoundary(int b) {
if (b <= 0 || b >= kStripWhiteKeyCount) return -1;
const int below = whiteNoteAt(b) - 1;
return (below >= 0 && !isNaturalKey(below)) ? below : -1;
}
} // namespace
@@ -28,42 +39,22 @@ StripLayout layoutStrip(int w, int h) {
const int cw = std::max(0, w);
const int ch = std::max(0, h);
StripLayout out;
out.keys = Rect::ltrb(0, 0, cw, ch);
out.band = Rect::ltrb(0, 0, cw, ch);
if (ch <= 0) return out;
out.whiteWidth = cw / kStripWhiteKeyCount;
if (out.whiteWidth <= 0) return out; // narrower than one pixel per white key
const int keysW = out.whiteWidth * kStripWhiteKeyCount;
const int left = (cw - keysW) / 2; // residue split evenly into the two end margins
out.keys = Rect::ltrb(left, 0, left + keysW, ch);
out.blackWidth = std::max(1, (out.whiteWidth * 3) / 5);
out.blackHeight = std::max(1, (ch * 3) / 5);
return out;
}
int keyLeftX(const StripLayout& layout, int note) {
const Rect& band = layout.keys;
const int bandWidth = std::max(0, band.width);
// note is a key (0..127); callers pass note+1 to get its right edge, 128 -> band right.
const int edge = note < 0 ? 0 : (note > kStripKeyCount ? kStripKeyCount : note);
return keyEdgeToX(band.x, bandWidth, edge);
}
Rect keyRect(const StripLayout& layout, int note) {
const int n = clampNote(note);
const int leftX = keyLeftX(layout, n);
const int rightX = keyLeftX(layout, n + 1);
return Rect::ltrb(leftX, layout.keys.y, std::max(leftX, rightX), layout.keys.bottom());
}
Rect rootMarkerRect(const StripLayout& layout, int rootNote) {
return keyRect(layout, rootNote);
}
int keyAtPoint(const StripLayout& layout, int x, int y) {
const Rect& band = layout.keys;
if (!contains(band, x, y)) return -1;
const int bandWidth = std::max(0, band.width);
if (bandWidth <= 0) return -1;
// Inverts keyEdgeToX: the key whose half-open [leftX, rightX) contains x.
const int offset = x - band.x;
int note = (offset * kStripKeyCount) / bandWidth;
return clampNote(note);
}
bool isNaturalKey(int note) {
const int n = note < 0 ? 0 : (note > kStripKeyCount - 1 ? kStripKeyCount - 1 : note);
const int n = clampNote(note);
static constexpr bool kNatural[12] = {
true, // 0 C
false, // 1 C#
@@ -81,20 +72,56 @@ bool isNaturalKey(int note) {
return kNatural[n % 12];
}
int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels) {
if (dxPixels == 0) return clampNote(startNote);
const int bandWidth = std::max(0, layout.keys.width);
if (bandWidth <= 0) return clampNote(startNote); // zero-width -> no motion
// Same linear mapping as keyAtPoint/keyEdgeToX (exact rational), not a truncated-integer
// bandWidth/128 key width — that drifted at the far end of the strip.
const int half = bandWidth / 2;
int shift;
if (dxPixels > 0) {
shift = (dxPixels * kStripKeyCount + half) / bandWidth;
} else {
shift = -(((-dxPixels) * kStripKeyCount + half) / bandWidth);
int whiteIndexOf(int note) {
const int n = clampNote(note);
return (n / 12) * 7 + kNaturalsBelowPc[n % 12];
}
Rect keyRect(const StripLayout& layout, int note) {
if (layout.keys.empty()) return Rect{};
const int n = clampNote(note);
const int wi = whiteIndexOf(n);
if (isNaturalKey(n)) {
const int left = layout.keys.x + wi * layout.whiteWidth;
return Rect::ltrb(left, layout.keys.y, left + layout.whiteWidth, layout.keys.bottom());
}
return clampNote(startNote + shift);
const int centre = layout.keys.x + wi * layout.whiteWidth;
const int left = centre - layout.blackWidth / 2;
return Rect::ltrb(left, layout.keys.y, left + layout.blackWidth,
layout.keys.y + layout.blackHeight);
}
Rect rootMarkerRect(const StripLayout& layout, int rootNote) {
return keyRect(layout, rootNote);
}
int keyAtPoint(const StripLayout& layout, int x, int y) {
if (!contains(layout.keys, x, y)) return -1;
const int offset = x - layout.keys.x;
const int wi = std::min(offset / layout.whiteWidth, kStripWhiteKeyCount - 1);
if (y < layout.keys.y + layout.blackHeight) {
// Only the two boundaries flanking this white key can carry an overlapping black.
const int candidates[2] = {wi, wi + 1};
for (const int b : candidates) {
const int note = blackNoteAtBoundary(b);
if (note >= 0 && contains(keyRect(layout, note), x, y)) return note;
}
}
return whiteNoteAt(wi);
}
int resolveDragNote(const StripLayout& layout, int x, int y) {
if (layout.keys.empty()) return -1;
const int cx = std::clamp(x, layout.keys.x, layout.keys.right() - 1);
const int cy = std::clamp(y, layout.keys.y, layout.keys.bottom() - 1);
return keyAtPoint(layout, cx, cy);
}
std::string noteName(int note) {
static constexpr const char* kNames[12] = {"C", "C#", "D", "D#", "E", "F",
"F#", "G", "G#", "A", "A#", "B"};
const int n = clampNote(note);
return std::string(kNames[n % 12]) + std::to_string(n / 12 - 1);
}
} // namespace reasampler::instrument::ui
+34 -21
View File
@@ -1,13 +1,14 @@
// keyboard_strip.h — layout + hit-test + drag math for the editor's keyboard strip.
// Mirror of embed_strip/mode_switch; the shell draws and marshals mouse events into these
// functions.
// keyboard_strip.h — piano-keyboard geometry for the editor's root strip: per-class key
// rects, hit-test, the root marker, and the note name a hovered key reports.
//
// The strip maps the full 128-key MIDI span across a horizontal band (the same idiom
// embed_strip uses). The loaded capture responds across that whole span, so the strip's
// job is the root marker: click a key, or drag the marker, to set the root.
// Same-class keys are one integer width by construction. An arbitrary band width is not
// divisible by the 75 white keys, so the residue lands in symmetric end margins — uniform
// key widths and gap-free edge-to-edge tiling cannot both hold, and uniformity wins.
#pragma once
#include <string>
#include "core/instrument/ui/editor_geometry.h" // Rect, contains
namespace reasampler::instrument::ui {
@@ -16,35 +17,47 @@ namespace reasampler::instrument::ui {
// stay independent.
inline constexpr int kStripKeyCount = 128;
// The keys band takes the whole strip area today; clamped so a degenerate size never
// yields an inverted rect.
// Naturals in MIDI 0..127 (C-1 .. G9): ten full octaves of seven, plus C D E F G.
inline constexpr int kStripWhiteKeyCount = 75;
struct StripLayout {
Rect keys;
Rect band; // the surface handed in, edge to edge
Rect keys; // the tiled key area, kStripWhiteKeyCount * whiteWidth, centred in band
int whiteWidth = 0;
int blackWidth = 0;
int blackHeight = 0; // black keys are short; below them the white key answers
};
// Divide a (w x h) strip area into its regions. Pure.
// Divide a (w x h) strip area into its key geometry. Pure. A band too narrow for one pixel
// per white key yields empty `keys` — nothing draws and nothing hit-tests.
StripLayout layoutStrip(int w, int h);
// x pixel of the LEFT edge of key `note` (0..127) under the linear 128-key map; key N
// occupies [keyLeftX(N), keyLeftX(N+1)). note==128 maps to the band's right edge.
int keyLeftX(const StripLayout& layout, int note);
// True when `note` (clamped to [0,127]) is a natural (white) key in 12-tone equal
// temperament; false for an accidental (black) key.
bool isNaturalKey(int note);
// Half-open rect of a single key `note`, clamped to [0,127].
// Naturals strictly below `note`. For a white key that is its own ordinal; for a black key
// it is the white-key boundary the accidental straddles.
int whiteIndexOf(int note);
// Rect of a single key, clamped to [0,127]. Whites are full band height and whiteWidth
// wide; blacks are blackHeight tall and blackWidth wide, centred on their boundary.
Rect keyRect(const StripLayout& layout, int note);
// Root-marker rect; equivalent to keyRect(layout, rootNote) but named so the intent reads
// at the call site.
Rect rootMarkerRect(const StripLayout& layout, int rootNote);
// MIDI note a point (x, y) lands on, or -1 outside the keys band.
// MIDI note a point (x, y) lands on, or -1 outside the tiled keys. A black key wins inside
// its own short zone; anywhere else the white key beneath answers.
int keyAtPoint(const StripLayout& layout, int x, int y);
// Resolves a drag to a new MIDI note: `startNote` shifted by round(dxPixels / keyWidth),
// clamped to [0,127]. The one arithmetic behind the root-marker drag.
int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels);
// The note a live drag resolves to: keyAtPoint with the point clamped into the key area, so
// a drag that wanders off the strip keeps tracking rather than dropping the edit. -1 only
// when there is no key area at all.
int resolveDragNote(const StripLayout& layout, int x, int y);
// True when `note` (clamped to [0,127]) is a natural (white) key in 12-tone equal
// temperament; false for an accidental (black) key.
bool isNaturalKey(int note);
// DAW convention (the one REAPER uses): MIDI 60 is C4, so MIDI 0 is C-1.
std::string noteName(int note);
} // namespace reasampler::instrument::ui
+54 -33
View File
@@ -10,14 +10,20 @@ namespace reasampler::instrument::ui {
namespace {
constexpr int kStripBandHeight = 40; // the root/piano strip's own height inside the row
// The toolbar row carries the whole control run, so it is taller than the Browse modal's
// plain kTitleHeight bar — the velocity knob cell (knob over label) sets the floor. Both
// rows still fit the band the allocator hands out (kTitleHeight + kChromeRowHeight).
constexpr int kToolbarHeight = 44;
constexpr int kStripBandHeight = 30;
// The control row's fixed right-anchored run, right to left.
constexpr int kRunGap = 6; // between adjacent items of the toolbar run
constexpr int kChanSegW = 52;
constexpr int kChanSegH = 18;
constexpr int kCurveBtnSize = 28;
constexpr int kVelCellW = 48;
constexpr int kCurveBtnSize = 24;
constexpr int kVelCellW = 44;
constexpr int kVelLabelH = 12;
constexpr int kPreviewBtnW = 64;
constexpr int kRunButtonH = 24; // Browse and Preview
} // namespace
@@ -25,46 +31,61 @@ ChromeRects chromeRects(const Rect& chrome, int knobSize) {
ChromeRects r;
if (chrome.empty()) return r;
const int titleH = std::min(kTitleHeight, chrome.height);
r.toolbar = Rect::ltrb(chrome.x, chrome.y, chrome.right(), chrome.y + titleH);
const int toolbarH = std::min(kToolbarHeight, chrome.height);
r.toolbar = Rect::ltrb(chrome.x, chrome.y, chrome.right(), chrome.y + toolbarH);
r.controls = Rect::ltrb(chrome.x, r.toolbar.bottom(), chrome.right(), chrome.bottom());
const int navTop = r.toolbar.y + 2;
const int navBot = std::max(navTop, r.toolbar.bottom() - 2);
r.navBrowse = Rect::ltrb(std::max(chrome.x, chrome.right() - kPad - kNavButtonWidth),
navTop, std::max(chrome.x, chrome.right() - kPad), navBot);
const Rect& row = r.toolbar;
const auto topFor = [&row](int h) { return row.y + (row.height - h) / 2; };
const auto leftOf = [&row](int edge, int w) { return std::max(row.x, edge - w); };
if (r.controls.empty()) return r;
const Rect& row = r.controls;
// The fixed run, right to left: Browse, Mono|Stereo, curve, velocity cell, preview.
const int navH = std::min(kRunButtonH, row.height);
const int navTop = topFor(navH);
const int navRight = std::max(row.x, row.right() - kPad);
r.navBrowse = Rect::ltrb(leftOf(navRight, kNavButtonWidth), navTop, navRight,
navTop + navH);
// Vertically centre the two heights the row uses: the tall strip band (which the preview
// button and velocity cell align to) and the smaller square/segment controls.
const int stripTop = row.y + (row.height - kStripBandHeight) / 2;
const int stripBot = stripTop + kStripBandHeight;
const int chanTop = row.y + (row.height - kChanSegH) / 2;
const int chanRight = row.right() - kPad;
r.chanStereo = Rect::ltrb(chanRight - kChanSegW, chanTop, chanRight, chanTop + kChanSegH);
r.chanMono = Rect::ltrb(r.chanStereo.x - kChanSegW, chanTop, r.chanStereo.x,
const int chanTop = topFor(kChanSegH);
const int chanRight = leftOf(r.navBrowse.x, kRunGap);
r.chanStereo = Rect::ltrb(leftOf(chanRight, kChanSegW), chanTop, chanRight,
chanTop + kChanSegH);
r.chanMono = Rect::ltrb(leftOf(r.chanStereo.x, kChanSegW), chanTop, r.chanStereo.x,
chanTop + kChanSegH);
const int curveTop = row.y + (row.height - kCurveBtnSize) / 2;
r.curveBtn = Rect::ltrb(r.chanMono.x - kPad - kCurveBtnSize, curveTop,
r.chanMono.x - kPad, curveTop + kCurveBtnSize);
const int curveTop = topFor(kCurveBtnSize);
const int curveRight = leftOf(r.chanMono.x, kRunGap);
r.curveBtn = Rect::ltrb(leftOf(curveRight, kCurveBtnSize), curveTop, curveRight,
curveTop + kCurveBtnSize);
r.velCell = Rect::ltrb(r.curveBtn.x - kPad - kVelCellW, stripTop,
r.curveBtn.x - kPad, stripBot);
const int knobLeft = r.velCell.x + (kVelCellW - knobSize) / 2;
const int cellH = std::min(row.height, knobSize + kVelLabelH);
const int cellTop = topFor(cellH);
const int cellRight = leftOf(r.curveBtn.x, kRunGap);
r.velCell = Rect::ltrb(leftOf(cellRight, kVelCellW), cellTop, cellRight, cellTop + cellH);
const int knobLeft = r.velCell.x + (r.velCell.width - knobSize) / 2;
r.velKnob = Rect::ltrb(knobLeft, r.velCell.y, knobLeft + knobSize,
r.velCell.y + knobSize);
r.velCell.y + std::min(knobSize, cellH));
r.velLabel = Rect::ltrb(r.velCell.x, r.velKnob.bottom(), r.velCell.right(),
r.velCell.bottom());
r.preview = Rect::ltrb(r.velCell.x - kPad - kPreviewBtnW, stripTop,
r.velCell.x - kPad, stripBot);
// Remainder width; clamped so a narrow window collapses the strip rather than inverting it.
r.rootStrip = Rect::ltrb(row.x + kPad, stripTop,
std::max(row.x + kPad, r.preview.x - kPad), stripBot);
const int prevTop = topFor(std::min(kRunButtonH, row.height));
const int prevRight = leftOf(r.velCell.x, kRunGap);
r.preview = Rect::ltrb(leftOf(prevRight, kPreviewBtnW), prevTop, prevRight,
prevTop + std::min(kRunButtonH, row.height));
// The title takes what the run leaves; clamped so a narrow window collapses it rather
// than inverting it.
r.title = Rect::ltrb(row.x + kPad, row.y, std::max(row.x + kPad, r.preview.x - kRunGap),
row.bottom());
if (r.controls.empty()) return r;
// The strip row belongs to the strip alone — inset only by the shared band pad, so it
// lines up with the waveform band directly beneath it.
const int stripH = std::min(kStripBandHeight, r.controls.height);
const int stripTop = r.controls.y + (r.controls.height - stripH) / 2;
r.rootStrip = Rect::ltrb(r.controls.x + kPad, stripTop,
std::max(r.controls.x + kPad, r.controls.right() - kPad),
stripTop + stripH);
return r;
}
+11 -10
View File
@@ -1,8 +1,8 @@
#pragma once
// sample_chrome.h — interior geometry of the Sample face's CHROME band: the toolbar row
// (title + Browse) over the control row (root/piano strip, preview trigger, preview-velocity
// knob cell, curve-preview button, Mono|Stereo toggle). Reads the band rect the allocator
// hands it (sample_bands) and never allocates vertical space of its own.
// (title + the whole right-anchored control run + Browse) over the strip row, which the
// piano strip has to itself. Reads the band rect the allocator hands it (sample_bands) and
// never allocates vertical space of its own.
#include "core/instrument/ui/editor_geometry.h" // Rect
@@ -11,21 +11,22 @@ namespace reasampler::instrument::ui {
inline constexpr int kNavButtonWidth = 62; // the Browse toolbar button
// Every interactive rect inside the chrome band, in one pass so draw and hit-test cannot
// derive them differently. The control row's right-anchored run is fixed-width (preview,
// velocity cell, curve button, channel toggle) and the root strip takes the remainder, so
// the strip grows with the window.
// derive them differently. The toolbar's fixed run is right-anchored and the title takes
// what is left of that row; the strip row carries nothing but the strip, so the strip grows
// with the window in both directions.
struct ChromeRects {
Rect toolbar; // full-width top row
Rect navBrowse; // right-anchored in the toolbar
Rect controls; // full-width second row
Rect rootStrip; // remainder-width, left
Rect preview;
Rect title; // the title text slot: the toolbar left of the control run
Rect preview; // ---- the right-anchored run, left to right ----
Rect velCell; // preview-velocity knob cell (knob + label band)
Rect velKnob;
Rect velLabel;
Rect curveBtn; // opens the velocity-curve popup
Rect chanMono;
Rect chanStereo;
Rect navBrowse;
Rect controls; // full-width second row
Rect rootStrip; // the piano strip: the whole row, inset only by the shared band pad
};
// `knobSize` is the deck knob square, passed in so this module does not depend on knob_deck.
+1 -1
View File
@@ -90,7 +90,7 @@ scattered `#ifdef`s in the VST shell, except the one described below).
- `reasampler_editor` — VST3 `IPlugView` LICE editor shell: hosts a LICE-drawn child window; the Sample face is home and Browse is a modal picker over it. Split on the Sample face's BAND axis, mirroring the pure `sample_bands` allocator: `editor_session` (session/bridge state, caches, commit-and-reload), `editor_controls` (parameter plumbing + the ONE `faceLayout` band resolve every paint and hit-test path shares), then matching paint and input sets — `editor_paint`/`editor_input` (dispatch + drag router + hover dispatch), `_chrome`, `_waveform`, `_deck` — plus the two band-independent surfaces (`_browse` for the modal picker, `_curve` for the velocity-curve popup) and `editor_platform` (IPlugView/Win32 window plumbing). Shared internals in `editor_internal.h`, no TU of its own. Drop-onto-editor ingest is NOT shipped (deferred).
- `reasampler_embed` — implements `IReaperUIEmbedInterface` so the instrument draws inline in the TCP/MCP without a plugin-owned HWND; delegates layout to `embed_strip`. A read-only readout: the loaded capture across the keyboard span with its root marked, plus the activity level. It takes no mouse input (there is nothing on the strip to select).
- `vst_entry` — VST3 entry point: `GetPluginFactory` export, class registration, channel-forked class UIDs.
- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` 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: the `Rect`↔kit adapters, small draw primitives (knob face / spectral strip / root marker / title band), label helpers, deck group ids, and the velocity-curve box derivation — the helpers more than one band TU needs.
- `editor_internal.h` — INTERNAL shared helpers for the `reasampler_editor` 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: the `Rect`↔kit adapters, small draw primitives (knob face / title band), label helpers, deck group ids, and the velocity-curve box derivation — the helpers more than one band TU needs. The piano-strip and root-key draws live in `editor_paint_chrome`, their only consumer, not here.
- `reasampler_vst.h` — shared identity constants for the ReaSampler VST3 instrument (Phase S): the plugin's class UID (the channel-selected `Steinberg::FUID`, built from the FOREVER-FROZEN macros in `core/wire/reasampler_uid.h`), vendor name/URL/email, so the processor, factory, and editor agree. A class UID is FOREVER-STABLE once shipped — minted once, never regenerated. *(Newly authored per this dispatch's brief — no existing root-CLAUDE.md bullet; verified by reading `src/shell/instrument/reasampler_vst.h` directly.)*
## Gotchas
+19 -13
View File
@@ -1,12 +1,12 @@
// editor_input_chrome.cpp — the CHROME band's input: the Browse nav, the preview trigger,
// the preview-velocity knob grab, the curve-button summon, the channel toggle, and the
// root-marker grab plus its live drag. Windows-only.
// piano strip's root grab plus its live drag. Windows-only.
#include "shell/instrument/reasampler_editor.h"
#ifdef _WIN32
#include "core/instrument/ui/keyboard_strip.h" // keyAtPoint / resolveDragNote (root marker)
#include "core/instrument/ui/keyboard_strip.h" // keyAtPoint / resolveDragNote (root key)
#include "shell/instrument/editor_internal.h"
#include "shell/instrument/reasampler_processor.h"
@@ -69,31 +69,32 @@ bool ReaSamplerEditor::mouseDownChrome(const FaceLayout& fl, int x, int y) {
return true;
}
// The root strip: grab the root marker. A plain click sets the root to the clicked key
// (applied below as the first delta==0 move).
if (cr.rootStrip.width > 0) {
// The piano strip: clicking a key sets the root, and holding tracks the pointer. The
// click itself lands below as the first (unmoved) drag resolve.
if (!cr.rootStrip.empty()) {
const StripLayout sl = layoutStrip(cr.rootStrip.width, cr.rootStrip.height);
const int note = keyAtPoint(sl, x - cr.rootStrip.x, y - cr.rootStrip.y);
if (note >= 0) {
if (keyAtPoint(sl, x - cr.rootStrip.x, y - cr.rootStrip.y) >= 0) {
drag_ = DragKind::kRootMarker;
dragStartX_ = x;
dragStartRoot_ = note;
dragStartParams_ = params_;
onMouseMove(x, y); // apply the click as the first delta==0 set
onMouseMove(x, y);
return true;
}
}
// A click on the control row's background is consumed so it can't fall through to a
// A click on the strip row's background is consumed so it can't fall through to a
// band the user cannot see under the chrome.
return contains(cr.controls, x, y);
}
void ReaSamplerEditor::dragChrome(const FaceLayout& fl, int x, int y) {
(void)y;
const Rect& stripArea = fl.chrome.rootStrip;
if (stripArea.width <= 0) return;
if (stripArea.empty()) return;
// Absolute tracking, not a pixel delta: with black keys overlaying whites there is no
// one pixels-per-semitone rate a delta could use.
const StripLayout sl = layoutStrip(stripArea.width, stripArea.height);
params_.rootOverride = resolveDragNote(sl, dragStartRoot_, x - dragStartX_);
const int note = resolveDragNote(sl, x - stripArea.x, y - stripArea.y);
if (note < 0) return;
params_.rootOverride = note;
invalidate(); // live feedback; the commit lands on WM_LBUTTONUP
}
@@ -107,6 +108,11 @@ ReaSamplerEditor::HoverTarget ReaSamplerEditor::hoverChrome(const FaceLayout& fl
if (contains(cr.curveBtn, x, y)) return {HoverKind::kCurveButton, -1};
if (contains(cr.chanMono, x, y)) return {HoverKind::kChanMono, -1};
if (contains(cr.chanStereo, x, y)) return {HoverKind::kChanStereo, -1};
if (!cr.rootStrip.empty()) {
const StripLayout sl = layoutStrip(cr.rootStrip.width, cr.rootStrip.height);
const int note = keyAtPoint(sl, x - cr.rootStrip.x, y - cr.rootStrip.y);
if (note >= 0) return {HoverKind::kStripKey, note};
}
return {};
}
+6 -58
View File
@@ -1,8 +1,8 @@
// 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 / spectral strip / root marker / title band), label
// helpers, deck group ids, and the velocity-curve box derivation. All inline.
// small draw primitives (knob face / title band), label helpers, deck group ids, and the
// velocity-curve box derivation. All inline.
#pragma once
@@ -14,6 +14,7 @@
#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"
@@ -22,7 +23,6 @@
#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/instrument/ui/keyboard_strip.h" // StripLayout / keyRect / isNaturalKey (spectral strip)
#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
@@ -55,15 +55,10 @@ inline instrument::engine::VelocityCurve::Box curveBoxFromRect(
(std::max)(0, r.height - 2 * kVelCurveInset)};
}
// A short MIDI-note label ("C4", "F#3") for the root badge. Middle C (60) is C4 (the
// common DAW convention REAPER uses).
// 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) {
static const char* kNames[12] = {"C", "C#", "D", "D#", "E", "F",
"F#", "G", "G#", "A", "A#", "B"};
if (note < 0) note = 0;
if (note > 127) note = 127;
const int octave = note / 12 - 1; // MIDI 0 = C-1; 60 = C4
return std::string(kNames[note % 12]) + std::to_string(octave);
return instrument::ui::noteName(note);
}
// A display name for a bank sample id: the snapshotted bank list first, then the
@@ -170,53 +165,6 @@ inline void drawKnobFace(LICE_IBitmap* bmp, const instrument::ui::Rect& knobRect
toLice(ui::roleColor(needleRole)), 1.0f, 0, true);
}
// Draws the pastel spectral keyboard-strip background: each MIDI key column filled with
// its spectral hue, accidentals darkened with an overlay wash so pitch position reads as
// a keyboard at a glance.
inline void drawSpectralStrip(LICE_IBitmap* bmp, const instrument::ui::Rect& stripArea) {
using instrument::ui::StripLayout;
if (stripArea.width <= 0 || stripArea.height <= 0) return;
const StripLayout sl = instrument::ui::layoutStrip(stripArea.width, stripArea.height);
const int sx = stripArea.x;
const int sy = stripArea.y;
const int h = stripArea.height;
const LICE_pixel darkKey = toLice(ui::roleColor(ui::Role::BgBase));
for (int n = 0; n <= 127; ++n) {
const instrument::ui::Rect k = instrument::ui::keyRect(sl, n);
const int x0 = k.x + sx;
const int x1 =
(n < 127) ? instrument::ui::keyRect(sl, n + 1).x + sx : stripArea.right();
const int cw = (std::max)(1, x1 - x0);
const ui::KitColor hue = ui::spectralColor(static_cast<double>(n) / 127.0);
LICE_FillRect(bmp, x0, sy, cw, h, toLice(hue), 0.55f, 0);
if (!instrument::ui::isNaturalKey(n)) {
LICE_FillRect(bmp, x0, sy, cw, h, darkKey, 0.55f, 0);
}
}
// Faint per-octave key ticks (hairline role) for orientation.
const LICE_pixel tick = toLice(ui::roleColor(ui::Role::LineHairline));
for (int n = 0; n <= 127; n += 12) {
const instrument::ui::Rect k = instrument::ui::keyRect(sl, n);
LICE_Line(bmp, k.x + sx, sy, k.x + sx, sy + h, tick, 1.0f, 0, false);
}
}
// Draws the single-capture root marker: an accent-primary bar with a soft static glow —
// the "this is live" mark.
inline void drawRootMarker(LICE_IBitmap* bmp, const instrument::ui::Rect& stripArea,
const instrument::ui::StripLayout& sl, int root) {
const int sx = stripArea.x;
const int sy = stripArea.y;
const int h = stripArea.height;
const instrument::ui::Rect marker = instrument::ui::rootMarkerRect(sl, root);
const int mw = (std::max)(2, marker.width);
const LICE_pixel accent = toLice(ui::roleColor(ui::Role::AccentPrimary));
const LICE_pixel glow = toLice(ui::roleColor(ui::Role::AccentHot));
// Static glow: a wider low-alpha halo behind the crisp bar (a drawn state, not a pulse).
LICE_FillRect(bmp, marker.x + sx - 3, sy, mw + 6, h, glow, 0.30f, 0);
LICE_FillRect(bmp, marker.x + sx, sy, mw, h, accent, 1.0f, 0);
}
#endif // _WIN32
} // namespace reasampler::vst
+3
View File
@@ -71,6 +71,9 @@ void ReaSamplerEditor::paintSample(LICE_IBitmap* bmp, int w, int h) {
// The curve popup: a centered sheet over the whole face, drawn last.
if (curvePopupOpen_) paintCurvePopup(bmp, w, h);
// The piano strip's note-name chip overhangs its band, so it goes on top of everything.
paintChromeTooltip(bmp, fl, w, h);
}
void ReaSamplerEditor::paintEmptyState(LICE_IBitmap* bmp, const Rect& area) {
+123 -20
View File
@@ -1,7 +1,7 @@
// editor_paint_chrome.cpp — the CHROME band's painter: the toolbar row (product title +
// live readout + Browse) and the control row (root/piano strip with its root marker, the
// preview trigger, the preview-velocity knob cell, the curve-preview button, and the
// Mono|Stereo toggle). Windows-only; all rects come from the pure sample_chrome interior.
// 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"
@@ -10,9 +10,11 @@
#include <cstdio>
#include <string>
#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 / spectral strip / root marker
#include "shell/instrument/editor_internal.h" // kit adapters + knob face
#include "shell/instrument/reasampler_processor.h"
namespace reasampler::vst {
@@ -21,9 +23,81 @@ 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<double>(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();
@@ -38,7 +112,7 @@ void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool
} else {
title += " [host: no bridge]";
}
drawTitleBand(bmp, cr.toolbar, title);
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".
@@ -50,19 +124,9 @@ void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool
drawButton(bmp, box, "Browse", st, /*warn=*/false);
}
// The control row draws only once a capture is loaded — with nothing picked there is no
// root, no preview and no channel decision to make.
if (empty || cr.controls.empty()) return;
fillSurface(bmp, toKitBox(cr.controls), Role::BgPanel, InteractionState::Rest);
// Root strip: the full 128-key spectral band with the root marked. The loaded capture
// responds across the whole strip, repitched from that root.
if (cr.rootStrip.width > 0) {
drawSpectralStrip(bmp, cr.rootStrip);
const StripLayout sl = layoutStrip(cr.rootStrip.width, cr.rootStrip.height);
drawRootMarker(bmp, cr.rootStrip, sl, effectiveRoot());
}
// 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).
{
@@ -106,11 +170,50 @@ void ReaSamplerEditor::paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool
: InteractionState::Rest);
fillSurface(bmp, toKitBox(cr.chanMono), Role::BgCell, monoState);
fillSurface(bmp, toKitBox(cr.chanStereo), Role::BgCell, stereoState);
kitTextCentered(bmp, cr.chanMono, "Mono", Font::Label,
kitTextCentered(bmp, cr.chanMono, "Mono", kToolbarFont,
!isStereo ? Role::BgBase : Role::TextPrimary);
kitTextCentered(bmp, cr.chanStereo, "Stereo", Font::Label,
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<int>(label.size()) * kTooltipCharPx;
const TooltipBox tb = computeTooltip(area.x + key.x, area.y + key.y, key.width, key.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, box.height,
toLice(roleColor(Role::LineHairline)), 1.0f, 0);
kitTextCentered(bmp, box, label.c_str(), Font::Label, Role::TextPrimary);
}
} // namespace reasampler::vst
+4 -1
View File
@@ -130,6 +130,7 @@ private:
kControl, // a knob-deck element (index = control id)
kCurveNode, // a velocity-curve control point (index = point index)
kVelKnob, // the chrome preview-velocity radial knob
kStripKey, // a piano-strip key (index = MIDI note); carries the name tooltip
kCurveButton, // the chrome mini curve-preview button (opens the popup)
kPopupClose, // the curve popup's Close (x) button
};
@@ -160,6 +161,9 @@ private:
// Chrome: title band + Browse nav + the control row (root strip, preview, velocity knob,
// curve button, channel toggle).
void paintChrome(LICE_IBitmap* bmp, const FaceLayout& fl, bool empty);
// The hovered piano key's note-name chip. Drawn after every band — it overhangs the
// chrome into whatever is below it.
void paintChromeTooltip(LICE_IBitmap* bmp, const FaceLayout& fl, int w, int h);
// Waveform: the channel lane(s), the loop/start markers, and the envelope overlay.
void paintWaveform(LICE_IBitmap* bmp, const Rect& band);
// Decks: the group fence + caption + compact caption toggles + radial knobs with
@@ -417,7 +421,6 @@ private:
int dragStartY_ = 0; // grab y (px), for the vertical scrollbar-thumb drag
int dragCurX_ = 0; // live cursor x (px) during a drag — updated in onMouseMove
int dragCurY_ = 0; // live cursor y (px) during a drag — updated in onMouseMove
int dragStartRoot_ = 60; // the root note at grab time
InstrumentParams dragStartParams_; // params_ snapshotted at grab; restored on capture-loss
// Waveform-marker drag: which marker + the marker set snapshotted at grab time, so the