use ratatui::{
Frame,
layout::{Constraint, Direction, Layout, Rect},
style::{Modifier, Style},
text::{Line, Span},
widgets::{List, ListItem, ListState, Paragraph},
};
use super::layout::{Theme, render_empty_state, str_width, styled_block, truncate};
use crate::app::{AppState, Focus, SidebarItem};
const MARKER: &str = "> ";
const MARKER_W: usize = 2;
const LIBRARY_H: u16 = 3;
pub fn render_sidebar(frame: &mut Frame<'_>, state: &AppState, area: Rect) {
let sections = Layout::default()
.direction(Direction::Vertical)
.constraints([Constraint::Length(LIBRARY_H), Constraint::Min(0)])
.split(area);
render_library(frame, state, sections[0]);
render_playlists(frame, state, sections[1]);
}
fn render_library(frame: &mut Frame<'_>, state: &AppState, area: Rect) {
let t = &state.theme;
let g = t.glyphs();
let selected = state.focus == Focus::Library;
let is_active = state.active_view == SidebarItem::Library;
let block = styled_block(" Library ", selected, t).style(t.apply_sidebar_bg(Style::default()));
let inner = block.inner(area);
frame.render_widget(block, area);
let label = entry(
&format!("{} All tracks", g.note),
state.library.tracks.len(),
(inner.width as usize).saturating_sub(MARKER_W),
);
let mut style = row_style(t, is_active, selected);
if selected {
style = style.patch(t.selection_style());
}
frame.render_widget(
Paragraph::new(Line::from(Span::styled(
format!("{}{label}", if selected { MARKER } else { " " }),
style,
))),
inner,
);
}
fn render_playlists(frame: &mut Frame<'_>, state: &AppState, area: Rect) {
let t = &state.theme;
let focused = state.focus == Focus::Playlists;
let selected_row = focused.then_some(state.playlist_cursor);
let block = styled_block(" Playlists ", selected_row.is_some(), t)
.style(t.apply_sidebar_bg(Style::default()));
let inner = block.inner(area);
frame.render_widget(block, area);
if state.library.playlists.is_empty() {
render_empty_state(frame, inner, "No playlists yet", "c", "to create one", t);
return;
}
let width = (inner.width as usize).saturating_sub(MARKER_W);
let items: Vec<ListItem<'_>> = state
.library
.playlists
.iter()
.enumerate()
.map(|(i, pl)| {
ListItem::new(entry(&pl.name, pl.tracks.len(), width)).style(row_style(
t,
state.active_view == SidebarItem::Playlist(pl.id),
selected_row == Some(i),
))
})
.collect();
let mut list_state = ListState::default();
list_state.select(selected_row);
frame.render_stateful_widget(
List::new(items)
.highlight_style(t.selection_style())
.highlight_symbol(MARKER),
inner,
&mut list_state,
);
}
fn row_style(t: &Theme, is_active: bool, is_cursor: bool) -> Style {
if is_active && is_cursor {
Style::default().fg(t.accent).add_modifier(Modifier::BOLD)
} else if is_cursor {
Style::default().fg(t.text).add_modifier(Modifier::BOLD)
} else if is_active {
Style::default().fg(t.accent)
} else {
Style::default().fg(t.text_dim)
}
}
fn entry(name: &str, count: usize, width: usize) -> String {
let count = count.to_string();
let room = width.saturating_sub(count.len() + 1);
if room == 0 {
return truncate(&count, width);
}
let name = truncate(name, room);
let pad = width.saturating_sub(str_width(&name) + count.len());
format!("{name}{blank:pad$}{count}", blank = "")
}