1use ratatui::layout::Rect;
9use ratatui::style::{Color, Modifier, Style};
10use ratatui::text::{Line, Span};
11use ratatui::widgets::{Block, Borders, Paragraph};
12use ratatui::Frame;
13
14use crate::audio::engine::EngineHandle;
15use crate::audio::preset::PresetKind;
16
17use super::app::AppState;
18
19const CELL: &str = "██";
20const DEAD: &str = "··";
21
22pub fn render(f: &mut Frame, area: Rect, engine: &EngineHandle, app: &AppState) {
23 let life = &app.life;
24 let tracks = engine.tracks.lock();
25
26 let bpm = engine.global.bpm.value();
27 let t = engine.phase_clock.value();
28 let beat = (t * bpm / 60.0).floor() as i64;
29 let cur_col = beat.rem_euclid(life.cols as i64) as usize;
30
31 let mut lines: Vec<Line> = Vec::with_capacity(life.rows);
32 for r in 0..life.rows {
33 let (color, label, muted) = match tracks.get(r) {
34 Some(track) => {
35 let snap_muted = track.params.mute.value() > 0.5;
36 (color_for(track.kind), track.kind.label(), snap_muted)
37 }
38 None => (Color::DarkGray, "—", true),
39 };
40
41 let mut spans: Vec<Span<'static>> = Vec::with_capacity(life.cols + 3);
42 spans.push(Span::styled(
43 format!(" {:>9} ", label),
44 Style::default().fg(if muted { Color::DarkGray } else { color }),
45 ));
46
47 for c in 0..life.cols {
48 let alive = life.alive(r, c);
49 let on_cursor = c == cur_col;
50 let base_style = if on_cursor {
51 Style::default().bg(Color::Rgb(25, 25, 40))
52 } else {
53 Style::default()
54 };
55 let span = if alive {
56 let fg = if muted {
57 dim(color)
58 } else {
59 color
60 };
61 Span::styled(
62 CELL,
63 base_style.fg(fg).add_modifier(Modifier::BOLD),
64 )
65 } else if on_cursor {
66 Span::styled("▕▏", base_style.fg(Color::Rgb(80, 80, 100)))
67 } else {
68 Span::styled(DEAD, Style::default().fg(Color::Rgb(28, 28, 32)))
69 };
70 spans.push(span);
71 }
72
73 lines.push(Line::from(spans));
74 }
75
76 drop(tracks);
77
78 let title = format!(
79 " life · gen {} · density {:>5.1}% ",
80 life.generation,
81 life.density() * 100.0,
82 );
83 let block = Block::default()
84 .borders(Borders::ALL)
85 .title(title)
86 .title_style(Style::default().add_modifier(Modifier::BOLD));
87 let para = Paragraph::new(lines).block(block);
88 f.render_widget(para, area);
89}
90
91fn color_for(kind: PresetKind) -> Color {
92 match kind {
93 PresetKind::PadZimmer => Color::Cyan,
94 PresetKind::DroneSub => Color::Magenta,
95 PresetKind::Shimmer => Color::LightYellow,
96 PresetKind::Heartbeat => Color::Red,
97 PresetKind::BassPulse => Color::Green,
98 }
99}
100
101fn dim(c: Color) -> Color {
102 match c {
103 Color::Cyan => Color::Rgb(40, 80, 80),
104 Color::Magenta => Color::Rgb(80, 40, 80),
105 Color::LightYellow => Color::Rgb(80, 80, 40),
106 Color::Red => Color::Rgb(80, 30, 30),
107 _ => Color::DarkGray,
108 }
109}