refactor: split editor.rs into editor/ widget modules

Move the egui editor from a single editor.rs into an editor/ module: mod.rs
(aggregator: create(), EditorState, layout, placeholder slider columns),
meter.rs (|L|GR|R| meters + ceiling lamp, owns MeterState), and plot.rs
(rolling in/out/GR plot, owns PlotState + PlotHistory). Each visualiser owns
its GUI state; the aggregator composes them. No behavior change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Mikkeli Matlock
2026-06-24 05:01:50 +09:00
parent 3ae860188e
commit eb2499bad3
4 changed files with 427 additions and 394 deletions
+184
View File
@@ -0,0 +1,184 @@
//! Rolling input/output/gain-reduction plot with per-channel tabs and a flow-speed selector.
//!
//! Histories for all four channels run continuously (cheap), so switching tabs shows that
//! channel's existing history. The scroll is time-based (a column every `window/PLOT_N` seconds)
//! so the window length — the flow speed — stays accurate regardless of frame rate, with
//! peak-preserving max accumulation between columns. Fed by the raw block-peak [`Meters`] feed.
use nih_plug::prelude::*;
use nih_plug_egui::egui::{self, pos2, vec2, Align2, Color32, CornerRadius, FontId, Sense, Stroke};
use std::sync::atomic::Ordering;
use super::METER_FLOOR_DB;
use crate::meters::{Meters, NUM_CHANNELS};
/// Height of the plot panel.
const PLOT_PANEL_H: f32 = 150.0;
/// Number of columns held in the history ring.
const PLOT_N: usize = 256;
const COLOR_IN: Color32 = Color32::from_rgb(90, 170, 235);
const COLOR_OUT: Color32 = Color32::from_rgb(90, 200, 110);
const COLOR_GR: Color32 = Color32::from_rgb(240, 150, 60);
/// Rolling history for all channels: a per-channel ring of (in_db, out_db, gr_db) columns.
struct PlotHistory {
in_db: [[f32; PLOT_N]; NUM_CHANNELS],
out_db: [[f32; PLOT_N]; NUM_CHANNELS],
gr_db: [[f32; PLOT_N]; NUM_CHANNELS],
write: usize,
len: usize,
}
impl Default for PlotHistory {
fn default() -> Self {
Self {
in_db: [[METER_FLOOR_DB; PLOT_N]; NUM_CHANNELS],
out_db: [[METER_FLOOR_DB; PLOT_N]; NUM_CHANNELS],
gr_db: [[0.0; PLOT_N]; NUM_CHANNELS],
write: 0,
len: 0,
}
}
}
impl PlotHistory {
/// Append one column of (in_db, out_db, gr_db) per channel.
fn push(&mut self, samples: &[(f32, f32, f32); NUM_CHANNELS]) {
for i in 0..NUM_CHANNELS {
self.in_db[i][self.write] = samples[i].0;
self.out_db[i][self.write] = samples[i].1;
self.gr_db[i][self.write] = samples[i].2;
}
self.write = (self.write + 1) % PLOT_N;
self.len = (self.len + 1).min(PLOT_N);
}
}
/// GUI-side state for the plot: selected channel, history ring, and time-based scroll cadence.
pub(super) struct PlotState {
/// Channel shown in the plot (0..NUM_CHANNELS: low/mid/high/all).
selected: usize,
history: PlotHistory,
/// Seconds of history shown across the full plot width — the flow speed (smaller = faster).
window_s: f64,
/// egui time of the last column pushed to the history ring (the cadence clock).
last_push: f64,
/// Per-channel max accumulator (in_db, out_db, gr_db) for the column currently being built.
acc: [(f32, f32, f32); NUM_CHANNELS],
}
impl Default for PlotState {
fn default() -> Self {
Self {
selected: 0,
history: PlotHistory::default(),
window_s: 5.0,
last_push: 0.0,
acc: [(METER_FLOOR_DB, METER_FLOOR_DB, 0.0); NUM_CHANNELS],
}
}
}
/// Draw the scrolling in/out/gain-reduction plot for the selected channel, plus the channel tabs
/// and flow-speed selector. History for all channels advances every frame regardless of the tab.
pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) {
let now = ui.ctx().input(|i| i.time);
// (Re)initialise the cadence clock on first use or after a long gap (e.g. tab hidden).
if state.last_push <= 0.0 || now - state.last_push > state.window_s {
state.last_push = now;
}
// Accumulate this frame's block peaks into the column currently being built.
for i in 0..NUM_CHANNELS {
let in_db = util::gain_to_db(meters.plot_in[i].load(Ordering::Relaxed));
let out_db = util::gain_to_db(meters.plot_out[i].load(Ordering::Relaxed));
let gr = meters.plot_gr[i].load(Ordering::Relaxed);
state.acc[i].0 = state.acc[i].0.max(in_db);
state.acc[i].1 = state.acc[i].1.max(out_db);
state.acc[i].2 = state.acc[i].2.max(gr);
}
// Emit columns on a fixed time grid so the window length stays accurate regardless of the
// frame rate. The while loop is bounded by PLOT_N thanks to the resync above.
let dt_col = state.window_s / PLOT_N as f64;
let mut pushed = false;
while now - state.last_push >= dt_col {
state.history.push(&state.acc);
state.last_push += dt_col;
pushed = true;
}
if pushed {
state.acc = [(METER_FLOOR_DB, METER_FLOOR_DB, 0.0); NUM_CHANNELS];
}
// Channel tabs + flow-speed selector + legend.
let labels = ["LOW", "MID", "HIGH", "ALL"];
let speeds = [2.0f64, 5.0, 15.0, 45.0];
ui.horizontal(|ui| {
ui.label("Plot:");
for (i, l) in labels.iter().enumerate() {
ui.selectable_value(&mut state.selected, i, *l);
}
ui.separator();
ui.label("Speed:");
let mut speed_changed = false;
for &w in &speeds {
if ui.selectable_value(&mut state.window_s, w, format!("{w:.0}s")).changed() {
speed_changed = true;
}
}
if speed_changed {
// Cadence changed: start the history fresh so the time axis is consistent.
state.history = PlotHistory::default();
state.acc = [(METER_FLOOR_DB, METER_FLOOR_DB, 0.0); NUM_CHANNELS];
state.last_push = now;
}
ui.separator();
ui.colored_label(COLOR_IN, "in");
ui.colored_label(COLOR_OUT, "out");
ui.colored_label(COLOR_GR, "GR");
});
let (rect, _) =
ui.allocate_exact_size(vec2(ui.available_width(), PLOT_PANEL_H), Sense::hover());
let p = ui.painter_at(rect);
p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(16, 16, 20));
let (top, bottom, left, right) =
(rect.top() + 4.0, rect.bottom() - 4.0, rect.left() + 4.0, rect.right() - 4.0);
let width = right - left;
let y_for_db = |db: f32| -> f32 {
let frac = ((db - METER_FLOOR_DB) / -METER_FLOOR_DB).clamp(0.0, 1.0);
bottom - frac * (bottom - top)
};
// Gridlines (dB).
for &g in &[0.0f32, -12.0, -24.0, -48.0] {
let y = y_for_db(g);
p.line_segment([pos2(left, y), pos2(right, y)], Stroke::new(1.0, Color32::from_gray(40)));
p.text(
pos2(left + 2.0, y),
Align2::LEFT_BOTTOM,
format!("{g:.0}"),
FontId::proportional(9.0),
Color32::from_gray(90),
);
}
let c = state.selected.min(NUM_CHANNELS - 1);
let (write, len) = (state.history.write, state.history.len);
if len >= 2 {
let draw_series = |series: &[f32; PLOT_N], to_db: &dyn Fn(f32) -> f32, color: Color32| {
let mut pts = Vec::with_capacity(len);
for k in 0..len {
let idx = (write + PLOT_N - len + k) % PLOT_N;
let pos = (PLOT_N - len + k) as f32 / (PLOT_N - 1) as f32; // newest hugs the right
pts.push(pos2(left + pos * width, y_for_db(to_db(series[idx]))));
}
p.add(egui::Shape::line(pts, Stroke::new(1.5, color)));
};
draw_series(&state.history.in_db[c], &|db| db, COLOR_IN);
draw_series(&state.history.out_db[c], &|db| db, COLOR_OUT);
// GR hangs from the 0 dB line: a reduction of X dB is drawn at the -X gridline.
draw_series(&state.history.gr_db[c], &|gr| -gr, COLOR_GR);
}
}