use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{List, ListItem, ListState, Paragraph},
};
use super::layout::{
Columns, GAP_S, NUM_W, Theme, cursor_spans_windowed, format_duration, row_marker, styled_block,
truncate,
};
use crate::app::{AppState, Focus};
pub fn render_tracklist(frame: &mut Frame<'_>, state: &AppState, area: Rect) {
let focused = state.focus == Focus::TrackList;
let t = &state.theme;
let has_filter = state.filter_active || !state.tracklist_filter.is_empty();
let title = format!(" {} ", state.active_view_name());
let block = styled_block(&title, focused, t).style(t.apply_panel_bg(Style::default()));
let inner = block.inner(area);
frame.render_widget(block, area);
let (list_rect, filter_rect) = if has_filter {
let s = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Min(0), Constraint::Length(1)])
.split(inner);
(s[0], Some(s[1]))
} else {
(inner, None)
};
if let Some(fr) = filter_rect {
render_filter_bar(frame, fr, state, t);
}
let table = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(1), Constraint::Length(1), Constraint::Min(0), ])
.split(list_rect);
let (header_rect, rule_rect, rows_rect) = (table[0], table[1], table[2]);
let cols = Columns::for_width(usize::from(list_rect.width));
render_table_header(frame, header_rect, rule_rect, &cols, focused, t);
let tracks = state.active_tracks();
let items: Vec<ListItem<'_>> = tracks
.iter()
.map(|track| {
let is_playing = state
.now_playing
.and_then(|np| state.queue.get(np))
.is_some_and(|qt| qt.id == track.id);
let marker = row_marker(is_playing, state.player.is_paused, state.elapsed(), t);
let num = Span::styled(
format!("{marker:>NUM_W$}{GAP_S}"),
Style::default().fg(if is_playing { t.accent } else { t.subtle }),
);
let title_style = if is_playing {
Style::default()
.fg(t.now_playing)
.add_modifier(Modifier::BOLD)
} else {
Style::default().fg(t.text)
};
let artist_style = Style::default().fg(t.text_dim);
let meta_style = Style::default().fg(t.subtle);
let cells = cols.cells(
&track.name,
track.artist.as_deref().unwrap_or(""),
track.album.as_deref().unwrap_or(""),
&track
.duration_secs
.map_or_else(String::new, format_duration),
);
let mut spans = vec![num];
for (n, cell) in cells.into_iter().enumerate() {
spans.push(cell.style(match n {
0 => title_style,
1 => artist_style,
_ => meta_style,
}));
}
ListItem::new(Line::from(spans))
})
.collect();
let mut list_state = ListState::default();
if focused {
list_state.select(Some(state.tracklist_cursor));
}
frame.render_stateful_widget(
List::new(items).highlight_style(t.selection_style()),
rows_rect,
&mut list_state,
);
}
fn render_filter_bar(frame: &mut Frame<'_>, area: Rect, state: &AppState, t: &Theme) {
let prefix_style = if state.filter_active {
Style::default().fg(t.accent).add_modifier(Modifier::BOLD)
} else {
Style::default().fg(t.text_dim)
};
let field_w = usize::from(area.width).saturating_sub(2);
let mut spans = vec![Span::styled("/ ", prefix_style)];
if state.filter_active {
spans.extend(cursor_spans_windowed(
&state.tracklist_filter,
state.tracklist_filter.len(),
field_w,
t,
));
} else {
spans.push(Span::styled(
truncate(&state.tracklist_filter, field_w),
Style::default().fg(t.text),
));
}
frame.render_widget(Paragraph::new(Line::from(spans)), area);
}
fn render_table_header(
frame: &mut Frame<'_>,
header: Rect,
rule: Rect,
cols: &Columns,
focused: bool,
t: &Theme,
) {
let label = if focused { t.accent } else { t.subtle };
let mut spans = vec![Span::raw(format!("{blank:NUM_W$}{GAP_S}", blank = ""))];
spans.extend(cols.cells("TITLE", "ARTIST", "ALBUM", "TIME"));
frame.render_widget(
Paragraph::new(Line::from(
spans
.into_iter()
.map(|sp| sp.style(Style::default().fg(label).add_modifier(Modifier::BOLD)))
.collect::<Vec<_>>(),
)),
header,
);
frame.render_widget(
Paragraph::new(Span::styled(
t.glyphs().rule.repeat(usize::from(rule.width)),
Style::default().fg(label),
)),
rule,
);
}