diff --git a/src/collection/mod.rs b/src/collection/mod.rs index 8834ef2..7f72d72 100644 --- a/src/collection/mod.rs +++ b/src/collection/mod.rs @@ -1,3 +1,5 @@ +//! Module for managing the music collection, i.e. "The Music Hoard". + use std::fmt; use crate::{ @@ -41,6 +43,8 @@ impl From for Error { } } +/// The collection manager. It is responsible for pulling information from both the library and the +/// database, ensuring its consistent and writing back any changes. pub struct CollectionManager { library: Box, database: Box, @@ -48,6 +52,7 @@ pub struct CollectionManager { } impl CollectionManager { + /// Create a new [`CollectionManager`] with the provided [`Library`] and [`Database`]. pub fn new( library: Box, database: Box, @@ -59,16 +64,19 @@ impl CollectionManager { } } + /// Rescan the library and integrate any updates into the collection. pub fn rescan_library(&mut self) -> Result<(), Error> { self.collection = self.library.list(&Query::default())?; Ok(()) } + /// Save the collection state to the database. pub fn save_to_database(&mut self) -> Result<(), Error> { self.database.write(&self.collection)?; Ok(()) } + /// Get the current collection. pub fn get_collection(&self) -> &Collection { &self.collection } diff --git a/src/library/mod.rs b/src/library/mod.rs index 3a441e8..b73079a 100644 --- a/src/library/mod.rs +++ b/src/library/mod.rs @@ -115,10 +115,10 @@ impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Self::CmdExecError(ref s) => write!(f, "the library failed to execute a command: {s}"), - Self::InvalidData(ref s) => write!(f, "the library returned invalid data: {s}"), + Self::InvalidData(ref s) => write!(f, "the library received invalid data: {s}"), Self::IoError(ref s) => write!(f, "the library experienced an I/O error: {s}"), - Self::ParseIntError(ref s) => write!(f, "the library returned an invalid integer: {s}"), - Self::Utf8Error(ref s) => write!(f, "the library returned invalid UTF-8: {s}"), + Self::ParseIntError(ref s) => write!(f, "the library received an invalid integer: {s}"), + Self::Utf8Error(ref s) => write!(f, "the library received invalid UTF-8: {s}"), } } } diff --git a/src/main.rs b/src/main.rs index 9cbaf1c..8e242dc 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,21 +6,17 @@ use std::path::PathBuf; use structopt::StructOpt; use musichoard::{ - database::{ - json::{JsonDatabase, JsonDatabaseFileBackend}, - DatabaseWrite, - }, - library::{ - beets::{BeetsLibrary, BeetsLibraryCommandExecutor}, - Library, Query, - }, collection::CollectionManager, + collection::CollectionManager, + database::json::{JsonDatabase, JsonDatabaseFileBackend}, + library::beets::{BeetsLibrary, BeetsLibraryCommandExecutor}, }; mod tui; use tui::{ app::App, - event::{Event, EventHandler}, - handler::handle_key_events, + event::{EventChannel, EventListener}, + handler::EventHandler, + ui::Ui, Tui, }; @@ -28,7 +24,7 @@ use tui::{ struct Opt { #[structopt( short = "b", - long = "beets-config", + long = "beets", name = "beets config file path", parse(from_os_str) )] @@ -58,25 +54,16 @@ fn main() { let collection_manager = CollectionManager::new(Box::new(beets), Box::new(database)); - let mut app = App::new(collection_manager).expect("failed to initialise app"); - // Initialize the terminal user interface. let backend = CrosstermBackend::new(io::stdout()); let terminal = Terminal::new(backend).expect("failed to initialise terminal"); - let events = EventHandler::new(); - let mut tui = Tui::new(terminal, events); - tui.init(); + let channel = EventChannel::new(); + let listener = EventListener::new(channel.sender()); + let handler = EventHandler::new(channel.receiver()); + let ui = Ui::new(); + let app = App::new(collection_manager).expect("failed to initialise app"); + let mut tui = Tui::new(terminal, listener, handler, ui, app); - // Main loop. - while app.is_running() { - tui.draw(&mut app); - match tui.events.next_event() { - Event::Key(key_event) => handle_key_events(key_event, &mut app), - Event::Mouse(_) => {} - Event::Resize(_, _) => {} - } - } - - // Exit the user interface. - tui.exit(); + // Run the TUI application. + tui.run(); } diff --git a/src/tui/app.rs b/src/tui/app.rs index 36e96dd..c7664ba 100644 --- a/src/tui/app.rs +++ b/src/tui/app.rs @@ -2,7 +2,7 @@ use musichoard::{collection::CollectionManager, Album, AlbumId, Artist, ArtistId use super::Error; -#[derive(Copy, Clone)] +#[derive(Copy, Clone, PartialEq, Eq)] pub enum Category { Artist, Album, @@ -16,17 +16,6 @@ pub struct Selection { track: u16, } -impl Default for Selection { - fn default() -> Self { - Selection { - active: Category::Artist, - artist: 0, - album: 0, - track: 0, - } - } -} - pub struct App { collection_manager: CollectionManager, selection: Selection, @@ -38,7 +27,12 @@ impl App { collection_manager.rescan_library()?; Ok(App { collection_manager, - selection: Selection::default(), + selection: Selection { + active: Category::Artist, + artist: 0, + album: 0, + track: 0, + }, running: true, }) } @@ -117,10 +111,10 @@ impl App { } fn increment_track_selection(&mut self) { - let artists: &Vec = self.collection_manager.get_collection(); - let albums: &Vec = &artists[self.selection.artist as usize].albums; - let tracks: &Vec = &albums[self.selection.album as usize].tracks; if let Some(result) = self.selection.track.checked_add(1) { + let artists: &Vec = self.collection_manager.get_collection(); + let albums: &Vec = &artists[self.selection.artist as usize].albums; + let tracks: &Vec = &albums[self.selection.album as usize].tracks; if (result as usize) < tracks.len() { self.selection.track = result; } diff --git a/src/tui/event.rs b/src/tui/event.rs index f98c8dd..07cf770 100644 --- a/src/tui/event.rs +++ b/src/tui/event.rs @@ -2,59 +2,85 @@ use crossterm::event::{self, Event as CrosstermEvent, KeyEvent, MouseEvent}; use std::sync::mpsc; use std::thread; -/// Terminal events. -#[derive(Clone, Copy, Debug)] +#[derive(Clone, Copy)] pub enum Event { - /// Key press. Key(KeyEvent), - /// Mouse click/scroll. Mouse(MouseEvent), - /// Terminal resize. Resize(u16, u16), } -/// Terminal event handler. -#[derive(Debug)] -pub struct EventHandler { - /// Event sender channel. +pub struct EventChannel { sender: mpsc::Sender, - /// Event receiver channel. receiver: mpsc::Receiver, - /// Event handler thread. - handler: thread::JoinHandle<()>, } -impl EventHandler { - /// Constructs a new instance of [`EventHandler`]. +#[derive(Clone)] +pub struct EventSender { + sender: mpsc::Sender, +} + +pub struct EventReceiver { + receiver: mpsc::Receiver, +} + +impl EventChannel { pub fn new() -> Self { let (sender, receiver) = mpsc::channel(); - let handler = { - let sender = sender.clone(); - thread::spawn(move || loop { - // Put this inside an if event::poll {...} if the display needs to be refreshed on a - // periodic basis. See - // https://github.com/tui-rs-revival/rust-tui-template/blob/master/src/event.rs. - match event::read().expect("unable to read event") { - CrosstermEvent::Key(e) => sender.send(Event::Key(e)), - CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), - CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), - _ => unimplemented!(), - } - .expect("failed to send terminal event") - }) - }; - Self { - sender, - receiver, - handler, + EventChannel { sender, receiver } + } + + pub fn sender(&self) -> EventSender { + EventSender { + sender: self.sender.clone(), } } - /// Receive the next event from the handler thread. - /// - /// This function will always block the current thread if - /// there is no data available and it's possible for more data to be sent. - pub fn next_event(&self) -> Event { - self.receiver.recv().expect("failed to receive terminal event") + pub fn receiver(self) -> EventReceiver { + EventReceiver { + receiver: self.receiver, + } + } +} + +impl EventSender { + pub fn send(&self, event: Event) { + self.sender + .send(event) + .expect("failed to send terminal event"); + } +} + +impl EventReceiver { + pub fn recv(&self) -> Event { + self.receiver + .recv() + .expect("failed to receive terminal event") + } +} + +pub struct EventListener { + events: Option, +} + +impl EventListener { + pub fn new(events: EventSender) -> EventListener { + EventListener { + events: Some(events), + } + } + + pub fn spawn(&mut self) { + let sender = self.events.take().unwrap(); + thread::spawn(move || loop { + // Put this inside an if event::poll {...} if the display needs to be refreshed on a + // periodic basis. See + // https://github.com/tui-rs-revival/rust-tui-template/blob/master/src/event.rs. + match event::read().expect("unable to read event") { + CrosstermEvent::Key(e) => sender.send(Event::Key(e)), + CrosstermEvent::Mouse(e) => sender.send(Event::Mouse(e)), + CrosstermEvent::Resize(w, h) => sender.send(Event::Resize(w, h)), + _ => unimplemented!(), + } + }); } } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index b177ca6..20ef5cb 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -1,35 +1,52 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use super::app::App; +use super::{app::App, event::{Event, EventReceiver}}; -/// Handles the key events and updates the state of [`App`]. -pub fn handle_key_events(key_event: KeyEvent, app: &mut App) { - match key_event.code { - // Exit application on `ESC` or `q`. - KeyCode::Esc | KeyCode::Char('q') => { - app.quit(); +pub struct EventHandler { + events: EventReceiver, +} + +impl EventHandler { + pub fn new(events: EventReceiver) -> Self { + EventHandler { events } + } + + pub fn handle_next_event(&mut self, app: &mut App) { + match self.events.recv() { + Event::Key(key_event) => Self::handle_key_event(app, key_event), + Event::Mouse(_) => {} + Event::Resize(_, _) => {} } - // Exit application on `Ctrl-C`. - KeyCode::Char('c') | KeyCode::Char('C') => { - if key_event.modifiers == KeyModifiers::CONTROL { + } + + fn handle_key_event(app: &mut App, key_event: KeyEvent) { + match key_event.code { + // Exit application on `ESC` or `q`. + KeyCode::Esc | KeyCode::Char('q') => { app.quit(); } + // Exit application on `Ctrl-C`. + KeyCode::Char('c') | KeyCode::Char('C') => { + if key_event.modifiers == KeyModifiers::CONTROL { + app.quit(); + } + } + // Category change. + KeyCode::Left => { + app.decrement_category(); + } + KeyCode::Right => { + app.increment_category(); + } + // Selection change. + KeyCode::Up => { + app.decrement_selection(); + } + KeyCode::Down => { + app.increment_selection(); + } + // Other keys. + _ => {} } - // Category change. - KeyCode::Left => { - app.decrement_category(); - } - KeyCode::Right => { - app.increment_category(); - } - // Selection change. - KeyCode::Up => { - app.decrement_selection(); - } - KeyCode::Down => { - app.increment_selection(); - } - // Other handlers you could add here. - _ => {} } } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index cff0299..4f25ed4 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -8,17 +8,15 @@ use std::io; pub mod app; pub mod event; pub mod handler; - -mod ui; - -use event::EventHandler; +pub mod ui; use self::app::App; +use self::event::EventListener; +use self::handler::EventHandler; +use self::ui::Ui; -/// Error type for the TUI. #[derive(Debug)] pub enum Error { - /// The collection manager failed. CollectionError(String), } @@ -28,46 +26,53 @@ impl From for Error { } } -/// Representation of a terminal user interface. -/// -/// It is responsible for setting up the terminal, -/// initializing the interface and handling the draw events. -#[derive(Debug)] pub struct Tui { - /// Interface to the Terminal. terminal: Terminal, - /// Terminal event handler. - pub events: EventHandler, + listener: EventListener, + handler: EventHandler, + ui: Ui, + app: App, } impl Tui { - /// Constructs a new instance of [`Tui`]. - pub fn new(terminal: Terminal, events: EventHandler) -> Self { - Self { terminal, events } + pub fn new( + terminal: Terminal, + listener: EventListener, + handler: EventHandler, + ui: Ui, + app: App, + ) -> Self { + Self { + terminal, + listener, + handler, + ui, + app, + } } - /// Initializes the terminal interface. - /// - /// It enables the raw mode and sets terminal properties. - pub fn init(&mut self) { + fn init(&mut self) { terminal::enable_raw_mode().unwrap(); crossterm::execute!(io::stdout(), EnterAlternateScreen, EnableMouseCapture).unwrap(); self.terminal.hide_cursor().unwrap(); self.terminal.clear().unwrap(); } - /// [`Draw`] the terminal interface by [`rendering`] the widgets. - /// - /// [`Draw`]: tui::Terminal::draw - /// [`rendering`]: crate::ui:render - pub fn draw(&mut self, app: &mut App) { - self.terminal.draw(|frame| ui::render(app, frame)).unwrap(); + pub fn run(&mut self) { + self.init(); + + self.listener.spawn(); + while self.app.is_running() { + self.terminal + .draw(|frame| self.ui.render(&mut self.app, frame)) + .unwrap(); + self.handler.handle_next_event(&mut self.app); + } + + self.exit(); } - /// Exits the terminal interface. - /// - /// It disables the raw mode and reverts back the terminal properties. - pub fn exit(&mut self) { + fn exit(&mut self) { terminal::disable_raw_mode().unwrap(); crossterm::execute!(io::stdout(), LeaveAlternateScreen, DisableMouseCapture).unwrap(); self.terminal.show_cursor().unwrap(); diff --git a/src/tui/ui.rs b/src/tui/ui.rs index 2295f2e..997b473 100644 --- a/src/tui/ui.rs +++ b/src/tui/ui.rs @@ -1,176 +1,204 @@ -use musichoard::{collection::Collection, TrackFormat}; +use musichoard::TrackFormat; use ratatui::{ backend::Backend, layout::{Alignment, Rect}, - style::{Color, Modifier, Style}, + style::{Color, Style}, widgets::{Block, BorderType, Borders, List, ListItem, ListState, Paragraph}, Frame, }; use super::app::{App, Category}; -/// Renders the user interface widgets. -pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { - // This is where you add new widgets. - // See the following resources: - // - https://docs.rs/ratatui/latest/ratatui/widgets/index.html - // - https://github.com/tui-rs-revival/ratatui/tree/master/examples +struct ArtistArea { + list: Rect, +} - let artists: Vec = app - .get_artists() - .iter() - .map(|id| ListItem::new(id.name.as_str())) - .collect(); +struct AlbumArea { + list: Rect, + info: Rect, +} - let frame_rect = frame.size(); - let width_over_three = frame_rect.width / 3; - let height_over_three = frame_rect.height / 3; +struct TrackArea { + list: Rect, + info: Rect, +} - let artists_rect = Rect { - x: frame_rect.x, - y: frame_rect.y, - width: width_over_three, - height: frame_rect.height, - }; +struct FrameAreas { + artists: ArtistArea, + albums: AlbumArea, + tracks: TrackArea, +} - let mut artists_state = ListState::default(); - artists_state.select(Some(app.selected_artist())); +struct SelectionList<'a> { + list: List<'a>, + state: ListState, + active: bool, +} - frame.render_stateful_widget( - List::new(artists) - .highlight_style(Style::default().bg( - if let Category::Artist = app.get_active_category() { - Color::DarkGray - } else { - Color::Black - }, - )) - .highlight_symbol(">> ") - .style(Style::default().fg(Color::White).bg(Color::Black)) - .block( - Block::default() - .title(" Artists ") - .title_alignment(Alignment::Center) - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ), - artists_rect, - &mut artists_state, - ); +struct ArtistState<'a> { + list: SelectionList<'a>, +} - let albums: Vec = app - .get_albums() - .iter() - .map(|id| ListItem::new(id.title.as_str())) - .collect(); +struct AlbumState<'a> { + list: SelectionList<'a>, + info: Paragraph<'a>, +} - let albums_rect = Rect { - x: artists_rect.x + artists_rect.width, - y: frame_rect.y, - width: width_over_three, - height: frame_rect.height - height_over_three, - }; +struct TrackState<'a> { + list: SelectionList<'a>, + info: Paragraph<'a>, +} - let mut albums_state = ListState::default(); - albums_state.select(Some(app.selected_album())); +struct AppState<'a> { + artists: ArtistState<'a>, + albums: AlbumState<'a>, + tracks: TrackState<'a>, +} - frame.render_stateful_widget( - List::new(albums) - .highlight_style(Style::default().bg( - if let Category::Album = app.get_active_category() { - Color::DarkGray - } else { - Color::Black - }, - )) - .highlight_symbol(">> ") - .style(Style::default().fg(Color::White).bg(Color::Black)) - .block( - Block::default() - .title(" Albums ") - .title_alignment(Alignment::Center) - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ), - albums_rect, - &mut albums_state, - ); +pub struct Ui {} - let album = app.get_albums()[app.selected_album()]; +impl Ui { + const COLOR_FG: Color = Color::White; + const COLOR_BG: Color = Color::Black; + const COLOR_HL: Color = Color::DarkGray; - let albums_info_rect = Rect { - x: albums_rect.x, - y: albums_rect.y + albums_rect.height, - width: albums_rect.width, - height: height_over_three, - }; + pub fn new() -> Self { + Ui {} + } - frame.render_widget( - Paragraph::new(format!( + fn construct_areas(frame: Rect) -> FrameAreas { + let width_one_third = frame.width / 3; + let height_one_third = frame.height / 3; + + let panel_width = width_one_third; + let panel_width_last = frame.width - 2 * panel_width; + let panel_height_top = frame.height - height_one_third; + let panel_height_bottom = height_one_third; + + let artist_list = Rect { + x: frame.x, + y: frame.y, + width: panel_width, + height: frame.height, + }; + + let album_list = Rect { + x: artist_list.x + artist_list.width, + y: frame.y, + width: panel_width, + height: panel_height_top, + }; + + let album_info = Rect { + x: album_list.x, + y: album_list.y + album_list.height, + width: album_list.width, + height: panel_height_bottom, + }; + + let track_list = Rect { + x: album_list.x + album_list.width, + y: frame.y, + width: panel_width_last, + height: panel_height_top, + }; + + let track_info = Rect { + x: track_list.x, + y: track_list.y + track_list.height, + width: track_list.width, + height: panel_height_bottom, + }; + + FrameAreas { + artists: ArtistArea { list: artist_list }, + albums: AlbumArea { + list: album_list, + info: album_info, + }, + tracks: TrackArea { + list: track_list, + info: track_info, + }, + } + } + + fn construct_artist_list(app: &App) -> ArtistState { + let artists = app.get_artists(); + let list = List::new( + artists + .iter() + .map(|id| ListItem::new(id.name.as_str())) + .collect::>(), + ); + + let selected_artist = app.selected_artist(); + + let mut state = ListState::default(); + state.select(Some(selected_artist)); + + let active = app.get_active_category() == Category::Artist; + + ArtistState { + list: SelectionList { + list, + state, + active, + }, + } + } + + fn construct_album_list(app: &App) -> AlbumState { + let albums = app.get_albums(); + let list = List::new( + albums + .iter() + .map(|id| ListItem::new(id.title.as_str())) + .collect::>(), + ); + + let selected_album = app.selected_album(); + + let mut state = ListState::default(); + state.select(Some(selected_album)); + + let active = app.get_active_category() == Category::Album; + + let album = albums[selected_album]; + let info = Paragraph::new(format!( "Title: {}\n\ Year: {}", album.title, album.year, - )) - .style(Style::default().fg(Color::White).bg(Color::Black)) - .block( - Block::default() - .title(" Album info ") - .title_alignment(Alignment::Center) - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ), - albums_info_rect, - ); + )); - let tracks: Vec = app - .get_tracks() - .iter() - .map(|t| ListItem::new(t.title.as_str())) - .collect(); + AlbumState { + list: SelectionList { + list, + state, + active, + }, + info, + } + } - let tracks_rect = Rect { - x: albums_rect.x + albums_rect.width, - y: frame_rect.y, - width: frame_rect.width - 2 * width_over_three, - height: frame_rect.height - height_over_three, - }; + fn construct_track_list(app: &App) -> TrackState { + let tracks = app.get_tracks(); + let list = List::new( + tracks + .iter() + .map(|id| ListItem::new(id.title.as_str())) + .collect::>(), + ); - let mut tracks_state = ListState::default(); - tracks_state.select(Some(app.selected_track())); + let selected_track = app.selected_track(); - frame.render_stateful_widget( - List::new(tracks) - .highlight_style(Style::default().bg( - if let Category::Track = app.get_active_category() { - Color::DarkGray - } else { - Color::Black - }, - )) - .highlight_symbol(">> ") - .style(Style::default().fg(Color::White).bg(Color::Black)) - .block( - Block::default() - .title(" Tracks ") - .title_alignment(Alignment::Center) - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ), - tracks_rect, - &mut tracks_state, - ); + let mut state = ListState::default(); + state.select(Some(selected_track)); - let track = app.get_tracks()[app.selected_track()]; + let active = app.get_active_category() == Category::Track; - let track_info_rect = Rect { - x: tracks_rect.x, - y: tracks_rect.y + tracks_rect.height, - width: tracks_rect.width, - height: height_over_three, - }; - - frame.render_widget( - Paragraph::new(format!( + let track = tracks[selected_track]; + let info = Paragraph::new(format!( "Track: {}\n\ Title: {}\n\ Artist: {}\n\ @@ -182,15 +210,112 @@ pub fn render(app: &mut App, frame: &mut Frame<'_, B>) { TrackFormat::Flac => "FLAC", TrackFormat::Mp3 => "MP3", }, - )) - .style(Style::default().fg(Color::White).bg(Color::Black)) - .block( - Block::default() - .title(" Track info ") - .title_alignment(Alignment::Center) - .borders(Borders::ALL) - .border_type(BorderType::Rounded), - ), - track_info_rect, - ); + )); + + TrackState { + list: SelectionList { + list, + state, + active, + }, + info, + } + } + + fn construct_app_state(app: &App) -> AppState { + AppState { + artists: Self::construct_artist_list(app), + albums: Self::construct_album_list(app), + tracks: Self::construct_track_list(app), + } + } + + fn style() -> Style { + Style::default().fg(Self::COLOR_FG).bg(Self::COLOR_BG) + } + + fn highlight_style(active: bool) -> Style { + Style::default().bg(if active { + Self::COLOR_HL + } else { + Self::COLOR_BG + }) + } + + fn block<'a>() -> Block<'a> { + Block::default() + .title_alignment(Alignment::Center) + .borders(Borders::ALL) + .border_type(BorderType::Rounded) + } + + fn render_list_widget( + title: &str, + mut list: SelectionList, + area: Rect, + frame: &mut Frame<'_, B>, + ) { + frame.render_stateful_widget( + list.list + .highlight_style(Self::highlight_style(list.active)) + .highlight_symbol(">> ") + .style(Self::style()) + .block(Self::block().title(format!(" {title} "))), + area, + &mut list.state, + ); + } + + fn render_info_widget( + title: &str, + paragraph: Paragraph, + area: Rect, + frame: &mut Frame<'_, B>, + ) { + frame.render_widget( + paragraph + .style(Self::style()) + .block(Self::block().title(format!(" {title} "))), + area, + ); + } + + fn render_artist_column( + state: ArtistState, + area: ArtistArea, + frame: &mut Frame<'_, B>, + ) { + Self::render_list_widget("Artists", state.list, area.list, frame); + } + + fn render_album_column( + state: AlbumState, + area: AlbumArea, + frame: &mut Frame<'_, B>, + ) { + Self::render_list_widget("Albums", state.list, area.list, frame); + Self::render_info_widget("Album info", state.info, area.info, frame); + } + + fn render_track_column( + state: TrackState, + area: TrackArea, + frame: &mut Frame<'_, B>, + ) { + Self::render_list_widget("Tracks", state.list, area.list, frame); + Self::render_info_widget("Track info", state.info, area.info, frame); + } + + pub fn render(&mut self, app: &App, frame: &mut Frame<'_, B>) { + // This is where you add new widgets. + // See the following resources: + // - https://docs.rs/ratatui/latest/ratatui/widgets/index.html + // - https://github.com/tui-rs-revival/ratatui/tree/master/examples + let areas = Self::construct_areas(frame.size()); + let app_state = Self::construct_app_state(app); + + Self::render_artist_column(app_state.artists, areas.artists, frame); + Self::render_album_column(app_state.albums, areas.albums, frame); + Self::render_track_column(app_state.tracks, areas.tracks, frame); + } }