diff --git a/src/editor/crossover.rs b/src/editor/crossover.rs index 6e6736a..f644c15 100644 --- a/src/editor/crossover.rs +++ b/src/editor/crossover.rs @@ -11,6 +11,7 @@ use nih_plug_egui::egui::{ }; use crate::params::Bari206Params; +use super::palette; /// Height of the handle strip. const GRAPH_H: f32 = 54.0; @@ -55,19 +56,19 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, setter: &ParamSett let xhi = x_for(hi); // Three band regions. - p.rect_filled(Rect::from_min_max(pos2(left, top), pos2(xlo, bottom)), CornerRadius::ZERO, Color32::from_rgb(28, 38, 52)); - p.rect_filled(Rect::from_min_max(pos2(xlo, top), pos2(xhi, bottom)), CornerRadius::ZERO, Color32::from_rgb(30, 48, 36)); - p.rect_filled(Rect::from_min_max(pos2(xhi, top), pos2(right, bottom)), CornerRadius::ZERO, Color32::from_rgb(52, 38, 30)); + 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| { - p.text(pos2(cx, top + 2.0), Align2::CENTER_TOP, t, FontId::proportional(11.0), Color32::from_gray(160)); + 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"); - handle(ui, &p, setter, "xover_lo", xlo, lo, ¶ms.crossover_low_hz, lo_min, lo_upper, left, width, top, bottom, Color32::from_rgb(120, 170, 230)); - handle(ui, &p, setter, "xover_hi", xhi, hi, ¶ms.crossover_high_hz, hi_lower, hi_max, left, width, top, bottom, Color32::from_rgb(230, 150, 120)); + handle(ui, &p, setter, "xover_lo", xlo, lo, ¶ms.crossover_low_hz, lo_min, lo_upper, left, width, top, bottom, palette::CHANNELS[0].handle); + handle(ui, &p, setter, "xover_hi", xhi, hi, ¶ms.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| { @@ -129,9 +130,9 @@ fn handle( setter.end_set_parameter(param); } - let col = if resp.dragged() || resp.hovered() { Color32::WHITE } else { color }; + 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, col)); - p.text(pos2(x, bottom - 1.0), Align2::CENTER_BOTTOM, fmt_hz(hz), FontId::proportional(10.0), Color32::from_gray(220)); + 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). diff --git a/src/editor/gain_curve.rs b/src/editor/gain_curve.rs index f81c69c..b566c14 100644 --- a/src/editor/gain_curve.rs +++ b/src/editor/gain_curve.rs @@ -5,12 +5,13 @@ //! the channel's current input level — its right edge rides the curve (width = input, height = out). use nih_plug::prelude::util; -use nih_plug_egui::egui::{self, pos2, vec2, Align2, Color32, CornerRadius, FontId, Sense, Stroke}; +use nih_plug_egui::egui::{self, pos2, vec2, Align2, CornerRadius, FontId, Sense, Stroke}; use std::sync::atomic::Ordering; use crate::dsp::compressor::Compressor; use crate::meters::Meters; use crate::params::Bari206Params; +use super::palette; /// Side length of the square plot. const CURVE_SIZE: f32 = 150.0; @@ -37,7 +38,7 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, selected: usize, m ui.label(format!("Curve: {}", labels[ch])); let (rect, _) = ui.allocate_exact_size(vec2(CURVE_SIZE, CURVE_SIZE), Sense::hover()); let p = ui.painter_at(rect); - p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(16, 16, 20)); + p.rect_filled(rect, CornerRadius::ZERO, palette::SURFACE); let inset = 2.0; let (left, right, top, bottom) = @@ -48,17 +49,17 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, selected: usize, m let y_for = |db: f32| bottom - (db - FLOOR_DB) / -FLOOR_DB * h; // -6 dBFS reference lines on both axes. - let g6 = Color32::from_gray(38); + let g6 = palette::GRID_FAINT; let x6 = x_for(-6.0); let y6 = y_for(-6.0); p.line_segment([pos2(x6, top), pos2(x6, bottom)], Stroke::new(1.0, g6)); p.line_segment([pos2(left, y6), pos2(right, y6)], Stroke::new(1.0, g6)); // Unity reference (out = in), bottom-left to top-right. - p.line_segment([pos2(left, bottom), pos2(right, top)], Stroke::new(1.0, Color32::from_gray(45))); + p.line_segment([pos2(left, bottom), pos2(right, top)], Stroke::new(1.0, palette::GRID)); // Threshold marker on the input axis — shifted left by pre-gain (the comp sees in + pre). let tx = x_for((threshold - pre).clamp(FLOOR_DB, 0.0)); - p.line_segment([pos2(tx, top), pos2(tx, bottom)], Stroke::new(1.0, Color32::from_rgb(80, 60, 45))); + p.line_segment([pos2(tx, top), pos2(tx, bottom)], Stroke::new(1.0, palette::THRESHOLD_LINE)); // Full wet transfer: drive into the comp, then makeup. (Mix not folded in.) let n = 96; @@ -74,7 +75,7 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, selected: usize, m let driven_now = util::gain_to_db(meters.input_level[ch].load(Ordering::Relaxed)); let ext_in = (driven_now - pre).clamp(FLOOR_DB, 0.0); // external input -> curve x let x_now = x_for(ext_in); - let fill_col = Color32::from_rgba_unmultiplied(120, 200, 160, 45); + let fill_col = palette::with_alpha(palette::CURVE, 45); for seg in pts.windows(2) { let a = seg[0]; let mut b = seg[1]; @@ -92,16 +93,16 @@ pub(super) fn draw(ui: &mut egui::Ui, params: &Bari206Params, selected: usize, m )); } - p.add(egui::Shape::line(pts, Stroke::new(1.6, Color32::from_rgb(120, 200, 160)))); + p.add(egui::Shape::line(pts, Stroke::new(1.6, palette::CURVE))); // Operating-point dot, on the curve at the current input. let driven = ext_in + pre; let gr = Compressor::gain_computer(driven, threshold, ratio, knee, low_slope, low_curve); let out_op = (driven + gr + makeup).clamp(FLOOR_DB, 0.0); - p.circle_filled(pos2(x_now, y_for(out_op)), 3.0, Color32::from_rgb(235, 240, 235)); + p.circle_filled(pos2(x_now, y_for(out_op)), 3.0, palette::OPERATING_POINT); // Corner dB ticks + the -6 dB reference. - p.text(pos2(left + 1.0, top + 1.0), Align2::LEFT_TOP, "0", FontId::proportional(9.0), Color32::from_gray(90)); - p.text(pos2(left + 1.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-60", FontId::proportional(9.0), Color32::from_gray(90)); - p.text(pos2(x6 + 2.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-6", FontId::proportional(9.0), Color32::from_gray(80)); + p.text(pos2(left + 1.0, top + 1.0), Align2::LEFT_TOP, "0", FontId::proportional(9.0), palette::TEXT_DIM); + p.text(pos2(left + 1.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-60", FontId::proportional(9.0), palette::TEXT_DIM); + p.text(pos2(x6 + 2.0, bottom - 1.0), Align2::LEFT_BOTTOM, "-6", FontId::proportional(9.0), palette::TEXT_DIM); } diff --git a/src/editor/meter.rs b/src/editor/meter.rs index 89f9629..a4e078e 100644 --- a/src/editor/meter.rs +++ b/src/editor/meter.rs @@ -15,6 +15,7 @@ use std::sync::atomic::Ordering; use super::METER_FLOOR_DB; use crate::meters::{Meters, NUM_CHANNELS}; +use super::palette; /// Full-scale of the gain-reduction bar (fills downward from the top). const GR_FULL_DB: f32 = 24.0; @@ -47,7 +48,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) { let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), METER_PANEL_H), Sense::hover()); let p = ui.painter_at(rect); - p.rect_filled(rect, CornerRadius::ZERO, Color32::from_rgb(20, 20, 24)); + p.rect_filled(rect, CornerRadius::ZERO, palette::SURFACE_RAISED); let top = rect.top() + 22.0; // leave a row at the top for the lamps let bottom = rect.bottom() - 18.0; // and a row at the bottom for the labels @@ -58,6 +59,22 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) { for i in 0..NUM_CHANNELS { let cell_left = rect.left() + i as f32 * cell_w; + + // Channel chrome. The four clusters are otherwise identical and told apart only by the + // label underneath, which is the one place channel identity is hardest to read. A dark + // wash behind the cell plus an accent rule under the bars fixes that without touching + // the bars themselves - see `palette`: hue is chrome, functional colour is data. + let cell = Rect::from_min_max( + pos2(cell_left + 1.0, rect.top()), + pos2(cell_left + cell_w - 1.0, rect.bottom()), + ); + p.rect_filled(cell, CornerRadius::ZERO, palette::CHANNELS[i].chrome); + p.rect_filled( + Rect::from_min_max(pos2(cell.left(), bottom + 2.0), pos2(cell.right(), bottom + 4.0)), + CornerRadius::ZERO, + palette::CHANNELS[i].edge, + ); + let group_w = bar_w * 3.0 + gap * 2.0; let bx = cell_left + (cell_w - group_w) * 0.5; @@ -71,16 +88,16 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) { let gr_db = meters.gain_reduction_db[i].load(Ordering::Relaxed); let gr_frac = (gr_db / GR_FULL_DB).clamp(0.0, 1.0); - v_bar(&p, bx, bar_w, top, bottom, l_frac, level_color(l_db), false); - v_bar(&p, bx + bar_w + gap, bar_w, top, bottom, gr_frac, Color32::from_rgb(240, 150, 60), true); - v_bar(&p, bx + 2.0 * (bar_w + gap), bar_w, top, bottom, r_frac, level_color(r_db), false); + v_bar(&p, bx, bar_w, top, bottom, l_frac, palette::level_color(l_db), false); + v_bar(&p, bx + bar_w + gap, bar_w, top, bottom, gr_frac, palette::SIGNAL_GR, true); + v_bar(&p, bx + 2.0 * (bar_w + gap), bar_w, top, bottom, r_frac, palette::level_color(r_db), false); p.text( pos2(cell_left + cell_w * 0.5, rect.bottom() - 2.0), Align2::CENTER_BOTTOM, labels[i], FontId::proportional(12.0), - Color32::from_gray(200), + palette::TEXT_BRIGHT, ); // Per-channel lamp: latch on output reaching 0 dBFS; the ALL channel also latches when the @@ -109,9 +126,9 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) { } } let lamp = if state.ceiling_trigger[i].is_some() { - Color32::from_rgb(255, 40, 40) + palette::LAMP_ON } else { - Color32::from_rgb(40, 12, 12) + palette::LAMP_OFF }; p.circle_filled(lamp_center, 5.0, lamp); } @@ -120,7 +137,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut MeterState) { /// Draw a vertical bar within `[top, bottom]`. `frac` is 0..1; `from_top` fills downward from the /// top (gain reduction) instead of upward from the bottom (level). fn v_bar(p: &Painter, x: f32, w: f32, top: f32, bottom: f32, frac: f32, fill: Color32, from_top: bool) { - let track = Color32::from_rgb(34, 34, 40); + let track = palette::TRACK; p.rect_filled(Rect::from_min_max(pos2(x, top), pos2(x + w, bottom)), CornerRadius::ZERO, track); let h = (bottom - top) * frac.clamp(0.0, 1.0); @@ -131,14 +148,3 @@ fn v_bar(p: &Painter, x: f32, w: f32, top: f32, bottom: f32, frac: f32, fill: Co }; p.rect_filled(filled, CornerRadius::ZERO, fill); } - -/// Level-bar colour: green below -6 dB, yellow approaching, red near 0 dBFS. -fn level_color(db: f32) -> Color32 { - if db >= -1.0 { - Color32::from_rgb(235, 70, 60) - } else if db >= -6.0 { - Color32::from_rgb(230, 200, 70) - } else { - Color32::from_rgb(90, 200, 110) - } -} diff --git a/src/editor/mod.rs b/src/editor/mod.rs index 69c4444..f0c9c19 100644 --- a/src/editor/mod.rs +++ b/src/editor/mod.rs @@ -23,6 +23,7 @@ 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). @@ -41,7 +42,7 @@ pub(crate) fn create(params: Arc, meters: Arc) -> Option< 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(); diff --git a/src/editor/palette.rs b/src/editor/palette.rs new file mode 100644 index 0000000..b341f79 --- /dev/null +++ b/src/editor/palette.rs @@ -0,0 +1,265 @@ +// SPDX-License-Identifier: GPL-3.0-or-later +//! The plugin's colour system. +//! +//! # The rule +//! +//! **Channel hue is chrome; functional colour is data.** Before picking a colour, ask what the +//! pixel is for. Is it showing a *value* — a level, a reduction, a clip? Then it comes from the +//! functional palette, and it means the same thing in every channel. Is it telling you *which +//! channel you are looking at*? Then it comes from that channel's hue, and it never touches the +//! data itself. +//! +//! This is what keeps the two systems from fighting. An earlier version of this UI used blue, +//! green and orange for LOW/MID/HIGH in the crossover *and* for input/output/gain-reduction in +//! the scope, so orange meant "the high band" in one widget and "gain reduction" in the one next +//! to it. +//! +//! # The two tiers +//! +//! 1. **Seeds** — the short, hand-picked list in the first half of this file. This is the part +//! you edit. Everything else is derived from it. +//! 2. **Derived** — [`Channel`] palettes built from a hue by [`channel`]. Change a hue and every +//! tint, edge and strip using it moves with it. +//! +//! All derivation is `const fn`, so the whole table is computed at compile time and costs nothing +//! at runtime. That is also why it uses plain HSV rather than `ecolor::Hsva`, whose conversion is +//! not `const`. +//! +//! # Editing this file +//! +//! * To recolour a channel, change one `HUE_*` constant. Never hand-edit a derived value. +//! * To retune the meter's headroom ramp, edit [`LEVEL_OK`] / [`LEVEL_WARN`] / [`LEVEL_HOT`] and +//! the thresholds in [`level_color`]. Both are deliberately literal rather than interpolated, +//! so you can set each one by eye without fighting a gradient. +//! * To change how loudly channel identity reads, edit the `(saturation, value)` pairs in +//! [`channel`]. +//! * Anything new belongs here and must be reachable from a seed — a colour written at a call +//! site is the next magic number. + +use nih_plug_egui::egui::Color32; + +// =========================================================================== +// TIER 1 — SEEDS. This is the part to edit. +// =========================================================================== + +// --- Channel identity ------------------------------------------------------ +// Hues in degrees: 0 = red, 120 = green, 240 = blue. Only the hue is chosen here; saturation and +// brightness are decided per role by `channel()`, so a channel reads as the same colour whether +// it is a 12%-brightness background wash or a full-strength drag handle. +// +// Keep neighbours far enough apart to survive being dimmed — most chrome lives at low +// saturation, where close hues merge. Roughly 60–90 degrees apart works; below ~40 does not. +pub const HUE_LOW: f32 = 212.0; // blue +pub const HUE_MID: f32 = 144.0; // green +pub const HUE_HIGH: f32 = 26.0; // orange +pub const HUE_ALL: f32 = 280.0; // violet — the aggregate, distinct from its three inputs + +// --- Functional colours ---------------------------------------------------- +// Outside the hue system on purpose: these mean the same thing in every channel, so they must +// NOT be derived from a channel hue. Red is always "you have hit a limit". + +/// Meter bar with comfortable headroom. +pub const LEVEL_OK: Color32 = Color32::from_rgb(90, 200, 110); +/// Meter bar approaching full scale. +pub const LEVEL_WARN: Color32 = Color32::from_rgb(230, 200, 70); +/// Meter bar at or near full scale. +pub const LEVEL_HOT: Color32 = Color32::from_rgb(235, 70, 60); + +/// Latched over/ceiling lamp, lit. +pub const LAMP_ON: Color32 = Color32::from_rgb(255, 40, 40); +/// The same lamp unlit — dark red rather than grey, so it reads as "off", not "absent". +pub const LAMP_OFF: Color32 = Color32::from_rgb(40, 12, 12); +/// Ceiling-hit tick marks along the top edge of the scope. +pub const CLIP: Color32 = Color32::from_rgb(235, 45, 45); + +// Scope traces. The scope is tabbed — one channel at a time — so these never compete with a +// channel hue: the tab chrome says which channel, these three say what each line is. +pub const SIGNAL_IN: Color32 = Color32::from_rgb(90, 170, 235); +pub const SIGNAL_OUT: Color32 = Color32::from_rgb(90, 200, 110); +pub const SIGNAL_GR: Color32 = Color32::from_rgb(240, 150, 60); + +/// The transfer curve in the gain-curve display, and its translucent fill. +pub const CURVE: Color32 = Color32::from_rgb(120, 200, 160); +/// The live operating-point dot riding that curve — near-white, so it reads on top of the fill. +pub const OPERATING_POINT: Color32 = Color32::from_rgb(235, 240, 235); +/// Threshold marker on the gain curve. Dim on purpose: a reference line, not data. +pub const THRESHOLD_LINE: Color32 = Color32::from_rgb(80, 60, 45); + +// --- Neutrals -------------------------------------------------------------- +// Eight ad-hoc greys collapsed to six named steps. If you need another, add it here rather than +// reaching for `from_gray` at a call site. + +/// Data panels — the scope and the gain curve. +pub const SURFACE: Color32 = Color32::from_rgb(16, 16, 20); +/// The meter panel, one step lighter so the cluster reads as its own surface. These were two +/// different blacks by accident before; they are two on purpose now. Set this equal to +/// [`SURFACE`] if you would rather they sat flush. +pub const SURFACE_RAISED: Color32 = Color32::from_rgb(20, 20, 24); +/// The empty part of a meter bar. +pub const TRACK: Color32 = Color32::from_rgb(34, 34, 40); + +/// Structural lines — the unity diagonal, panel edges. +pub const GRID: Color32 = Color32::from_gray(45); +/// Reference lines that should recede — dB gridlines, −6 dB marks. +pub const GRID_FAINT: Color32 = Color32::from_gray(38); + +/// Axis numbers and other text that must not compete with the data. +pub const TEXT_DIM: Color32 = Color32::from_gray(90); +/// Body text — band names on the crossover strip. +pub const TEXT: Color32 = Color32::from_gray(160); +/// Emphasised text — channel labels, readouts. +pub const TEXT_BRIGHT: Color32 = Color32::from_gray(210); +/// Hover and drag feedback, where the point is to be unmistakable. +pub const TEXT_HOVER: Color32 = Color32::WHITE; + +// =========================================================================== +// TIER 2 — DERIVED. Generated from the seeds above; do not hand-edit. +// =========================================================================== + +/// One channel's colours, all derived from a single hue. +pub struct Channel { + /// Background wash behind a channel's cell. Very dark — it must associate the bars drawn on + /// top of it with a channel without competing with them. + pub chrome: Color32, + /// Band strip on the crossover widget: between [`Channel::chrome`] and [`Channel::edge`]. + pub strip: Color32, + /// Accent rule under a meter cell, and the scope's tab underline. Bright enough to identify + /// at a glance. + pub edge: Color32, + /// Full strength — drag handles and anything else the user grabs. + pub handle: Color32, +} + +/// Build a channel's palette from its hue. +/// +/// The `(saturation, value)` pairs below decide how loudly channel identity reads. Raise +/// `CHROME`'s value and the cell washes get more obvious; lower it and they recede into the +/// panel background. `CHROME` is deliberately near the floor: it sits *behind* meter bars. +pub const fn channel(hue: f32) -> Channel { + const CHROME: (f32, f32) = (0.45, 0.115); + const STRIP: (f32, f32) = (0.42, 0.20); + const EDGE: (f32, f32) = (0.60, 0.55); + const HANDLE: (f32, f32) = (0.55, 0.90); + Channel { + chrome: hsv(hue, CHROME.0, CHROME.1), + strip: hsv(hue, STRIP.0, STRIP.1), + edge: hsv(hue, EDGE.0, EDGE.1), + handle: hsv(hue, HANDLE.0, HANDLE.1), + } +} + +/// Indexed by the `LOW`/`MID`/`HIGH`/`ALL` consts in `lib.rs`, so `CHANNELS[i]` lines up with +/// `meters.level_l[i]` and friends without a lookup step. +pub const CHANNELS: [Channel; 4] = [ + channel(HUE_LOW), + channel(HUE_MID), + channel(HUE_HIGH), + channel(HUE_ALL), +]; + +/// HSV to RGB, `const` so [`CHANNELS`] is built at compile time. +/// +/// `h` in degrees (wrapped), `s` and `v` in `0..=1`. The standard sextant algorithm, written out +/// here only because `ecolor::Hsva`'s conversion is not a `const fn`. +pub const fn hsv(h: f32, s: f32, v: f32) -> Color32 { + // Wrap into 0..360. A loop rather than `%` because float remainder is not available in a + // const context on this toolchain; hues are small numbers, so this costs nothing. + let mut h = h; + while h < 0.0 { + h += 360.0; + } + while h >= 360.0 { + h -= 360.0; + } + let sector = h / 60.0; + let i = sector as i32; + let f = sector - i as f32; + let p = v * (1.0 - s); + let q = v * (1.0 - s * f); + let t = v * (1.0 - s * (1.0 - f)); + let (r, g, b) = match i { + 0 => (v, t, p), + 1 => (q, v, p), + 2 => (p, v, t), + 3 => (p, q, v), + 4 => (t, p, v), + _ => (v, p, q), + }; + Color32::from_rgb(to_u8(r), to_u8(g), to_u8(b)) +} + +/// Clamping float-to-byte, `const` for the same reason as [`hsv`]. +const fn to_u8(x: f32) -> u8 { + let v = x * 255.0 + 0.5; + if v <= 0.0 { + 0 + } else if v >= 255.0 { + 255 + } else { + v as u8 + } +} + +/// Meter bar colour for an output level in dB. +/// +/// Functional, not per-channel: −6 dB means the same thing in every band. Thresholds and colours +/// are both literal so either can be tuned by eye. +pub fn level_color(db: f32) -> Color32 { + if db >= -1.0 { + LEVEL_HOT + } else if db >= -6.0 { + LEVEL_WARN + } else { + LEVEL_OK + } +} + +/// The same colour at `a`/255 opacity, for translucent fills under a trace. +/// +/// Not `const`: `Color32::from_rgba_unmultiplied` is not a `const fn`, and the premultiplied +/// constructor that is would push the multiply out to every call site. +pub fn with_alpha(c: Color32, a: u8) -> Color32 { + Color32::from_rgba_unmultiplied(c.r(), c.g(), c.b(), a) +} + +/// Apply the palette to egui's own widgets, so `ParamSlider`, labels and separators sit in the +/// same world as the hand-painted ones instead of on stock egui defaults. +/// +/// Call once when the editor is created. Widget colours are per interaction state: `weak_bg_fill` +/// is an unfilled slider track, `bg_fill` the filled portion. Interactive states borrow the ALL +/// channel's hue, since egui widgets are not per-band — change that to a dedicated accent seed if +/// you would rather they were neutral. +pub fn apply_visuals(ctx: &nih_plug_egui::egui::Context) { + use nih_plug_egui::egui::{Stroke, Visuals}; + + let accent = &CHANNELS[3]; + let mut v = Visuals::dark(); + + v.panel_fill = SURFACE; + v.window_fill = SURFACE; + v.extreme_bg_color = SURFACE; + v.faint_bg_color = SURFACE_RAISED; + + let w = &mut v.widgets; + w.noninteractive.bg_fill = SURFACE_RAISED; + w.noninteractive.weak_bg_fill = SURFACE_RAISED; + w.noninteractive.bg_stroke = Stroke::new(1.0, GRID_FAINT); + w.noninteractive.fg_stroke = Stroke::new(1.0, TEXT); + + w.inactive.bg_fill = TRACK; + w.inactive.weak_bg_fill = TRACK; + w.inactive.bg_stroke = Stroke::new(1.0, GRID); + w.inactive.fg_stroke = Stroke::new(1.0, TEXT_BRIGHT); + + w.hovered.bg_fill = accent.edge; + w.hovered.weak_bg_fill = accent.strip; + w.hovered.bg_stroke = Stroke::new(1.0, accent.edge); + w.hovered.fg_stroke = Stroke::new(1.0, TEXT_HOVER); + + w.active.bg_fill = accent.handle; + w.active.weak_bg_fill = accent.edge; + w.active.bg_stroke = Stroke::new(1.0, accent.handle); + w.active.fg_stroke = Stroke::new(1.0, TEXT_HOVER); + + ctx.set_visuals(v); +} diff --git a/src/editor/plot.rs b/src/editor/plot.rs index a301cce..807d8ee 100644 --- a/src/editor/plot.rs +++ b/src/editor/plot.rs @@ -12,15 +12,16 @@ use nih_plug_egui::egui::{self, pos2, vec2, Align2, Color32, CornerRadius, FontI use super::METER_FLOOR_DB; use crate::meters::{Meters, BUCKET_HZ, NUM_CHANNELS}; +use super::palette; /// 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); +const COLOR_IN: Color32 = palette::SIGNAL_IN; +const COLOR_OUT: Color32 = palette::SIGNAL_OUT; +const COLOR_GR: Color32 = palette::SIGNAL_GR; /// Rolling history for all channels: a per-channel ring of (in_db, out_db, gr_db) columns. struct PlotHistory { @@ -156,7 +157,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) { 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)); + p.rect_filled(rect, CornerRadius::ZERO, palette::SURFACE); let (top, bottom, left, right) = (rect.top() + 4.0, rect.bottom() - 4.0, rect.left() + 4.0, rect.right() - 4.0); @@ -169,13 +170,13 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) { // 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.line_segment([pos2(left, y), pos2(right, y)], Stroke::new(1.0, palette::GRID_FAINT)); p.text( pos2(left + 2.0, y), Align2::LEFT_BOTTOM, format!("{g:.0}"), FontId::proportional(9.0), - Color32::from_gray(90), + palette::TEXT_DIM, ); } @@ -193,7 +194,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) { // Fill as a strip of per-segment convex quads down to the baseline. A single // concave polygon mis-tessellates in egui (it fans from one corner, leaving stray // triangles), so build convex pieces — one box per time unit — instead. - let fill_col = Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), 40); + let fill_col = palette::with_alpha(color, 40); for seg in pts.windows(2) { let (a, b) = (seg[0], seg[1]); p.add(egui::Shape::convex_polygon( @@ -214,7 +215,7 @@ pub(super) fn draw(ui: &mut egui::Ui, meters: &Meters, state: &mut PlotState) { // hit the ceiling (global — shown on every channel's view). One column wide, so runs of // hits merge into a continuous segment and a lone hit is just a dot. Nothing otherwise. let dx = width / (PLOT_N - 1) as f32; - let marker = Color32::from_rgb(235, 45, 45); + let marker = palette::CLIP; for k in 0..len { let idx = (write + PLOT_N - len + k) % PLOT_N; if state.history.hit[idx] {