Skip to main content

rust_synth/tui/
beats.rs

1//! Animated BPM grid — 16 squares, highlights current beat position.
2
3use ratatui::layout::Rect;
4use ratatui::style::{Color, Modifier, Style};
5use ratatui::text::{Line, Span};
6use ratatui::widgets::{Block, Borders, Paragraph};
7use ratatui::Frame;
8
9use crate::audio::engine::EngineHandle;
10use crate::math::pulse::{beat_phase, phrase_phase};
11
12const STEPS: usize = 16;
13const PHRASE_BEATS: f32 = 16.0; // 4 bars × 4 beats
14
15pub fn render(f: &mut Frame, area: Rect, engine: &EngineHandle) {
16    let t = engine.phase_clock.value() as f64;
17    let bpm = engine.global.bpm.value() as f64;
18    let beat = beat_phase(t, bpm);
19    let current = (beat * STEPS as f64) as usize % STEPS;
20
21    // Beat row: 16 squares, current one lit, every 4th brighter baseline.
22    let mut beat_spans: Vec<Span> = Vec::with_capacity(STEPS * 2 + 1);
23    for i in 0..STEPS {
24        let (glyph, color) = if i == current {
25            ("██", Color::Yellow)
26        } else if i % 4 == 0 {
27            ("▓▓", Color::Blue)
28        } else {
29            ("░░", Color::DarkGray)
30        };
31        beat_spans.push(Span::styled(glyph, Style::default().fg(color)));
32        beat_spans.push(Span::raw(" "));
33    }
34
35    // Phrase row: 16 small blocks for phrase progress.
36    let phr = phrase_phase(t, bpm, PHRASE_BEATS as f64);
37    let phr_idx = (phr * STEPS as f64) as usize % STEPS;
38    let mut phrase_spans: Vec<Span> = Vec::with_capacity(STEPS * 2 + 1);
39    for i in 0..STEPS {
40        let (glyph, color) = if i <= phr_idx {
41            ("▰ ", Color::Magenta)
42        } else {
43            ("▱ ", Color::DarkGray)
44        };
45        phrase_spans.push(Span::styled(glyph, Style::default().fg(color)));
46    }
47
48    let text = vec![
49        Line::from(vec![Span::styled(
50            format!(" beat  {bpm:>5.1} bpm  step {}/{}  ", current + 1, STEPS),
51            Style::default().fg(Color::Gray),
52        )]),
53        Line::from(beat_spans),
54        Line::from(""),
55        Line::from(vec![Span::styled(
56            format!(" phrase  {:.0}%  ({:.0}/{:.0} beats)", phr * 100.0, phr * PHRASE_BEATS as f64, PHRASE_BEATS),
57            Style::default().fg(Color::Gray),
58        )]),
59        Line::from(phrase_spans),
60    ];
61
62    let para = Paragraph::new(text).block(
63        Block::default()
64            .borders(Borders::ALL)
65            .title(" tempo ")
66            .title_style(Style::default().add_modifier(Modifier::BOLD)),
67    );
68    f.render_widget(para, area);
69}