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 let short = short_tag(label);
48 spans.push(Span::styled(
49 format!(" {short:<3} "),
50 Style::default().fg(if muted { Color::DarkGray } else { color }),
51 ));
52
53 for c in 0..life.cols {
54 let alive = life.alive(r, c);
55 let on_cursor = c == cur_col;
56 let base_style = if on_cursor {
57 Style::default().bg(Color::Rgb(25, 25, 40))
58 } else {
59 Style::default()
60 };
61 let span = if alive {
62 let fg = if muted {
63 dim(color)
64 } else {
65 color
66 };
67 Span::styled(
68 CELL,
69 base_style.fg(fg).add_modifier(Modifier::BOLD),
70 )
71 } else if on_cursor {
72 Span::styled("▕▏", base_style.fg(Color::Rgb(80, 80, 100)))
73 } else {
74 Span::styled(DEAD, Style::default().fg(Color::Rgb(28, 28, 32)))
75 };
76 spans.push(span);
77 }
78
79 lines.push(Line::from(spans));
80 }
81
82 drop(tracks);
83
84 let title = format!(
85 " life · gen {} · density {:>5.1}% ",
86 life.generation,
87 life.density() * 100.0,
88 );
89 let block = Block::default()
90 .borders(Borders::ALL)
91 .title(title)
92 .title_style(Style::default().add_modifier(Modifier::BOLD));
93 let para = Paragraph::new(lines).block(block);
94 f.render_widget(para, area);
95}
96
97fn short_tag(label: &str) -> &'static str {
98 match label {
99 "Pad" => "Pad",
100 "Drone" => "Drn",
101 "Shimmer" => "Shm",
102 "Heartbeat" => "Hrt",
103 "Bass" => "Bas",
104 "Bell" => "Bll",
105 "SuperSaw" => "Sup",
106 "Pluck" => "Plk",
107 _ => "—",
108 }
109}
110
111fn color_for(kind: PresetKind) -> Color {
112 match kind {
113 PresetKind::PadZimmer => Color::Cyan,
114 PresetKind::DroneSub => Color::Magenta,
115 PresetKind::Shimmer => Color::LightYellow,
116 PresetKind::Heartbeat => Color::Red,
117 PresetKind::BassPulse => Color::Green,
118 PresetKind::Bell => Color::LightBlue,
119 PresetKind::SuperSaw => Color::LightGreen,
120 PresetKind::PluckSaw => Color::Yellow,
121 }
122}
123
124fn dim(c: Color) -> Color {
125 match c {
126 Color::Cyan => Color::Rgb(40, 80, 80),
127 Color::Magenta => Color::Rgb(80, 40, 80),
128 Color::LightYellow => Color::Rgb(80, 80, 40),
129 Color::Red => Color::Rgb(80, 30, 30),
130 _ => Color::DarkGray,
131 }
132}