159 lines
6.0 KiB
C++
159 lines
6.0 KiB
C++
// param_slider.cpp — see param_slider.h. Pure control-surface geometry; no host types.
|
|
|
|
#include "core/instrument/ui/param_slider.h"
|
|
|
|
#include "core/util/clamp01.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace reasampler::instrument::ui {
|
|
|
|
using util::clamp01;
|
|
|
|
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 -----------------------------------------------------------------------
|
|
|
|
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
|