diff --git a/src/core/musichoard/musichoard.rs b/src/core/musichoard/musichoard.rs index 3eb55bf..041569b 100644 --- a/src/core/musichoard/musichoard.rs +++ b/src/core/musichoard/musichoard.rs @@ -716,25 +716,29 @@ mod tests { let mut expected = FULL_COLLECTION.to_owned(); expected.sort_unstable(); - let mut mh = MusicHoard::default(); - mh.library_cache = left - .clone() - .into_iter() - .map(|a| (a.id.clone(), a)) - .collect(); - mh.collection = right.clone(); + let mut mh = MusicHoard { + collection: right.clone(), + library_cache: left + .clone() + .into_iter() + .map(|a| (a.id.clone(), a)) + .collect(), + ..Default::default() + }; mh.merge_collections(); assert_eq!(expected, mh.collection); // The merge is completely non-overlapping so it should be commutative. - let mut mh = MusicHoard::default(); - mh.library_cache = right - .clone() - .into_iter() - .map(|a| (a.id.clone(), a)) - .collect(); - mh.collection = left.clone(); + let mut mh = MusicHoard { + collection: left.clone(), + library_cache: right + .clone() + .into_iter() + .map(|a| (a.id.clone(), a)) + .collect(), + ..Default::default() + }; mh.merge_collections(); assert_eq!(expected, mh.collection); @@ -750,25 +754,29 @@ mod tests { let mut expected = FULL_COLLECTION.to_owned(); expected.sort_unstable(); - let mut mh = MusicHoard::default(); - mh.library_cache = left - .clone() - .into_iter() - .map(|a| (a.id.clone(), a)) - .collect(); - mh.collection = right.clone(); + let mut mh = MusicHoard { + collection: right.clone(), + library_cache: left + .clone() + .into_iter() + .map(|a| (a.id.clone(), a)) + .collect(), + ..Default::default() + }; mh.merge_collections(); assert_eq!(expected, mh.collection); // The merge does not overwrite any data so it should be commutative. - let mut mh = MusicHoard::default(); - mh.library_cache = right - .clone() - .into_iter() - .map(|a| (a.id.clone(), a)) - .collect(); - mh.collection = left.clone(); + let mut mh = MusicHoard { + collection: left.clone(), + library_cache: right + .clone() + .into_iter() + .map(|a| (a.id.clone(), a)) + .collect(), + ..Default::default() + }; mh.merge_collections(); assert_eq!(expected, mh.collection); @@ -797,25 +805,29 @@ mod tests { expected.last_mut().as_mut().unwrap().sort = artist_sort.clone(); expected.rotate_right(1); - let mut mh = MusicHoard::default(); - mh.library_cache = left - .clone() - .into_iter() - .map(|a| (a.id.clone(), a)) - .collect(); - mh.collection = right.clone(); + let mut mh = MusicHoard { + collection: right.clone(), + library_cache: left + .clone() + .into_iter() + .map(|a| (a.id.clone(), a)) + .collect(), + ..Default::default() + }; mh.merge_collections(); assert_eq!(expected, mh.collection); // The merge overwrites the sort data, but no data is erased so it should be commutative. - let mut mh = MusicHoard::default(); - mh.library_cache = right - .clone() - .into_iter() - .map(|a| (a.id.clone(), a)) - .collect(); - mh.collection = left.clone(); + let mut mh = MusicHoard { + collection: left.clone(), + library_cache: right + .clone() + .into_iter() + .map(|a| (a.id.clone(), a)) + .collect(), + ..Default::default() + }; mh.merge_collections(); assert_eq!(expected, mh.collection); diff --git a/src/main.rs b/src/main.rs index 097c620..5419d1a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -20,13 +20,7 @@ use musichoard::{ MusicHoardBuilder, NoDatabase, NoLibrary, }; -use tui::{ - event::EventChannel, - handler::EventHandler, - listener::EventListener, - ui::{render::Renderer, Ui}, - Tui, -}; +use tui::{App, EventChannel, EventHandler, EventListener, Tui, Ui}; #[derive(StructOpt)] struct Opt { @@ -73,11 +67,11 @@ fn with(builder: MusicHoardBuilder) { let listener = EventListener::new(channel.sender()); let handler = EventHandler::new(channel.receiver()); - let ui = Ui::new(music_hoard).expect("failed to initialise ui"); - let renderer = Renderer; + let app = App::new(music_hoard).expect("failed to initialise application"); + let ui = Ui; // Run the TUI application. - Tui::run(terminal, ui, renderer, handler, listener).expect("failed to run tui"); + Tui::run(terminal, app, ui, handler, listener).expect("failed to run tui"); } fn with_database(db_opt: DbOpt, builder: MusicHoardBuilder) { diff --git a/src/tui/ui/mod.rs b/src/tui/app.rs similarity index 65% rename from src/tui/ui/mod.rs rename to src/tui/app.rs index 7f28d90..bd4d0e9 100644 --- a/src/tui/ui/mod.rs +++ b/src/tui/app.rs @@ -1,21 +1,16 @@ -pub mod render; - use std::fmt; use musichoard::collection::{album::Album, artist::Artist, track::Track, Collection}; -use ratatui::{backend::Backend, widgets::ListState, Frame}; +use ratatui::widgets::ListState; -use crate::tui::{ - ui::render::IRender, - {lib::IMusicHoard, Error}, -}; +use crate::tui::{lib::IMusicHoard, Error}; #[derive(Debug)] -pub enum UiError { +pub enum AppError { Lib(String), } -impl fmt::Display for UiError { +impl fmt::Display for AppError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match *self { Self::Lib(ref s) => write!(f, "the musichoard library returned an error: {s}"), @@ -23,54 +18,52 @@ impl fmt::Display for UiError { } } -impl From for UiError { - fn from(err: musichoard::Error) -> UiError { - UiError::Lib(err.to_string()) +impl From for AppError { + fn from(err: musichoard::Error) -> AppError { + AppError::Lib(err.to_string()) } } -pub enum UiState { +pub enum AppState { Browse(BS), Info(IS), Reload(RS), Error(ES), } -impl UiState { +impl AppState { fn is_browse(&self) -> bool { - matches!(self, UiState::Browse(_)) + matches!(self, AppState::Browse(_)) } fn is_info(&self) -> bool { - matches!(self, UiState::Info(_)) + matches!(self, AppState::Info(_)) } fn is_reload(&self) -> bool { - matches!(self, UiState::Reload(_)) + matches!(self, AppState::Reload(_)) } fn is_error(&self) -> bool { - matches!(self, UiState::Error(_)) + matches!(self, AppState::Error(_)) } } -pub trait IUi { - type BS: IUiBrowse; - type IS: IUiInfo; - type RS: IUiReload; - type ES: IUiError; +pub trait IAppInteract { + type BS: IAppInteractBrowse; + type IS: IAppInteractInfo; + type RS: IAppInteractReload; + type ES: IAppInteractError; fn is_running(&self) -> bool; fn quit(&mut self); - fn save(&mut self) -> Result<(), UiError>; + fn save(&mut self) -> Result<(), AppError>; - fn state(&mut self) -> UiState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES>; - - fn render(&mut self, renderer: &R, frame: &mut Frame<'_, B>); + fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES>; } -pub trait IUiBrowse { +pub trait IAppInteractBrowse { fn increment_category(&mut self); fn decrement_category(&mut self); fn increment_selection(&mut self); @@ -81,109 +74,46 @@ pub trait IUiBrowse { fn show_reload_menu(&mut self); } -pub trait IUiInfo { +pub trait IAppInteractInfo { fn hide_info_overlay(&mut self); } -pub trait IUiReload { +pub trait IAppInteractReload { fn reload_library(&mut self); fn reload_database(&mut self); fn go_back(&mut self); } -pub trait IUiError { +pub trait IAppInteractError { fn dismiss_error(&mut self); } -struct TrackSelection { - state: ListState, +// It would be preferable to have a getter for each field separately. However, the selection field +// needs to be mutably accessible requiring a mutable borrow of the entire struct if behind a trait. +// This in turn complicates simultaneous field access since only a single mutable borrow is allowed. +// Therefore, all fields are grouped into a single struct and returned as a batch. +pub trait IAppAccess { + fn get(&mut self) -> AppPublic; } -struct AlbumSelection { - state: ListState, - track: TrackSelection, +pub struct AppPublic<'app> { + pub collection: &'app Collection, + pub selection: &'app mut Selection, + pub state: &'app AppState<(), (), (), String>, } -struct ArtistSelection { - state: ListState, - album: AlbumSelection, +pub struct ArtistSelection { + pub state: ListState, + pub album: AlbumSelection, } -impl TrackSelection { - fn initialise(tracks: Option<&[Track]>) -> Self { - let mut state = ListState::default(); - if let Some(tracks) = tracks { - state.select(if !tracks.is_empty() { Some(0) } else { None }); - } else { - state.select(None); - }; - TrackSelection { state } - } - - fn increment(&mut self, tracks: &[Track]) { - if let Some(index) = self.state.selected() { - if let Some(result) = index.checked_add(1) { - if result < tracks.len() { - self.state.select(Some(result)); - } - } - } - } - - fn decrement(&mut self, _tracks: &[Track]) { - if let Some(index) = self.state.selected() { - if let Some(result) = index.checked_sub(1) { - self.state.select(Some(result)); - } - } - } +pub struct AlbumSelection { + pub state: ListState, + pub track: TrackSelection, } -impl AlbumSelection { - fn initialise(albums: Option<&[Album]>) -> Self { - let mut state = ListState::default(); - let track: TrackSelection; - if let Some(albums) = albums { - state.select(if !albums.is_empty() { Some(0) } else { None }); - track = TrackSelection::initialise(albums.first().map(|a| a.tracks.as_slice())); - } else { - state.select(None); - track = TrackSelection::initialise(None); - } - AlbumSelection { state, track } - } - - fn increment(&mut self, albums: &[Album]) { - if let Some(index) = self.state.selected() { - if let Some(result) = index.checked_add(1) { - if result < albums.len() { - self.state.select(Some(result)); - self.track = TrackSelection::initialise(Some(&albums[result].tracks)); - } - } - } - } - - fn increment_track(&mut self, albums: &[Album]) { - if let Some(index) = self.state.selected() { - self.track.increment(&albums[index].tracks); - } - } - - fn decrement(&mut self, albums: &[Album]) { - if let Some(index) = self.state.selected() { - if let Some(result) = index.checked_sub(1) { - self.state.select(Some(result)); - self.track = TrackSelection::initialise(Some(&albums[result].tracks)); - } - } - } - - fn decrement_track(&mut self, albums: &[Album]) { - if let Some(index) = self.state.selected() { - self.track.decrement(&albums[index].tracks); - } - } +pub struct TrackSelection { + pub state: ListState, } impl ArtistSelection { @@ -245,6 +175,83 @@ impl ArtistSelection { } } +impl AlbumSelection { + fn initialise(albums: Option<&[Album]>) -> Self { + let mut state = ListState::default(); + let track: TrackSelection; + if let Some(albums) = albums { + state.select(if !albums.is_empty() { Some(0) } else { None }); + track = TrackSelection::initialise(albums.first().map(|a| a.tracks.as_slice())); + } else { + state.select(None); + track = TrackSelection::initialise(None); + } + AlbumSelection { state, track } + } + + fn increment(&mut self, albums: &[Album]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_add(1) { + if result < albums.len() { + self.state.select(Some(result)); + self.track = TrackSelection::initialise(Some(&albums[result].tracks)); + } + } + } + } + + fn increment_track(&mut self, albums: &[Album]) { + if let Some(index) = self.state.selected() { + self.track.increment(&albums[index].tracks); + } + } + + fn decrement(&mut self, albums: &[Album]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_sub(1) { + self.state.select(Some(result)); + self.track = TrackSelection::initialise(Some(&albums[result].tracks)); + } + } + } + + fn decrement_track(&mut self, albums: &[Album]) { + if let Some(index) = self.state.selected() { + self.track.decrement(&albums[index].tracks); + } + } +} + +impl TrackSelection { + fn initialise(tracks: Option<&[Track]>) -> Self { + let mut state = ListState::default(); + if let Some(tracks) = tracks { + state.select(if !tracks.is_empty() { Some(0) } else { None }); + } else { + state.select(None); + }; + TrackSelection { state } + } + + fn increment(&mut self, tracks: &[Track]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_add(1) { + if result < tracks.len() { + self.state.select(Some(result)); + } + } + } + } + + fn decrement(&mut self, _tracks: &[Track]) { + if let Some(index) = self.state.selected() { + if let Some(result) = index.checked_sub(1) { + self.state.select(Some(result)); + } + } + } +} + #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum Category { Artist, @@ -253,19 +260,19 @@ pub enum Category { } pub struct Selection { - active: Category, - artist: ArtistSelection, + pub active: Category, + pub artist: ArtistSelection, } impl Selection { - fn new(artists: Option<&[Artist]>) -> Self { + pub fn new(artists: Option<&[Artist]>) -> Self { Selection { active: Category::Artist, artist: ArtistSelection::initialise(artists), } } - fn increment_category(&mut self) { + pub fn increment_category(&mut self) { self.active = match self.active { Category::Artist => Category::Album, Category::Album => Category::Track, @@ -273,7 +280,7 @@ impl Selection { }; } - fn decrement_category(&mut self) { + pub fn decrement_category(&mut self) { self.active = match self.active { Category::Artist => Category::Artist, Category::Album => Category::Artist, @@ -281,7 +288,7 @@ impl Selection { }; } - fn increment_selection(&mut self, collection: &Collection) { + pub fn increment_selection(&mut self, collection: &Collection) { match self.active { Category::Artist => self.increment_artist(collection), Category::Album => self.increment_album(collection), @@ -289,7 +296,7 @@ impl Selection { } } - fn decrement_selection(&mut self, collection: &Collection) { + pub fn decrement_selection(&mut self, collection: &Collection) { match self.active { Category::Artist => self.decrement_artist(collection), Category::Album => self.decrement_album(collection), @@ -322,29 +329,29 @@ impl Selection { } } -pub struct Ui { +pub struct App { running: bool, music_hoard: MH, selection: Selection, - state: UiState<(), (), (), String>, + state: AppState<(), (), (), String>, } -impl Ui { +impl App { pub fn new(mut music_hoard: MH) -> Result { // FIXME: if either returns an error start in an error state music_hoard.load_from_database()?; music_hoard.rescan_library()?; let selection = Selection::new(Some(music_hoard.get_collection())); - Ok(Ui { + Ok(App { running: true, music_hoard, selection, - state: UiState::Browse(()), + state: AppState::Browse(()), }) } } -impl IUi for Ui { +impl IAppInteract for App { type BS = Self; type IS = Self; type RS = Self; @@ -358,37 +365,21 @@ impl IUi for Ui { self.running = false; } - fn save(&mut self) -> Result<(), UiError> { + fn save(&mut self) -> Result<(), AppError> { Ok(self.music_hoard.save_to_database()?) } - fn state(&mut self) -> UiState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES> { + fn state(&mut self) -> AppState<&mut Self::BS, &mut Self::IS, &mut Self::RS, &mut Self::ES> { match self.state { - UiState::Browse(_) => UiState::Browse(self), - UiState::Info(_) => UiState::Info(self), - UiState::Reload(_) => UiState::Reload(self), - UiState::Error(_) => UiState::Error(self), - } - } - - // FIXME: rendering logic should be in render.rs - reverse Renderer/Ui relationship so that TUI - // calls are renderer.render(ui, frame); Then rename ui -> app and render -> ui as it used to be - // originally. - fn render(&mut self, _: &R, frame: &mut Frame<'_, B>) { - let collection: &Collection = &self.music_hoard.get_collection(); - let selection: &mut Selection = &mut self.selection; - - R::render_collection(collection, selection, frame); - match self.state { - UiState::Info(_) => R::render_info_overlay(collection, selection, frame), - UiState::Reload(_) => R::render_reload_overlay(frame), - UiState::Error(ref msg) => R::render_error_overlay(msg, frame), - _ => {} + AppState::Browse(_) => AppState::Browse(self), + AppState::Info(_) => AppState::Info(self), + AppState::Reload(_) => AppState::Reload(self), + AppState::Error(_) => AppState::Error(self), } } } -impl IUiBrowse for Ui { +impl IAppInteractBrowse for App { fn increment_category(&mut self) { self.selection.increment_category(); } @@ -409,23 +400,23 @@ impl IUiBrowse for Ui { fn show_info_overlay(&mut self) { assert!(self.state.is_browse()); - self.state = UiState::Info(()); + self.state = AppState::Info(()); } fn show_reload_menu(&mut self) { assert!(self.state.is_browse()); - self.state = UiState::Reload(()); + self.state = AppState::Reload(()); } } -impl IUiInfo for Ui { +impl IAppInteractInfo for App { fn hide_info_overlay(&mut self) { assert!(self.state.is_info()); - self.state = UiState::Browse(()); + self.state = AppState::Browse(()); } } -impl IUiReload for Ui { +impl IAppInteractReload for App { fn reload_library(&mut self) { let result = self.music_hoard.rescan_library(); self.refresh(result); @@ -438,44 +429,51 @@ impl IUiReload for Ui { fn go_back(&mut self) { assert!(self.state.is_reload()); - self.state = UiState::Browse(()); + self.state = AppState::Browse(()); } } -trait IUiReloadPrivate { +trait IAppInteractReloadPrivate { fn refresh(&mut self, result: Result<(), musichoard::Error>); } -impl IUiReloadPrivate for Ui { +impl IAppInteractReloadPrivate for App { fn refresh(&mut self, result: Result<(), musichoard::Error>) { assert!(self.state.is_reload()); match result { Ok(()) => { self.selection = Selection::new(Some(self.music_hoard.get_collection())); - self.state = UiState::Browse(()) + self.state = AppState::Browse(()) } - Err(err) => self.state = UiState::Error(err.to_string()), + Err(err) => self.state = AppState::Error(err.to_string()), } } } -impl IUiError for Ui { +impl IAppInteractError for App { fn dismiss_error(&mut self) { assert!(self.state.is_error()); - self.state = UiState::Browse(()); + self.state = AppState::Browse(()); + } +} + +impl IAppAccess for App { + fn get(&mut self) -> AppPublic { + AppPublic { + collection: self.music_hoard.get_collection(), + selection: &mut self.selection, + state: &self.state, + } } } #[cfg(test)] mod tests { - use crate::tui::lib::MockIMusicHoard; - use crate::tui::testmod::COLLECTION; - use crate::tui::tests::{terminal, ui}; - use crate::tui::ui::render::Renderer; + use crate::tui::{lib::MockIMusicHoard, testmod::COLLECTION}; use super::*; - pub fn music_hoard(collection: Collection) -> MockIMusicHoard { + fn music_hoard(collection: Collection) -> MockIMusicHoard { let mut music_hoard = MockIMusicHoard::new(); music_hoard @@ -645,11 +643,11 @@ mod tests { #[test] fn running() { - let mut ui = Ui::new(music_hoard(COLLECTION.to_owned())).unwrap(); - assert!(ui.is_running()); + let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap(); + assert!(app.is_running()); - ui.quit(); - assert!(!ui.is_running()); + app.quit(); + assert!(!app.is_running()); } #[test] @@ -661,99 +659,99 @@ mod tests { .times(1) .return_once(|| Ok(())); - let mut ui = Ui::new(music_hoard).unwrap(); + let mut app = App::new(music_hoard).unwrap(); - let result = ui.save(); + let result = app.save(); assert!(result.is_ok()); } #[test] fn modifiers() { - let mut ui = Ui::new(music_hoard(COLLECTION.to_owned())).unwrap(); - assert!(ui.is_running()); + let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap(); + assert!(app.is_running()); - assert_eq!(ui.selection.active, Category::Artist); - assert_eq!(ui.selection.artist.state.selected(), Some(0)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(0)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + assert_eq!(app.selection.active, Category::Artist); + assert_eq!(app.selection.artist.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(0)); - ui.increment_selection(); - assert_eq!(ui.selection.active, Category::Artist); - assert_eq!(ui.selection.artist.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(0)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + app.increment_selection(); + assert_eq!(app.selection.active, Category::Artist); + assert_eq!(app.selection.artist.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(0)); - ui.increment_category(); - assert_eq!(ui.selection.active, Category::Album); - assert_eq!(ui.selection.artist.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(0)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + app.increment_category(); + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(0)); - ui.increment_selection(); - assert_eq!(ui.selection.active, Category::Album); - assert_eq!(ui.selection.artist.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + app.increment_selection(); + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(0)); - ui.increment_category(); - assert_eq!(ui.selection.active, Category::Track); - assert_eq!(ui.selection.artist.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + app.increment_category(); + assert_eq!(app.selection.active, Category::Track); + assert_eq!(app.selection.artist.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(0)); - ui.increment_selection(); - assert_eq!(ui.selection.active, Category::Track); - assert_eq!(ui.selection.artist.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(1)); + app.increment_selection(); + assert_eq!(app.selection.active, Category::Track); + assert_eq!(app.selection.artist.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(1)); - ui.increment_category(); - assert_eq!(ui.selection.active, Category::Track); - assert_eq!(ui.selection.artist.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(1)); + app.increment_category(); + assert_eq!(app.selection.active, Category::Track); + assert_eq!(app.selection.artist.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(1)); - ui.decrement_selection(); - assert_eq!(ui.selection.active, Category::Track); - assert_eq!(ui.selection.artist.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + app.decrement_selection(); + assert_eq!(app.selection.active, Category::Track); + assert_eq!(app.selection.artist.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(0)); - ui.increment_selection(); - ui.decrement_category(); - assert_eq!(ui.selection.active, Category::Album); - assert_eq!(ui.selection.artist.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(1)); + app.increment_selection(); + app.decrement_category(); + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(1)); - ui.decrement_selection(); - assert_eq!(ui.selection.active, Category::Album); - assert_eq!(ui.selection.artist.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(0)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + app.decrement_selection(); + assert_eq!(app.selection.active, Category::Album); + assert_eq!(app.selection.artist.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(0)); - ui.increment_selection(); - ui.decrement_category(); - assert_eq!(ui.selection.active, Category::Artist); - assert_eq!(ui.selection.artist.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + app.increment_selection(); + app.decrement_category(); + assert_eq!(app.selection.active, Category::Artist); + assert_eq!(app.selection.artist.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(0)); - ui.decrement_selection(); - assert_eq!(ui.selection.active, Category::Artist); - assert_eq!(ui.selection.artist.state.selected(), Some(0)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(0)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + app.decrement_selection(); + assert_eq!(app.selection.active, Category::Artist); + assert_eq!(app.selection.artist.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(0)); - ui.increment_category(); - ui.increment_selection(); - ui.decrement_category(); - ui.decrement_selection(); - ui.decrement_category(); - assert_eq!(ui.selection.active, Category::Artist); - assert_eq!(ui.selection.artist.state.selected(), Some(0)); - assert_eq!(ui.selection.artist.album.state.selected(), Some(1)); - assert_eq!(ui.selection.artist.album.track.state.selected(), Some(0)); + app.increment_category(); + app.increment_selection(); + app.decrement_category(); + app.decrement_selection(); + app.decrement_category(); + assert_eq!(app.selection.active, Category::Artist); + assert_eq!(app.selection.artist.state.selected(), Some(0)); + assert_eq!(app.selection.artist.album.state.selected(), Some(1)); + assert_eq!(app.selection.artist.album.track.state.selected(), Some(0)); } #[test] @@ -761,7 +759,7 @@ mod tests { let mut collection = COLLECTION.to_owned(); collection[0].albums[0].tracks = vec![]; - let mut app = Ui::new(music_hoard(collection)).unwrap(); + let mut app = App::new(music_hoard(collection)).unwrap(); assert!(app.is_running()); assert_eq!(app.selection.active, Category::Artist); @@ -790,7 +788,7 @@ mod tests { let mut collection = COLLECTION.to_owned(); collection[0].albums = vec![]; - let mut app = Ui::new(music_hoard(collection)).unwrap(); + let mut app = App::new(music_hoard(collection)).unwrap(); assert!(app.is_running()); assert_eq!(app.selection.active, Category::Artist); @@ -829,7 +827,7 @@ mod tests { #[test] fn no_artists() { - let mut app = Ui::new(music_hoard(vec![])).unwrap(); + let mut app = App::new(music_hoard(vec![])).unwrap(); assert!(app.is_running()); assert_eq!(app.selection.active, Category::Artist); @@ -882,49 +880,30 @@ mod tests { #[test] fn info_overlay() { - let mut terminal = terminal(); - let renderer = Renderer; + let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap(); + assert!(app.state().is_browse()); - let mut ui = ui(COLLECTION.to_owned()); - assert!(ui.state().is_browse()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + app.show_info_overlay(); + assert!(app.state().is_info()); - ui.show_info_overlay(); - assert!(ui.state().is_info()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); - - // Change the artist (which has a multi-link entry). - ui.increment_selection(); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); - - ui.hide_info_overlay(); - assert!(ui.state().is_browse()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + app.hide_info_overlay(); + assert!(app.state().is_browse()); } #[test] fn reload_go_back() { - let mut terminal = terminal(); - let renderer = Renderer; - let music_hoard = music_hoard(COLLECTION.to_owned()); + let mut app = App::new(music_hoard(COLLECTION.to_owned())).unwrap(); + assert!(app.state().is_browse()); - let mut ui = Ui::new(music_hoard).unwrap(); - assert!(ui.state().is_browse()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + app.show_reload_menu(); + assert!(app.state().is_reload()); - ui.show_reload_menu(); - assert!(ui.state().is_reload()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); - - ui.go_back(); - assert!(ui.state().is_browse()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + app.go_back(); + assert!(app.state().is_browse()); } #[test] fn reload_database() { - let mut terminal = terminal(); - let renderer = Renderer; let mut music_hoard = music_hoard(COLLECTION.to_owned()); music_hoard @@ -932,23 +911,18 @@ mod tests { .times(1) .return_once(|| Ok(())); - let mut ui = Ui::new(music_hoard).unwrap(); - assert!(ui.state().is_browse()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + let mut app = App::new(music_hoard).unwrap(); + assert!(app.state().is_browse()); - ui.show_reload_menu(); - assert!(ui.state().is_reload()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + app.show_reload_menu(); + assert!(app.state().is_reload()); - ui.reload_database(); - assert!(ui.state().is_browse()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + app.reload_database(); + assert!(app.state().is_browse()); } #[test] fn reload_library() { - let mut terminal = terminal(); - let renderer = Renderer; let mut music_hoard = music_hoard(COLLECTION.to_owned()); music_hoard @@ -956,23 +930,18 @@ mod tests { .times(1) .return_once(|| Ok(())); - let mut ui = Ui::new(music_hoard).unwrap(); - assert!(ui.state().is_browse()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + let mut app = App::new(music_hoard).unwrap(); + assert!(app.state().is_browse()); - ui.show_reload_menu(); - assert!(ui.state().is_reload()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + app.show_reload_menu(); + assert!(app.state().is_reload()); - ui.reload_library(); - assert!(ui.state().is_browse()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + app.reload_library(); + assert!(app.state().is_browse()); } #[test] fn reload_error() { - let mut terminal = terminal(); - let renderer = Renderer; let mut music_hoard = music_hoard(COLLECTION.to_owned()); music_hoard @@ -980,29 +949,25 @@ mod tests { .times(1) .return_once(|| Err(musichoard::Error::DatabaseError(String::from("get rekt")))); - let mut ui = Ui::new(music_hoard).unwrap(); - assert!(ui.state().is_browse()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + let mut app = App::new(music_hoard).unwrap(); + assert!(app.state().is_browse()); - ui.show_reload_menu(); - assert!(ui.state().is_reload()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + app.show_reload_menu(); + assert!(app.state().is_reload()); - ui.reload_database(); - assert!(ui.state().is_error()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + app.reload_database(); + assert!(app.state().is_error()); - ui.dismiss_error(); - assert!(ui.state().is_browse()); - terminal.draw(|frame| ui.render(&renderer, frame)).unwrap(); + app.dismiss_error(); + assert!(app.state().is_browse()); } #[test] fn errors() { - let ui_err: UiError = musichoard::Error::DatabaseError(String::from("get rekt")).into(); + let app_err: AppError = musichoard::Error::DatabaseError(String::from("get rekt")).into(); - assert!(!ui_err.to_string().is_empty()); + assert!(!app_err.to_string().is_empty()); - assert!(!format!("{:?}", ui_err).is_empty()); + assert!(!format!("{:?}", app_err).is_empty()); } } diff --git a/src/tui/event.rs b/src/tui/event.rs index b05da5f..3963487 100644 --- a/src/tui/event.rs +++ b/src/tui/event.rs @@ -2,14 +2,14 @@ use crossterm::event::{KeyEvent, MouseEvent}; use std::fmt; use std::sync::mpsc; -use crate::tui::ui::UiError; +use crate::tui::app::AppError; #[derive(Debug)] pub enum EventError { Send(Event), Recv, Io(std::io::Error), - Ui(String), + App(String), } impl fmt::Display for EventError { @@ -20,8 +20,11 @@ impl fmt::Display for EventError { Self::Io(ref e) => { write!(f, "an I/O error was triggered during event handling: {e}") } - Self::Ui(ref s) => { - write!(f, "the UI returned an error during event handling: {s}") + Self::App(ref s) => { + write!( + f, + "the application returned an error during event handling: {s}" + ) } } } @@ -39,9 +42,9 @@ impl From for EventError { } } -impl From for EventError { - fn from(err: UiError) -> EventError { - EventError::Ui(err.to_string()) +impl From for EventError { + fn from(err: AppError) -> EventError { + EventError::App(err.to_string()) } } @@ -102,8 +105,6 @@ mod tests { use crossterm::event::{KeyCode, KeyEvent, KeyEventKind, KeyEventState, KeyModifiers}; - use crate::tui::ui::UiError; - use super::*; #[test] @@ -147,16 +148,16 @@ mod tests { })); let recv_err = EventError::Recv; let io_err = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "interrupted")); - let ui_err: EventError = UiError::Lib(String::from("lib error")).into(); + let app_err: EventError = AppError::Lib(String::from("lib error")).into(); assert!(!send_err.to_string().is_empty()); assert!(!recv_err.to_string().is_empty()); assert!(!io_err.to_string().is_empty()); - assert!(!ui_err.to_string().is_empty()); + assert!(!app_err.to_string().is_empty()); assert!(!format!("{:?}", send_err).is_empty()); assert!(!format!("{:?}", recv_err).is_empty()); assert!(!format!("{:?}", io_err).is_empty()); - assert!(!format!("{:?}", ui_err).is_empty()); + assert!(!format!("{:?}", app_err).is_empty()); } } diff --git a/src/tui/handler.rs b/src/tui/handler.rs index 1f66e0f..1ffacb0 100644 --- a/src/tui/handler.rs +++ b/src/tui/handler.rs @@ -1,39 +1,41 @@ +// FIXME: Can code here be less verbose use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; #[cfg(test)] use mockall::automock; use crate::tui::{ + app::{ + AppState, IAppInteract, IAppInteractBrowse, IAppInteractError, IAppInteractInfo, + IAppInteractReload, + }, event::{Event, EventError, EventReceiver}, - ui::{IUi, IUiBrowse, IUiError, IUiInfo, UiState}, }; -use super::ui::IUiReload; - #[cfg_attr(test, automock)] -pub trait IEventHandler { - fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError>; +pub trait IEventHandler { + fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError>; } -trait IEventHandlerPrivate { - fn handle_key_event(ui: &mut UI, key_event: KeyEvent) -> Result<(), EventError>; +trait IEventHandlerPrivate { + fn handle_key_event(app: &mut APP, key_event: KeyEvent) -> Result<(), EventError>; fn handle_browse_key_event( - ui: &mut ::BS, + app: &mut ::BS, key_event: KeyEvent, ) -> Result<(), EventError>; fn handle_info_key_event( - ui: &mut ::IS, + app: &mut ::IS, key_event: KeyEvent, ) -> Result<(), EventError>; fn handle_reload_key_event( - ui: &mut ::RS, + app: &mut ::RS, key_event: KeyEvent, ) -> Result<(), EventError>; fn handle_error_key_event( - ui: &mut ::ES, + app: &mut ::ES, key_event: KeyEvent, ) -> Result<(), EventError>; - fn quit(ui: &mut UI) -> Result<(), EventError>; + fn quit(app: &mut APP) -> Result<(), EventError>; } pub struct EventHandler { @@ -47,10 +49,10 @@ impl EventHandler { } } -impl IEventHandler for EventHandler { - fn handle_next_event(&self, ui: &mut UI) -> Result<(), EventError> { +impl IEventHandler for EventHandler { + fn handle_next_event(&self, app: &mut APP) -> Result<(), EventError> { match self.events.recv()? { - Event::Key(key_event) => Self::handle_key_event(ui, key_event)?, + Event::Key(key_event) => Self::handle_key_event(app, key_event)?, Event::Mouse(_) => {} Event::Resize(_, _) => {} }; @@ -58,31 +60,35 @@ impl IEventHandler for EventHandler { } } -impl IEventHandlerPrivate for EventHandler { - fn handle_key_event(ui: &mut UI, key_event: KeyEvent) -> Result<(), EventError> { +impl IEventHandlerPrivate for EventHandler { + fn handle_key_event(app: &mut APP, key_event: KeyEvent) -> Result<(), EventError> { match key_event.code { // Exit application on `ESC` or `q`. KeyCode::Esc | KeyCode::Char('q') => { - Self::quit(ui)?; + Self::quit(app)?; } // Exit application on `Ctrl-C`. KeyCode::Char('c') | KeyCode::Char('C') => { if key_event.modifiers == KeyModifiers::CONTROL { - Self::quit(ui)?; + Self::quit(app)?; } } - _ => match ui.state() { - UiState::Browse(browse) => { - >::handle_browse_key_event(browse, key_event)?; + _ => match app.state() { + AppState::Browse(browse) => { + >::handle_browse_key_event( + browse, key_event, + )?; } - UiState::Info(info) => { - >::handle_info_key_event(info, key_event)?; + AppState::Info(info) => { + >::handle_info_key_event(info, key_event)?; } - UiState::Reload(reload) => { - >::handle_reload_key_event(reload, key_event)?; + AppState::Reload(reload) => { + >::handle_reload_key_event( + reload, key_event, + )?; } - UiState::Error(error) => { - >::handle_error_key_event(error, key_event)?; + AppState::Error(error) => { + >::handle_error_key_event(error, key_event)?; } }, }; @@ -90,20 +96,20 @@ impl IEventHandlerPrivate for EventHandler { } fn handle_browse_key_event( - ui: &mut ::BS, + app: &mut ::BS, key_event: KeyEvent, ) -> Result<(), EventError> { match key_event.code { // Category change. - KeyCode::Left => ui.decrement_category(), - KeyCode::Right => ui.increment_category(), + KeyCode::Left => app.decrement_category(), + KeyCode::Right => app.increment_category(), // Selection change. - KeyCode::Up => ui.decrement_selection(), - KeyCode::Down => ui.increment_selection(), + KeyCode::Up => app.decrement_selection(), + KeyCode::Down => app.increment_selection(), // Toggle overlay. - KeyCode::Char('m') | KeyCode::Char('M') => ui.show_info_overlay(), + KeyCode::Char('m') | KeyCode::Char('M') => app.show_info_overlay(), // Toggle Reload - KeyCode::Char('g') | KeyCode::Char('G') => ui.show_reload_menu(), + KeyCode::Char('g') | KeyCode::Char('G') => app.show_reload_menu(), // Othey keys. _ => {} } @@ -112,12 +118,12 @@ impl IEventHandlerPrivate for EventHandler { } fn handle_info_key_event( - ui: &mut ::IS, + app: &mut ::IS, key_event: KeyEvent, ) -> Result<(), EventError> { match key_event.code { // Toggle overlay. - KeyCode::Char('m') | KeyCode::Char('M') => ui.hide_info_overlay(), + KeyCode::Char('m') | KeyCode::Char('M') => app.hide_info_overlay(), // Othey keys. _ => {} } @@ -126,15 +132,15 @@ impl IEventHandlerPrivate for EventHandler { } fn handle_reload_key_event( - ui: &mut ::RS, + app: &mut ::RS, key_event: KeyEvent, ) -> Result<(), EventError> { match key_event.code { // Reload keys. - KeyCode::Char('l') | KeyCode::Char('L') => ui.reload_library(), - KeyCode::Char('d') | KeyCode::Char('D') => ui.reload_database(), + KeyCode::Char('l') | KeyCode::Char('L') => app.reload_library(), + KeyCode::Char('d') | KeyCode::Char('D') => app.reload_database(), // Return. - KeyCode::Char('g') | KeyCode::Char('G') => ui.go_back(), + KeyCode::Char('g') | KeyCode::Char('G') => app.go_back(), // Othey keys. _ => {} } @@ -143,17 +149,17 @@ impl IEventHandlerPrivate for EventHandler { } fn handle_error_key_event( - ui: &mut ::ES, + app: &mut ::ES, _key_event: KeyEvent, ) -> Result<(), EventError> { // Any key dismisses the error. - ui.dismiss_error(); + app.dismiss_error(); Ok(()) } - fn quit(ui: &mut UI) -> Result<(), EventError> { - ui.quit(); - ui.save()?; + fn quit(app: &mut APP) -> Result<(), EventError> { + app.quit(); + app.save()?; Ok(()) } } diff --git a/src/tui/mod.rs b/src/tui/mod.rs index 4534798..bf5b616 100644 --- a/src/tui/mod.rs +++ b/src/tui/mod.rs @@ -1,9 +1,15 @@ -pub mod event; -pub mod handler; -pub mod listener; -pub mod ui; - +mod app; +mod event; +mod handler; mod lib; +mod listener; +mod ui; + +pub use app::App; +pub use event::EventChannel; +pub use handler::EventHandler; +pub use listener::EventListener; +pub use ui::Ui; use crossterm::event::{DisableMouseCapture, EnableMouseCapture}; use crossterm::terminal::{self, EnterAlternateScreen, LeaveAlternateScreen}; @@ -12,10 +18,13 @@ use ratatui::Terminal; use std::io; use std::marker::PhantomData; -use self::event::EventError; -use self::handler::IEventHandler; -use self::listener::IEventListener; -use self::ui::{render::IRender, IUi}; +use crate::tui::{ + app::{IAppAccess, IAppInteract}, + event::EventError, + handler::IEventHandler, + listener::IEventListener, + ui::IUi, +}; #[derive(Debug, PartialEq, Eq)] pub enum Error { @@ -43,12 +52,12 @@ impl From for Error { } } -pub struct Tui { +pub struct Tui { terminal: Terminal, - _phantom: PhantomData<(R, UI)>, + _phantom: PhantomData<(UI, APP)>, } -impl Tui { +impl Tui { fn init(&mut self) -> Result<(), Error> { self.terminal.hide_cursor()?; self.terminal.clear()?; @@ -67,13 +76,16 @@ impl Tui { fn main_loop( &mut self, - mut ui: UI, - renderer: R, - handler: impl IEventHandler, + mut app: APP, + _ui: UI, + handler: impl IEventHandler, ) -> Result<(), Error> { - while ui.is_running() { - self.terminal.draw(|frame| ui.render(&renderer, frame))?; - handler.handle_next_event(&mut ui)?; + while app.is_running() { + // FIXME: rendering logic should be in render.rs - reverse Renderer/Ui relationship so + // that TAPP calls are renderer.render(ui, frame); Then rename ui -> app and render -> ui + // as it used to be originally. + self.terminal.draw(|frame| UI::render(&mut app, frame))?; + handler.handle_next_event(&mut app)?; } Ok(()) @@ -81,9 +93,9 @@ impl Tui { fn main( term: Terminal, + app: APP, ui: UI, - renderer: R, - handler: impl IEventHandler, + handler: impl IEventHandler, listener: impl IEventListener, ) -> Result<(), Error> { let mut tui = Tui { @@ -94,7 +106,7 @@ impl Tui { tui.init()?; let listener_handle = listener.spawn(); - let result = tui.main_loop(ui, renderer, handler); + let result = tui.main_loop(app, ui, handler); match result { Ok(_) => { @@ -111,7 +123,7 @@ impl Tui { Ok(err) => return Err(err.into()), // Calling std::panic::resume_unwind(err) as recommended by the Rust docs // will not produce an error message. The panic error message is printed at - // the location of the panic which at the time is hidden by the TUI. + // the location of the panic which at the time is hidden by the TAPP. Err(_) => return Err(Error::ListenerPanic), } } @@ -141,13 +153,13 @@ impl Tui { pub fn run( term: Terminal, + app: APP, ui: UI, - renderer: R, - handler: impl IEventHandler, + handler: impl IEventHandler, listener: impl IEventListener, ) -> Result<(), Error> { Self::enable()?; - let result = Self::main(term, ui, renderer, handler, listener); + let result = Self::main(term, app, ui, handler, listener); match result { Ok(_) => { Self::disable()?; @@ -176,10 +188,8 @@ mod tests { use musichoard::collection::Collection; use crate::tui::{ - handler::MockIEventHandler, - lib::MockIMusicHoard, - listener::MockIEventListener, - ui::{render::Renderer, Ui}, + app::App, handler::MockIEventHandler, lib::MockIMusicHoard, listener::MockIEventListener, + ui::Ui, }; use super::*; @@ -190,7 +200,7 @@ mod tests { Terminal::new(backend).unwrap() } - pub fn music_hoard(collection: Collection) -> MockIMusicHoard { + fn music_hoard(collection: Collection) -> MockIMusicHoard { let mut music_hoard = MockIMusicHoard::new(); music_hoard.expect_load_from_database().returning(|| Ok(())); @@ -200,8 +210,8 @@ mod tests { music_hoard } - pub fn ui(collection: Collection) -> Ui { - Ui::new(music_hoard(collection)).unwrap() + fn app(collection: Collection) -> App { + App::new(music_hoard(collection)).unwrap() } fn listener() -> MockIEventListener { @@ -215,12 +225,12 @@ mod tests { listener } - fn handler() -> MockIEventHandler> { + fn handler() -> MockIEventHandler> { let mut handler = MockIEventHandler::new(); handler .expect_handle_next_event() - .return_once(|ui: &mut Ui| { - ui.quit(); + .return_once(|app: &mut App| { + app.quit(); Ok(()) }); handler @@ -229,21 +239,21 @@ mod tests { #[test] fn run() { let terminal = terminal(); - let ui = ui(COLLECTION.to_owned()); - let renderer = Renderer; + let app = app(COLLECTION.to_owned()); + let ui = Ui; let listener = listener(); let handler = handler(); - let result = Tui::main(terminal, ui, renderer, handler, listener); + let result = Tui::main(terminal, app, ui, handler, listener); assert!(result.is_ok()); } #[test] fn event_error() { let terminal = terminal(); - let ui = ui(COLLECTION.to_owned()); - let renderer = Renderer; + let app = app(COLLECTION.to_owned()); + let ui = Ui; let listener = listener(); @@ -252,7 +262,7 @@ mod tests { .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); - let result = Tui::main(terminal, ui, renderer, handler, listener); + let result = Tui::main(terminal, app, ui, handler, listener); assert!(result.is_err()); assert_eq!( result.unwrap_err(), @@ -263,8 +273,8 @@ mod tests { #[test] fn listener_error() { let terminal = terminal(); - let ui = ui(COLLECTION.to_owned()); - let renderer = Renderer; + let app = app(COLLECTION.to_owned()); + let ui = Ui; let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error")); let listener_handle: thread::JoinHandle = thread::spawn(|| error); @@ -278,7 +288,7 @@ mod tests { .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); - let result = Tui::main(terminal, ui, renderer, handler, listener); + let result = Tui::main(terminal, app, ui, handler, listener); assert!(result.is_err()); let error = EventError::Io(io::Error::new(io::ErrorKind::Interrupted, "error")); @@ -288,8 +298,8 @@ mod tests { #[test] fn listener_panic() { let terminal = terminal(); - let ui = ui(COLLECTION.to_owned()); - let renderer = Renderer; + let app = app(COLLECTION.to_owned()); + let ui = Ui; let listener_handle: thread::JoinHandle = thread::spawn(|| panic!()); while !listener_handle.is_finished() {} @@ -302,7 +312,7 @@ mod tests { .expect_handle_next_event() .return_once(|_| Err(EventError::Recv)); - let result = Tui::main(terminal, ui, renderer, handler, listener); + let result = Tui::main(terminal, app, ui, handler, listener); assert!(result.is_err()); assert_eq!(result.unwrap_err(), Error::ListenerPanic); } diff --git a/src/tui/ui/render.rs b/src/tui/ui.rs similarity index 86% rename from src/tui/ui/render.rs rename to src/tui/ui.rs index 63b60d0..05e0ec9 100644 --- a/src/tui/ui/render.rs +++ b/src/tui/ui.rs @@ -12,21 +12,10 @@ use ratatui::{ Frame, }; -use crate::tui::ui::{Category, Selection}; +use crate::tui::app::{AppState, Category, IAppAccess, Selection}; -pub trait IRender { - fn render_collection( - artists: &Collection, - selection: &mut Selection, - frame: &mut Frame<'_, B>, - ); - fn render_info_overlay( - artists: &Collection, - selection: &mut Selection, - frame: &mut Frame<'_, B>, - ); - fn render_reload_overlay(frame: &mut Frame<'_, B>); - fn render_error_overlay, B: Backend>(msg: S, frame: &mut Frame<'_, B>); +pub trait IUi { + fn render(app: &mut APP, frame: &mut Frame<'_, B>); } struct ArtistArea { @@ -314,9 +303,9 @@ impl<'a, 'b> TrackState<'a, 'b> { } } -pub struct Renderer; +pub struct Ui; -impl Renderer { +impl Ui { fn style(_active: bool) -> Style { Style::default().fg(Color::White).bg(Color::Black) } @@ -403,9 +392,7 @@ impl Renderer { Self::render_list_widget("Tracks", st.list, st.state, st.active, ar.list, fr); Self::render_info_widget("Track info", st.info, st.active, ar.info, fr); } -} -impl IRender for Renderer { fn render_collection( artists: &Collection, selection: &mut Selection, @@ -496,56 +483,59 @@ impl IRender for Renderer { } } +impl IUi for Ui { + fn render(app: &mut APP, frame: &mut Frame<'_, B>) { + let app = app.get(); + + let collection = app.collection; + let selection = app.selection; + + Self::render_collection(collection, selection, frame); + match app.state { + AppState::Info(_) => Self::render_info_overlay(collection, selection, frame), + AppState::Reload(_) => Self::render_reload_overlay(frame), + AppState::Error(ref msg) => Self::render_error_overlay(msg, frame), + _ => {} + } + } +} + #[cfg(test)] mod tests { - use crate::tui::{testmod::COLLECTION, tests::terminal}; + use crate::tui::{app::AppPublic, testmod::COLLECTION, tests::terminal}; use super::*; - fn draw_test_suite(artists: &Collection, selection: &mut Selection) { - let mut terminal = terminal(); - - terminal - .draw(|frame| Renderer::render_collection(artists, selection, frame)) - .unwrap(); - - terminal - .draw(|frame| Renderer::render_info_overlay(artists, selection, frame)) - .unwrap(); - - terminal - .draw(|frame| { - Renderer::render_collection(artists, selection, frame); - Renderer::render_info_overlay(artists, selection, frame); - }) - .unwrap(); - - terminal - .draw(|frame| { - Renderer::render_collection(artists, selection, frame); - Renderer::render_reload_overlay(frame); - }) - .unwrap(); - - terminal - .draw(|frame| { - Renderer::render_collection(artists, selection, frame); - Renderer::render_error_overlay("get rekt scrub", frame); - }) - .unwrap(); + // Automock does not support returning types with generic lifetimes. + impl IAppAccess for AppPublic<'_> { + fn get(&mut self) -> AppPublic { + AppPublic { + collection: self.collection, + selection: self.selection, + state: self.state, + } + } } - #[test] - fn stateless() { + fn draw_test_suite(collection: &Collection, selection: &mut Selection) { let mut terminal = terminal(); - terminal - .draw(|frame| Renderer::render_reload_overlay(frame)) - .unwrap(); + let mut app = AppPublic { + collection, + selection, + state: &AppState::Browse(()), + }; + terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); - terminal - .draw(|frame| Renderer::render_error_overlay("get rekt scrub", frame)) - .unwrap(); + app.state = &AppState::Info(()); + terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); + + app.state = &AppState::Reload(()); + terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); + + let binding = AppState::Error(String::from("get rekt scrub")); + app.state = &binding; + terminal.draw(|frame| Ui::render(&mut app, frame)).unwrap(); } #[test] @@ -558,16 +548,23 @@ mod tests { #[test] fn collection() { - let artists: &Collection = &COLLECTION; + let artists = &COLLECTION; let mut selection = Selection::new(Some(artists)); - draw_test_suite(&artists, &mut selection); + draw_test_suite(artists, &mut selection); // Change the track (which has a different track format). selection.increment_category(); selection.increment_category(); selection.increment_selection(artists); - draw_test_suite(&artists, &mut selection); + draw_test_suite(artists, &mut selection); + + // Change the artist (which has a multi-link entry). + selection.decrement_category(); + selection.decrement_category(); + selection.increment_selection(artists); + + draw_test_suite(artists, &mut selection); } }