Files
reasampler/src/core/instrument/ui/param_slider.cpp
T

160 lines
6.1 KiB
C++

// param_slider.cpp — see param_slider.h. PURE control-surface geometry for the S12/S15/S16
// editor parameter panel. No host types; only the shared Rect + contains().
#include "core/instrument/ui/param_slider.h"
#include "core/util/clamp01.h"
#include <algorithm>
#include <cmath>
namespace reasampler::instrument::ui {
using util::clamp01; // the ONE unit-interval clamp (Q-W1, T4-24)
std::vector<ControlRow> layoutControls(const Rect& panel,
const std::vector<ControlDesc>& controls) {
std::vector<ControlRow> out;
if (controls.empty() || panel.width <= 0 || panel.height <= 0) return out;
out.reserve(controls.size());
// The label column is clamped so a narrow panel still leaves a control column.
const int labelW = (std::min)(kControlLabelWidth, (std::max)(0, panel.width / 2));
int rowTop = panel.y;
for (const ControlDesc& d : controls) {
ControlRow r;
r.id = d.id;
r.kind = d.kind;
const int rowBottom = rowTop + kControlRowHeight;
r.row = Rect::ltrb(panel.x, rowTop, panel.right(), rowBottom);
r.label = Rect::ltrb(panel.x, rowTop, panel.x + labelW, rowBottom);
r.control = Rect::ltrb(panel.x + labelW, rowTop, panel.right(), rowBottom);
out.push_back(r);
rowTop = rowBottom + kControlRowGap;
}
return out;
}
Rect toggleSegmentRect(const Rect& control, int seg) {
if (seg < 0 || seg >= kToggleSegments) return Rect{};
const int w = control.width;
if (w <= 0 || control.height <= 0) return Rect{};
const int segW = w / kToggleSegments;
const int left = control.x + seg * segW;
// The last segment absorbs the width remainder so the segments tile the whole control.
const int right = (seg == kToggleSegments - 1) ? control.right() : left + segW;
return Rect::ltrb(left, control.y, right, control.bottom());
}
int toggleSegmentHitTest(const Rect& control, int x, int y) {
if (!contains(control, x, y)) return -1;
for (int seg = 0; seg < kToggleSegments; ++seg) {
if (contains(toggleSegmentRect(control, seg), x, y)) return seg;
}
return -1;
}
Rect sliderTrackRect(const Rect& control) {
// Inset a half-handle at each end so the handle stays fully inside the control at value
// 0 and 1. The handle CENTER ranges across [track.x, track.right()].
const int half = kSliderHandleWidth / 2;
if (control.width <= kSliderHandleWidth || control.height <= 0) return Rect{};
return Rect::ltrb(control.x + half, control.y, control.right() - half, control.bottom());
}
Rect sliderHandleRect(const Rect& control, double value) {
const Rect track = sliderTrackRect(control);
if (track.width <= 0) return Rect{};
if (value < 0.0) value = 0.0;
if (value > 1.0) value = 1.0;
const int span = track.width; // handle-center movable span
const int centerX = track.x + static_cast<int>(value * span + 0.5);
const int half = kSliderHandleWidth / 2;
return Rect::ltrb(centerX - half, control.y, centerX - half + kSliderHandleWidth,
control.bottom());
}
double valueAtPoint(const Rect& control, int x) {
const Rect track = sliderTrackRect(control);
const int span = track.width;
if (span <= 0) return 0.0;
if (x <= track.x) return 0.0;
if (x >= track.right()) return 1.0;
return static_cast<double>(x - track.x) / static_cast<double>(span);
}
// --- Radial knob (Wave A FA4) ---------------------------------------------------------
namespace {
constexpr double kPi = 3.14159265358979323846;
// Normalize an angle in degrees to [0, 360).
double normDeg(double deg) {
deg = std::fmod(deg, 360.0);
if (deg < 0.0) deg += 360.0;
// Guard: fmod can return exactly 360.0 on some implementations due to floating-point
// rounding; fold it back to 0.
if (deg >= 360.0) deg -= 360.0;
return deg;
}
} // namespace
KnobGeometry computeKnob(const Rect& cell) {
if (cell.width <= 0 || cell.height <= 0) return KnobGeometry{};
KnobGeometry g;
g.centerX = (cell.x + cell.right()) / 2.0;
g.centerY = (cell.y + cell.bottom()) / 2.0;
g.radius = (std::min)(cell.width, cell.height) / 2.0;
return g;
}
bool knobHitTest(const KnobGeometry& knob, int x, int y) {
if (knob.radius <= 0.0) return false;
const double dx = x - knob.centerX;
const double dy = y - knob.centerY;
// Boundary exclusive: matches the module's half-open Rect convention.
return dx * dx + dy * dy < knob.radius * knob.radius;
}
double knobSweepDeg(const KnobArc& arc) {
const double sweep = normDeg(arc.endDeg) - normDeg(arc.startDeg);
// An end at-or-behind the start wraps clockwise past 12 o'clock; equal angles mean a
// full circle.
return sweep <= 0.0 ? sweep + 360.0 : sweep;
}
double knobValueAngleDeg(const KnobArc& arc, double value) {
return normDeg(normDeg(arc.startDeg) + knobSweepDeg(arc) * clamp01(value));
}
KnobPoint knobNeedlePoint(const KnobGeometry& knob, const KnobArc& arc, double value) {
// Clock angle -> screen direction: 0° points up (-y), 90° points right (+x).
const double rad = knobValueAngleDeg(arc, value) * kPi / 180.0;
return KnobPoint{knob.centerX + knob.radius * std::sin(rad),
knob.centerY - knob.radius * std::cos(rad)};
}
double knobDragValue(double startValue, int dyPixels, int dragRangePixels) {
const double start = clamp01(startValue);
if (dragRangePixels <= 0) return start;
// Screen y grows downward: an upward drag (negative dy) increases the value.
return clamp01(start - static_cast<double>(dyPixels) / dragRangePixels);
}
int controlAtPoint(const std::vector<ControlRow>& rows, int x, int y) {
for (const ControlRow& r : rows) {
if (r.kind == ControlKind::Toggle) {
if (contains(r.control, x, y)) return r.id;
} else if (r.kind == ControlKind::Knob) {
if (knobHitTest(computeKnob(r.control), x, y)) return r.id;
} else { // Slider — the interactive area is the track
if (contains(sliderTrackRect(r.control), x, y)) return r.id;
}
}
return -1;
}
} // namespace reasampler::instrument::ui