// 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 #include namespace reasampler::instrument::ui { using util::clamp01; std::vector layoutControls(const Rect& panel, const std::vector& controls) { std::vector 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(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(x - track.x) / static_cast(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(dyPixels) / dragRangePixels); } int controlAtPoint(const std::vector& 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