Skip to main content

rust_synth/tui/
life.rs

1//! Game of Life — chunky pixel-art renderer.
2//!
3//! One text line per grid row, each cell rendered as a solid 2-character
4//! block `██` in the track's preset colour. Dead cells are near-invisible
5//! `··`. The current beat column gets a subtle backlight so you can see
6//! the "playhead" crawl across.
7
8use 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        // Compact 3-char label: "Pad", "Bas", "Hrt", "Drn", "Shm", "Bll",
43        // "Sup", "Plk".  The colour alone identifies the preset kind;
44        // the short tag is just a hint when new users are learning the
45        // layout.  Saves ~7 chars of horizontal space per row so the
46        // whole grid + label comfortably fits 80-col terminals.
47        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}