diff --git a/crates/ratatui/src/theme/build.rs b/crates/ratatui/src/theme/build.rs index 47c51f4..62fe010 100644 --- a/crates/ratatui/src/theme/build.rs +++ b/crates/ratatui/src/theme/build.rs @@ -58,6 +58,7 @@ pub fn build(p: &Palette) -> ThemeColors { header: HeaderColors { tempo_bg: rgb(tint(p.bg, p.tempo_color, 0.30)), tempo_fg: rgb(p.tempo_color), + beat_bg: rgb(tint(p.bg, p.tempo_color, 0.45)), bank_bg: rgb(tint(p.bg, p.bank_color, 0.25)), bank_fg: rgb(p.bank_color), pattern_bg: rgb(tint(p.bg, p.pattern_color, 0.25)), diff --git a/crates/ratatui/src/theme/mod.rs b/crates/ratatui/src/theme/mod.rs index b714302..ece899b 100644 --- a/crates/ratatui/src/theme/mod.rs +++ b/crates/ratatui/src/theme/mod.rs @@ -175,6 +175,7 @@ pub struct TileColors { pub struct HeaderColors { pub tempo_bg: Color, pub tempo_fg: Color, + pub beat_bg: Color, pub bank_bg: Color, pub bank_fg: Color, pub pattern_bg: Color, diff --git a/src/views/render.rs b/src/views/render.rs index 6c879b6..123308f 100644 --- a/src/views/render.rs +++ b/src/views/render.rs @@ -293,15 +293,14 @@ fn render_header( let pad = Padding::vertical(1); - let [logo_area, transport_area, live_area, tempo_area, bank_area, pattern_area, stats_area] = + let [logo_area, transport_area, tempo_area, bank_area, pattern_area, stats_area] = Layout::horizontal([ Constraint::Length(5), Constraint::Min(12), - Constraint::Length(9), - Constraint::Min(14), + Constraint::Min(20), Constraint::Fill(1), Constraint::Fill(2), - Constraint::Min(20), + Constraint::Min(24), ]) .areas(area); @@ -317,43 +316,76 @@ fn render_header( logo_area, ); - // Transport block - let (transport_bg, transport_text) = if app.playback.playing { + // Transport block (with fill indicator) + let fill = app.live_keys.fill(); + let (transport_bg, transport_label) = if app.playback.playing { (theme.status.playing_bg, " ▶ PLAYING ") } else { (theme.status.stopped_bg, " ■ STOPPED ") }; - let transport_style = Style::new().bg(transport_bg).fg(theme.ui.text_primary); + let fill_span = if fill { + Span::styled("F", Style::new().fg(theme.status.fill_on).bg(transport_bg)) + } else { + Span::styled(" ", Style::new().bg(transport_bg)) + }; + let transport_line = Line::from(vec![ + Span::styled(transport_label, Style::new().fg(theme.ui.text_primary).bg(transport_bg)), + fill_span, + Span::styled(" ", Style::new().bg(transport_bg)), + ]); frame.render_widget( - Paragraph::new(transport_text) - .block(Block::default().padding(pad).style(transport_style)) + Paragraph::new(transport_line) + .block(Block::default().padding(pad).style(Style::new().bg(transport_bg))) .alignment(Alignment::Center), transport_area, ); - // Fill indicator - let fill = app.live_keys.fill(); - let fill_fg = if fill { - theme.status.fill_on - } else { - theme.status.fill_off - }; - let fill_style = Style::new().bg(theme.status.fill_bg).fg(fill_fg); + // Tempo + bar:beat position block (beat segments as background fills) + let tempo_bg = theme.header.tempo_bg; + let tempo_fg = theme.ui.text_primary; + let quantum = link.quantum(); + let quantum_int = quantum.max(1.0) as usize; + + // Base background frame.render_widget( - Paragraph::new(if fill { "F" } else { "·" }) - .block(Block::default().padding(pad).style(fill_style)) - .alignment(Alignment::Center), - live_area, + Block::default().style(Style::new().bg(tempo_bg)), + tempo_area, ); - // Tempo block - let tempo_style = Style::new() - .bg(theme.header.tempo_bg) - .fg(theme.ui.text_primary) - .add_modifier(Modifier::BOLD); + // Beat segment highlight (like CPU meter but divided into quantum segments) + if app.playback.playing && quantum_int <= 16 { + let phase = link.phase(); + let beat_in_bar = phase.floor() as usize; + let seg_w = tempo_area.width / quantum_int as u16; + let seg_x = tempo_area.x + seg_w * beat_in_bar as u16; + let seg_width = if beat_in_bar == quantum_int - 1 { + tempo_area.width - seg_w * beat_in_bar as u16 + } else { + seg_w + }; + frame.render_widget( + Block::default().style(Style::new().bg(theme.header.beat_bg)), + Rect { + x: seg_x, + width: seg_width, + ..tempo_area + }, + ); + } + + // Text overlay + let tempo_text = if app.playback.playing { + let phase = link.phase(); + let beat_in_bar = phase.floor() as usize + 1; + let bar = (link.beat() / quantum).floor() as usize + 1; + format!(" {:.1} BPM {bar}:{beat_in_bar} ", link.tempo()) + } else { + format!(" {:.1} BPM ─:─ ", link.tempo()) + }; frame.render_widget( - Paragraph::new(format!(" {:.1} BPM ", link.tempo())) - .block(Block::default().padding(pad).style(tempo_style)) + Paragraph::new(tempo_text) + .block(Block::default().padding(pad)) + .style(Style::new().fg(tempo_fg).add_modifier(Modifier::BOLD)) .alignment(Alignment::Center), tempo_area, ); @@ -393,16 +425,27 @@ fn render_header( .get_iter(app.editor_ctx.bank, app.editor_ctx.pattern) .map(|iter| format!(" · #{}", iter + 1)) .unwrap_or_default(); - let pattern_text = format!( - " {} · {} steps{}{}{} ", - pattern_name, pattern.length, speed_info, page_info, iter_info - ); - let pattern_style = Style::new() - .bg(theme.header.pattern_bg) - .fg(theme.ui.text_primary); + let pattern_bg = theme.header.pattern_bg; + let active_count = snapshot.active_patterns.len(); + let active_info = format!(" · ▶{active_count}"); + let active_style = if active_count > 0 { + Style::new().bg(pattern_bg).fg(theme.ui.text_primary) + } else { + Style::new().bg(pattern_bg).fg(theme.ui.text_muted) + }; + let pattern_line = Line::from(vec![ + Span::styled( + format!( + " {} · {} steps{}{}{} ", + pattern_name, pattern.length, speed_info, page_info, iter_info + ), + Style::new().bg(pattern_bg).fg(theme.ui.text_primary), + ), + Span::styled(active_info, active_style), + ]); frame.render_widget( - Paragraph::new(pattern_text) - .block(Block::default().padding(pad).style(pattern_style)) + Paragraph::new(pattern_line) + .block(Block::default().padding(pad).style(Style::new().bg(pattern_bg))) .alignment(Alignment::Center), pattern_area, );