// keyboard_strip.h — PURE layout + hit-test + drag math for the S10 capture-first // editor's keyboard strip. NO VST3, NO REAPER, NO SWELL/LICE types at the boundary. // The mirror of editor_geometry / embed_strip / mode_switch: the fiddly rectangle + // note-mapping arithmetic lives here so it is unit-tested outside the DAW, while the // editor shell (reasampler_editor.cpp) draws the strip and marshals mouse events into // these functions. // // The strip maps the full 128-key MIDI span across a horizontal band (the same key-span // idiom embed_strip uses). It serves TWO faces of the S10 editor: // * the SINGLE-CAPTURE fast path (default): one loaded capture with a ROOT MARKER on // the strip, click-a-key (or drag the marker) sets the capture's root note; and // * the opt-in ZONES panel (S10-Z, demoted): each performance zone drawn as a bar over // the keys it covers, with edge-grab resize handles + a body move-handle so a drag // sets low/high (edges) or moves the span (body), and a key-click sets the zone root. // // All interaction resolves through the pure DRAG-DELTA resolver here: the shell captures // a grab on WM_LBUTTONDOWN, feeds each WM_MOUSEMOVE's pixel delta back through // resolveDragNote, and commits the resolved note(s) on WM_LBUTTONUP. Live feedback is the // shell re-drawing the in-flight note; one coherent edit lands on release. // // It reuses the same Rect + contains() as editor_geometry (one shared geometry idiom), // so this header depends on editor_geometry.h rather than redefining a rectangle type. #pragma once #include "editor_geometry.h" // Rect, contains — one shared geometry idiom namespace reasampler::vst { // The full MIDI key span the strip maps across its width: 128 keys (0..127). Named // distinctly from embed_strip's kEmbedKeyCount (same value) so the two strips stay // independent — the editor strip may grow octave labels/metrics the embed strip never does. inline constexpr int kStripKeyCount = 128; // The width (px) of an edge-grab hit region at each end of a zone bar: a drag started // within this many pixels of the bar's left/right edge resizes that edge; a drag started // anywhere else on the bar moves the whole span. A zone narrower than 2*this has no body // move-handle (both edges win their halves) — deliberate: a 1-key zone is all edges. inline constexpr int kStripEdgeGrabWidth = 6; // The strip's regions, derived from the (w x h) band the shell allots it. The keys band // takes the whole area today (a future octave-label lane can carve a sub-band here without // changing callers). Clamped so a degenerate (tiny/zero) size never yields an inverted rect. struct StripLayout { Rect keys; // the key band: the 128-key span maps linearly across keys.width() }; // Divide a (w x h) strip area into its regions. Pure: same inputs -> same layout. A zero or // negative size yields empty rects (no inversion). StripLayout layoutStrip(int w, int h); // The x pixel (inside the keys band) of the LEFT edge of key `note` (0..127). The 128-key // span maps linearly across keys.width(); key N occupies the half-open pixel range // [keyLeftX(N), keyLeftX(N+1)). Notes are clamped to [0,127]; note==128 maps to the band's // right edge (so a key's right edge is keyLeftX(note+1)). Pure. int keyLeftX(const StripLayout& layout, int note); // The half-open pixel rect of a single key `note` (0..127): [keyLeftX(note), // keyLeftX(note+1)) horizontally, the full keys-band height. A malformed (out-of-range) // note clamps to [0,127]. Pure. Rect keyRect(const StripLayout& layout, int note); // The rect of the ROOT MARKER for the single-capture fast path: the key cell of `rootNote`, // drawn as a highlighted key. Equivalent to keyRect(layout, rootNote) — a named entry point // so the shell's intent (this is the root marker, not just any key) reads at the call site, // and so a future marker shape (a triangle over the key) has one place to change. Pure. Rect rootMarkerRect(const StripLayout& layout, int rootNote); // The MIDI note a point (x, y) lands on, or -1 for a point outside the keys band. Backs // click-to-set-root (single capture) and click-a-key-sets-zone-root (zones). Pure. int keyAtPoint(const StripLayout& layout, int x, int y); // The horizontal sub-rect of the keys band for a zone spanning [lowNote, highNote] // (inclusive): [keyLeftX(low), keyLeftX(high+1)) horizontally, the full band height. Notes // clamp to [0,127] and low clamps to <= high, so a malformed zone yields an in-band // (possibly zero-width) rect, never an inverted one. Mirrors embed_strip::zoneSegmentRect. // Pure. Rect zoneBarRect(const StripLayout& layout, int lowNote, int highNote); // Which part of a zone bar a grab landed on. The shell uses this to decide what a drag // edits: an edge resizes that boundary; the body moves the whole span; none means the grab // missed the bar entirely (the shell may treat that as a key-click to set the root, or as a // deselect). enum class ZoneGrab { kNone, // the point is not on this zone's bar kLowEdge, // within kStripEdgeGrabWidth of the bar's LEFT edge -> resize low kHighEdge, // within kStripEdgeGrabWidth of the bar's RIGHT edge -> resize high kBody, // on the bar but not an edge -> move the whole span }; // Classify a grab at (x, y) against ONE zone's bar (low..high). Returns kNone when the // point is off the bar (or off the keys band). On the bar: kLowEdge/kHighEdge when within // kStripEdgeGrabWidth of that edge, else kBody. A narrow bar (< 2*kStripEdgeGrabWidth) // resolves the near half to each edge (no body). The LOW edge wins a tie at the exact // midpoint of a narrow bar (deterministic). Pure. ZoneGrab zoneGrabAt(const StripLayout& layout, int lowNote, int highNote, int x, int y); // The zone (index into `lows`/`highs`, draw order) whose bar a grab at (x, y) lands on, // plus which part of it, or {-1, kNone} for a point off every bar. First covering zone in // draw order wins (first-match, mirroring the core's Keymap::resolve + embed_strip). The // arrays are parallel (lows[i]/highs[i] is zone i's inclusive range); `count` is their // length. Pure — no host containers at the boundary (a raw pointer pair, like // embed_strip::zoneAtPoint). struct ZoneBarHit { int zoneIndex = -1; ZoneGrab grab = ZoneGrab::kNone; }; ZoneBarHit zoneBarAtPoint(const StripLayout& layout, const int* lows, const int* highs, int count, int x, int y); // Resolve a drag to a new MIDI note. Given the note the grabbed field held at grab time // (`startNote`) and the horizontal pixel delta since grab (`dxPixels`), returns the note // the field should now hold: startNote shifted by round(dxPixels / keyWidth), clamped to // [0,127]. keyWidth is derived from the layout (band width / 128); a zero-width band pins // the result to startNote (no motion). This is the single arithmetic behind edge-resize, // body-move (apply to both edges with the SAME delta so the span is preserved), and // root-marker drag. Pure — rounding is to the nearest key so a half-key drag flips at the // key centre. Returns startNote unchanged for dxPixels==0. int resolveDragNote(const StripLayout& layout, int startNote, int dxPixels); // Returns true when `note` (0..127) is a NATURAL (white) key in standard 12-tone equal // temperament; false when it is an ACCIDENTAL (black) key. Notes out of the [0,127] // range are clamped to [0,127] before classification (i.e. this never throws/UBs on a // bad input). The 12 semitone positions within an octave: // Natural (white): 0(C) 2(D) 4(E) 5(F) 7(G) 9(A) 11(B) // Accidental (black): 1(C#) 3(D#) 6(F#) 8(G#) 10(A#) // Used by the shell to overlay the two-tone bright/dark piano-key pattern over the // pastel spectral fill (S-VIEW-7). Pure — no layout required, no host types. bool isNaturalKey(int note); } // namespace reasampler::vst