Files
codename-206/src/editor/crossover.rs
T

144 lines
5.5 KiB
Rust
Raw Normal View History

// SPDX-License-Identifier: GPL-3.0-or-later
//! Draggable crossover handles — replaces the two plain crossover sliders.
//!
//! A horizontal log-frequency strip split into LOW / MID / HIGH by two draggable handles, plus a
//! number box per crossover (double-click to type an exact value). The handles enforce a dynamic
//! limit: lo/mid can never exceed mid/hi (and vice-versa), on top of each param's own range.
use nih_plug::prelude::*;
use nih_plug_egui::egui::{
self, pos2, vec2, Align2, Color32, CornerRadius, DragValue, FontId, Painter, Rect, Sense, Stroke,
};
2026-08-31 22:55:41 +09:00
use crate::params::Bari206Params;
2026-08-31 23:28:26 +09:00
use super::palette;
/// Height of the handle strip.
const GRAPH_H: f32 = 54.0;
/// Displayed frequency axis (log), independent of the params' own ranges.
const DISP_MIN_HZ: f32 = 20.0;
const DISP_MAX_HZ: f32 = 20_000.0;
fn log_span() -> f32 {
DISP_MAX_HZ.ln() - DISP_MIN_HZ.ln()
}
fn fmt_hz(hz: f32) -> String {
if hz >= 1000.0 {
format!("{:.2} kHz", hz / 1000.0)
} else {
format!("{:.0} Hz", hz)
}
}
2026-08-31 22:55:41 +09:00
pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, setter: &ParamSetter) {
let lo = params.crossover_low_hz.value();
let hi = params.crossover_high_hz.value();
// Each param's own min/max (normalized 0/1 map to the range ends).
let lo_min = params.crossover_low_hz.preview_plain(0.0);
let lo_max = params.crossover_low_hz.preview_plain(1.0);
let hi_min = params.crossover_high_hz.preview_plain(0.0);
let hi_max = params.crossover_high_hz.preview_plain(1.0);
// Dynamic limits so the two never cross: lo <= hi.
let lo_upper = lo_max.min(hi);
let hi_lower = hi_min.max(lo);
ui.label("Crossover");
let (rect, _) =
ui.allocate_exact_size(vec2(ui.available_width(), GRAPH_H), Sense::hover());
let p = ui.painter_at(rect);
let (left, right, top, bottom) =
(rect.left() + 2.0, rect.right() - 2.0, rect.top() + 2.0, rect.bottom() - 2.0);
let width = right - left;
let x_for = |hz: f32| left + (hz.max(1.0).ln() - DISP_MIN_HZ.ln()) / log_span() * width;
let xlo = x_for(lo);
let xhi = x_for(hi);
// Three band regions.
2026-08-31 23:28:26 +09:00
p.rect_filled(Rect::from_min_max(pos2(left, top), pos2(xlo, bottom)), CornerRadius::ZERO, palette::CHANNELS[0].strip);
p.rect_filled(Rect::from_min_max(pos2(xlo, top), pos2(xhi, bottom)), CornerRadius::ZERO, palette::CHANNELS[1].strip);
p.rect_filled(Rect::from_min_max(pos2(xhi, top), pos2(right, bottom)), CornerRadius::ZERO, palette::CHANNELS[2].strip);
let band_label = |cx: f32, t: &str| {
2026-08-31 23:28:26 +09:00
p.text(pos2(cx, top + 2.0), Align2::CENTER_TOP, t, FontId::proportional(11.0), palette::TEXT);
};
band_label((left + xlo) * 0.5, "LOW");
band_label((xlo + xhi) * 0.5, "MID");
band_label((xhi + right) * 0.5, "HIGH");
2026-08-31 23:28:26 +09:00
handle(ui, &p, setter, "xover_lo", xlo, lo, &params.crossover_low_hz, lo_min, lo_upper, left, width, top, bottom, palette::CHANNELS[0].handle);
handle(ui, &p, setter, "xover_hi", xhi, hi, &params.crossover_high_hz, hi_lower, hi_max, left, width, top, bottom, palette::CHANNELS[2].handle);
// Number boxes (double-click to type). Clamped to the same dynamic limits.
ui.horizontal(|ui| {
ui.label("Lo/Mid");
let mut v = lo as f64;
if ui
.add(DragValue::new(&mut v).range(lo_min as f64..=lo_upper as f64).speed(0.5).suffix(" Hz"))
.changed()
{
set_clamped(setter, &params.crossover_low_hz, v as f32, lo_min, lo_upper);
}
ui.add_space(16.0);
ui.label("Mid/Hi");
let mut v = hi as f64;
if ui
.add(DragValue::new(&mut v).range(hi_lower as f64..=hi_max as f64).speed(2.0).suffix(" Hz"))
.changed()
{
set_clamped(setter, &params.crossover_high_hz, v as f32, hi_lower, hi_max);
}
});
}
/// One draggable vertical handle. Drives `param` from the pointer's x (log-frequency), clamped to
/// `[min, max]` (which already encodes the dynamic lo<=hi limit), with proper begin/end gestures.
#[allow(clippy::too_many_arguments)]
fn handle(
ui: &egui::Ui,
p: &Painter,
setter: &ParamSetter,
id_salt: &str,
x: f32,
hz: f32,
param: &FloatParam,
min: f32,
max: f32,
left: f32,
width: f32,
top: f32,
bottom: f32,
color: Color32,
) {
let hit = Rect::from_min_max(pos2(x - 5.0, top), pos2(x + 5.0, bottom));
let resp = ui
.interact(hit, ui.id().with(id_salt), Sense::drag())
.on_hover_cursor(egui::CursorIcon::ResizeHorizontal);
if resp.drag_started() {
setter.begin_set_parameter(param);
}
if resp.dragged() {
if let Some(pos) = resp.interact_pointer_pos() {
let frac = ((pos.x - left) / width).clamp(0.0, 1.0);
let new = (DISP_MIN_HZ.ln() + frac * log_span()).exp().clamp(min, max);
setter.set_parameter(param, new);
}
}
if resp.drag_stopped() {
setter.end_set_parameter(param);
}
2026-08-31 23:28:26 +09:00
let col = if resp.dragged() || resp.hovered() { palette::TEXT_HOVER } else { color };
p.line_segment([pos2(x, top), pos2(x, bottom)], Stroke::new(2.0_f32, col));
2026-08-31 23:28:26 +09:00
p.text(pos2(x, bottom - 1.0), Align2::CENTER_BOTTOM, fmt_hz(hz), FontId::proportional(10.0), palette::TEXT_BRIGHT);
}
/// Set a param to `value` clamped to `[min, max]`, wrapped in its own gesture (for the number box).
fn set_clamped(setter: &ParamSetter, param: &FloatParam, value: f32, min: f32, max: f32) {
setter.begin_set_parameter(param);
setter.set_parameter(param, value.clamp(min, max));
setter.end_set_parameter(param);
}