1361d7c919
Colour was 35 call sites and ~29 distinct literals spread across four widget files, with no egui Style or Visuals set at all - so the hand-painted widgets were hand-coloured while ParamSlider and the labels sat on stock egui defaults, which is most of why the UI did not read as one thing. The audit turned up more than duplication. Two different panel blacks ((16,16,20) and (20,20,24)) for the same intended surface; three reds, two greens and two blues close enough to look like mistakes; eight greys doing about four jobs. And a genuine collision: blue/green/orange meant LOW/MID/HIGH in the crossover and input/output/gain-reduction in the scope, so orange meant "the high band" in one widget and "gain reduction" in the one beside it. The rule that resolves it, and that the module documents: channel hue is chrome, functional colour is data. A pixel showing a value comes from the functional palette and means the same thing in every channel; a pixel saying which channel you are looking at comes from that channel's hue and never touches the data. The scope turns out not to need arbitration at all - it is tabbed, one channel at a time, so its three traces never compete with a hue. Two tiers. Seeds are a short hand-picked list: four channel hues, the functional colours, six neutrals. Everything else derives from a hue via `channel()`, so recolouring a channel is one constant. Derivation is const fn - including the HSV conversion, written out because ecolor's is not const - so the whole table is built at compile time and costs nothing at runtime. Chosen so the hues describe the colours already in use: the derived crossover strips land within 1-3 units of the literals they replace. The two drag handles shift by ~17 on one channel, being now consistently saturated with the rest. Also lands the one deliberate visual change: the meter's four cells were identical and distinguishable only by the label underneath, which is where channel identity was hardest to read. They now carry a dark channel wash and an accent rule, while the bars stay strictly functional. Visuals are wired from the same seeds in the editor's build closure, so egui's own widgets stop looking foreign. meter.rs's local level_color is gone; the palette owns the headroom ramp rather than there being two places to edit it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LU1NRL7fAaZTtFMTTPn2b1
119 lines
5.6 KiB
Rust
119 lines
5.6 KiB
Rust
// SPDX-License-Identifier: GPL-3.0-or-later
|
|
//! 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::{Bari206Params, CompressorParams};
|
|
use crate::Bari206;
|
|
|
|
mod crossover;
|
|
mod gain_curve;
|
|
mod meter;
|
|
mod palette;
|
|
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<Bari206Params>, meters: Arc<Meters>) -> Option<Box<dyn Editor>> {
|
|
let egui_state = params.editor_state.clone();
|
|
create_egui_editor(
|
|
params.editor_state.clone(),
|
|
EditorState::default(),
|
|
|egui_ctx, _| palette::apply_visuals(egui_ctx),
|
|
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(Bari206::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, &meters));
|
|
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);
|
|
});
|
|
});
|
|
});
|
|
},
|
|
)
|
|
}
|