use crate::Theme;
use egui::{Color32, Rect, Response, Sense, Stroke, Ui, Vec2};
use egui_cha::ViewCtx;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MeterMode {
#[default]
VU,
Peak,
RMS,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum MeterOrientation {
#[default]
Vertical,
Horizontal,
}
#[derive(Debug, Clone)]
pub struct LevelMeter {
width: f32,
height: f32,
mode: MeterMode,
orientation: MeterOrientation,
stereo: bool,
peak_hold: bool,
show_scale: bool,
min_db: f32,
max_db: f32,
yellow_threshold: f32,
red_threshold: f32,
}
impl Default for LevelMeter {
fn default() -> Self {
Self {
width: 24.0,
height: 200.0,
mode: MeterMode::default(),
orientation: MeterOrientation::default(),
stereo: false,
peak_hold: true,
show_scale: true,
min_db: -60.0,
max_db: 6.0,
yellow_threshold: -12.0,
red_threshold: -3.0,
}
}
}
impl LevelMeter {
pub fn new() -> Self {
Self::default()
}
pub fn size(mut self, width: f32, height: f32) -> Self {
self.width = width;
self.height = height;
self
}
pub fn mode(mut self, mode: MeterMode) -> Self {
self.mode = mode;
self
}
pub fn orientation(mut self, orientation: MeterOrientation) -> Self {
self.orientation = orientation;
self
}
pub fn stereo(mut self, stereo: bool) -> Self {
self.stereo = stereo;
self
}
pub fn peak_hold(mut self, enabled: bool) -> Self {
self.peak_hold = enabled;
self
}
pub fn show_scale(mut self, show: bool) -> Self {
self.show_scale = show;
self
}
pub fn range(mut self, min_db: f32, max_db: f32) -> Self {
self.min_db = min_db;
self.max_db = max_db;
self
}
pub fn yellow_at(mut self, db: f32) -> Self {
self.yellow_threshold = db;
self
}
pub fn red_at(mut self, db: f32) -> Self {
self.red_threshold = db;
self
}
fn db_to_normalized(&self, db: f32) -> f32 {
let clamped = db.clamp(self.min_db, self.max_db);
(clamped - self.min_db) / (self.max_db - self.min_db)
}
fn get_level_color(&self, db: f32, theme: &Theme) -> Color32 {
if db >= self.red_threshold {
theme.state_danger } else if db >= self.yellow_threshold {
theme.state_warning } else {
theme.state_success }
}
pub fn show_with<Msg>(&self, ctx: &mut ViewCtx<'_, Msg>, level_db: f32) {
self.show_internal(ctx.ui, level_db, level_db, None);
}
pub fn show_stereo_with<Msg>(&self, ctx: &mut ViewCtx<'_, Msg>, left_db: f32, right_db: f32) {
self.show_internal(ctx.ui, left_db, right_db, None);
}
pub fn show(&self, ui: &mut Ui, level_db: f32) -> Response {
self.show_internal(ui, level_db, level_db, None)
}
pub fn show_with_peak(&self, ui: &mut Ui, level_db: f32, peak_db: f32) -> Response {
self.show_internal(ui, level_db, level_db, Some((peak_db, peak_db)))
}
pub fn show_stereo(&self, ui: &mut Ui, left_db: f32, right_db: f32) -> Response {
self.show_internal(ui, left_db, right_db, None)
}
pub fn show_stereo_with_peak(
&self,
ui: &mut Ui,
left_db: f32,
right_db: f32,
left_peak: f32,
right_peak: f32,
) -> Response {
self.show_internal(ui, left_db, right_db, Some((left_peak, right_peak)))
}
fn show_internal(
&self,
ui: &mut Ui,
left_db: f32,
right_db: f32,
peaks: Option<(f32, f32)>,
) -> Response {
let theme = Theme::current(ui.ctx());
let scale_width = if self.show_scale { 24.0 } else { 0.0 };
let total_width = self.width + scale_width;
let (rect, response) =
ui.allocate_exact_size(Vec2::new(total_width, self.height), Sense::hover());
if !ui.is_rect_visible(rect) {
return response;
}
let painter = ui.painter();
let meter_rect = if self.show_scale {
Rect::from_min_size(
rect.min + Vec2::new(scale_width, 0.0),
Vec2::new(self.width, self.height),
)
} else {
rect
};
painter.rect_filled(meter_rect, 2.0, theme.bg_tertiary);
painter.rect_stroke(
meter_rect,
2.0,
Stroke::new(1.0, theme.border),
egui::StrokeKind::Outside,
);
if self.show_scale {
self.draw_scale(ui, rect, &theme);
}
let bar_padding = 2.0;
let inner_rect = meter_rect.shrink(bar_padding);
if self.stereo {
let bar_width = (inner_rect.width() - 2.0) / 2.0;
let left_rect =
Rect::from_min_size(inner_rect.min, Vec2::new(bar_width, inner_rect.height()));
self.draw_meter_bar(ui, left_rect, left_db, peaks.map(|(l, _)| l), &theme);
let right_rect = Rect::from_min_size(
inner_rect.min + Vec2::new(bar_width + 2.0, 0.0),
Vec2::new(bar_width, inner_rect.height()),
);
self.draw_meter_bar(ui, right_rect, right_db, peaks.map(|(_, r)| r), &theme);
} else {
self.draw_meter_bar(ui, inner_rect, left_db, peaks.map(|(l, _)| l), &theme);
}
response
}
fn draw_meter_bar(
&self,
ui: &mut Ui,
rect: Rect,
level_db: f32,
peak_db: Option<f32>,
theme: &Theme,
) {
let painter = ui.painter();
let normalized = self.db_to_normalized(level_db);
match self.orientation {
MeterOrientation::Vertical => {
let segments = 30;
let segment_height = rect.height() / segments as f32;
let segment_gap = 1.0;
for i in 0..segments {
let segment_normalized = (i as f32 + 0.5) / segments as f32;
let segment_db = self.min_db + segment_normalized * (self.max_db - self.min_db);
let y = rect.max.y - (i as f32 + 1.0) * segment_height;
let segment_rect = Rect::from_min_size(
egui::pos2(rect.min.x, y + segment_gap / 2.0),
Vec2::new(rect.width(), segment_height - segment_gap),
);
if segment_normalized <= normalized {
let color = self.get_level_color(segment_db, theme);
painter.rect_filled(segment_rect, 1.0, color);
} else {
painter.rect_filled(segment_rect, 1.0, theme.bg_secondary);
}
}
if self.peak_hold {
if let Some(peak) = peak_db {
let peak_normalized = self.db_to_normalized(peak);
let peak_y = rect.max.y - peak_normalized * rect.height();
let peak_color = self.get_level_color(peak, theme);
painter.hline(
rect.min.x..=rect.max.x,
peak_y,
Stroke::new(2.0, peak_color),
);
}
}
}
MeterOrientation::Horizontal => {
let segments = 30;
let segment_width = rect.width() / segments as f32;
let segment_gap = 1.0;
for i in 0..segments {
let segment_normalized = (i as f32 + 0.5) / segments as f32;
let segment_db = self.min_db + segment_normalized * (self.max_db - self.min_db);
let x = rect.min.x + i as f32 * segment_width;
let segment_rect = Rect::from_min_size(
egui::pos2(x + segment_gap / 2.0, rect.min.y),
Vec2::new(segment_width - segment_gap, rect.height()),
);
if segment_normalized <= normalized {
let color = self.get_level_color(segment_db, theme);
painter.rect_filled(segment_rect, 1.0, color);
} else {
painter.rect_filled(segment_rect, 1.0, theme.bg_secondary);
}
}
if self.peak_hold {
if let Some(peak) = peak_db {
let peak_normalized = self.db_to_normalized(peak);
let peak_x = rect.min.x + peak_normalized * rect.width();
let peak_color = self.get_level_color(peak, theme);
painter.vline(
peak_x,
rect.min.y..=rect.max.y,
Stroke::new(2.0, peak_color),
);
}
}
}
}
}
fn draw_scale(&self, ui: &mut Ui, rect: Rect, theme: &Theme) {
let painter = ui.painter();
let scale_marks = [-60, -48, -36, -24, -18, -12, -6, -3, 0, 3, 6];
for &db in &scale_marks {
let db_f32 = db as f32;
if db_f32 < self.min_db || db_f32 > self.max_db {
continue;
}
let normalized = self.db_to_normalized(db_f32);
let y = rect.max.y - normalized * self.height;
painter.hline(
rect.min.x + 16.0..=rect.min.x + 22.0,
y,
Stroke::new(1.0, theme.text_secondary),
);
let label = if db == 0 {
"0".to_string()
} else {
format!("{}", db)
};
painter.text(
egui::pos2(rect.min.x + 14.0, y),
egui::Align2::RIGHT_CENTER,
label,
egui::FontId::proportional(9.0),
theme.text_secondary,
);
}
}
}