S12: editor scale + S15/S16 control surfaces

Pure browser_scroll/note_entry/param_slider modules (+ CTest) for scroll,
type-to-filter search, numeric note entry, and the AHDSR/Trigger/pitch-engine/
pitch-env control panel. Editor shell draws + routes through them; params edit
the selected zone's ZonePlayParams via commitAndReload.
This commit is contained in:
2026-07-27 01:09:19 -04:00
parent ddc2ec1e75
commit 4f30124ef7
13 changed files with 1667 additions and 31 deletions
+104
View File
@@ -0,0 +1,104 @@
// param_slider.h — PURE control-surface layout + hit-test + value<->pixel mapping for the
// S12/S15/S16 editor parameter panel. NO VST3, NO REAPER, NO SWELL/LICE types at the
// boundary, and — deliberately — NO sampler_core / sample_map engine types either. The
// mirror of keyboard_strip / waveform_view / mode_switch: the fiddly slider-track and
// toggle-segment arithmetic lives here, unit-tested outside the DAW, while the editor shell
// draws each row (label + track/segments + handle) and routes clicks/drags into these
// functions, owning the control-id -> engine-param binding + the value DOMAIN mapping.
//
// WHY IT EXISTS (S12 + the S15/S16 control surfaces deferred here). The setup / Zones surface
// grows a stack of parameter controls: the S15 play-mode toggle (Gate|Trigger), the AHDSR
// amp-envelope sliders (attack/hold/decay/sustain/release), the Trigger %-length + fade
// controls, the S16 Varispeed|Preserve engine toggle, and the AD pitch-envelope
// enable/attack/decay/depth. They are two shapes only — a two-segment TOGGLE and a
// horizontal SLIDER — laid out as a vertical stack of fixed-height rows. This module lays out
// that stack and maps a slider's NORMALIZED value (0..1) to/from its handle pixel; the shell
// converts each control's engine value (frames, seconds, a fraction, a signed semitone
// depth) to/from that 0..1 with its own domain knowledge (this module stays engine-free so it
// tests without the audio core).
//
// It reuses editor_geometry's Rect + contains() (one shared geometry idiom).
#pragma once
#include <vector>
#include "editor_geometry.h" // Rect, contains — one shared geometry idiom
namespace reasampler::vst {
// Fixed control-panel metrics, exposed so the shell and tests agree.
inline constexpr int kControlRowHeight = 22; // one control row (incl. its inter-row gap)
inline constexpr int kControlRowGap = 4; // vertical gap below each row
inline constexpr int kControlLabelWidth = 92; // the label column at the row's left
inline constexpr int kSliderHandleWidth = 8; // the draggable slider handle width (px)
inline constexpr int kToggleSegments = 2; // a toggle is always two segments
// A control is one of two shapes. Toggle = a two-segment selector (the active segment
// highlights); Slider = a horizontal track with a draggable handle over a 0..1 value.
enum class ControlKind { Toggle, Slider };
// One control the shell places in the panel, in stack order. `id` is the shell's own control
// identifier (an int the shell casts from its ControlId enum) returned by the hit-test so the
// shell routes the interaction to the right engine param — this module never interprets it.
struct ControlDesc {
int id = 0;
ControlKind kind = ControlKind::Slider;
};
// The laid-out geometry of one control row: its full row rect plus the interactive sub-rect
// (the track for a Slider, the whole control area for a Toggle — the shell splits a Toggle
// into segments via toggleSegmentRect). `index` is the control's position in the stack.
struct ControlRow {
int id = 0;
ControlKind kind = ControlKind::Slider;
Rect row; // the full row (label column + control column)
Rect label; // the label column at the left
Rect control; // the control column to the right of the label (track / toggle area)
};
// Lay out `controls` as a vertical stack of fixed-height rows inside `panel`, top-down. Each
// row is kControlRowHeight tall with kControlRowGap below it; the label column takes the left
// kControlLabelWidth (clamped so it never exceeds the panel), the control column the rest. A
// row whose top falls past the panel bottom is still returned (the shell clips at paint /
// suppresses it) so the stack geometry is deterministic regardless of panel height. An empty
// control list or a degenerate panel yields an empty vector. Pure.
std::vector<ControlRow> layoutControls(const Rect& panel,
const std::vector<ControlDesc>& controls);
// The rect of segment `seg` (0..kToggleSegments-1) within a toggle control's `control` rect,
// splitting it into kToggleSegments equal segments left-to-right (the last absorbs any width
// remainder, mirror of mode_switch's segment split). An out-of-range segment or a degenerate
// control rect yields an empty rect. Pure.
Rect toggleSegmentRect(const Rect& control, int seg);
// The toggle segment a point lands on within a toggle control's `control` rect, or -1 for a
// miss (outside the control area). Pure.
int toggleSegmentHitTest(const Rect& control, int x, int y);
// The slider track sub-rect inside a slider control's `control` rect: the control inset so the
// handle (kSliderHandleWidth) stays fully within the control at value 0 and 1 (a half-handle
// margin at each end). The handle CENTER ranges across [track.left, track.right] as the value
// ranges [0,1]. The shell draws the track fill + handle here. A degenerate control yields an
// empty rect. Pure.
Rect sliderTrackRect(const Rect& control);
// The handle rect for a slider at normalized `value` (clamped to [0,1]) within `control`: a
// kSliderHandleWidth-wide bar centered at the value's position along sliderTrackRect. A
// degenerate control yields an empty rect. Pure — the inverse of valueAtPoint.
Rect sliderHandleRect(const Rect& control, double value);
// Map a point x to a normalized slider value [0,1] within `control` (the handle-center range).
// x at/left of the track start -> 0; at/right of the end -> 1; linear between. A degenerate
// track (zero movable span) -> 0. Pure — the inverse of sliderHandleRect's position map; the
// shell converts the returned 0..1 into its engine domain (frames/seconds/fraction/semitones).
double valueAtPoint(const Rect& control, int x);
// The control a point lands on, given the laid-out `rows`. Returns the control id (ControlDesc
// id) whose interactive area (a Slider's track, a Toggle's whole control area) contains the
// point, or -1 for a miss (a gap, the label column, or outside every row). The FIRST matching
// row wins (rows never overlap, so at most one matches). Pure — the shell's routing entry
// point: on a hit it reads the value (valueAtPoint / toggleSegmentHitTest) and commits.
int controlAtPoint(const std::vector<ControlRow>& rows, int x, int y);
} // namespace reasampler::vst