use super::*;
pub(super) fn render_tracks(frame: &mut Frame, area: Rect, nav: &NavState, snap: &TransportSnapshot) {
let vis = nav.visible_tracks();
if nav.can_scroll_up() {
frame.render_widget(
Paragraph::new(Span::styled("\u{25B2} more", theme::dim())).alignment(Alignment::Center),
Rect::new(area.x, area.y, HEADER_W, 1));
}
let solo_on = nav.tracks.iter().any(|t| t.soloed);
for (vi, track) in vis.iter().enumerate() {
let ai = nav.track_scroll + vi;
let y = area.y + vi as u16 * TRACK_H;
if y + TRACK_H > area.y + area.height { break; }
let cur = nav.focused_pane == Pane::Tracks && nav.track_cursor == ai;
let sel = cur && nav.track_selected;
let dim = track.muted || (solo_on && !track.soloed);
let (vu_l, _) = track.vu_levels();
let ctx = TrackCtx {
track, index: ai, is_cursor: cur, is_selected: sel,
is_dimmed: dim, vu_level: if dim { 0.0 } else { vu_l }, nav,
};
let r = Rect::new(area.x, y, area.width, TRACK_H);
render_track_row(frame, r, &ctx, snap);
}
if nav.can_scroll_down() {
let y = area.y + area.height - 1;
frame.render_widget(
Paragraph::new(Span::styled("\u{25BC} more", theme::dim())).alignment(Alignment::Center),
Rect::new(area.x, y, HEADER_W, 1));
}
}
pub(super) fn render_track_row(frame: &mut Frame, area: Rect, ctx: &TrackCtx, snap: &TransportSnapshot) {
let cols = Layout::default()
.direction(Direction::Horizontal)
.constraints([Constraint::Length(HEADER_W), Constraint::Length(1), Constraint::Min(4)])
.split(area);
render_header(frame, cols[0], ctx);
let sep: Vec<Line> = (0..area.height)
.map(|_| Line::from(Span::styled("\u{2502}", theme::border_style())))
.collect();
frame.render_widget(Paragraph::new(sep), cols[1]);
render_clips(frame, cols[2], ctx, snap);
}
pub(super) fn render_header(frame: &mut Frame, area: Rect, ctx: &TrackCtx) {
let TrackCtx { track, index, is_cursor: cur, is_selected: sel, is_dimmed: dim, vu_level, nav, .. } = *ctx;
let tc = theme::track_color(track.color_index);
let id = (b'A' + index as u8) as char;
let is_special = matches!(track.kind, TrackKind::SendA | TrackKind::SendB | TrackKind::Master);
let ac = if sel { "\u{2588}" } else { "\u{2590}" };
let ac_s = if cur || sel { Style::default().fg(tc).bg(theme::bg_val()) }
else { Style::default().fg(theme::dim_color(tc, if dim { 15 } else { 30 })).bg(theme::bg_val()) };
let id_s = Style::default().fg(theme::dim_color(tc, if dim { 20 } else { 40 })).bg(theme::bg_val());
let vu_w = 3usize;
let vu_filled = (vu_w as f32 * vu_level) as usize;
let arm_s = if track.armed {
Style::default().fg(theme::rec_active_val()).bg(theme::bg_val())
} else {
theme::dim()
};
let mut r0: Vec<Span> = vec![
Span::styled(ac, ac_s),
Span::styled(format!("{id}"), id_s),
Span::styled(" ", theme::bg()),
];
if !is_special {
let fx_f = sel && nav.track_element == TrackElement::Fx;
let v_f = sel && nav.track_element == TrackElement::Volume;
r0.push(Span::styled("fx", theme::btn_style(!track.fx_chain.is_empty(), fx_f, tc)));
r0.push(Span::styled(" ", theme::bg()));
r0.push(Span::styled(format!("v{:<2}", (track.volume * 99.0) as u8), theme::btn_style(false, v_f, tc)));
r0.push(Span::styled(if track.armed { " \u{25CF}" } else { " " }, arm_s));
}
let m_f = sel && nav.track_element == TrackElement::Mute;
let s_f = sel && nav.track_element == TrackElement::Solo;
let solo_s = if track.soloed {
Style::default().fg(theme::solo_active_fg())
.bg(if s_f { theme::solo_focused_bg() } else { theme::solo_active_bg() })
.add_modifier(Modifier::BOLD)
} else {
theme::btn_style(false, s_f, tc)
};
let vu_filled = vu_filled.min(vu_w);
let vu_bar: String = "\u{2588}".repeat(vu_filled) + &"\u{2591}".repeat(vu_w - vu_filled);
let vu_s = Style::default()
.fg(theme::dim_color(tc, if dim { 20 } else { 55 }))
.bg(theme::piano_black_bg());
let r1: Vec<Span> = vec![
Span::styled(ac, ac_s),
Span::styled(" ", theme::bg()),
Span::styled("m", theme::btn_style(track.muted, m_f, tc)),
Span::styled(" ", theme::bg()),
Span::styled("s", solo_s),
Span::styled(" ", theme::bg()),
Span::styled(vu_bar, vu_s),
];
let r2 = Line::from(vec![
Span::styled(ac, ac_s),
Span::styled("\u{2500}".repeat(HEADER_W as usize - 1), theme::border_style()),
]);
let lines = vec![
Line::from(r0),
Line::from(r1),
r2,
];
frame.render_widget(Paragraph::new(lines), area);
}
pub(super) fn render_clips(frame: &mut Frame, area: Rect, ctx: &TrackCtx, snap: &TransportSnapshot) {
let TrackCtx { track, is_selected: sel, is_dimmed: dim, nav, .. } = *ctx;
let tc = theme::track_color(track.color_index);
let (w, h) = (area.width as usize, area.height as usize);
if w == 0 || h == 0 { return; }
let bw = w / VISIBLE_BARS;
if bw == 0 { return; }
crate::debug_log::log("CLIP_GRID", &format!("w={w} h={h} alloc={}bytes", w * h * std::mem::size_of::<(char, Style)>()));
let mut grid: Vec<Vec<(char, Style)>> = vec![vec![(' ', theme::bg()); w]; h];
for b in 1..VISIBLE_BARS {
let x = b * bw;
if x < w {
let major = b % 4 == 0;
let s = Style::default()
.fg(if major { theme::grid_major() } else { theme::grid_minor() })
.bg(theme::bg_val());
let ch = if major { '\u{2502}' } else { '\u{2506}' };
for row in &mut grid { row[x] = (ch, s); }
}
}
let ticks_per_bar = Transport::PPQ * 4;
let total_visible_ticks = (VISIBLE_BARS as i64) * ticks_per_bar;
for (ci, clip) in track.clips.iter().enumerate() {
let focused = sel && matches!(nav.track_element, TrackElement::Clip(i) if i == ci);
let clip_end_tick = clip.start_tick + clip.length_ticks;
let cx = (clip.start_tick as usize * w) / total_visible_ticks as usize;
let ce = (clip_end_tick as usize * w) / total_visible_ticks as usize;
let ce = ce.max(cx + 1).min(w);
if cx >= w { break; }
let bg = theme::bg_val();
let cbg = if focused {
Color::Rgb(
(theme::tc_r(tc) as u16 * 18 / 100 + theme::tc_r(bg) as u16) as u8,
(theme::tc_g(tc) as u16 * 18 / 100 + theme::tc_g(bg) as u16) as u8,
(theme::tc_b(tc) as u16 * 18 / 100 + theme::tc_b(bg) as u16) as u8,
)
} else if clip.has_content {
Color::Rgb(
(theme::tc_r(tc) as u16 * 8 / 100 + theme::tc_r(bg) as u16) as u8,
(theme::tc_g(tc) as u16 * 8 / 100 + theme::tc_g(bg) as u16) as u8,
(theme::tc_b(tc) as u16 * 8 / 100 + theme::tc_b(bg) as u16) as u8,
)
} else { theme::bg_val() };
let cfg = if dim { theme::dim_color(tc,18) } else if focused { tc } else if clip.has_content { theme::dim_color(tc,55) } else { theme::dim_color(tc,20) };
if clip.has_content {
let afg = if dim { theme::dim_color(tc,25) } else if focused { tc } else { theme::dim_color(tc,65) };
for x in cx..ce { grid[0][x] = ('\u{2580}', Style::default().fg(afg).bg(cbg)); }
}
let body_style = Style::default().fg(theme::dim_color(tc, 15)).bg(cbg);
for row in grid.iter_mut().take(h.saturating_sub(1)).skip(1) {
if let Some(cells) = row.get_mut(cx..ce) {
let len = cells.len();
for (j, cell) in cells.iter_mut().enumerate() {
let edge = j == 0 || j == len - 1;
*cell = (if edge { '\u{2502}' } else { ' ' }, body_style);
}
}
}
for x in cx..ce {
if grid[h-1][x].0 == ' ' {
grid[h-1][x] = ('\u{2581}', Style::default().fg(if clip.has_content { cfg } else { theme::dim_color(tc,12) }).bg(cbg));
}
}
let ns = format!("{}", clip.number);
let n_s = Style::default().fg(if focused { theme::amber_bright_val() } else { theme::dim_color(tc, if dim { 20 } else { 40 }) }).bg(cbg);
for (i, ch) in ns.chars().enumerate() {
let x = cx+i+1;
if x < ce && 1 < h { grid[1][x] = (ch, n_s); }
}
}
let last_row = h - 1;
let div_s = theme::border_style();
for x in bw..w {
grid[last_row][x] = ('\u{2500}', div_s);
}
let name = track.name.to_lowercase();
let name_s = Style::default()
.fg(if dim { theme::dim_color(tc, 30) } else { theme::dim_color(tc, 65) })
.bg(theme::bg_val());
for (i, ch) in name.chars().enumerate() {
let x = i + 1;
if x < bw && x < w {
grid[last_row][x] = (ch, name_s);
}
}
if snap.playing {
let ph = snap.position_ticks as f64 / (Transport::PPQ * 4) as f64;
let px = (ph * bw as f64) as usize;
if px < w {
for row in &mut grid {
let bg = row[px].1.bg.unwrap_or(theme::bg_val());
row[px] = ('\u{2502}', Style::default().fg(theme::amber_val()).bg(bg));
}
}
}
let lines: Vec<Line> = grid_to_lines(grid);
frame.render_widget(Paragraph::new(lines), area);
}