1use ratatui::layout::{Constraint, Direction, Layout, Rect};
4use ratatui::style::{Color, Modifier, Style};
5use ratatui::widgets::{Block, Borders, Gauge};
6use ratatui::Frame;
7
8use super::app::{AppState, Focus};
9use crate::audio::engine::EngineHandle;
10use crate::audio::preset::{lfo_target_name, LFO_TARGETS};
11
12pub fn render(f: &mut Frame, area: Rect, engine: &EngineHandle, app: &AppState) {
13 let tracks = engine.tracks.lock();
14 let Some(track) = tracks.get(app.selected_track) else {
15 return;
16 };
17 let s = track.params.snapshot();
18
19 let focus_style = if app.focus == Focus::Params {
20 Style::default().fg(Color::Yellow)
21 } else {
22 Style::default().fg(Color::Gray)
23 };
24 let outer = Block::default()
25 .borders(Borders::ALL)
26 .title(format!(
27 " params · {} · {} {} ",
28 track.name,
29 track.kind.label(),
30 if app.focus == Focus::Params { "◀" } else { " " }
31 ))
32 .border_style(focus_style);
33 let inner = outer.inner(area);
34 f.render_widget(outer, area);
35
36 let rows = Layout::default()
37 .direction(Direction::Vertical)
38 .constraints([Constraint::Length(1); 13])
39 .split(inner);
40
41 let lfo_target_idx = (s.lfo_target.round() as u32) % LFO_TARGETS;
42 let items: [(&str, f32, String); 13] = [
43 ("gain ", s.gain, format!("{:>4.2}", s.gain)),
44 ("cutoff ", norm_log(s.cutoff, 40.0, 12000.0), format!("{:>5.0} Hz", s.cutoff)),
45 ("resonance", (s.resonance / 0.70).min(1.0), format!("{:>4.2}", s.resonance)),
46 ("detune ", (s.detune + 50.0) / 100.0, format!("{:>+3.0} ct", s.detune)),
47 ("freq ", norm_log(s.freq, 20.0, 880.0), format!("{:>5.1} Hz", s.freq)),
48 ("reverb ", s.reverb_mix, format!("{:>4.2}", s.reverb_mix)),
49 ("supermass", s.supermass, format!("{:>4.2}", s.supermass)),
50 ("pulse ", s.pulse_depth, format!("{:>4.2}", s.pulse_depth)),
51 ("lfo rate", norm_log(s.lfo_rate, 0.01, 20.0), format!("{:>5.2} Hz", s.lfo_rate)),
52 ("lfo depth", s.lfo_depth, format!("{:>4.2}", s.lfo_depth)),
53 ("lfo tgt ", (lfo_target_idx as f32) / (LFO_TARGETS - 1) as f32,
54 lfo_target_name(lfo_target_idx).to_string()),
55 ("character", s.character, format!("{:>4.2}", s.character)),
56 ("arp ", s.arp, format!("{:>4.2}", s.arp)),
57 ];
58
59 for (i, ((name, v, label), row)) in items.iter().zip(rows.iter()).enumerate() {
60 let selected = i == app.selected_param && app.focus == Focus::Params;
61 let style = if selected {
62 Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD)
63 } else {
64 Style::default().fg(Color::Gray)
65 };
66 let arrow = if selected { "▶ " } else { " " };
67 let g = Gauge::default()
68 .block(Block::default())
69 .gauge_style(style)
70 .ratio(v.clamp(0.0, 1.0) as f64)
71 .label(format!("{arrow}{name} {label}"));
72 f.render_widget(g, *row);
73 }
74}
75
76fn norm_log(v: f32, lo: f32, hi: f32) -> f32 {
77 let v = v.max(lo);
78 ((v.ln() - lo.ln()) / (hi.ln() - lo.ln())).clamp(0.0, 1.0)
79}