//! egui editor: assembly + control layout. //! //! The aggregator. Builds the editor window and lays out the heading, the meter panel //! ([`meter`]), the rolling plot ([`plot`]), and the (placeholder) per-channel slider columns. //! Each visualiser owns its GUI state and drawing in its submodule; this module wires them //! together and holds the shared [`EditorState`]. When the UI is redesigned the slider columns //! get replaced and the visualisers stay as self-contained widgets. use nih_plug::prelude::*; use nih_plug_egui::{ create_egui_editor, egui::{self, Vec2}, resizable_window::ResizableWindow, widgets, }; use std::sync::Arc; use crate::meters::Meters; use crate::params::{Codename206Params, CompressorParams}; use crate::Codename206; mod crossover; mod gain_curve; mod meter; mod plot; /// Bottom of the dB scale shared by the meters and the plot (top is 0 dBFS). const METER_FLOOR_DB: f32 = -60.0; /// GUI-side editor state (not persisted): the per-widget state for the meter panel and the plot. #[derive(Default)] struct EditorState { meter: meter::MeterState, plot: plot::PlotState, } /// Build the plugin editor over shared handles to the params and meter state. pub(crate) fn create(params: Arc, meters: Arc) -> Option> { let egui_state = params.editor_state.clone(); create_egui_editor( params.editor_state.clone(), EditorState::default(), |_, _| {}, move |egui_ctx, setter, state| { // Keep frames coming so the meters animate and the lamp can time out while open. egui_ctx.request_repaint(); // One column of controls for a single compressor channel (placeholder layout). let band_col = |ui: &mut egui::Ui, title: &str, p: &CompressorParams| { // Roughly in signal order: input drive -> low shaper -> compressor -> output. ui.strong(title); ui.label("Pre-gain"); ui.add(widgets::ParamSlider::for_param(&p.pre_gain_db, setter)); ui.add(widgets::ParamSlider::for_param(&p.detection, setter)); ui.label("Low Slope"); ui.add(widgets::ParamSlider::for_param(&p.low_slope, setter)); ui.label("Low Curve"); ui.add(widgets::ParamSlider::for_param(&p.low_curve, setter)); ui.label("Threshold"); ui.add(widgets::ParamSlider::for_param(&p.threshold_db, setter)); ui.label("Ratio"); ui.add(widgets::ParamSlider::for_param(&p.ratio, setter)); ui.label("Knee"); ui.add(widgets::ParamSlider::for_param(&p.knee_db, setter)); ui.label("Attack"); ui.add(widgets::ParamSlider::for_param(&p.attack_ms, setter)); ui.label("Release"); ui.add(widgets::ParamSlider::for_param(&p.release_ms, setter)); ui.label("Makeup"); ui.add(widgets::ParamSlider::for_param(&p.makeup_db, setter)); ui.label("Mix"); ui.add(widgets::ParamSlider::for_param(&p.mix, setter)); }; // Resizable window; vertical scroll so every control stays reachable even when the // window is small. (Placeholder layout — the redesign will replace the slider columns.) ResizableWindow::new("editor") .min_size(Vec2::new(480.0, 320.0)) .show(egui_ctx, egui_state.as_ref(), |ui| { egui::ScrollArea::vertical().show(ui, |ui| { ui.heading(Codename206::NAME); meter::draw(ui, &meters, &mut state.meter); ui.separator(); // Gain curve (left, square) beside the scrolling plot (right, fills the rest). let selected = state.plot.selected; ui.horizontal_top(|ui| { ui.vertical(|ui| gain_curve::draw(ui, ¶ms, selected)); ui.vertical(|ui| plot::draw(ui, &meters, &mut state.plot)); }); ui.separator(); crossover::draw(ui, ¶ms, setter); ui.separator(); // Global controls stacked vertically so they never overflow sideways. egui::Grid::new("globals").num_columns(2).show(ui, |ui| { ui.label("Look-ahead"); ui.add(widgets::ParamSlider::for_param(¶ms.look_ahead_ms, setter)); ui.end_row(); ui.label("Ceiling"); ui.add(widgets::ParamSlider::for_param(¶ms.output_ceiling_db, setter)); ui.end_row(); ui.label("Lim Release"); ui.add(widgets::ParamSlider::for_param(¶ms.limiter_release_ms, setter)); ui.end_row(); }); ui.separator(); ui.columns(4, |cols| { band_col(&mut cols[0], "LOW", ¶ms.low); band_col(&mut cols[1], "MID", ¶ms.mid); band_col(&mut cols[2], "HIGH", ¶ms.high); band_col(&mut cols[3], "ALL", ¶ms.all); }); }); }); }, ) }