//! High-fidelity waveform visualizer with bloom effect.
use super::VoiceModeTheme;
use eframe::egui::{self, Color32, Pos2, Rect, Stroke, Ui};
/// Background color (pure black)
const BACKGROUND: Color32 = Color32::from_rgb(0, 0, 0);
/// Render waveform with custom color.
pub fn waveform_visualizer_colored(ui: &mut Ui, data: &[f32], is_active: bool, color: Color32) {
let available = ui.available_size();
let (response, painter) = ui.allocate_painter(available, egui::Sense::hover());
let rect = response.rect;
// 1. Carbon fiber background
let bg_color = Color32::from_rgb(15, 15, 15);
painter.rect_filled(rect, 2.0, bg_color);
// Diagonal hash lines for carbon fiber effect
let pattern_color = Color32::from_rgba_unmultiplied(30, 30, 30, 255);
let step = 6.0;
// We use a clip rect to ensure lines don't bleed out (though with 2.0 rounding it's minimal)
{
let clipped_painter = painter.with_clip_rect(rect);
let mut x = rect.left() - rect.height(); // Start far enough left to cover diagonals
while x < rect.right() {
let p1 = Pos2::new(x, rect.bottom());
let p2 = Pos2::new(x + rect.height(), rect.top());
clipped_painter.line_segment([p1, p2], Stroke::new(1.0, pattern_color));
x += step;
}
}
// 2. Tech Border
let border_color = Color32::from_rgb(60, 65, 70); // Metallic grey
painter.rect_stroke(rect, 2.0, Stroke::new(2.0, border_color));
// Inner thin border for depth
painter.rect_stroke(rect.shrink(2.0), 2.0, Stroke::new(1.0, Color32::from_rgb(30, 35, 40)));
// 3. Corner accents (Brackets)
let accent_color = if is_active {
color
} else {
Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), 128)
};
let bracket_len = 15.0;
let bracket_stroke = Stroke::new(2.0, accent_color);
// Top-Left
painter.line_segment([rect.min, rect.min + egui::vec2(bracket_len, 0.0)], bracket_stroke);
painter.line_segment([rect.min, rect.min + egui::vec2(0.0, bracket_len)], bracket_stroke);
// Top-Right
let tr = rect.max - egui::vec2(0.0, rect.height());
painter.line_segment([tr, tr - egui::vec2(bracket_len, 0.0)], bracket_stroke);
painter.line_segment([tr, tr + egui::vec2(0.0, bracket_len)], bracket_stroke);
// Bottom-Left
let bl = rect.min + egui::vec2(0.0, rect.height());
painter.line_segment([bl, bl + egui::vec2(bracket_len, 0.0)], bracket_stroke);
painter.line_segment([bl, bl - egui::vec2(0.0, bracket_len)], bracket_stroke);
// Bottom-Right
painter.line_segment([rect.max, rect.max - egui::vec2(bracket_len, 0.0)], bracket_stroke);
painter.line_segment([rect.max, rect.max - egui::vec2(0.0, bracket_len)], bracket_stroke);
if data.is_empty() {
return;
}
// Determine orientation
let is_vertical = rect.height() > rect.width();
// Glow core color
let core_color = Color32::from_rgba_unmultiplied(
((color.r() as u32 + 255) / 2) as u8,
((color.g() as u32 + 255) / 2) as u8,
((color.b() as u32 + 255) / 2) as u8,
255,
);
// Draw glow layers
for glow_pass in (1..=4).rev() {
let alpha = 40 / glow_pass;
let stroke_width = 2.0 + (glow_pass as f32 * 2.0);
let glow_color = Color32::from_rgba_unmultiplied(
color.r(), color.g(), color.b(), alpha as u8,
);
if is_vertical {
draw_waveform_path_vertical(&painter, rect, data, glow_color, stroke_width);
} else {
draw_waveform_path_horizontal(&painter, rect, data, glow_color, stroke_width);
}
}
// Draw core
if is_vertical {
draw_waveform_path_vertical(&painter, rect, data, core_color, 2.0);
} else {
draw_waveform_path_horizontal(&painter, rect, data, core_color, 2.0);
}
// Center line
let line_color = Color32::from_rgba_unmultiplied(color.r(), color.g(), color.b(), 40);
if is_vertical {
let center_x = rect.center().x;
painter.line_segment(
[Pos2::new(center_x, rect.top()), Pos2::new(center_x, rect.bottom())],
Stroke::new(1.0, line_color),
);
} else {
let center_y = rect.center().y;
painter.line_segment(
[Pos2::new(rect.left(), center_y), Pos2::new(rect.right(), center_y)],
Stroke::new(1.0, line_color),
);
}
}
/// Render the waveform visualizer.
/// Automatically switches between horizontal and vertical orientation based on aspect ratio.
pub fn waveform_visualizer(ui: &mut Ui, theme: &VoiceModeTheme, data: &[f32], is_active: bool) {
let available = ui.available_size();
let (response, painter) = ui.allocate_painter(available, egui::Sense::hover());
let rect = response.rect;
// Background
painter.rect_filled(rect, 8.0, theme.background);
// Border with subtle glow
let border_color = if is_active {
theme.mace_windu
} else {
theme.lapis_lazuli
};
painter.rect_stroke(rect, 8.0, Stroke::new(2.0, border_color));
// Draw waveform
if data.is_empty() {
return;
}
// Determine orientation based on aspect ratio
let is_vertical = rect.height() > rect.width();
// Get color based on state
let wave_color = theme.state_color(is_active, false, false);
let core_color = theme.glow_core(wave_color);
// Draw glow layers (outer to inner for proper blending)
for glow_pass in (1..=4).rev() {
let alpha = 40 / glow_pass;
let stroke_width = 2.0 + (glow_pass as f32 * theme.glow_radius / 2.0);
let glow_color = Color32::from_rgba_unmultiplied(
wave_color.r(),
wave_color.g(),
wave_color.b(),
alpha as u8,
);
if is_vertical {
draw_waveform_path_vertical(&painter, rect, data, glow_color, stroke_width);
} else {
draw_waveform_path_horizontal(&painter, rect, data, glow_color, stroke_width);
}
}
// Draw core (brightest, thinnest)
if is_vertical {
draw_waveform_path_vertical(&painter, rect, data, core_color, 2.0);
} else {
draw_waveform_path_horizontal(&painter, rect, data, core_color, 2.0);
}
// Draw center line
let line_color = Color32::from_rgba_unmultiplied(
wave_color.r(),
wave_color.g(),
wave_color.b(),
40,
);
if is_vertical {
let center_x = rect.center().x;
painter.line_segment(
[Pos2::new(center_x, rect.top()), Pos2::new(center_x, rect.bottom())],
Stroke::new(1.0, line_color),
);
} else {
let center_y = rect.center().y;
painter.line_segment(
[Pos2::new(rect.left(), center_y), Pos2::new(rect.right(), center_y)],
Stroke::new(1.0, line_color),
);
}
}
/// Draw horizontal waveform (wave goes left-to-right, amplitude up/down)
fn draw_waveform_path_horizontal(
painter: &egui::Painter,
rect: Rect,
data: &[f32],
color: Color32,
stroke_width: f32,
) {
let center_y = rect.center().y;
let height = rect.height() * 0.4;
let step = rect.width() / data.len() as f32;
let points: Vec<Pos2> = data
.iter()
.enumerate()
.map(|(i, &sample)| {
let x = rect.left() + (i as f32 * step);
let y = center_y - (sample * height);
Pos2::new(x, y)
})
.collect();
if points.len() >= 2 {
for window in points.windows(2) {
painter.line_segment([window[0], window[1]], Stroke::new(stroke_width, color));
}
}
}
/// Draw vertical waveform (wave goes top-to-bottom, amplitude left/right)
fn draw_waveform_path_vertical(
painter: &egui::Painter,
rect: Rect,
data: &[f32],
color: Color32,
stroke_width: f32,
) {
let center_x = rect.center().x;
let width = rect.width() * 0.4;
let step = rect.height() / data.len() as f32;
let points: Vec<Pos2> = data
.iter()
.enumerate()
.map(|(i, &sample)| {
let y = rect.top() + (i as f32 * step);
let x = center_x + (sample * width);
Pos2::new(x, y)
})
.collect();
if points.len() >= 2 {
for window in points.windows(2) {
painter.line_segment([window[0], window[1]], Stroke::new(stroke_width, color));
}
}
}
/// Render a circular audio level meter.
pub fn audio_level_meter(ui: &mut Ui, theme: &VoiceModeTheme, level: f32, is_active: bool) {
let size = egui::vec2(60.0, 60.0);
let (response, painter) = ui.allocate_painter(size, egui::Sense::hover());
let rect = response.rect;
let center = rect.center();
let radius = rect.width().min(rect.height()) / 2.0 - 4.0;
// Background circle
painter.circle_filled(center, radius, theme.surface);
// Level arc
let level_color = theme.state_color(is_active, false, false);
let level_radius = radius * level.clamp(0.0, 1.0);
// Glow
for i in 1..=3 {
let glow_alpha = 60 / i;
let glow_radius = level_radius + (i as f32 * 3.0);
let glow_color = Color32::from_rgba_unmultiplied(
level_color.r(),
level_color.g(),
level_color.b(),
glow_alpha as u8,
);
painter.circle_stroke(center, glow_radius, Stroke::new(2.0, glow_color));
}
// Core
painter.circle_filled(center, level_radius, theme.glow_core(level_color));
// Border
painter.circle_stroke(center, radius, Stroke::new(2.0, level_color));
}