Selected item is always at the bottom of list #41

Merged
wojtek merged 7 commits from 40---selected-item-is-always-at-the-bottom-of-list into main 2023-04-27 19:05:37 +02:00
5 changed files with 1040 additions and 1107 deletions
Showing only changes of commit e9981a4bc1 - Show all commits

View File

@ -20,10 +20,8 @@ use musichoard::{
}; };
mod tui; mod tui;
use tui::{ use tui::ui::MhUi;
app::TuiApp, event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, ui::Ui, use tui::{event::EventChannel, handler::TuiEventHandler, listener::TuiEventListener, Tui};
Tui,
};
#[derive(StructOpt)] #[derive(StructOpt)]
struct Opt { struct Opt {
@ -52,12 +50,10 @@ fn with<LIB: Library, DB: Database>(lib: LIB, db: DB) {
let listener = TuiEventListener::new(channel.sender()); let listener = TuiEventListener::new(channel.sender());
let handler = TuiEventHandler::new(channel.receiver()); let handler = TuiEventHandler::new(channel.receiver());
let ui = Ui::new(); let ui = MhUi::new(collection_manager).expect("failed to initialise ui");
let app = TuiApp::new(collection_manager).expect("failed to initialise app");
// Run the TUI application. // Run the TUI application.
Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui"); Tui::run(terminal, ui, handler, listener).expect("failed to run tui");
} }
fn main() { fn main() {

File diff suppressed because it is too large Load Diff

View File

@ -4,17 +4,17 @@ use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
use mockall::automock; use mockall::automock;
use super::{ use super::{
app::App,
event::{Event, EventError, EventReceiver}, event::{Event, EventError, EventReceiver},
ui::Ui,
}; };
#[cfg_attr(test, automock)] #[cfg_attr(test, automock)]
pub trait EventHandler<APP> { pub trait EventHandler<UI> {
fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError>; fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>;
} }
trait EventHandlerPrivate<APP> { trait EventHandlerPrivate<UI> {
fn handle_key_event(app: &mut APP, key_event: KeyEvent); fn handle_key_event(ui: &mut UI, key_event: KeyEvent);
} }
pub struct TuiEventHandler { pub struct TuiEventHandler {
@ -28,10 +28,10 @@ impl TuiEventHandler {
} }
} }
impl<APP: App> EventHandler<APP> for TuiEventHandler { impl<UI: Ui> EventHandler<UI> for TuiEventHandler {
fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> { fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError> {
match self.events.recv()? { match self.events.recv()? {
Event::Key(key_event) => Self::handle_key_event(app, key_event), Event::Key(key_event) => Self::handle_key_event(ui, key_event),
Event::Mouse(_) => {} Event::Mouse(_) => {}
Event::Resize(_, _) => {} Event::Resize(_, _) => {}
}; };
@ -39,32 +39,32 @@ impl<APP: App> EventHandler<APP> for TuiEventHandler {
} }
} }
impl<APP: App> EventHandlerPrivate<APP> for TuiEventHandler { impl<UI: Ui> EventHandlerPrivate<UI> for TuiEventHandler {
fn handle_key_event(app: &mut APP, key_event: KeyEvent) { fn handle_key_event(ui: &mut UI, key_event: KeyEvent) {
match key_event.code { match key_event.code {
// Exit application on `ESC` or `q`. // Exit application on `ESC` or `q`.
KeyCode::Esc | KeyCode::Char('q') => { KeyCode::Esc | KeyCode::Char('q') => {
app.quit(); ui.quit();
} }
// Exit application on `Ctrl-C`. // Exit application on `Ctrl-C`.
KeyCode::Char('c') | KeyCode::Char('C') => { KeyCode::Char('c') | KeyCode::Char('C') => {
if key_event.modifiers == KeyModifiers::CONTROL { if key_event.modifiers == KeyModifiers::CONTROL {
app.quit(); ui.quit();
} }
} }
// Category change. // Category change.
KeyCode::Left => { KeyCode::Left => {
app.decrement_category(); ui.decrement_category();
} }
KeyCode::Right => { KeyCode::Right => {
app.increment_category(); ui.increment_category();
} }
// Selection change. // Selection change.
KeyCode::Up => { KeyCode::Up => {
app.decrement_selection(); ui.decrement_selection();
} }
KeyCode::Down => { KeyCode::Down => {
app.increment_selection(); ui.increment_selection();
} }
// Other keys. // Other keys.
_ => {} _ => {}

View File

@ -6,13 +6,11 @@ use ratatui::Terminal;
use std::io; use std::io;
use std::marker::PhantomData; use std::marker::PhantomData;
pub mod app;
pub mod event; pub mod event;
pub mod handler; pub mod handler;
pub mod listener; pub mod listener;
pub mod ui; pub mod ui;
use self::app::App;
use self::event::EventError; use self::event::EventError;
use self::handler::EventHandler; use self::handler::EventHandler;
use self::listener::EventListener; use self::listener::EventListener;
@ -44,12 +42,12 @@ impl From<EventError> for Error {
} }
} }
pub struct Tui<B: Backend, APP> { pub struct Tui<B: Backend, UI> {
terminal: Terminal<B>, terminal: Terminal<B>,
_phantom: PhantomData<APP>, _phantom: PhantomData<UI>,
} }
impl<B: Backend, APP: App> Tui<B, APP> { impl<B: Backend, UI: Ui> Tui<B, UI> {
fn init(&mut self) -> Result<(), Error> { fn init(&mut self) -> Result<(), Error> {
self.terminal.hide_cursor()?; self.terminal.hide_cursor()?;
self.terminal.clear()?; self.terminal.clear()?;
@ -66,15 +64,10 @@ impl<B: Backend, APP: App> Tui<B, APP> {
self.exit(); self.exit();
} }
fn main_loop( fn main_loop(&mut self, mut ui: UI, handler: impl EventHandler<UI>) -> Result<(), Error> {
&mut self, while ui.is_running() {
mut app: APP, self.terminal.draw(|frame| ui.render(frame))?;
ui: Ui<APP>, handler.handle_next_event(&mut ui)?;
handler: impl EventHandler<APP>,
) -> Result<(), Error> {
while app.is_running() {
self.terminal.draw(|frame| ui.render(&app, frame))?;
handler.handle_next_event(&mut app)?;
} }
Ok(()) Ok(())
@ -82,9 +75,8 @@ impl<B: Backend, APP: App> Tui<B, APP> {
fn main( fn main(
term: Terminal<B>, term: Terminal<B>,
app: APP, ui: UI,
ui: Ui<APP>, handler: impl EventHandler<UI>,
handler: impl EventHandler<APP>,
listener: impl EventListener, listener: impl EventListener,
) -> Result<(), Error> { ) -> Result<(), Error> {
let mut tui = Tui { let mut tui = Tui {
@ -95,7 +87,7 @@ impl<B: Backend, APP: App> Tui<B, APP> {
tui.init()?; tui.init()?;
let listener_handle = listener.spawn(); let listener_handle = listener.spawn();
let result = tui.main_loop(app, ui, handler); let result = tui.main_loop(ui, handler);
match result { match result {
Ok(_) => { Ok(_) => {
@ -142,13 +134,12 @@ impl<B: Backend, APP: App> Tui<B, APP> {
pub fn run( pub fn run(
term: Terminal<B>, term: Terminal<B>,
app: APP, ui: UI,
ui: Ui<APP>, handler: impl EventHandler<UI>,
handler: impl EventHandler<APP>,
listener: impl EventListener, listener: impl EventListener,
) -> Result<(), Error> { ) -> Result<(), Error> {
Self::enable()?; Self::enable()?;
let result = Self::main(term, app, ui, handler, listener); let result = Self::main(term, ui, handler, listener);
match result { match result {
Ok(_) => { Ok(_) => {
Self::disable()?; Self::disable()?;
@ -165,155 +156,151 @@ impl<B: Backend, APP: App> Tui<B, APP> {
// GRCOV_EXCL_STOP // GRCOV_EXCL_STOP
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
use std::{io, thread}; // use std::{io, thread};
use musichoard::collection::{self, Collection}; // use musichoard::collection::{self, Collection};
use ratatui::{backend::TestBackend, Terminal}; // use ratatui::{backend::TestBackend, Terminal};
use crate::tests::{MockCollectionManager, COLLECTION}; // use crate::tests::{MockCollectionManager, COLLECTION};
use super::{ // use super::{
app::{App, TuiApp}, // app::TuiApp, event::EventError, handler::MockEventHandler, listener::MockEventListener,
event::EventError, // ui::Ui, Error, Tui,
handler::MockEventHandler, // };
listener::MockEventListener,
ui::Ui,
Error, Tui,
};
pub fn terminal() -> Terminal<TestBackend> { // pub fn terminal() -> Terminal<TestBackend> {
let backend = TestBackend::new(150, 30); // let backend = TestBackend::new(150, 30);
Terminal::new(backend).unwrap() // Terminal::new(backend).unwrap()
} // }
pub fn app(collection: Collection) -> TuiApp<MockCollectionManager> { // pub fn app(collection: Collection) -> TuiApp<MockCollectionManager> {
let mut collection_manager = MockCollectionManager::new(); // let mut collection_manager = MockCollectionManager::new();
collection_manager // collection_manager
.expect_rescan_library() // .expect_rescan_library()
.returning(|| Ok(())); // .returning(|| Ok(()));
collection_manager // collection_manager
.expect_get_collection() // .expect_get_collection()
.return_const(collection); // .return_const(collection);
TuiApp::new(collection_manager).unwrap() // TuiApp::new(collection_manager).unwrap()
} // }
fn listener() -> MockEventListener { // fn listener() -> MockEventListener {
let mut listener = MockEventListener::new(); // let mut listener = MockEventListener::new();
listener.expect_spawn().return_once(|| { // listener.expect_spawn().return_once(|| {
thread::spawn(|| { // thread::spawn(|| {
thread::park(); // thread::park();
return EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "unparked")); // return EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "unparked"));
}) // })
}); // });
listener // listener
} // }
fn handler() -> MockEventHandler<TuiApp<MockCollectionManager>> { // fn handler() -> MockEventHandler<TuiApp<MockCollectionManager>> {
let mut handler = MockEventHandler::new(); // let mut handler = MockEventHandler::new();
handler.expect_handle_next_event().return_once( // handler.expect_handle_next_event().return_once(
|app: &mut TuiApp<MockCollectionManager>| { // |app: &mut TuiApp<MockCollectionManager>| {
app.quit(); // app.quit();
Ok(()) // Ok(())
}, // },
); // );
handler // handler
} // }
#[test] // #[test]
fn run() { // fn run() {
let terminal = terminal(); // let terminal = terminal();
let app = app(COLLECTION.to_owned()); // let app = app(COLLECTION.to_owned());
let ui = Ui::new(); // let ui = Ui::new();
let listener = listener(); // let listener = listener();
let handler = handler(); // let handler = handler();
let result = Tui::main(terminal, app, ui, handler, listener); // let result = Tui::main(terminal, app, ui, handler, listener);
assert!(result.is_ok()); // assert!(result.is_ok());
} // }
#[test] // #[test]
fn event_error() { // fn event_error() {
let terminal = terminal(); // let terminal = terminal();
let app = app(COLLECTION.to_owned()); // let app = app(COLLECTION.to_owned());
let ui = Ui::new(); // let ui = Ui::new();
let listener = listener(); // let listener = listener();
let mut handler = MockEventHandler::new(); // let mut handler = MockEventHandler::new();
handler // handler
.expect_handle_next_event() // .expect_handle_next_event()
.return_once(|_| Err(EventError::Recv)); // .return_once(|_| Err(EventError::Recv));
let result = Tui::main(terminal, app, ui, handler, listener); // let result = Tui::main(terminal, app, ui, handler, listener);
assert!(result.is_err()); // assert!(result.is_err());
assert_eq!( // assert_eq!(
result.unwrap_err(), // result.unwrap_err(),
Error::Event(EventError::Recv.to_string()) // Error::Event(EventError::Recv.to_string())
); // );
} // }
#[test] // #[test]
fn listener_error() { // fn listener_error() {
let terminal = terminal(); // let terminal = terminal();
let app = app(COLLECTION.to_owned()); // let app = app(COLLECTION.to_owned());
let ui = Ui::new(); // let ui = Ui::new();
let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error")); // let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error"));
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| error); // let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| error);
while !listener_handle.is_finished() {} // while !listener_handle.is_finished() {}
let mut listener = MockEventListener::new(); // let mut listener = MockEventListener::new();
listener.expect_spawn().return_once(|| listener_handle); // listener.expect_spawn().return_once(|| listener_handle);
let mut handler = MockEventHandler::new(); // let mut handler = MockEventHandler::new();
handler // handler
.expect_handle_next_event() // .expect_handle_next_event()
.return_once(|_| Err(EventError::Recv)); // .return_once(|_| Err(EventError::Recv));
let result = Tui::main(terminal, app, ui, handler, listener); // let result = Tui::main(terminal, app, ui, handler, listener);
assert!(result.is_err()); // assert!(result.is_err());
let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error")); // let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error"));
assert_eq!(result.unwrap_err(), Error::Event(error.to_string())); // assert_eq!(result.unwrap_err(), Error::Event(error.to_string()));
} // }
#[test] // #[test]
fn listener_panic() { // fn listener_panic() {
let terminal = terminal(); // let terminal = terminal();
let app = app(COLLECTION.to_owned()); // let app = app(COLLECTION.to_owned());
let ui = Ui::new(); // let ui = Ui::new();
let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| panic!()); // let listener_handle: thread::JoinHandle<EventError> = thread::spawn(|| panic!());
while !listener_handle.is_finished() {} // while !listener_handle.is_finished() {}
let mut listener = MockEventListener::new(); // let mut listener = MockEventListener::new();
listener.expect_spawn().return_once(|| listener_handle); // listener.expect_spawn().return_once(|| listener_handle);
let mut handler = MockEventHandler::new(); // let mut handler = MockEventHandler::new();
handler // handler
.expect_handle_next_event() // .expect_handle_next_event()
.return_once(|_| Err(EventError::Recv)); // .return_once(|_| Err(EventError::Recv));
let result = Tui::main(terminal, app, ui, handler, listener); // let result = Tui::main(terminal, app, ui, handler, listener);
assert!(result.is_err()); // assert!(result.is_err());
assert_eq!(result.unwrap_err(), Error::ListenerPanic); // assert_eq!(result.unwrap_err(), Error::ListenerPanic);
} // }
#[test] // #[test]
fn errors() { // fn errors() {
let collection_err: Error = collection::Error::DatabaseError(String::from("")).into(); // let collection_err: Error = collection::Error::DatabaseError(String::from("")).into();
let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into(); // let io_err: Error = io::Error::new(io::ErrorKind::Interrupted, "error").into();
let event_err: Error = EventError::Recv.into(); // let event_err: Error = EventError::Recv.into();
let listener_err = Error::ListenerPanic; // let listener_err = Error::ListenerPanic;
assert!(!format!("{:?}", collection_err).is_empty()); // assert!(!format!("{:?}", collection_err).is_empty());
assert!(!format!("{:?}", io_err).is_empty()); // assert!(!format!("{:?}", io_err).is_empty());
assert!(!format!("{:?}", event_err).is_empty()); // assert!(!format!("{:?}", event_err).is_empty());
assert!(!format!("{:?}", listener_err).is_empty()); // assert!(!format!("{:?}", listener_err).is_empty());
} // }
} // }

View File

@ -1,6 +1,7 @@
use std::marker::PhantomData; use musichoard::{
collection::{Collection, CollectionManager},
use musichoard::TrackFormat; Album, AlbumId, Artist, Track, TrackFormat,
};
use ratatui::{ use ratatui::{
backend::Backend, backend::Backend,
layout::{Alignment, Rect}, layout::{Alignment, Rect},
@ -9,7 +10,276 @@ use ratatui::{
Frame, Frame,
}; };
use super::app::{App, Category}; use super::Error;
struct TrackSelection {
selection: ListState,
}
struct AlbumSelection {
selection: ListState,
track: TrackSelection,
}
struct ArtistSelection {
selection: ListState,
album: AlbumSelection,
}
impl TrackSelection {
fn initialise(tracks: Option<&[Track]>) -> Self {
let mut selection = ListState::default();
if let Some(tracks) = tracks {
selection.select(if !tracks.is_empty() { Some(0) } else { None });
} else {
selection.select(None);
};
TrackSelection { selection }
}
fn selection(&mut self) -> &mut ListState {
&mut self.selection
}
fn increment(&mut self, tracks: &[Track]) {
if let Some(index) = self.selection.selected() {
if let Some(result) = index.checked_add(1) {
if result < tracks.len() {
self.selection.select(Some(result));
}
}
}
}
fn decrement(&mut self, _tracks: &[Track]) {
if let Some(index) = self.selection.selected() {
if let Some(result) = index.checked_sub(1) {
self.selection.select(Some(result));
}
}
}
}
impl AlbumSelection {
fn initialise(albums: Option<&[Album]>) -> Self {
let mut selection = ListState::default();
let track: TrackSelection;
if let Some(albums) = albums {
selection.select(if !albums.is_empty() { Some(0) } else { None });
track = TrackSelection::initialise(albums.get(0).map(|a| a.tracks.as_slice()));
} else {
selection.select(None);
track = TrackSelection::initialise(None);
}
AlbumSelection { selection, track }
}
fn selection(&mut self) -> &mut ListState {
&mut self.selection
}
fn track_selection(&mut self) -> &mut ListState {
self.track.selection()
}
fn increment(&mut self, albums: &[Album]) {
if let Some(index) = self.selection.selected() {
if let Some(result) = index.checked_add(1) {
if result < albums.len() {
self.selection.select(Some(result));
self.track = TrackSelection::initialise(Some(&albums[result].tracks));
}
}
}
}
fn increment_track(&mut self, albums: &[Album]) {
if let Some(index) = self.selection.selected() {
self.track.increment(&albums[index].tracks);
}
}
fn decrement(&mut self, albums: &[Album]) {
if let Some(index) = self.selection.selected() {
if let Some(result) = index.checked_sub(1) {
self.selection.select(Some(result));
self.track = TrackSelection::initialise(Some(&albums[result].tracks));
}
}
}
fn decrement_track(&mut self, albums: &[Album]) {
if let Some(index) = self.selection.selected() {
self.track.decrement(&albums[index].tracks);
}
}
}
impl ArtistSelection {
fn initialise(artists: Option<&[Artist]>) -> Self {
let mut selection = ListState::default();
let album: AlbumSelection;
if let Some(artists) = artists {
selection.select(if !artists.is_empty() { Some(0) } else { None });
album = AlbumSelection::initialise(artists.get(0).map(|a| a.albums.as_slice()));
} else {
selection.select(None);
album = AlbumSelection::initialise(None);
}
ArtistSelection { selection, album }
}
fn selection(&mut self) -> &mut ListState {
&mut self.selection
}
fn album_selection(&mut self) -> &mut ListState {
self.album.selection()
}
fn track_selection(&mut self) -> &mut ListState {
self.album.track_selection()
}
fn increment(&mut self, artists: &[Artist]) {
if let Some(index) = self.selection.selected() {
if let Some(result) = index.checked_add(1) {
if result < artists.len() {
self.selection.select(Some(result));
self.album = AlbumSelection::initialise(Some(&artists[result].albums));
}
}
}
}
fn increment_album(&mut self, artists: &[Artist]) {
if let Some(index) = self.selection.selected() {
self.album.increment(&artists[index].albums);
}
}
fn increment_track(&mut self, artists: &[Artist]) {
if let Some(index) = self.selection.selected() {
self.album.increment_track(&artists[index].albums);
}
}
fn decrement(&mut self, artists: &[Artist]) {
if let Some(index) = self.selection.selected() {
if let Some(result) = index.checked_sub(1) {
self.selection.select(Some(result));
self.album = AlbumSelection::initialise(Some(&artists[result].albums));
}
}
}
fn decrement_album(&mut self, artists: &[Artist]) {
if let Some(index) = self.selection.selected() {
self.album.decrement(&artists[index].albums);
}
}
fn decrement_track(&mut self, artists: &[Artist]) {
if let Some(index) = self.selection.selected() {
self.album.decrement_track(&artists[index].albums);
}
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub enum Category {
Artist,
Album,
Track,
}
struct Selection {
active: Category,
artist: ArtistSelection,
}
impl Selection {
fn new(artists: Option<&[Artist]>) -> Self {
Selection {
active: Category::Artist,
artist: ArtistSelection::initialise(artists),
}
}
fn artist_selection(&mut self) -> &mut ListState {
self.artist.selection()
}
fn album_selection(&mut self) -> &mut ListState {
self.artist.album_selection()
}
fn track_selection(&mut self) -> &mut ListState {
self.artist.track_selection()
}
fn increment_category(&mut self) {
self.active = match self.active {
Category::Artist => Category::Album,
Category::Album => Category::Track,
Category::Track => Category::Track,
};
}
fn decrement_category(&mut self) {
self.active = match self.active {
Category::Artist => Category::Artist,
Category::Album => Category::Artist,
Category::Track => Category::Album,
};
}
fn increment_selection(&mut self, collection: &Collection) {
match self.active {
Category::Artist => self.increment_artist(collection),
Category::Album => self.increment_album(collection),
Category::Track => self.increment_track(collection),
}
}
fn decrement_selection(&mut self, collection: &Collection) {
match self.active {
Category::Artist => self.decrement_artist(collection),
Category::Album => self.decrement_album(collection),
Category::Track => self.decrement_track(collection),
}
}
fn increment_artist(&mut self, artists: &[Artist]) {
self.artist.increment(artists);
}
fn decrement_artist(&mut self, artists: &[Artist]) {
self.artist.decrement(artists);
}
fn increment_album(&mut self, artists: &[Artist]) {
self.artist.increment_album(artists);
}
fn decrement_album(&mut self, artists: &[Artist]) {
self.artist.decrement_album(artists);
}
fn increment_track(&mut self, artists: &[Artist]) {
self.artist.increment_track(artists);
}
fn decrement_track(&mut self, artists: &[Artist]) {
self.artist.decrement_track(artists);
}
}
pub struct MhUi<CM> {
collection_manager: CM,
selection: Selection,
running: bool,
}
struct ArtistArea { struct ArtistArea {
list: Rect, list: Rect,
@ -33,7 +303,7 @@ struct FrameAreas {
struct SelectionList<'a> { struct SelectionList<'a> {
list: List<'a>, list: List<'a>,
state: ListState, state: &'a mut ListState,
} }
struct ArtistState<'a> { struct ArtistState<'a> {
@ -53,21 +323,15 @@ struct TrackState<'a> {
active: bool, active: bool,
} }
struct AppState<'a> { impl<CM: CollectionManager> MhUi<CM> {
artists: ArtistState<'a>, pub fn new(mut collection_manager: CM) -> Result<Self, Error> {
albums: AlbumState<'a>, collection_manager.rescan_library()?;
tracks: TrackState<'a>, let selection = Selection::new(Some(collection_manager.get_collection()));
} Ok(MhUi {
collection_manager,
pub struct Ui<APP> { selection,
_phantom: PhantomData<APP>, running: true,
} })
impl<APP: App> Ui<APP> {
pub fn new() -> Self {
Ui {
_phantom: PhantomData,
}
} }
fn construct_areas(frame: Rect) -> FrameAreas { fn construct_areas(frame: Rect) -> FrameAreas {
@ -127,21 +391,17 @@ impl<APP: App> Ui<APP> {
} }
} }
fn construct_artist_list(app: &APP) -> ArtistState { fn construct_artist_state(&mut self) -> ArtistState {
let artists = app.get_artist_ids(); let artists = self.collection_manager.get_collection();
let list = List::new( let list = List::new(
artists artists
.iter() .iter()
.map(|id| ListItem::new(id.name.as_str())) .map(|a| ListItem::new(a.id.name.as_str()))
.collect::<Vec<ListItem>>(), .collect::<Vec<ListItem>>(),
); );
let selected_artist = app.selected_artist(); let active = self.selection.active == Category::Artist;
let state = self.selection.artist_selection();
let mut state = ListState::default();
state.select(selected_artist);
let active = app.get_active_category() == Category::Artist;
ArtistState { ArtistState {
list: SelectionList { list, state }, list: SelectionList { list, state },
@ -149,8 +409,18 @@ impl<APP: App> Ui<APP> {
} }
} }
fn construct_album_list(app: &APP) -> AlbumState { fn construct_album_state(&mut self) -> AlbumState {
let albums = app.get_album_ids(); let albums: Vec<&AlbumId> =
if let Some(artist_index) = self.selection.artist.selection.selected() {
self.collection_manager.get_collection()[artist_index]
.albums
.iter()
.map(|a| &a.id)
.collect()
} else {
vec![]
};
let list = List::new( let list = List::new(
albums albums
.iter() .iter()
@ -158,14 +428,10 @@ impl<APP: App> Ui<APP> {
.collect::<Vec<ListItem>>(), .collect::<Vec<ListItem>>(),
); );
let selected_album = app.selected_album(); let active = self.selection.active == Category::Album;
let state = self.selection.album_selection();
let mut state = ListState::default(); let album = state.selected().map(|i| albums[i]);
state.select(selected_album);
let active = app.get_active_category() == Category::Album;
let album = selected_album.map(|i| albums[i]);
let info = Paragraph::new(format!( let info = Paragraph::new(format!(
"Title: {}\n\ "Title: {}\n\
Year: {}", Year: {}",
@ -182,8 +448,21 @@ impl<APP: App> Ui<APP> {
} }
} }
fn construct_track_list(app: &APP) -> TrackState { fn construct_track_state(&mut self) -> TrackState {
let tracks = app.get_track_ids(); let tracks: Vec<&Track> =
if let Some(artist_index) = self.selection.artist.selection.selected() {
if let Some(album_index) = self.selection.artist.album.selection.selected() {
self.collection_manager.get_collection()[artist_index].albums[album_index]
.tracks
.iter()
.collect()
} else {
vec![]
}
} else {
vec![]
};
let list = List::new( let list = List::new(
tracks tracks
.iter() .iter()
@ -191,14 +470,10 @@ impl<APP: App> Ui<APP> {
.collect::<Vec<ListItem>>(), .collect::<Vec<ListItem>>(),
); );
let selected_track = app.selected_track(); let active = self.selection.active == Category::Track;
let state = self.selection.track_selection();
let mut state = ListState::default(); let track = state.selected().map(|i| tracks[i]);
state.select(selected_track);
let active = app.get_active_category() == Category::Track;
let track = selected_track.map(|i| tracks[i]);
let info = Paragraph::new(format!( let info = Paragraph::new(format!(
"Track: {}\n\ "Track: {}\n\
Title: {}\n\ Title: {}\n\
@ -226,14 +501,6 @@ impl<APP: App> Ui<APP> {
} }
} }
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(_active: bool) -> Style { fn style(_active: bool) -> Style {
Style::default().fg(Color::White).bg(Color::Black) Style::default().fg(Color::White).bg(Color::Black)
} }
@ -292,78 +559,109 @@ impl<APP: App> Ui<APP> {
); );
} }
fn render_artist_column<B: Backend>( fn render_artist_column<B: Backend>(&mut self, area: ArtistArea, frame: &mut Frame<'_, B>) {
state: ArtistState, let state = self.construct_artist_state();
area: ArtistArea,
frame: &mut Frame<'_, B>,
) {
Self::render_list_widget("Artists", state.list, state.active, area.list, frame); Self::render_list_widget("Artists", state.list, state.active, area.list, frame);
} }
fn render_album_column<B: Backend>( fn render_album_column<B: Backend>(&mut self, area: AlbumArea, frame: &mut Frame<'_, B>) {
state: AlbumState, let state = self.construct_album_state();
area: AlbumArea,
frame: &mut Frame<'_, B>,
) {
Self::render_list_widget("Albums", state.list, state.active, area.list, frame); Self::render_list_widget("Albums", state.list, state.active, area.list, frame);
Self::render_info_widget("Album info", state.info, state.active, area.info, frame); Self::render_info_widget("Album info", state.info, state.active, area.info, frame);
} }
fn render_track_column<B: Backend>( fn render_track_column<B: Backend>(&mut self, area: TrackArea, frame: &mut Frame<'_, B>) {
state: TrackState, let state = self.construct_track_state();
area: TrackArea,
frame: &mut Frame<'_, B>,
) {
Self::render_list_widget("Tracks", state.list, state.active, area.list, frame); Self::render_list_widget("Tracks", state.list, state.active, area.list, frame);
Self::render_info_widget("Track info", state.info, state.active, area.info, frame); Self::render_info_widget("Track info", state.info, state.active, area.info, frame);
} }
}
pub fn render<B: Backend>(&self, app: &APP, frame: &mut Frame<'_, B>) { pub trait Ui {
fn is_running(&self) -> bool;
fn quit(&mut self);
fn increment_category(&mut self);
fn decrement_category(&mut self);
fn increment_selection(&mut self);
fn decrement_selection(&mut self);
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>);
}
impl<CM: CollectionManager> Ui for MhUi<CM> {
fn is_running(&self) -> bool {
self.running
}
fn quit(&mut self) {
self.running = false;
}
fn increment_category(&mut self) {
self.selection.increment_category();
}
fn decrement_category(&mut self) {
self.selection.decrement_category();
}
fn increment_selection(&mut self) {
self.selection
.increment_selection(self.collection_manager.get_collection());
}
fn decrement_selection(&mut self) {
self.selection
.decrement_selection(self.collection_manager.get_collection());
}
fn render<B: Backend>(&mut self, frame: &mut Frame<'_, B>) {
let areas = Self::construct_areas(frame.size()); 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_artist_column(areas.artists, frame);
Self::render_album_column(app_state.albums, areas.albums, frame); self.render_album_column(areas.albums, frame);
Self::render_track_column(app_state.tracks, areas.tracks, frame); self.render_track_column(areas.tracks, frame);
} }
} }
#[cfg(test)] // #[cfg(test)]
mod tests { // mod tests {
// This is UI so the only sensible unit test is to run the code through various app states. // // This is UI so the only sensible unit test is to run the code through various app states.
use crate::{ // use crate::{
tests::COLLECTION, // tests::COLLECTION,
tui::{ // tui::{
app::App, // app::App,
tests::{app, terminal}, // tests::{app, terminal},
}, // },
}; // };
use super::Ui; // use super::Ui;
#[test] // #[test]
fn empty() { // fn empty() {
let mut terminal = terminal(); // let mut terminal = terminal();
let app = app(vec![]); // let app = app(vec![]);
let ui = Ui::new(); // let ui = Ui::new();
terminal.draw(|frame| ui.render(&app, frame)).unwrap(); // terminal.draw(|frame| ui.render(&app, frame)).unwrap();
} // }
#[test] // #[test]
fn collection() { // fn collection() {
let mut terminal = terminal(); // let mut terminal = terminal();
let mut app = app(COLLECTION.to_owned()); // let mut app = app(COLLECTION.to_owned());
let ui = Ui::new(); // let ui = Ui::new();
terminal.draw(|frame| ui.render(&app, frame)).unwrap(); // terminal.draw(|frame| ui.render(&app, frame)).unwrap();
// Change the track (which has a different track format). // // Change the track (which has a different track format).
app.increment_category(); // app.increment_category();
app.increment_category(); // app.increment_category();
app.increment_selection(); // app.increment_selection();
terminal.draw(|frame| ui.render(&app, frame)).unwrap(); // terminal.draw(|frame| ui.render(&app, frame)).unwrap();
} // }
} // }