use ratatui::{ buffer::Buffer, layout::{ Alignment, Constraint, Direction::{self}, Layout, Rect, }, style::{Color, Modifier, Style, Stylize}, symbols, text::Line, widgets::{ Block, BorderType::Rounded, Clear, LineGauge, List, ListDirection, ListItem, Paragraph, Row, StatefulWidget, Table, Widget, }, }; use crate::app::App; use crate::app::Focus; impl Widget for &mut App { fn render(self, area: Rect, buf: &mut Buffer) { let position = self.player.get_pos().as_secs_f64(); let outer_layout = Layout::default() .direction(Direction::Horizontal) .constraints([ Constraint::Percentage(25), Constraint::Percentage(50), Constraint::Percentage(25), ]) .split(area); let left_area = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Fill(100)]) .split(outer_layout[0]); let right_area = Layout::default() .direction(Direction::Vertical) .constraints([Constraint::Percentage(60), Constraint::Percentage(40)]) .split(outer_layout[2]); let center_area = Layout::default() .direction(Direction::Vertical) .constraints([ Constraint::Min(8), // Album list Constraint::Length(6), ]) .split(outer_layout[1]); let now_playing_block = Block::bordered() .title(format!( "Now Playing | Vol: {:.0} | Rate: {}Hz", self.player.volume() * 100.0, self.sample_rate )) .border_type(Rounded); let inner = now_playing_block.inner(center_area[1]); Widget::render(now_playing_block, center_area[1], buf); let player_area = Layout::vertical([ Constraint::Fill(1), Constraint::Length(4), Constraint::Fill(1), ]) .split(inner); let now_playing = Layout::vertical([ Constraint::Length(1), Constraint::Length(1), Constraint::Length(1), ]) .split(player_area[1]); let progress = Layout::horizontal([ Constraint::Length(3), // ▶ Constraint::Length(2), // % Constraint::Fill(1), // bar Constraint::Length(15), // time ]) .split(now_playing[2]); // Dynamic coloring based on pane focus status let artist_border_color = if self.focus == Focus::Artists { Color::Green } else { Color::Gray }; let album_border_color = if self.focus == Focus::Albums { Color::Green } else { Color::Gray }; let song_border_color = if self.focus == Focus::Songs { Color::Green } else { Color::Gray }; let queue_border_color = if self.focus == Focus::Queue { Color::Green } else { Color::Gray }; // Artists let artists: Vec = self .library .artists .iter() .map(|artist| ListItem::new(artist.to_string())) .collect(); let artist_list = List::new(artists) .block( Block::bordered() .title("Artists") .border_style(Style::new().fg(artist_border_color)) .border_type(Rounded), ) .style(Style::new().white()) .highlight_symbol(">>") .repeat_highlight_symbol(true) .highlight_style(Style::new().reversed()) .direction(ListDirection::TopToBottom); StatefulWidget::render(artist_list, left_area[0], buf, &mut self.artist_state); // Info let instructions = Line::from(vec![ " Help ".gray().bold(), " ".blue().bold(), ]); // Albums let albums: Vec = self .library .albums_for_artist( &self.library.artists[self.artist_state.selected().expect("None")].clone(), ) .iter() .map(|albums| ListItem::new(albums.to_string())) .collect(); let album_list = List::new(albums) .block( Block::bordered() .title("Albums") .title_bottom(instructions.right_aligned()) .border_style(Style::new().fg(album_border_color)) .border_type(Rounded), ) .style(Style::new().white()) .highlight_symbol(">>") .repeat_highlight_symbol(true) .highlight_style(Style::new().reversed()) .direction(ListDirection::TopToBottom); StatefulWidget::render(album_list, center_area[0], buf, &mut self.album_state); // Songs let song_indexes: Vec = self.current_selected_song(); let song_items: Vec = song_indexes .iter() .map(|&index| { ListItem::new(format!( "{} ({})", self.library.songs[index].title.clone(), format_time(self.library.songs[index].duration.unwrap()) )) }) .collect(); let song_list = List::new(song_items) .block( Block::bordered() .title("Songs") .border_style(Style::new().fg(song_border_color)) .border_type(Rounded), ) .style(Style::new().white()) .highlight_symbol(">>") .repeat_highlight_symbol(true) .highlight_style(Style::new().reversed()) .direction(ListDirection::TopToBottom); StatefulWidget::render(song_list, right_area[0], buf, &mut self.song_state); // Queue let queue_items: Vec = self .queue .iter() .map(|&index| { ListItem::new(format!( "{} ({}))", self.library.songs[index].title.as_str(), format_time(self.library.songs[index].duration.unwrap()) )) }) .collect(); let queue_list = List::new(queue_items) .block( Block::bordered() .title("Queue") .border_style(Style::new().fg(queue_border_color)) .border_type(Rounded), ) .style(Style::new().white()) .highlight_symbol(">>") .repeat_highlight_symbol(true) .highlight_style(Style::new()) .direction(ListDirection::TopToBottom); StatefulWidget::render(queue_list, right_area[1], buf, &mut self.queue_state); // Help Menu if self.control_popup { let popup_block = Block::bordered().title("Controls").border_type(Rounded); Widget::render(Clear, center_area[0], buf); let header = Row::new(["", "Key", "Function"]) .style(Style::new().bold()) .bottom_margin(1); let rows = self.get_controls(); let widths = [Constraint::Percentage(10), Constraint::Percentage(45), Constraint::Percentage(45)]; let help_menu = Table::new(rows, widths) .header(header) .column_spacing(1) .style(Color::White) .block(popup_block); Widget::render(help_menu, center_area[0], buf); } // Play area if let Some(song) = self.current_song() { let duration = song.duration.unwrap_or(1.0); Paragraph::new(song.title.as_str()) .alignment(Alignment::Center) .render(now_playing[0], buf); Paragraph::new(format!("› {}", song.artist)) .alignment(Alignment::Center) .style(Style::new().fg(Color::DarkGray)) .render(now_playing[1], buf); let percent = position / duration; let line_gauge = LineGauge::default() .filled_style(Style::new().green().add_modifier(Modifier::BOLD)) .unfilled_style( Style::new() .gray() .add_modifier(Modifier::BOLD | Modifier::DIM), ) .label(format!("{:>3.0}%", percent * 100.0)) .ratio(percent.clamp(0.0, 1.0)) .filled_symbol(symbols::line::HORIZONTAL) .unfilled_symbol(symbols::line::HORIZONTAL); Widget::render(line_gauge, progress[2], buf); Paragraph::new(format!( " {} / {}", format_time(position), format_time(duration), )) .render(progress[3], buf); } else { Paragraph::new("Nothing is playing").render(now_playing[0], buf); let line_gauge = LineGauge::default() .ratio(0.0) .filled_symbol(symbols::line::HORIZONTAL) .unfilled_symbol(symbols::line::HORIZONTAL); Widget::render(line_gauge, progress[2], buf); } Paragraph::new(if self.player.is_paused() { "||" } else { ">" }).render(progress[0], buf); } } fn format_time(seconds: f64) -> String { let secs = seconds as u64; format!("{}:{:02}", secs / 60, secs % 60) }