use ratatui::{
Frame,
layout::{Constraint, Direction, Layout},
style::{Color, Style},
text::{Line, Span},
widgets::{Block, Borders, Paragraph},
};
#[derive(Clone)]
pub struct UiState {
pub device_name: String,
pub current_db: f32,
pub display_db: f32,
pub threshold_db: i32,
pub status: String,
}
pub fn create_gradient_bar(width: usize, ratio: f64) -> Line<'static> {
let filled = (ratio * width as f64) as usize;
let partial_fill = (ratio * width as f64) - filled as f64;
let mut spans = Vec::new();
for i in 0..width {
let color = if i < width / 3 {
Color::Green
} else if i < 2 * width / 3 {
Color::Yellow
} else {
Color::Red
};
let ch = if i < filled {
'█' } else if i == filled && partial_fill > 0.0 {
match (partial_fill * 8.0) as usize {
0 => '░',
1 => '░',
2 => '▒',
3 => '▒',
4 => '▓',
5 => '▓',
6 => '█',
_ => '█',
}
} else {
'░' };
spans.push(Span::styled(ch.to_string(), Style::default().fg(color)));
}
Line::from(spans)
}
pub fn create_db_labels(width: usize, threshold_db: i32) -> Line<'static> {
let mut spans = Vec::new();
let threshold_ratio = ((threshold_db as f64 + 60.0) / 60.0).clamp(0.0, 1.0);
let threshold_pos = (threshold_ratio * (width - 1) as f64).round() as usize;
for i in 0..width {
if i == threshold_pos {
spans.push(Span::styled(
"▲".to_string(),
Style::default().fg(Color::White),
));
continue;
}
let label = if i == 0 {
"-60".to_string()
} else if i == width - 1 {
"0".to_string()
} else if i == width / 3 {
"-40".to_string()
} else if i == 2 * width / 3 {
"-20".to_string()
} else {
" ".to_string()
};
let color = if i < width / 3 {
Color::Green
} else if i < 2 * width / 3 {
Color::Yellow
} else {
Color::Red
};
spans.push(Span::styled(label, Style::default().fg(color)));
}
Line::from(spans)
}
pub fn render_ui(f: &mut Frame, state: &UiState) {
let size = f.size();
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3),
Constraint::Length(3),
Constraint::Length(2),
Constraint::Min(1),
])
.split(size);
let device_block = Block::default().title("Device").borders(Borders::ALL);
let device_text = Paragraph::new(state.device_name.as_str()).block(device_block);
f.render_widget(device_text, chunks[0]);
let status_block = Block::default().title("Status").borders(Borders::ALL);
let status_text = Paragraph::new(state.status.as_str()).block(status_block);
f.render_widget(status_text, chunks[1]);
let width = chunks[2].width as usize;
let threshold_pos =
(((state.threshold_db as f32 + 60.0) / 60.0).clamp(0.0, 1.0) * (width - 2) as f32) as usize;
let mut bar = String::new();
for i in 0..(width - 2) {
if i == threshold_pos {
bar.push('|');
} else {
bar.push('─');
}
}
let threshold_text = Paragraph::new(format!("Threshold: {} dB\n{}", state.threshold_db, bar));
f.render_widget(threshold_text, chunks[2]);
let min_db = crate::constants::audio::MIN_DB_LEVEL;
let db_range = -min_db; let db_ratio = ((state.display_db - min_db) / db_range).clamp(0.0, 1.0) as f64;
let bar_width =
(chunks[3].width as usize).saturating_sub(crate::constants::ui::BAR_BORDER_WIDTH);
let bar_line = create_gradient_bar(bar_width, db_ratio);
let label_line = create_db_labels(bar_width, state.threshold_db);
let gauge = Paragraph::new(vec![bar_line, label_line]).block(
Block::default()
.title(format!(
"Current dB: {:.1} (Raw: {:.1})",
state.display_db, state.current_db
))
.borders(Borders::ALL),
);
f.render_widget(gauge, chunks[3]);
}